Compare commits

...

6 Commits

Author SHA1 Message Date
Kirill Ilin
7c59b6dc51 chore(tenant): generate readme and app definition for schedulingClass support
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-15 22:39:49 +05:00
Kirill Ilin
43d49b6e46 feat(lineage-webhook): verify SchedulingClass CR before injection
Before injecting schedulerName and annotation, the webhook now
checks that the referenced SchedulingClass CR actually exists.
If the CRD is not installed or the CR is missing, injection is
skipped so that pods are not stuck Pending on a non-existent
scheduler.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-15 22:33:43 +05:00
Kirill Ilin
1024ee7607 feat(lineage-webhook): inject schedulerName for tenant scheduling
When a namespace carries the scheduling.cozystack.io/class label,
the webhook injects schedulerName=cozystack-scheduler and the
scheduler.cozystack.io/scheduling-class annotation into every Pod.
This covers all workloads in the tenant namespace regardless of
whether the operator CRD supports schedulerName natively.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-15 22:27:35 +05:00
Kirill Ilin
6046a31e8c feat(tenant): add scheduling.cozystack.io/class label to namespace
The label is read by the lineage-controller-webhook to inject
schedulerName and scheduling-class annotation into all pods
in the tenant namespace.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-15 22:26:02 +05:00
Kirill Ilin
0387fae62c feat(dashboard): add SchedulingClass dropdown for Tenant form
Add an API-backed listInput dropdown for the schedulingClass field
in the Tenant creation form. The dropdown lists available
SchedulingClass CRs from cozystack.io/v1alpha1.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-15 22:08:01 +05:00
Kirill Ilin
b821c0748e feat(tenant): add schedulingClass parameter for tenant workloads
Allow administrators to assign a SchedulingClass CR to a tenant.
The schedulingClass is inherited by child tenants and cannot be
overridden once set by a parent.

Assisted-By: Claude AI
Signed-off-by: Kirill Ilin <stitch14@yandex.ru>
2026-03-15 22:00:25 +05:00
7 changed files with 113 additions and 10 deletions

View File

