Esempio n. 1
0
def test_rule_from_volume_with_claim(
    deltas_annotation_key,
    fx_deltas,
    volume_zone_label,
    provisioner_annotation,
):
    ctx = Context({'deltas_annotation_key': deltas_annotation_key})

    pv, pvc = make_volume_and_claim(
        ctx=ctx,
        volume_annotations=provisioner_annotation,
        claim_annotations={
            ctx.config['deltas_annotation_key']: fx_deltas,
        },
        volume_zone_label=volume_zone_label,
    )

    if pvc.namespace and not pvc.namespace == 'default':
        expected_rule_name = f'pvc-{pvc.namespace}-{pvc.name}'
    else:
        expected_rule_name = f'pvc-{pvc.name}'

    loop = asyncio.get_event_loop()
    with mock_kube([pv, pvc]) as _mocked:
        deltas_annotation_key = ctx.config['deltas_annotation_key']
        rule = loop.run_until_complete(
            rule_from_pv(ctx, pv, deltas_annotation_key))
        assert rule.source == pvc.obj['metadata']['selfLink']
        assert deltas_annotation_key in pvc.annotations
        assert rule.name == expected_rule_name
        assert rule.deltas == parse_deltas(fx_deltas)
Esempio n. 2
0
    def read_volume(name):
        _log = _logger.new(volume_name=name, )
        env_name = name.replace('-', '_').upper()
        deltas_str = os.environ.get('VOLUME_{}_DELTAS'.format(env_name))
        if not deltas_str:
            raise ConfigError(
                'A volume {} was defined, but VOLUME_{}_DELTAS is not set'.
                format(name, env_name))

        zone = os.environ.get('VOLUME_{}_ZONE'.format(env_name))
        if not zone:
            raise ConfigError(
                'A volume {} was defined, but VOLUME_{}_ZONE is not set'.
                format(name, env_name))

        _log = _log.bind(
            deltas_str=deltas_str,
            zone=zone,
        )

        rule = Rule(
            name=name,
            deltas=parse_deltas(deltas_str),
            gce_disk=name,
            gce_disk_zone=zone,
        )

        _log.info(events.Rule.ADDED_FROM_CONFIG, rule=rule)

        return rule
Esempio n. 3
0
def test_rule_from_volume(fx_context: Context, fx_deltas: str,
                          label_zone: Optional[Dict[str, str]],
                          annotation_provisioned_by: Optional[Dict[str, str]],
                          _spec_gce_persistent_disk: Optional[Dict[str, Any]]):
    annotations = {}

    annotations.update({
        fx_context.config['deltas_annotation_key']: fx_deltas,
    })

    if annotation_provisioned_by is not None:
        annotations.update(annotation_provisioned_by)

    labels = {}

    if label_zone is not None:
        labels.update(label_zone)

    spec = {}

    if _spec_gce_persistent_disk is not None:
        spec.update(_spec_gce_persistent_disk)

    source_pv = make_resource(
        pykube.objects.PersistentVolume,
        'source-pv',
        annotations=annotations,
        labels=labels,
        spec=spec,
    )

    expected_rule_name = f'pv-{source_pv.name}'

    loop = asyncio.get_event_loop()

    with mock_kube([source_pv]):
        rule: Rule = loop.run_until_complete(
            rule_from_pv(fx_context, source_pv,
                         fx_context.config['deltas_annotation_key']))
        assert rule.name == expected_rule_name
        assert rule.deltas == parse_deltas(fx_deltas)
