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) <noreply@anthropic.com>
Signed-off-by: Timofei Larkin <lllamnyp@gmail.com>
This commit is contained in:
Timofei Larkin 2026-03-18 11:03:15 +03:00
parent 7c59b6dc51
commit bdeb3a7e8c
5 changed files with 76 additions and 53 deletions

1
go.mod
View file

@ -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

2
go.sum
View file

@ -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=

View file

@ -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 {

View file

@ -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:

View file

@ -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