Example #1
0
def get_state(body, extra_fields=None):
    """
    Extract only the relevant fields for the state comparisons.

    The framework ignores all the system fields (mostly from metadata)
    and the status senza completely. Except for some well-known and useful
    metadata, such as labels and annotations (except for sure garbage).

    A special set of fields can be provided even if they are supposed
    to be removed. This is used, for example, for handlers which react
    to changes in the specific fields in the status stenza,
    while the rest of the status stenza is removed.
    """

    # Always use a copy, so that future changes do not affect the extracted state.
    orig = copy.deepcopy(body)
    body = copy.deepcopy(body)

    # The top-level identifying fields never change, so there is not need to track them.
    if 'apiVersion' in body:
        del body['apiVersion']
    if 'kind' in body:
        del body['kind']

    # Purge the whole stenzas with system info (extra-fields are restored below).
    if 'metadata' in body:
        del body['metadata']
    if 'status' in body:
        del body['status']

    # We want some selected metadata to be tracked implicitly.
    dicts.cherrypick(
        src=orig,
        dst=body,
        fields=[
            'metadata.labels',
            'metadata.annotations',  # but not all of them! deleted below.
        ])

    # But we do not want not all of the annotations, only potentially useful.
    annotations = body.get('metadata', {}).get('annotations', {})
    for annotation in list(annotations):
        if annotation == LAST_SEEN_ANNOTATION:
            del annotations[annotation]
        if annotation == 'kubectl.kubernetes.io/last-applied-configuration':
            del annotations[annotation]

    # Restore all explicitly whitelisted extra-fields from the original body.
    dicts.cherrypick(src=orig, dst=body, fields=extra_fields)

    # Cleanup the parent structs if they have become empty, for consistent state comparison.
    if 'annotations' in body.get('metadata',
                                 {}) and not body['metadata']['annotations']:
        del body['metadata']['annotations']
    if 'metadata' in body and not body['metadata']:
        del body['metadata']
    if 'status' in body and not body['status']:
        del body['status']
    return body
Example #2
0
    def build(
            self,
            *,
            body: bodies.Body,
            extra_fields: Optional[Iterable[dicts.FieldSpec]] = None,
    ) -> bodies.BodyEssence:
        """
        Extract only the relevant fields for the state comparisons.

        The framework ignores all the system fields (mostly from metadata)
        and the status senza completely. Except for some well-known and useful
        metadata, such as labels and annotations (except for sure garbage).

        A special set of fields can be provided even if they are supposed
        to be removed. This is used, for example, for handlers which react
        to changes in the specific fields in the status stanza,
        while the rest of the status stanza is removed.

        It is generally not a good idea to override this method in custom
        stores, unless a different definition of an object's essence is needed.
        """

        # Always use a copy, so that future changes do not affect the extracted essence.
        essence = cast(Dict[Any, Any], copy.deepcopy(dict(body)))

        # The top-level identifying fields never change, so there is not need to track them.
        if 'apiVersion' in essence:
            del essence['apiVersion']
        if 'kind' in essence:
            del essence['kind']

        # Purge the whole stenzas with system info (extra-fields are restored below).
        if 'metadata' in essence:
            del essence['metadata']
        if 'status' in essence:
            del essence['status']

        # We want some selected metadata to be tracked implicitly.
        dicts.cherrypick(src=body, dst=essence, fields=[
            'metadata.labels',
            'metadata.annotations',  # but not all of them! deleted below.
        ], picker=copy.deepcopy)

        # But we do not want all the annotations, only the potentially useful ones.
        # Also exclude the annotations of other Kopf-based operators' storages.
        annotations = essence.get('metadata', {}).get('annotations', {})
        ignored_prefixes = self._detect_marked_prefixes(annotations)
        for annotation in list(annotations):
            if any(annotation.startswith(f'{prefix}/') for prefix in ignored_prefixes):
                del annotations[annotation]
            elif annotation == 'kubectl.kubernetes.io/last-applied-configuration':
                del annotations[annotation]

        # Restore all explicitly whitelisted extra-fields from the original body.
        dicts.cherrypick(src=body, dst=essence, fields=extra_fields, picker=copy.deepcopy)

        self.remove_empty_stanzas(cast(bodies.BodyEssence, essence))
        return cast(bodies.BodyEssence, essence)
Example #3
0
def test_copied_object_picked_on_request():
    src = {'tested-key': {'key': 'val'}}
    dst = {}
    cherrypick(src=src, dst=dst, fields=['tested-key'], picker=copy.copy)

    assert dst == {'tested-key': {'key': 'val'}}
    assert dst['tested-key'] == src['tested-key']
    assert dst['tested-key'] is not src['tested-key']

    src['tested-key']['key'] = 'another-val'
    assert dst['tested-key']['key'] == 'val'
Example #4
0
def test_exact_object_picked_by_default():
    src = {'tested-key': {'key': 'val'}}
    dst = {}
    cherrypick(src=src, dst=dst, fields=['tested-key'])

    assert dst == {'tested-key': {'key': 'val'}}
    assert dst['tested-key'] == src['tested-key']
    assert dst['tested-key'] is src['tested-key']

    src['tested-key']['key'] = 'replaced-val'
    assert dst['tested-key']['key'] == 'replaced-val'
Example #5
0
def test_overrides_existing_keys():
    src = {'ignored-key': 'src-val', 'tested-key': 'src-val'}
    dst = {'ignored-key': 'dst-val', 'tested-key': 'dst-val'}
    cherrypick(src=src, dst=dst, fields=['tested-key'])
    assert dst == {'ignored-key': 'dst-val', 'tested-key': 'src-val'}
Example #6
0
def test_fails_on_nonmapping_dst_key():
    src = {'sub': {'ignored-key': 'src-val', 'tested-key': 'src-val'}}
    dst = {'sub': 'scalar-value'}
    with pytest.raises(TypeError):
        cherrypick(src=src, dst=dst, fields=['sub.tested-key'])
Example #7
0
def test_ensures_dst_subdicts():
    src = {'sub': {'ignored-key': 'src-val', 'tested-key': 'src-val'}}
    dst = {}
    cherrypick(src=src, dst=dst, fields=['sub.tested-key'])
    assert dst == {'sub': {'tested-key': 'src-val'}}
Example #8
0
def test_skips_absent_src_subkeys():
    src = {'sub': {'ignored-key': 'src-val'}}
    dst = {'sub': {'ignored-key': 'dst-val', 'tested-key': 'dst-val'}}
    cherrypick(src=src, dst=dst, fields=['sub.tested-key'])
    assert dst == {'sub': {'ignored-key': 'dst-val', 'tested-key': 'dst-val'}}
Example #9
0
def test_adds_absent_dst_keys():
    src = {'ignored-key': 'src-val', 'tested-key': 'src-val'}
    dst = {'ignored-key': 'dst-val'}
    cherrypick(src=src, dst=dst, fields=['tested-key'])
    assert dst == {'ignored-key': 'dst-val', 'tested-key': 'src-val'}