cloudfoundry/cf-k8s-controllers

View on GitHub
controllers/webhooks/validation/duplicate_validator.go

Summary

Maintainability
A
1 hr
Test Coverage
package validation

import (
    "context"

    "code.cloudfoundry.org/korifi/controllers/webhooks"
    "github.com/go-logr/logr"
    k8serrors "k8s.io/apimachinery/pkg/api/errors"
    "sigs.k8s.io/controller-runtime/pkg/client"
)

const DuplicateNameErrorType = "DuplicateNameError"

type DuplicateValidator struct {
    nameRegistry webhooks.NameRegistry
}

func NewDuplicateValidator(nameRegistry webhooks.NameRegistry) *DuplicateValidator {
    return &DuplicateValidator{
        nameRegistry: nameRegistry,
    }
}

func (v DuplicateValidator) ValidateCreate(ctx context.Context, logger logr.Logger, namespace string, obj webhooks.UniqueClientObject) error {
    logger = logger.WithName("duplicateValidator.ValidateCreate")
    err := v.nameRegistry.RegisterName(ctx, namespace, obj.UniqueName(), obj.GetNamespace(), obj.GetName())
    if err != nil {
        logger.Info("failed to register name during create",
            "name", obj.UniqueName(),
            "namespace", namespace,
            "reason", err,
        )

        if k8serrors.IsAlreadyExists(err) {
            return duplicateError(obj)
        }

        return unknownError()
    }

    return nil
}

func (v DuplicateValidator) ValidateUpdate(ctx context.Context, logger logr.Logger, namespace string, oldObj, obj webhooks.UniqueClientObject) error {
    if oldObj.UniqueName() == obj.UniqueName() {
        return nil
    }

    logger = logger.
        WithName("duplicateValidator.ValidateUpdate").
        WithValues("namespace", namespace, "oldName", oldObj.UniqueName(), "newName", obj.UniqueName())

    err := v.nameRegistry.TryLockName(ctx, namespace, oldObj.UniqueName())
    if err != nil {
        logger.Info("failed to acquire lock on old name", "reason", err)

        if k8serrors.IsNotFound(err) {
            isOwned, ownershipErr := v.nameRegistry.CheckNameOwnership(ctx, namespace, obj.UniqueName(), obj.GetNamespace(), obj.GetName())
            if ownershipErr != nil {
                logger.Error(ownershipErr, "failed to check ownership on new name")
                return unknownError()
            }

            if isOwned {
                logger.Info("unique name is already owned by updated object",
                    "name", obj.UniqueName(),
                    "updatedObjectKind", obj.GetObjectKind(),
                    "object", client.ObjectKeyFromObject(obj),
                )
                return nil
            }
        }

        return unknownError()
    }

    logger.V(1).Info("locked-old-name")

    err = v.nameRegistry.RegisterName(ctx, namespace, obj.UniqueName(), obj.GetNamespace(), obj.GetName())
    if err != nil {
        // cannot register new name, so unlock old registry entry allowing future renames
        unlockErr := v.nameRegistry.UnlockName(ctx, namespace, oldObj.UniqueName())
        if unlockErr != nil {
            // A locked registry entry will remain, so future name updates will fail until operator intervenes
            logger.Info("failed to release lock on old name",
                "reason", unlockErr,
            )
        }

        logger.Info("failed to register new name during update",
            "reason", err,
        )

        if k8serrors.IsAlreadyExists(err) {
            return duplicateError(obj)
        }

        return unknownError()
    }
    logger.V(1).Info("registered-new-name")

    err = v.nameRegistry.DeregisterName(ctx, namespace, oldObj.UniqueName())
    if err != nil {
        // We cannot unclaim the old name. It will remain claimed until an operator intervenes.
        logger.Info("failed to deregister old name during update",
            "reason", err,
        )
    }
    logger.V(1).Info("deregistered-old-name")

    return nil
}

func (v DuplicateValidator) ValidateDelete(ctx context.Context, logger logr.Logger, namespace string, obj webhooks.UniqueClientObject) error {
    logger = logger.WithName("duplicateValidator.ValidateDelete")
    err := v.nameRegistry.DeregisterName(ctx, namespace, obj.UniqueName())
    if err != nil {
        logger.Info("failed to deregister name during delete",
            "namespace", namespace,
            "name", obj.UniqueName(),
            "reason", err,
        )

        if k8serrors.IsNotFound(err) {
            return nil
        }

        return unknownError()
    }

    return nil
}

func duplicateError(obj webhooks.UniqueClientObject) error {
    return ValidationError{
        Type:    DuplicateNameErrorType,
        Message: obj.UniqueValidationErrorMessage(),
    }.ExportJSONError()
}

func unknownError() error {
    return ValidationError{
        Type:    UnknownErrorType,
        Message: UnknownErrorMessage,
    }.ExportJSONError()
}

// check interface is implemented correctly
var _ webhooks.NameValidator = DuplicateValidator{}