Esempio n. 4
0
async def rule_from_snapshotrule(
    ctx: Context,
    resource: SnapshotRule
) -> Optional[Rule]:
    """This tries to build a rule within a `SnapshotRule` resource -
    the resource that we custom designed for this purpose.

    This is invoked whenever Kubernetes tells us that such a resource
    was created, deleted, or updated.

    There are two separate ways a `SnapshotRule` can be used:

    - A `SnapshotRule` resource can refer to a specific Cloud disk
      id to be snapshotted, e.g. 'example-disk' on 'gcloud'. This
      skips Kubernetes entirely.

    - A `SnapshotRule` resource can refer to a `PersistentVolumeClaim`.
      The disk this claim is bound to is the one we will snapshot.
      Rather than defining the snapshot interval etc. as annotations
      of the claim, they are defined here, in a separate resource.
    """
    _log = _logger.new(resource=resource, rule=resource.obj)

    spec = resource.obj.get('spec', {})

    # Validate the deltas
    try:
        deltas_str = resource.obj.get('spec', {}).get('deltas')
        try:
            deltas = parse_deltas(deltas_str)
        except DeltasParseError as exc:
            raise AnnotationError(
                'Invalid delta string',
                deltas_str=deltas_str
            ) from exc

        if deltas is None or not deltas:
            raise AnnotationError(
                'parse_deltas returned invalid deltas',
                deltas_str=deltas_str,
                deltas=deltas,
            )
    except AnnotationError:
        _log.exception(
            'rule.invalid',
            key_hints=['rule.metadata.name'],
        )
        return

    # Refers to a disk from a cloud provider
    if spec.get('disk'):
        # Validate the backend
        backend_name = spec.get('backend')
        try:
            backend = get_backend(backend_name)
        except ConfigurationError as e:
            _log.exception(
                'rule.invalid',
                message=e.message,
                backend=backend_name
            )
            return

        # Validate the disk identifier
        disk = resource.obj.get('spec', {}).get('disk')
        try:
            disk = backend.validate_disk_identifier(disk)
        except ValueError:
            _log.exception(
                'rule.invalid',
                key_hints=['rule.metadata.name'],
            )
            return

        rule = Rule(
            name=rule_name_from_k8s_source(resource),
            deltas=deltas,
            backend=backend_name,
            disk=disk
        )
        return rule

    # Refers to a volume claim
    if spec.get('persistentVolumeClaim'):

        # Find the claim
        volume_claim = await get_resource_or_none(
            ctx.kube_client,
            pykube.objects.PersistentVolumeClaim,
            spec.get('persistentVolumeClaim'),
            namespace=resource.metadata['namespace']
        )

        if not volume_claim:
            _log.warning(
                events.Rule.PENDING,
                reason='Volume claim does not exist',
                key_hints=['rule.metadata.name'],
            )
            raise RuleDependsOn(
                'The volume claim targeted by this SnapshotRule does not exist yet',
                kind='PersistentVolumeClaim',
                namespace=resource.metadata['namespace'],
                name=spec.get('persistentVolumeClaim')
            )

        # Find the volume
        try:
            volume = await volume_from_pvc(ctx, volume_claim)
        except VolumeNotFound:
            _log.warning(
                events.Rule.PENDING,
                reason='Volume claim is not bound',
                key_hints=['rule.metadata.name'],
            )
            raise RuleDependsOn(
                'The volume claim targeted by this SnapshotRule is not bound yet',
                kind='PersistentVolumeClaim',
                namespace=resource.metadata['namespace'],
                name=spec.get('persistentVolumeClaim')
            )

        return await rule_from_pv(ctx, volume, deltas, source=resource)
Esempio n. 5
0
def test_parse_deltas(deltas, expected_timedeltas):
    parsed_deltas = parse_deltas(deltas)
    assert parsed_deltas == expected_timedeltas
Esempio n. 6
0
async def rule_from_resource(
    ctx: Context, resource: Union[pykube.objects.PersistentVolume,
                                  pykube.objects.PersistentVolumeClaim,
                                  SnapshotRule]
) -> Rule:
    """Given a Kubernetes resource, converts it to a snapshot `Rule`
    instance, or returns None.

    How this process works, depends on the resource given.

    - If the resource is a volume, we read the disk and delta info
      from there.

    - If the resource is a volume claim, we look for the volume.

    - A `SnapshotRule` custom resource directly defines the disk.
    """

    _log = _logger.new(resource=resource, )

    if (isinstance(resource, SnapshotRule)):
        # Validate the backen
        backend_name = resource.obj.get('spec', {}).get('backend')
        try:
            backend = get_backend(backend_name)
        except ConfigurationError as e:
            _log.exception('rule.invalid',
                           message=e.message,
                           backend=backend_name)
            return

        # Validate the deltas
        try:
            deltas_str = resource.obj.get('spec', {}).get('deltas')
            try:
                deltas = parse_deltas(deltas_str)
            except DeltasParseError as exc:
                raise AnnotationError('Invalid delta string',
                                      deltas_str=deltas_str) from exc

            if deltas is None or not deltas:
                raise AnnotationError(
                    'parse_deltas returned invalid deltas',
                    deltas_str=deltas_str,
                    deltas=deltas,
                )
        except AnnotationError:
            _log.exception(
                'rule.invalid',
                key_hints=['volume.metadata.name'],
            )
            return

        # Validate the disk identifier
        disk = resource.obj.get('spec', {}).get('disk')
        try:
            disk = backend.validate_disk_identifier(disk)
        except ValueError:
            _log.exception(
                'rule.invalid',
                key_hints=['volume.metadata.name'],
            )
            return

        rule = Rule(name=rule_name_from_k8s_source(resource),
                    deltas=deltas,
                    backend=backend_name,
                    disk=disk)
        return rule

    if isinstance(resource, pykube.objects.PersistentVolumeClaim):
        try:
            volume = await volume_from_pvc(ctx, resource)
        except VolumeNotFound:
            _log.exception(
                events.Volume.NOT_FOUND,
                key_hints=[
                    'resource.metadata.name',
                ],
            )
            return

    elif isinstance(resource, pykube.objects.PersistentVolume):
        volume = resource

    else:
        raise RuntimeError(f'{resource} is not supported.')

    volume_name = volume.name
    _log = _log.bind(
        volume_name=volume_name,
        volume=volume.obj,
    )

    try:
        rule = await rule_from_pv(
            ctx,
            volume,
            ctx.config.get('deltas_annotation_key'),
            use_claim_name=ctx.config.get('use_claim_name'))
        return rule
    except AnnotationNotFound as exc:
        _log.info(
            events.Annotation.NOT_FOUND,
            key_hints=['volume.metadata.name'],
            exc_info=exc,
        )
    except AnnotationError:
        _log.exception(
            events.Annotation.ERROR,
            key_hints=['volume.metadata.name'],
        )
    except UnsupportedVolume as exc:
        _log.info(
            events.Volume.UNSUPPORTED,
            key_hints=['volume.metadata.name'],
            exc_info=exc,
        )