Files
cozystack/internal/lineagecontrollerwebhook/appdef_validator.go
IvanHunters 0bae19fccc feat(lineage-webhook): refuse a second ApplicationDefinition for a kind
A definition declaring an application kind that another definition already
declares now fails when it is applied, instead of sitting next to the first
one where it has no effect. An update that keeps its kind is allowed, so a
duplicate that predates the check keeps upgrading, and so is a second
definition from the same Helm release, since a release that renames its
definition object creates the new one before it deletes the old one.

The webhook is registered with failurePolicy Ignore: ApplicationDefinitions
ship with the platform itself, so an outage of the webhook must never block
a bootstrap or an upgrade, and a failure to read the existing definitions
lets the request through with a warning rather than answering with an error,
which Ignore would not cover. It is the feedback layer; which definition
manages a kind is decided by the ownership rule whatever it lets through.

Assisted-by: LLM
Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-09-25 09:39:35 +03:00

95 lines
4.0 KiB
Go

package lineagecontrollerwebhook
import (
"context"
"fmt"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
"github.com/cozystack/cozystack/internal/shared/appdefowner"
)
// ApplicationDefinitionKindValidatorPath is where the kind-uniqueness check is
// served; the chart's ValidatingWebhookConfiguration points at it.
const ApplicationDefinitionKindValidatorPath = "/validate-applicationdefinition"
// ApplicationDefinitionKindValidator refuses an ApplicationDefinition that
// declares an application kind another definition already declares, so a
// second definition for a kind fails loudly when it is applied instead of
// sitting next to the first one. It is the feedback layer, not the guarantee:
// the webhook is registered with failurePolicy Ignore, so an outage of the
// webhook never blocks a platform install or upgrade, and appdefowner decides
// which definition manages a kind whatever reaches the cluster.
type ApplicationDefinitionKindValidator struct {
reader client.Reader
decoder admission.Decoder
}
// NewApplicationDefinitionKindValidator builds the validator over reader,
// normally the manager's cached client.
func NewApplicationDefinitionKindValidator(reader client.Reader, scheme *runtime.Scheme) *ApplicationDefinitionKindValidator {
return &ApplicationDefinitionKindValidator{reader: reader, decoder: admission.NewDecoder(scheme)}
}
// Handle implements admission.Handler.
func (v *ApplicationDefinitionKindValidator) Handle(ctx context.Context, req admission.Request) admission.Response {
def := &cozyv1alpha1.ApplicationDefinition{}
if err := v.decoder.DecodeRaw(req.Object, def); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
kind := def.Spec.Application.Kind
if req.Operation == admissionv1.Update {
old := &cozyv1alpha1.ApplicationDefinition{}
if err := v.decoder.DecodeRaw(req.OldObject, old); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
// A definition that keeps its kind is not claiming anything new. This
// also lets a duplicate that predates the check keep upgrading.
if old.Spec.Application.Kind == kind {
return admission.Allowed("")
}
}
defs := &cozyv1alpha1.ApplicationDefinitionList{}
if err := v.reader.List(ctx, defs); err != nil {
// An error answer is a verdict that refuses the request, and
// failurePolicy Ignore only covers a webhook that does not answer. The
// check is feedback, not the guarantee, so let the request through and
// say so.
return admission.Allowed("").WithWarnings(fmt.Sprintf("application kind uniqueness was not checked: list ApplicationDefinitions: %v", err))
}
if kind == "" || !claimedByOther(defs.Items, def) {
return admission.Allowed("")
}
return admission.Denied(fmt.Sprintf("application kind %s is already owned by ApplicationDefinition %s; delete that definition first to hand the kind over, or declare a different kind", kind, appdefowner.Owners(defs.Items)[kind]))
}
// claimedByOther reports whether a definition other than def declares def's
// kind. A definition from the same Helm release does not count: a release that
// renames its definition object creates the new one before it deletes the old
// one, and refusing that would fail the upgrade carrying the rename.
func claimedByOther(defs []cozyv1alpha1.ApplicationDefinition, def *cozyv1alpha1.ApplicationDefinition) bool {
for i := range defs {
other := &defs[i]
if other.Name == def.Name || other.Spec.Application.Kind != def.Spec.Application.Kind {
continue
}
if sameHelmRelease(other, def) {
continue
}
return true
}
return false
}
func sameHelmRelease(a, b *cozyv1alpha1.ApplicationDefinition) bool {
const name, namespace = "meta.helm.sh/release-name", "meta.helm.sh/release-namespace"
an := a.GetAnnotations()[name]
return an != "" && an == b.GetAnnotations()[name] &&
a.GetAnnotations()[namespace] == b.GetAnnotations()[namespace]
}