Exemplo n.º 1
0
async def read_obj(
        *,
        resource: resources.Resource,
        namespace: Optional[str] = None,
        name: Optional[str] = None,
        default: Union[_T, _UNSET] = _UNSET.token,
        context: Optional[auth.APIContext] = None,  # injected by the decorator
) -> Union[bodies.RawBody, _T]:
    if context is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    is_namespaced = await discovery.is_namespaced(resource=resource,
                                                  context=context)
    namespace = namespace if is_namespaced else None

    try:
        url = resource.get_url(server=context.server,
                               namespace=namespace,
                               name=name)
        rsp = await errors.parse_response(await context.session.get(url))
        return cast(bodies.RawBody, rsp)

    except (errors.APINotFoundError, errors.APIForbiddenError):
        if not isinstance(default, _UNSET):
            return default
        raise
Exemplo n.º 2
0
async def read_obj(
        *,
        resource: resources.Resource,
        namespace: Optional[str] = None,
        name: Optional[str] = None,
        default: Union[_T, _UNSET] = _UNSET.token,
        context: Optional[auth.APIContext] = None,  # injected by the decorator
) -> Union[bodies.Body, _T]:
    if context is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    is_namespaced = await discovery.is_namespaced(resource=resource, context=context)
    namespace = namespace if is_namespaced else None

    try:
        response = await context.session.get(
            url=resource.get_url(server=context.server, namespace=namespace, name=name),
        )
        response.raise_for_status()
        respdata = await response.json()
        return cast(bodies.Body, respdata)

    except aiohttp.ClientResponseError as e:
        if e.status in [403, 404] and not isinstance(default, _UNSET):
            return default
        raise
Exemplo n.º 3
0
async def watch_objs(
        *,
        settings: configuration.OperatorSettings,
        resource: resources.Resource,
        namespace: Optional[str] = None,
        timeout: Optional[float] = None,
        since: Optional[str] = None,
        context: Optional[auth.APIContext] = None,  # injected by the decorator
        freeze_waiter: asyncio_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.")

    is_namespaced = await discovery.is_namespaced(resource=resource, context=context)
    namespace = namespace if is_namespaced else None

    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 freeze-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,
            ),
        )
        response.raise_for_status()

        response_close_callback = lambda _: response.close()
        freeze_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:
            freeze_waiter.remove_done_callback(response_close_callback)

    except (aiohttp.ClientConnectionError, aiohttp.ClientPayloadError, asyncio.TimeoutError):
        pass
Exemplo n.º 4
0
async def patch_obj(
        *,
        resource: resources.Resource,
        patch: patches.Patch,
        namespace: Optional[str] = None,
        name: Optional[str] = None,
        body: Optional[bodies.Body] = None,
        session: Optional[auth.APISession] = None,  # injected by the decorator
) -> None:
    """
    Patch a resource of specific kind.

    Either the namespace+name should be specified, or the body,
    which is used only to get namespace+name identifiers.

    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).
    """
    if session is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    if body is not None and (name is not None or namespace is not None):
        raise TypeError(
            "Either body, or name+namespace can be specified. Got both.")

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

    is_namespaced = await discovery.is_namespaced(resource=resource,
                                                  session=session)
    namespace = namespace if is_namespaced else None

    if body is None:
        body = cast(bodies.Body, {'metadata': {'name': name}})
        if namespace is not None:
            body['metadata']['namespace'] = namespace

    try:
        await session.patch(
            url=resource.get_url(server=session.server,
                                 namespace=namespace,
                                 name=name),
            headers={'Content-Type': 'application/merge-patch+json'},
            json=patch,
            raise_for_status=True,
        )
    except aiohttp.ClientResponseError as e:
        if e.status == 404:
            pass
        else:
            raise
