cloudfoundry/cf-k8s-controllers

View on GitHub
controllers/coordination/name_registry.go

Summary

Maintainability
A
0 mins
Test Coverage
A
100%
package coordination

import (
    "context"
    "crypto/sha1"
    "fmt"

    "code.cloudfoundry.org/korifi/controllers/webhooks"
    "github.com/go-logr/logr"
    coordinationv1 "k8s.io/api/coordination/v1"
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    "k8s.io/apimachinery/pkg/types"
    controllerruntime "sigs.k8s.io/controller-runtime"
    "sigs.k8s.io/controller-runtime/pkg/client"
    "sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

const (
    hashedNamePrefix = "n-"

    EntityTypeAnnotation     = "coordination.cloudfoundry.org/entity-type"
    NamespaceAnnotation      = "coordination.cloudfoundry.org/namespace"
    NameAnnotation           = "coordination.cloudfoundry.org/name"
    OwnerNamespaceAnnotation = "coordination.cloudfoundry.org/owner-namespace"
    OwnerNameAnnotation      = "coordination.cloudfoundry.org/owner-name"
)

var (
    unlockedIdentity = "none"
    lockedIdentity   = "locked"
)

//+kubebuilder:rbac:groups=coordination.k8s.io,resources=leases,verbs=get;create;patch;delete;list;watch

type NameRegistry struct {
    client     client.Client
    entityType string
    logger     logr.Logger
}

func NewNameRegistry(client client.Client, entityType string) NameRegistry {
    return NameRegistry{
        client:     client,
        entityType: entityType,
        logger:     controllerruntime.Log.WithName("name-registry").WithValues("entityType", entityType),
    }
}

func (r NameRegistry) RegisterName(ctx context.Context, namespace, name, ownerNamespace, ownerName string) error {
    logger := r.logger.WithName("register-name").WithValues("namespace", namespace, "name", name)

    if isDryRun(ctx, logger) {
        return nil
    }

    hashedName := r.hashName(name)

    lease := &coordinationv1.Lease{
        ObjectMeta: metav1.ObjectMeta{
            Name:      hashedName,
            Namespace: namespace,
            Annotations: map[string]string{
                EntityTypeAnnotation:     r.entityType,
                NamespaceAnnotation:      namespace,
                NameAnnotation:           name,
                OwnerNamespaceAnnotation: ownerNamespace,
                OwnerNameAnnotation:      ownerName,
            },
        },
        Spec: coordinationv1.LeaseSpec{
            HolderIdentity: &unlockedIdentity,
        },
    }

    if err := r.client.Create(ctx, lease); err != nil {
        r.logger.Error(err, "failed-to-register-name")
        return fmt.Errorf("creating a lease failed: %w", err)
    }

    logger.V(1).Info("registered-name", "hashedName", hashedName)

    return nil
}

func (r NameRegistry) DeregisterName(ctx context.Context, namespace, name string) error {
    logger := r.logger.WithName("deregister-name").WithValues("namespace", namespace, "name", name)

    if isDryRun(ctx, logger) {
        return nil
    }

    hashedName := r.hashName(name)
    lease := &coordinationv1.Lease{
        ObjectMeta: metav1.ObjectMeta{
            Name:      hashedName,
            Namespace: namespace,
        },
    }
    if err := r.client.Delete(ctx, lease); client.IgnoreNotFound(err) != nil {
        logger.Error(err, "failed-to-deregister-name")
        return fmt.Errorf("deleting a lease failed: %w", err)
    }

    logger.V(1).Info("deregistered-name", "hashedName", hashedName)

    return nil
}

func (r NameRegistry) TryLockName(ctx context.Context, namespace, name string) error {
    logger := r.logger.WithName("try-lock-name").WithValues("namespace", namespace, "name", name)

    if isDryRun(ctx, logger) {
        return nil
    }

    lease := &coordinationv1.Lease{
        ObjectMeta: metav1.ObjectMeta{
            Name:      r.hashName(name),
            Namespace: namespace,
        },
    }
    jsonPatch := fmt.Sprintf(`[
    {"op":"test", "path":"/spec/holderIdentity", "value": "%s"},
    {"op":"replace", "path":"/spec/holderIdentity", "value": "%s"}
    ]`, unlockedIdentity, lockedIdentity)

    if err := r.client.Patch(ctx, lease, client.RawPatch(types.JSONPatchType, []byte(jsonPatch))); err != nil {
        logger.Info("failed-to-patch-existing-lease", "reason", err)
        return fmt.Errorf("failed to acquire lock on lease: %w", err)
    }

    return nil
}

func (r NameRegistry) UnlockName(ctx context.Context, namespace, name string) error {
    logger := r.logger.WithName("try-lock-name").WithValues("namespace", namespace, "name", name)

    if isDryRun(ctx, logger) {
        return nil
    }

    lease := &coordinationv1.Lease{
        ObjectMeta: metav1.ObjectMeta{
            Name:      r.hashName(name),
            Namespace: namespace,
        },
    }
    jsonPatch := fmt.Sprintf(`[
    {"op":"test", "path":"/spec/holderIdentity", "value": "%s"},
    {"op":"replace", "path":"/spec/holderIdentity", "value": "%s"}
    ]`, lockedIdentity, unlockedIdentity)

    if err := r.client.Patch(ctx, lease, client.RawPatch(types.JSONPatchType, []byte(jsonPatch))); err != nil {
        logger.Error(err, "failed-to-unlock-lease")
        return fmt.Errorf("failed to release lock on lease: %w", err)
    }

    return nil
}

func (r NameRegistry) CheckNameOwnership(ctx context.Context, namespace, name, ownerNamespace, ownerName string) (bool, error) {
    r.logger.V(1).Info("checking-name-ownership")

    var lease coordinationv1.Lease
    err := r.client.Get(ctx, client.ObjectKey{Namespace: namespace, Name: r.hashName(name)}, &lease)
    if err != nil {
        return false, err
    }

    return lease.Annotations[OwnerNamespaceAnnotation] == ownerNamespace && lease.Annotations[OwnerNameAnnotation] == ownerName, nil
}

func (r NameRegistry) hashName(name string) string {
    input := fmt.Sprintf("%s::%s", r.entityType, name)
    return fmt.Sprintf("%s%x", hashedNamePrefix, sha1.Sum([]byte(input)))
}

func isDryRun(ctx context.Context, logger logr.Logger) bool {
    req, err := admission.RequestFromContext(ctx)
    if err != nil {
        return false
    }

    result := req.DryRun != nil && *req.DryRun
    if result {
        logger.V(1).Info("skipping dry-run requests")
    }

    return result
}

// check we implement the interface
var _ webhooks.NameRegistry = NameRegistry{}