mirror of
https://github.com/cozystack/cozystack.git
synced 2026-03-15 19:38:55 +00:00
Compare commits
6 Commits
main
...
feat/sched
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c59b6dc51 | ||
|
|
43d49b6e46 | ||
|
|
1024ee7607 | ||
|
|
6046a31e8c | ||
|
|
0387fae62c | ||
|
|
b821c0748e |
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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: []
|
||||
|
||||
Reference in New Issue
Block a user