@@ -230,6 +230,10 @@ func applyListInputOverrides(schema map[string]any, kind string, openAPIProps ma
kafkaProps["storageClass"] = storageClassListInput()
zkProps := ensureSchemaPath(schema, "spec", "zookeeper")
zkProps["storageClass"] = storageClassListInput()
case "Tenant":
specProps := ensureSchemaPath(schema, "spec")
specProps["schedulingClass"] = schedulingClassListInput()
}
}
@@ -246,6 +250,20 @@ func storageClassListInput() map[string]any {
}
}
// schedulingClassListInput returns a listInput field config for a schedulingClass dropdown
// backed by the cluster's available SchedulingClass CRs.
func schedulingClassListInput() map[string]any {
return map[string]any{
"type": "listInput",
"customProps": map[string]any{
"valueUri": "/api/clusters/{cluster}/k8s/apis/cozystack.io/v1alpha1/schedulingclasses",
"keysToValue": []any{"metadata", "name"},
"keysToLabel": []any{"metadata", "name"},
"allowEmpty": true,
},
}
}
// ensureArrayItemProps ensures that parentProps[fieldName].items.properties exists
// and returns the items properties map. Used for overriding fields inside array items.
func ensureArrayItemProps(parentProps map[string]any, fieldName string) map[string]any {

View File

@@ -8,11 +8,14 @@ import (
"strings"
"github.com/cozystack/cozystack/pkg/lineage"
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/schema"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/rest"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
"sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -31,6 +34,11 @@ 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
@@ -115,6 +123,11 @@ func (h *LineageControllerWebhook) Handle(ctx context.Context, req admission.Req
h.applyLabels(obj, labels)
if err := h.applySchedulingClass(ctx, obj, req.Namespace); err != nil {
logger.Error(err, "error applying scheduling class")
return admission.Errored(500, fmt.Errorf("error applying scheduling class: %w", err))
}
mutated, err := json.Marshal(obj)
if err != nil {
return admission.Errored(500, fmt.Errorf("marshal mutated pod: %w", err))
@@ -185,6 +198,57 @@ func (h *LineageControllerWebhook) applyLabels(o *unstructured.Unstructured, lab
o.SetLabels(existing)
}
// applySchedulingClass injects schedulerName and scheduling-class annotation
// into Pods whose namespace carries the scheduling.cozystack.io/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 {
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)
}
schedulingClass, ok := ns.Labels[SchedulingClassLabel]
if !ok || schedulingClass == "" {
return nil
}
// Verify that the referenced SchedulingClass CR exists.
// If the CRD is not installed or the CR is missing, skip injection
// so that pods are not stuck Pending on a non-existent scheduler.
_, err := h.dynClient.Resource(schedulingClassGVR).Get(ctx, schedulingClass, metav1.GetOptions{})
if err != nil {
logger := log.FromContext(ctx)
logger.Info("SchedulingClass not found, skipping scheduler injection",
"schedulingClass", schedulingClass, "namespace", namespace)
return nil
}
if err := unstructured.SetNestedField(obj.Object, CozystackSchedulerName, "spec", "schedulerName"); err != nil {
return fmt.Errorf("setting schedulerName: %w", err)
}
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = make(map[string]string)
}
annotations[SchedulingClassAnnotation] = schedulingClass
obj.SetAnnotations(annotations)
return nil
}
var schedulingClassGVR = schema.GroupVersionResource{
Group: "cozystack.io",
Version: "v1alpha1",
Resource: "schedulingclasses",
}
func (h *LineageControllerWebhook) decodeUnstructured(req admission.Request, out *unstructured.Unstructured) error {
if h.decoder != nil {
if err := h.decoder.Decode(req, out); err == nil {

View File

@@ -74,14 +74,15 @@ tenant-u1
### Common parameters
| Name | Description | Type | Value |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------- |
| `host` | The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host). | `string` | `""` |
| `etcd` | Deploy own Etcd cluster. | `bool` | `false` |
| `monitoring` | Deploy own Monitoring Stack. | `bool` | `false` |
| `ingress` | Deploy own Ingress Controller. | `bool` | `false` |
| `seaweedfs` | Deploy own SeaweedFS. | `bool` | `false` |
| `resourceQuotas` | Define resource quotas for the tenant. | `map[string]quantity` | `{}` |
| Name | Description | Type | Value |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | ------- |
| `host` | The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host). | `string` | `""` |
| `etcd` | Deploy own Etcd cluster. | `bool` | `false` |
| `monitoring` | Deploy own Monitoring Stack. | `bool` | `false` |
| `ingress` | Deploy own Ingress Controller. | `bool` | `false` |
| `seaweedfs` | Deploy own SeaweedFS. | `bool` | `false` |
| `schedulingClass` | The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads. | `string` | `""` |
| `resourceQuotas` | Define resource quotas for the tenant. | `map[string]quantity` | `{}` |
## Configuration

View File

@@ -38,6 +38,12 @@
{{- if .Values.seaweedfs }}
{{- $seaweedfs = $tenantName }}
{{- end }}
{{/* SchedulingClass: inherited from parent, can be set if parent has none */}}
{{- $schedulingClass := $parentNamespace.schedulingClass | default "" }}
{{- if and (not $schedulingClass) .Values.schedulingClass }}
{{- $schedulingClass = .Values.schedulingClass }}
{{- end }}
---
apiVersion: v1
kind: Namespace
@@ -58,6 +64,9 @@ metadata:
namespace.cozystack.io/monitoring: {{ $monitoring | quote }}
namespace.cozystack.io/seaweedfs: {{ $seaweedfs | quote }}
namespace.cozystack.io/host: {{ $computedHost | quote }}
{{- with $schedulingClass }}
scheduling.cozystack.io/class: {{ . | quote }}
{{- end }}
alpha.kubevirt.io/auto-memory-limits-ratio: "1.0"
ownerReferences:
- apiVersion: v1
@@ -86,4 +95,7 @@ stringData:
monitoring: {{ $monitoring | quote }}
seaweedfs: {{ $seaweedfs | quote }}
host: {{ $computedHost | quote }}
{{- with $schedulingClass }}
schedulingClass: {{ . | quote }}
{{- end }}
{{- end }}

View File

@@ -39,6 +39,11 @@
"x-kubernetes-int-or-string": true
}
},
"schedulingClass": {
"description": "The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.",
"type": "string",
"default": ""
},
"seaweedfs": {
"description": "Deploy own SeaweedFS.",
"type": "boolean",

View File

@@ -17,5 +17,8 @@ ingress: false
## @param {bool} seaweedfs - Deploy own SeaweedFS.
seaweedfs: false
## @param {string} [schedulingClass] - The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.
schedulingClass: ""
## @param {map[string]quantity} resourceQuotas - Define resource quotas for the tenant.
resourceQuotas: {}

View File

@@ -8,7 +8,7 @@ spec:
singular: tenant
plural: tenants
openAPISchema: |-
{"title":"Chart Values","type":"object","properties":{"etcd":{"description":"Deploy own Etcd cluster.","type":"boolean","default":false},"host":{"description":"The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).","type":"string","default":""},"ingress":{"description":"Deploy own Ingress Controller.","type":"boolean","default":false},"monitoring":{"description":"Deploy own Monitoring Stack.","type":"boolean","default":false},"resourceQuotas":{"description":"Define resource quotas for the tenant.","type":"object","default":{},"additionalProperties":{"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}},"seaweedfs":{"description":"Deploy own SeaweedFS.","type":"boolean","default":false}}}
{"title":"Chart Values","type":"object","properties":{"etcd":{"description":"Deploy own Etcd cluster.","type":"boolean","default":false},"host":{"description":"The hostname used to access tenant services (defaults to using the tenant name as a subdomain for its parent tenant host).","type":"string","default":""},"ingress":{"description":"Deploy own Ingress Controller.","type":"boolean","default":false},"monitoring":{"description":"Deploy own Monitoring Stack.","type":"boolean","default":false},"resourceQuotas":{"description":"Define resource quotas for the tenant.","type":"object","default":{},"additionalProperties":{"pattern":"^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$","anyOf":[{"type":"integer"},{"type":"string"}],"x-kubernetes-int-or-string":true}},"schedulingClass":{"description":"The name of a SchedulingClass CR to apply scheduling constraints for this tenant's workloads.","type":"string","default":""},"seaweedfs":{"description":"Deploy own SeaweedFS.","type":"boolean","default":false}}}
release:
prefix: tenant-
labels:
@@ -23,7 +23,7 @@ spec:
plural: Tenants
description: Separated tenant namespace
icon: PHN2ZyB3aWR0aD0iMTQ0IiBoZWlnaHQ9IjE0NCIgdmlld0JveD0iMCAwIDE0NCAxNDQiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxNDQiIGhlaWdodD0iMTQ0IiByeD0iMjQiIGZpbGw9InVybCgjcGFpbnQwX2xpbmVhcl82ODdfMzQwMykiLz4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwXzY4N18zNDAzKSI+CjxwYXRoIGQ9Ik03MiAyOUM2Ni4zOTI2IDI5IDYxLjAxNDggMzEuMjM4OCA1Ny4wNDk3IDM1LjIyNEM1My4wODQ3IDM5LjIwOTEgNTAuODU3MSA0NC42MTQxIDUwLjg1NzEgNTAuMjVDNTAuODU3MSA1NS44ODU5IDUzLjA4NDcgNjEuMjkwOSA1Ny4wNDk3IDY1LjI3NkM2MS4wMTQ4IDY5LjI2MTIgNjYuMzkyNiA3MS41IDcyIDcxLjVDNzcuNjA3NCA3MS41IDgyLjk4NTIgNjkuMjYxMiA4Ni45NTAzIDY1LjI3NkM5MC45MTUzIDYxLjI5MDkgOTMuMTQyOSA1NS44ODU5IDkzLjE0MjkgNTAuMjVDOTMuMTQyOSA0NC42MTQxIDkwLjkxNTMgMzkuMjA5MSA4Ni45NTAzIDM1LjIyNEM4Mi45ODUyIDMxLjIzODggNzcuNjA3NCAyOSA3MiAyOVpNNjAuOTgyNiA4My4zMDM3QzYwLjQ1NCA4Mi41ODk4IDU5LjU5NTEgODIuMTkxNCA1OC43MTk2IDgyLjI3NDRDNDUuMzg5NyA4My43MzU0IDM1IDk1LjEwNzQgMzUgMTA4LjkwM0MzNSAxMTEuNzI2IDM3LjI3OTUgMTE0IDQwLjA3MSAxMTRIMTAzLjkyOUMxMDYuNzM3IDExNCAxMDkgMTExLjcwOSAxMDkgMTA4LjkwM0MxMDkgOTUuMTA3NCA5OC42MTAzIDgzLjc1MiA4NS4yNjM4IDgyLjI5MUM4NC4zODg0IDgyLjE5MTQgODMuNTI5NSA4Mi42MDY0IDgzLjAwMDkgODMuMzIwM0w3NC4wOTc4IDk1LjI0MDJDNzMuMDQwNiA5Ni42NTE0IDcwLjkyNjMgOTYuNjUxNCA2OS44NjkyIDk1LjI0MDJMNjAuOTY2MSA4My4zMjAzTDYwLjk4MjYgODMuMzAzN1oiIGZpbGw9ImJsYWNrIi8+CjwvZz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhcl82ODdfMzQwMyIgeDE9IjcyIiB5MT0iMTQ0IiB4Mj0iLTEuMjgxN2UtMDUiIHkyPSI0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+CjxzdG9wIHN0b3AtY29sb3I9IiNDMEQ2RkYiLz4KPHN0b3Agb2Zmc2V0PSIwLjMiIHN0b3AtY29sb3I9IiNDNERBRkYiLz4KPHN0b3Agb2Zmc2V0PSIwLjY1IiBzdG9wLWNvbG9yPSIjRDNFOUZGIi8+CjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iI0U5RkZGRiIvPgo8L2xpbmVhckdyYWRpZW50Pgo8Y2xpcFBhdGggaWQ9ImNsaXAwXzY4N18zNDAzIj4KPHJlY3Qgd2lkdGg9Ijc0IiBoZWlnaHQ9Ijg1IiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzUgMjkpIi8+CjwvY2xpcFBhdGg+CjwvZGVmcz4KPC9zdmc+Cg==
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "etcd"], ["spec", "monitoring"], ["spec", "ingress"], ["spec", "seaweedfs"], ["spec", "resourceQuotas"]]
keysOrder: [["apiVersion"], ["appVersion"], ["kind"], ["metadata"], ["metadata", "name"], ["spec", "host"], ["spec", "etcd"], ["spec", "monitoring"], ["spec", "ingress"], ["spec", "seaweedfs"], ["spec", "schedulingClass"], ["spec", "resourceQuotas"]]
secrets:
exclude: []
include: []