From bdeb3a7e8c7240254fdb2d6dc3ef3be9c067a502 Mon Sep 17 00:00:00 2001 From: Timofei Larkin Date: Wed, 18 Mar 2026 11:03:15 +0300 Subject: [PATCH] feat(lineage-webhook): import scheduling constants from cozystack-scheduler Import SchedulingClassAnnotation, SchedulingClassLabel, SchedulerName, and GVR components from the cozystack-scheduler/pkg/apis sub-module instead of defining them locally. This ensures the webhook and scheduler stay in sync on label/annotation keys. Also standardize the namespace label from scheduling.cozystack.io/class to scheduler.cozystack.io/scheduling-class for consistency with the scheduler, and resolve scheduling class from the owner Application CR (via a new SchedulingClass() stub method) before falling back to the namespace label. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Timofei Larkin --- go.mod | 1 + go.sum | 2 + internal/lineagecontrollerwebhook/webhook.go | 118 ++++++++++-------- packages/apps/tenant/templates/namespace.yaml | 2 +- pkg/apis/apps/v1alpha1/types.go | 6 + 5 files changed, 76 insertions(+), 53 deletions(-) diff --git a/go.mod b/go.mod index 3ecda336..00e8e129 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-semver v0.3.1 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/cozystack/cozystack-scheduler/pkg/apis v0.1.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch v4.12.0+incompatible // indirect diff --git a/go.sum b/go.sum index 0a69ac87..52942562 100644 --- a/go.sum +++ b/go.sum @@ -20,6 +20,8 @@ github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cozystack/apimachinery v0.0.0-20251219010959-1f91eabae46c h1:C2wIfH/OzhU9XOK/e6Ik9cg7nZ1z6fN4lf6a3yFdik8= github.com/cozystack/apimachinery v0.0.0-20251219010959-1f91eabae46c/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +github.com/cozystack/cozystack-scheduler/pkg/apis v0.1.1 h1:9uLa/8J4lRx3sNuaVH8UZqgZvnskHATPo5GxEKh0iSM= +github.com/cozystack/cozystack-scheduler/pkg/apis v0.1.1/go.mod h1:kPeS9YPB4ENbvNINEkkp0SX8FB+gvwoOHGOHMT3Tg9Y= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/internal/lineagecontrollerwebhook/webhook.go b/internal/lineagecontrollerwebhook/webhook.go index 6955518e..cf871fb0 100644 --- a/internal/lineagecontrollerwebhook/webhook.go +++ b/internal/lineagecontrollerwebhook/webhook.go @@ -11,6 +11,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" @@ -21,12 +22,14 @@ import ( "sigs.k8s.io/controller-runtime/pkg/webhook/admission" cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1" + appsv1alpha1 "github.com/cozystack/cozystack/pkg/apis/apps/v1alpha1" corev1alpha1 "github.com/cozystack/cozystack/pkg/apis/core/v1alpha1" + schedulerapi "github.com/cozystack/cozystack-scheduler/pkg/apis/v1alpha1" ) var ( - NoAncestors = fmt.Errorf("no managed apps found in lineage") - AncestryAmbiguous = fmt.Errorf("object ancestry is ambiguous") + NoAncestors = errors.New("no managed apps found in lineage") + AncestryAmbiguous = errors.New("object ancestry is ambiguous") ) const ( @@ -34,11 +37,6 @@ const ( ManagerGroupKey = "apps.cozystack.io/application.group" ManagerKindKey = "apps.cozystack.io/application.kind" ManagerNameKey = "apps.cozystack.io/application.name" - - // Scheduling constants - SchedulingClassLabel = "scheduling.cozystack.io/class" - SchedulingClassAnnotation = "scheduler.cozystack.io/scheduling-class" - CozystackSchedulerName = "cozystack-scheduler" ) // getResourceSelectors returns the appropriate ApplicationDefinitionResources for a given GroupKind @@ -103,27 +101,25 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req return admission.Errored(400, fmt.Errorf("decode object: %w", err)) } - labels, err := h.computeLabels(ctx, obj) - for { - if err != nil && errors.Is(err, NoAncestors) { - break // not a problem, mark object as unmanaged - } - if err != nil && errors.Is(err, AncestryAmbiguous) { - warn = append(warn, "object ancestry ambiguous, using first ancestor found") - break - } - if err != nil { - logger.Error(err, "error computing lineage labels") - return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err)) - } - if err == nil { - break - } + owner, err := h.getOwner(ctx, obj) + switch { + case err != nil && errors.Is(err, AncestryAmbiguous): + warn = append(warn, "object ancestry ambiguous, using first ancestor found") + case err != nil && errors.Is(err, NoAncestors): + // not a problem, mark object as unmanaged + case err != nil: + logger.Error(err, "error computing lineage labels") + return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err)) + } + labels, err := h.computeLabels(ctx, obj, owner) + if err != nil { + logger.Error(err, "error computing lineage labels") + return admission.Errored(500, fmt.Errorf("error computing lineage labels: %w", err)) } h.applyLabels(obj, labels) - if err := h.applySchedulingClass(ctx, obj, req.Namespace); err != nil { + if err := h.applySchedulingClass(ctx, obj, owner, req.Namespace); err != nil { logger.Error(err, "error applying scheduling class") return admission.Errored(500, fmt.Errorf("error applying scheduling class: %w", err)) } @@ -136,23 +132,30 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req return admission.PatchResponseFromRaw(req.Object.Raw, mutated).WithWarnings(warn...) } -func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstructured.Unstructured) (map[string]string, error) { +func (h *LineageControllerWebhook) getOwner(ctx context.Context, o *unstructured.Unstructured) (*unstructured.Unstructured, error) { owners := lineage.WalkOwnershipGraph(ctx, h.dynClient, h.mapper, h, o) if len(owners) == 0 { - return map[string]string{ManagedObjectKey: "false"}, NoAncestors + return nil, NoAncestors } obj, err := owners[0].GetUnstructured(ctx, h.dynClient, h.mapper) if err != nil { return nil, err } - gv, err := schema.ParseGroupVersion(obj.GetAPIVersion()) - if err != nil { - // should never happen, we got an APIVersion right from the API - return nil, fmt.Errorf("could not parse APIVersion %s to a group and version: %w", obj.GetAPIVersion(), err) - } if len(owners) > 1 { err = AncestryAmbiguous } + return obj, err +} + +func (h *LineageControllerWebhook) computeLabels(ctx context.Context, obj *unstructured.Unstructured, owner *unstructured.Unstructured) (map[string]string, error) { + if owner == nil { + return nil, nil + } + gv, err := schema.ParseGroupVersion(owner.GetAPIVersion()) + if err != nil { + // should never happen, we got an APIVersion right from the API + return nil, fmt.Errorf("could not parse APIVersion %s to a group and version: %w", owner.GetAPIVersion(), err) + } labels := map[string]string{ // truncate apigroup to first 63 chars ManagedObjectKey: "true", @@ -166,25 +169,25 @@ func (h *LineageControllerWebhook) computeLabels(ctx context.Context, o *unstruc } return s }(gv.Group), - ManagerKindKey: obj.GetKind(), - ManagerNameKey: obj.GetName(), + ManagerKindKey: owner.GetKind(), + ManagerNameKey: owner.GetName(), } templateLabels := map[string]string{ - "kind": strings.ToLower(obj.GetKind()), - "name": obj.GetName(), - "namespace": o.GetNamespace(), + "kind": strings.ToLower(owner.GetKind()), + "name": owner.GetName(), + "namespace": obj.GetNamespace(), } cfg := h.config.Load().(*runtimeConfig) - crd := cfg.appCRDMap[appRef{gv.Group, obj.GetKind()}] - resourceSelectors := h.getResourceSelectors(o.GroupVersionKind().GroupKind(), crd) + crd := cfg.appCRDMap[appRef{gv.Group, owner.GetKind()}] + resourceSelectors := h.getResourceSelectors(obj.GroupVersionKind().GroupKind(), crd) labels[corev1alpha1.TenantResourceLabelKey] = func(b bool) string { if b { return corev1alpha1.TenantResourceLabelValue } return "false" - }(matchResourceToExcludeInclude(ctx, o.GetName(), templateLabels, o.GetLabels(), resourceSelectors)) - return labels, err + }(matchResourceToExcludeInclude(ctx, obj.GetName(), templateLabels, obj.GetLabels(), resourceSelectors)) + return labels, nil } func (h *LineageControllerWebhook) applyLabels(o *unstructured.Unstructured, labels map[string]string) { @@ -199,22 +202,33 @@ func (h *LineageControllerWebhook) applyLabels(o *unstructured.Unstructured, lab } // applySchedulingClass injects schedulerName and scheduling-class annotation -// into Pods whose namespace carries the scheduling.cozystack.io/class label. +// into Pods whose namespace carries the scheduler.cozystack.io/scheduling-class label. // If the referenced SchedulingClass CR does not exist (e.g. the scheduler // package is not installed), the injection is silently skipped so that pods // are not left Pending. -func (h *LineageControllerWebhook) applySchedulingClass(ctx context.Context, obj *unstructured.Unstructured, namespace string) error { +func (h *LineageControllerWebhook) applySchedulingClass(ctx context.Context, obj, owner *unstructured.Unstructured, namespace string) error { if obj.GetKind() != "Pod" { return nil } - ns := &corev1.Namespace{} - if err := h.Get(ctx, client.ObjectKey{Name: namespace}, ns); err != nil { - return fmt.Errorf("getting namespace %s: %w", namespace, err) + // Determine scheduling class: owner Application field takes priority, + // then fall back to namespace label. + var schedulingClass string + if owner != nil { + var app appsv1alpha1.Application + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(owner.Object, &app); err == nil { + schedulingClass = app.SchedulingClass() + } + } + if schedulingClass == "" { + ns := &corev1.Namespace{} + if err := h.Get(ctx, client.ObjectKey{Name: namespace}, ns); err != nil { + return fmt.Errorf("getting namespace %s: %w", namespace, err) + } + schedulingClass = ns.Labels[schedulerapi.SchedulingClassLabel] } - schedulingClass, ok := ns.Labels[SchedulingClassLabel] - if !ok || schedulingClass == "" { + if schedulingClass == "" { return nil } @@ -229,7 +243,7 @@ func (h *LineageControllerWebhook) applySchedulingClass(ctx context.Context, obj return nil } - if err := unstructured.SetNestedField(obj.Object, CozystackSchedulerName, "spec", "schedulerName"); err != nil { + if err := unstructured.SetNestedField(obj.Object, schedulerapi.SchedulerName, "spec", "schedulerName"); err != nil { return fmt.Errorf("setting schedulerName: %w", err) } @@ -237,16 +251,16 @@ func (h *LineageControllerWebhook) applySchedulingClass(ctx context.Context, obj if annotations == nil { annotations = make(map[string]string) } - annotations[SchedulingClassAnnotation] = schedulingClass + annotations[schedulerapi.SchedulingClassAnnotation] = schedulingClass obj.SetAnnotations(annotations) return nil } var schedulingClassGVR = schema.GroupVersionResource{ - Group: "cozystack.io", - Version: "v1alpha1", - Resource: "schedulingclasses", + Group: schedulerapi.Group, + Version: schedulerapi.Version, + Resource: schedulerapi.Resource, } func (h *LineageControllerWebhook) decodeUnstructured(req admission.Request, out *unstructured.Unstructured) error { diff --git a/packages/apps/tenant/templates/namespace.yaml b/packages/apps/tenant/templates/namespace.yaml index 0a364e0a..dfb83730 100644 --- a/packages/apps/tenant/templates/namespace.yaml +++ b/packages/apps/tenant/templates/namespace.yaml @@ -65,7 +65,7 @@ metadata: namespace.cozystack.io/seaweedfs: {{ $seaweedfs | quote }} namespace.cozystack.io/host: {{ $computedHost | quote }} {{- with $schedulingClass }} - scheduling.cozystack.io/class: {{ . | quote }} + scheduler.cozystack.io/scheduling-class: {{ . | quote }} {{- end }} alpha.kubevirt.io/auto-memory-limits-ratio: "1.0" ownerReferences: diff --git a/pkg/apis/apps/v1alpha1/types.go b/pkg/apis/apps/v1alpha1/types.go index 51247efb..3332ce55 100644 --- a/pkg/apis/apps/v1alpha1/types.go +++ b/pkg/apis/apps/v1alpha1/types.go @@ -52,6 +52,12 @@ type ApplicationStatus struct { ExternalIPsCount int32 `json:"externalIPsCount,omitempty"` } +// SchedulingClass returns the scheduling class requested by this Application. +// TODO: read from a dedicated Application field once the struct is extended. +func (in Application) SchedulingClass() string { + return "" +} + // GetConditions returns the status conditions of the object. func (in Application) GetConditions() []metav1.Condition { return in.Status.Conditions