controllers/coordination/name_registry.go
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{}