Esempio n. 1
0
async def patch_obj(*, resource, patch, namespace=None, name=None, body=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 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
    if body is None:
        nskw = {} if namespace is None else dict(namespace=namespace)
        body = {'metadata': {'name': name}}
        body['metadata'].update(nskw)

    api = auth.get_pykube_api()
    cls = classes._make_cls(resource=resource)
    obj = cls(api, body)

    loop = asyncio.get_running_loop()
    await loop.run_in_executor(config.WorkersConfig.get_syn_executor(),
                               obj.patch, patch)
Esempio n. 2
0
def list_objs_rv(
        *,
        resource: resources.Resource,
        namespace: Optional[str] = None,
) -> 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.
    """
    api = auth.get_pykube_api()
    cls = classes._make_cls(resource=resource)
    namespace = namespace if issubclass(cls, pykube.objects.NamespacedAPIObject) else None
    lst = cls.objects(api, namespace=pykube.all if namespace is None else namespace)
    rsp = lst.response

    items: List[bodies.Body] = []
    resource_version = rsp.get('metadata', {}).get('resourceVersion', None)
    for item in rsp['items']:
        # FIXME: fix in pykube to inject the missing item's fields from the list's metainfo.
        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
Esempio n. 3
0
async def _make_cls(
    resource: resources.Resource, ) -> Type[pykube.objects.APIObject]:

    loop = asyncio.get_running_loop()
    api = auth.get_pykube_api()
    fn = functools.partial(api.resource_list, resource.api_version)
    rsp = await loop.run_in_executor(config.WorkersConfig.get_syn_executor(),
                                     fn)

    api_resources = rsp['resources']
    resource_kind = next(
        (r['kind'] for r in api_resources if r['name'] == resource.plural),
        None)
    is_namespaced = next(
        (r['namespaced']
         for r in api_resources if r['name'] == resource.plural), None)
    if not resource_kind:
        raise pykube.ObjectDoesNotExist(f"No such CRD: {resource.name}")

    cls_name = resource.plural
    cls_base = pykube.objects.NamespacedAPIObject if is_namespaced else pykube.objects.APIObject
    cls = type(
        cls_name, (cls_base, ), {
            'version': resource.api_version,
            'endpoint': resource.plural,
            'kind': resource_kind,
        })
    return cls
Esempio n. 4
0
async def read_obj(
    *,
    resource: resources.Resource,
    namespace: Optional[str] = None,
    name: Optional[str] = None,
    default: Union[_T, _UNSET] = _UNSET.token,
) -> Union[bodies.Body, _T]:
    try:
        loop = asyncio.get_running_loop()
        api = auth.get_pykube_api()
        cls = await classes._make_cls(resource=resource)
        namespace = namespace if issubclass(
            cls, pykube.objects.NamespacedAPIObject) else None
        qry = cls.objects(api, namespace=namespace)
        fn = functools.partial(qry.get_by_name, name=name)
        obj = await loop.run_in_executor(
            config.WorkersConfig.get_syn_executor(), fn)
        return cast(bodies.Body, obj.obj)
    except pykube.ObjectDoesNotExist:
        if not isinstance(default, _UNSET):
            return default
        raise
    except requests.exceptions.HTTPError as e:
        if not isinstance(default, _UNSET) and e.response.status_code in [
                403, 404
        ]:
            return default
        raise
Esempio n. 5
0
async def patch_obj(
    *,
    resource: resources.Resource,
    patch: patches.Patch,
    namespace: Optional[str] = None,
    name: Optional[str] = None,
    body: Optional[bodies.Body] = None,
) -> 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 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
    if body is None:
        body = cast(bodies.Body, {'metadata': {'name': name}})
        if namespace is not None:
            body['metadata']['namespace'] = namespace

    api = auth.get_pykube_api()
    cls = classes._make_cls(resource=resource)
    obj = cls(api, body)

    # The handler could delete its own object, so we have nothing to patch. It is okay, ignore.
    try:
        loop = asyncio.get_running_loop()
        await loop.run_in_executor(config.WorkersConfig.get_syn_executor(),
                                   obj.patch, patch)
    except pykube.ObjectDoesNotExist:
        pass
    except pykube.exceptions.HTTPError as e:
        if e.code == 404:
            pass
        else:
            raise
    except requests.exceptions.HTTPError as e:
        if e.response.status_code == 404:
            pass
        else:
            raise
Esempio n. 6
0
def read_crd(*, resource, default=_UNSET_):
    try:
        api = auth.get_pykube_api()
        cls = pykube.CustomResourceDefinition
        obj = cls.objects(api, namespace=None).get_by_name(name=resource.name)
        return obj.obj

    except pykube.ObjectDoesNotExist:
        if default is not _UNSET_:
            return default
        raise
    except requests.exceptions.HTTPError as e:
        if e.response.status_code in [403, 404] and default is not _UNSET_:
            return default
        raise
Esempio n. 7
0
def read_obj(*, resource, namespace=None, name=None, default=_UNSET_):
    try:
        api = auth.get_pykube_api()
        cls = classes._make_cls(resource=resource)
        namespace = namespace if issubclass(
            cls, pykube.objects.NamespacedAPIObject) else None
        obj = cls.objects(api, namespace=namespace).get_by_name(name=name)
        return obj.obj
    except pykube.ObjectDoesNotExist:
        if default is not _UNSET_:
            return default
        raise
    except requests.exceptions.HTTPError as e:
        if e.response.status_code in [403, 404] and default is not _UNSET_:
            return default
        raise
Esempio n. 8
0
def read_crd(
        *,
        resource: resources.Resource,
        default: Union[_T, _UNSET] = _UNSET.token,
) -> Union[bodies.Body, _T]:
    try:
        api = auth.get_pykube_api()
        cls = pykube.CustomResourceDefinition
        obj = cls.objects(api, namespace=None).get_by_name(name=resource.name)
        return cast(bodies.Body, obj.obj)

    except pykube.ObjectDoesNotExist:
        if not isinstance(default, _UNSET):
            return default
        raise
    except requests.exceptions.HTTPError as e:
        if not isinstance(default, _UNSET) and e.response.status_code in [403, 404]:
            return default
        raise
Esempio n. 9
0
def list_objs(*, resource, namespace=None):
    """
    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.
    """
    api = auth.get_pykube_api()
    cls = classes._make_cls(resource=resource)
    namespace = namespace if issubclass(
        cls, pykube.objects.NamespacedAPIObject) else None
    lst = cls.objects(api,
                      namespace=pykube.all if namespace is None else namespace)
    return lst.response
Esempio n. 10
0
async def list_objs_rv(
    *,
    resource: resources.Resource,
    namespace: Optional[str] = None,
) -> 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.
    """
    loop = asyncio.get_running_loop()
    api = auth.get_pykube_api()
    cls = await classes._make_cls(resource=resource)
    namespace = namespace if issubclass(
        cls, pykube.objects.NamespacedAPIObject) else None
    qry = cls.objects(api,
                      namespace=pykube.all if namespace is None else namespace)
    fn = lambda: qry.response  # it is a property, so cannot be threaded without lambdas.
    rsp = await loop.run_in_executor(config.WorkersConfig.get_syn_executor(),
                                     fn)

    items: List[bodies.Body] = []
    resource_version = rsp.get('metadata', {}).get('resourceVersion', None)
    for item in rsp['items']:
        # FIXME: fix in pykube to inject the missing item's fields from the list's metainfo.
        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
Esempio n. 11
0
def read_obj(
        *,
        resource: resources.Resource,
        namespace: Optional[str] = None,
        name: Optional[str] = None,
        default: Union[_T, _UNSET] = _UNSET.token,
) -> Union[bodies.Body, _T]:
    try:
        api = auth.get_pykube_api()
        cls = classes._make_cls(resource=resource)
        namespace = namespace if issubclass(cls, pykube.objects.NamespacedAPIObject) else None
        obj = cls.objects(api, namespace=namespace).get_by_name(name=name)
        return cast(bodies.Body, obj.obj)
    except pykube.ObjectDoesNotExist:
        if not isinstance(default, _UNSET):
            return default
        raise
    except requests.exceptions.HTTPError as e:
        if not isinstance(default, _UNSET) and e.response.status_code in [403, 404]:
            return default
        raise
Esempio n. 12
0
def _make_cls(resource) -> Type[pykube.objects.APIObject]:
    api = auth.get_pykube_api()
    api_resources = api.resource_list(resource.api_version)['resources']
    resource_kind = next(
        (r['kind'] for r in api_resources if r['name'] == resource.plural),
        None)
    is_namespaced = next(
        (r['namespaced']
         for r in api_resources if r['name'] == resource.plural), None)
    if not resource_kind:
        raise pykube.ObjectDoesNotExist(f"No such CRD: {resource.name}")

    cls_name = resource.plural
    cls_base = pykube.objects.NamespacedAPIObject if is_namespaced else pykube.objects.APIObject
    cls = type(
        cls_name, (cls_base, ), {
            'version': resource.api_version,
            'endpoint': resource.plural,
            'kind': resource_kind,
        })
    return cls
Esempio n. 13
0
async def watch_objs(
    *,
    resource: resources.Resource,
    namespace: Optional[str] = None,
    timeout: Optional[float] = None,
    since: Optional[str] = None,
) -> 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.
    """
    src: Iterator[PykubeWatchEvent]

    params = {}
    if timeout is not None:
        params['timeoutSeconds'] = timeout

    api = auth.get_pykube_api(timeout=None)
    cls = await classes._make_cls(resource=resource)
    namespace = namespace if issubclass(
        cls, pykube.objects.NamespacedAPIObject) else None
    lst = cls.objects(api,
                      namespace=pykube.all if namespace is None else namespace)
    src = lst.watch(since=since, params=params)
    async for event in streaming_aiter(iter(src)):
        yield cast(bodies.RawEvent, {
            'type': event.type,
            'object': event.object.obj,
        })
Esempio n. 14
0
def watch_objs(*, resource, namespace=None, timeout=None, since=None):
    """
    Watch 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.
    """
    api = auth.get_pykube_api(timeout=timeout)
    cls = classes._make_cls(resource=resource)
    namespace = namespace if issubclass(
        cls, pykube.objects.NamespacedAPIObject) else None
    lst = cls.objects(api,
                      namespace=pykube.all if namespace is None else namespace)
    src = lst.watch(since=since)
    return iter({
        'type': event.type,
        'object': event.object.obj
    } for event in src)
Esempio n. 15
0
async def read_crd(
    *,
    resource: resources.Resource,
    default: Union[_T, _UNSET] = _UNSET.token,
) -> Union[bodies.Body, _T]:
    try:
        loop = asyncio.get_running_loop()
        api = auth.get_pykube_api()
        cls = pykube.CustomResourceDefinition
        qry = cls.objects(api, namespace=None)
        fn = functools.partial(qry.get_by_name, name=resource.name)
        obj = await loop.run_in_executor(
            config.WorkersConfig.get_syn_executor(), fn)
        return cast(bodies.Body, obj.obj)
    except pykube.ObjectDoesNotExist:
        if not isinstance(default, _UNSET):
            return default
        raise
    except requests.exceptions.HTTPError as e:
        if not isinstance(default, _UNSET) and e.response.status_code in [
                403, 404
        ]:
            return default
        raise
Esempio n. 16
0
async def post_event(*, obj=None, ref=None, type, reason, message=''):
    """
    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.
    """

    # Object reference - similar to the owner reference, but different.
    if obj is not None and ref is not None:
        raise TypeError(
            "Only one of obj= and ref= is allowed for a posted event. Got both."
        )
    if obj is None and ref is None:
        raise TypeError(
            "One of obj= and ref= is required for a posted event. Got none.")
    if ref is None:
        ref = hierarchies.build_object_reference(obj)

    now = datetime.datetime.utcnow()
    namespace = ref['namespace'] or 'default'

    # 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}'

    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': 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:
        api = auth.get_pykube_api()
        obj = pykube.Event(api, body)

        loop = asyncio.get_running_loop()
        await loop.run_in_executor(config.WorkersConfig.get_syn_executor(),
                                   obj.create)

    except (requests.exceptions.HTTPError, pykube.exceptions.HTTPError) as e:
        # Events are helpful but auxiliary, they should not fail the handling cycle.
        # Yet we want to notice that something went wrong (in logs).
        logger.warning(
            "Failed to post an event. Ignoring and continuing. "
            f"Error: {e!r}. "
            f"Event: type={type!r}, reason={reason!r}, message={message!r}.")