Exemplo n.º 5
0
async def watch_objs(
        *,
        resource: resources.Resource,
        namespace: Optional[str] = None,
        timeout: Optional[float] = None,
        since: Optional[str] = None,
        session: Optional[auth.APISession] = None,  # injected by the decorator
) -> AsyncIterator[bodies.RawEvent]:
    """
    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 session is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    is_namespaced = await discovery.is_namespaced(resource=resource,
                                                  session=session)
    namespace = namespace if is_namespaced else None

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

    response = await session.get(
        url=resource.get_url(server=session.server,
                             namespace=namespace,
                             params=params),
        timeout=aiohttp.ClientTimeout(total=None),
    )
    response.raise_for_status()

    async with response:
        async for line in response.content:
            event = cast(bodies.RawEvent, json.loads(line.decode("utf-8")))
            yield event
Exemplo n.º 6
0
async def list_objs_rv(
        *,
        resource: resources.Resource,
        namespace: Optional[str] = None,
        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.")

    is_namespaced = await discovery.is_namespaced(resource=resource,
                                                  context=context)
    namespace = namespace if is_namespaced else None

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

    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
Exemplo n.º 7
0
async def list_objs_rv(
        *,
        resource: resources.Resource,
        namespace: Optional[str] = None,
        session: Optional[auth.APISession] = None,  # injected by the decorator
) -> Tuple[Collection[bodies.Body], 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 session is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    # TODO: also add cluster-wide resource when --namespace is set?
    response = await session.get(url=resource.get_url(server=session.server,
                                                      namespace=namespace), )
    response.raise_for_status()
    rsp = await response.json()

    items: List[bodies.Body] = []
    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
Exemplo n.º 8
0
async def read_obj(
        *,
        resource: resources.Resource,
        namespace: Optional[str] = None,
        name: Optional[str] = None,
        default: Union[_T, _UNSET] = _UNSET.token,
        session: Optional[auth.APISession] = None,  # injected by the decorator
) -> Union[bodies.Body, _T]:
    if session is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    try:
        # TODO: also add cluster-wide resource when --namespace is set?
        response = await session.get(url=resource.get_url(
            server=session.server, namespace=namespace, name=name), )
        response.raise_for_status()
        respdata = await response.json()
        return cast(bodies.Body, respdata)

    except aiohttp.ClientResponseError as e:
        if e.status in [403, 404] and not isinstance(default, _UNSET):
            return default
        raise
Exemplo n.º 9
0
def test_url_of_builtin_resource_list_namespaced_with_subresource():
    resource = Resource('', 'v1', 'plural')
    with pytest.raises(ValueError):
        resource.get_url(namespace='ns-a.b', subresource='status')
Exemplo n.º 10
0
async def patch_obj(
        *,
        resource: resources.Resource,
        patch: patches.Patch,
        namespace: Optional[str] = None,
        name: Optional[str] = None,
        body: Optional[bodies.Body] = None,
        context: Optional[auth.APIContext] = None,  # injected by the decorator
) -> bodies.RawBody:
    """
    Patch a resource of specific kind.

    Either the namespace+name should be specified, or the body,
    which is used only to get namespace+name identifiers.

    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.
    """
    if context is None:
        raise RuntimeError("API instance is not injected by the decorator.")

    if body is not None and (name is not None or namespace is not None):
        raise TypeError("Either body, or name+namespace can be specified. Got both.")

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

    is_namespaced = await discovery.is_namespaced(resource=resource, context=context)
    namespace = namespace if is_namespaced else None

    if body is None:
        body = cast(bodies.Body, {'metadata': {'name': name}})
        if namespace is not None:
            body['metadata']['namespace'] = namespace

    as_subresource = await discovery.is_status_subresource(resource=resource, context=context)
    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!
    patched_body = bodies.RawBody()
    try:
        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,
                raise_for_status=True,
            )
            patched_body = await response.json()

        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},
                raise_for_status=True,
            )
            patched_body['status'] = await response.json()

    except aiohttp.ClientResponseError as e:
        if e.status == 404:
            pass
        else:
            raise

    return patched_body
Exemplo n.º 11
0
def test_url_of_builtin_resource_item_cluster_scoped():
    resource = Resource('', 'v1', 'plural')
    url = resource.get_url(name='name-a.b')
    assert url == '/api/v1/plural/name-a.b'
Exemplo n.º 12
0
def test_url_of_builtin_resource_item_namespaced():
    resource = Resource('', 'v1', 'plural')
    url = resource.get_url(namespace='ns-a.b', name='name-a.b')
    assert url == '/api/v1/namespaces/ns-a.b/plural/name-a.b'
Exemplo n.º 13
0
def test_url_of_custom_resource_item_namespaced():
    resource = Resource('group', 'version', 'plural')
    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'
Exemplo n.º 14
0
def test_url_of_builtin_resource_list_cluster_scoped():
    resource = Resource('', 'v1', 'plural')
    url = resource.get_url()
    assert url == '/api/v1/plural'
Exemplo n.º 15
0
def test_url_of_custom_resource_list_cluster_scoped():
    resource = Resource('group', 'version', 'plural')
    url = resource.get_url()
    assert url == '/apis/group/version/plural'
Exemplo n.º 16
0
def test_url_of_custom_resource_item_cluster_scoped():
    resource = Resource('group', 'version', 'plural')
    url = resource.get_url(name='name-a.b')
    assert url == '/apis/group/version/plural/name-a.b'
Exemplo n.º 17
0
async def patch_obj(
        *,
        resource: resources.Resource,
        namespace: Optional[str],
        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.")

    is_namespaced = await discovery.is_namespaced(resource=resource, context=context)
    namespace = namespace if is_namespaced else None

    as_subresource = await discovery.is_status_subresource(resource=resource, context=context)
    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
Exemplo n.º 18
0
def test_url_of_builtin_resource_list_cluster_scoped_with_subresource():
    resource = Resource('', 'v1', 'plural')
    with pytest.raises(ValueError):
        resource.get_url(subresource='status')
Exemplo n.º 19
0
def test_url_of_custom_resource_list_namespaced_with_subresource():
    resource = Resource('group', 'version', 'plural')
    with pytest.raises(ValueError):
        resource.get_url(namespace='ns-a.b', subresource='status')
Exemplo n.º 20
0
def test_url_of_custom_resource_list_cluster_scoped_with_subresource():
    resource = Resource('group', 'version', 'plural')
    with pytest.raises(ValueError):
        resource.get_url(subresource='status')
Exemplo n.º 21
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'