feat(platform): add resourcePreset labels

Signed-off-by: Andrey Kolkov <androndo@gmail.com>
This commit is contained in:
Andrey Kolkov 2026-04-17 14:16:40 +04:00
parent 4053fad9ed
commit 2c141d3f38
18 changed files with 367 additions and 4 deletions

View file

@ -35,6 +35,11 @@ const (
// namespaceMonitoringLabel is the namespace label that indicates which tenant
// namespace hosts the monitoring stack (VictoriaMetrics/Prometheus).
namespaceMonitoringLabel = "namespace.cozystack.io/monitoring"
workloadLabelPrefix = "workloads.cozystack.io/"
// workloadMonitorLabel is reserved: it names the WorkloadMonitor that owns
// the Workload and is always set by the reconciler, so it is never copied
// from monitor labels.
workloadMonitorLabel = workloadLabelPrefix + "monitor"
// vmSelectService is the well-known service name for VictoriaMetrics vmselect
// within a monitoring namespace. Port 8481, path /select/0/prometheus.
vmSelectService = "vmselect-shortterm"
@ -283,16 +288,21 @@ func (r *WorkloadMonitorReconciler) reconcileBucketClaimForMonitor(
}
}
monitorLabels := r.getMonitorLabels(monitor)
_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
updateOwnerReferences(workload.GetObjectMeta(), &bc)
if workload.Labels == nil {
workload.Labels = make(map[string]string)
}
// Apply monitor-level labels first so source-object labels can override on conflict
for k, v := range monitorLabels {
workload.Labels[k] = v
}
for k, v := range bc.Labels {
workload.Labels[k] = v
}
workload.Labels["workloads.cozystack.io/monitor"] = monitor.Name
workload.Labels[workloadMonitorLabel] = monitor.Name
workload.Status.Kind = monitor.Spec.Kind
workload.Status.Type = monitor.Spec.Type
@ -345,14 +355,22 @@ func (r *WorkloadMonitorReconciler) reconcileServiceForMonitor(
resourceLabel = fmt.Sprintf("%s.ipaddresspool.metallb.io/requests.ipaddresses", resourceLabel)
resources[resourceLabel] = quantity
monitorLabels := r.getMonitorLabels(monitor)
_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
// Update owner references with the new monitor
updateOwnerReferences(workload.GetObjectMeta(), &svc)
// Apply monitor-level labels first so source-object labels can override on conflict
if workload.Labels == nil {
workload.Labels = make(map[string]string)
}
for k, v := range monitorLabels {
workload.Labels[k] = v
}
for k, v := range svc.Labels {
workload.Labels[k] = v
}
workload.Labels["workloads.cozystack.io/monitor"] = monitor.Name
workload.Labels[workloadMonitorLabel] = monitor.Name
// Fill Workload status fields:
workload.Status.Kind = monitor.Spec.Kind
@ -396,14 +414,22 @@ func (r *WorkloadMonitorReconciler) reconcilePVCForMonitor(
resources[resourceLabel] = resourceQuantity
}
monitorLabels := r.getMonitorLabels(monitor)
_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
// Update owner references with the new monitor
updateOwnerReferences(workload.GetObjectMeta(), &pvc)
// Apply monitor-level labels first so source-object labels can override on conflict
if workload.Labels == nil {
workload.Labels = make(map[string]string)
}
for k, v := range monitorLabels {
workload.Labels[k] = v
}
for k, v := range pvc.Labels {
workload.Labels[k] = v
}
workload.Labels["workloads.cozystack.io/monitor"] = monitor.Name
workload.Labels[workloadMonitorLabel] = monitor.Name
// Fill Workload status fields:
workload.Status.Kind = monitor.Spec.Kind
@ -470,14 +496,22 @@ func (r *WorkloadMonitorReconciler) reconcilePodForMonitor(
}
metaLabels := r.getWorkloadMetadata(&pod)
monitorLabels := r.getMonitorLabels(monitor)
_, err := ctrl.CreateOrUpdate(ctx, r.Client, workload, func() error {
// Update owner references with the new monitor
updateOwnerReferences(workload.GetObjectMeta(), &pod)
// Apply monitor-level labels first so source-object labels can override on conflict
if workload.Labels == nil {
workload.Labels = make(map[string]string)
}
for k, v := range monitorLabels {
workload.Labels[k] = v
}
for k, v := range pod.Labels {
workload.Labels[k] = v
}
workload.Labels["workloads.cozystack.io/monitor"] = monitor.Name
workload.Labels[workloadMonitorLabel] = monitor.Name
// Add workload meta to labels
for k, v := range metaLabels {
@ -720,3 +754,21 @@ func (r *WorkloadMonitorReconciler) getWorkloadMetadata(obj client.Object) map[s
}
return labels
}
// getMonitorLabels extracts workloads.cozystack.io/* labels from a WorkloadMonitor
// so they can be propagated onto Workload objects created for pods, PVCs, services,
// or bucket claims. The monitor label "workloads.cozystack.io/monitor" is reserved
// and set separately per Workload, so it is excluded here.
func (r *WorkloadMonitorReconciler) getMonitorLabels(monitor *cozyv1alpha1.WorkloadMonitor) map[string]string {
labels := make(map[string]string)
for k, v := range monitor.GetLabels() {
if !strings.HasPrefix(k, workloadLabelPrefix) {
continue
}
if k == workloadMonitorLabel {
continue
}
labels[k] = v
}
return labels
}

View file

@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"testing"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
@ -141,6 +142,199 @@ func TestReconcile_OperationalTrue_WhenEnoughReplicas(t *testing.T) {
}
}
func TestGetMonitorLabels(t *testing.T) {
tests := []struct {
name string
labels map[string]string
expected map[string]string
}{
{
name: "nil labels",
labels: nil,
expected: map[string]string{},
},
{
name: "only workloads.cozystack.io/* labels are propagated",
labels: map[string]string{
"workloads.cozystack.io/resource-preset": "medium",
"app.kubernetes.io/name": "postgres",
"custom.example.com/team": "platform",
},
expected: map[string]string{
"workloads.cozystack.io/resource-preset": "medium",
},
},
{
name: "monitor label is reserved and excluded",
labels: map[string]string{
"workloads.cozystack.io/resource-preset": "small",
"workloads.cozystack.io/monitor": "should-be-dropped",
},
expected: map[string]string{
"workloads.cozystack.io/resource-preset": "small",
},
},
{
name: "multiple workloads.cozystack.io labels propagate",
labels: map[string]string{
"workloads.cozystack.io/resource-preset": "large",
"workloads.cozystack.io/tier": "db",
},
expected: map[string]string{
"workloads.cozystack.io/resource-preset": "large",
"workloads.cozystack.io/tier": "db",
},
},
{
name: "no matching labels returns empty map",
labels: map[string]string{
"app.kubernetes.io/name": "postgres",
},
expected: map[string]string{},
},
}
r := &WorkloadMonitorReconciler{}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
monitor := &cozyv1alpha1.WorkloadMonitor{
ObjectMeta: metav1.ObjectMeta{Labels: tc.labels},
}
got := r.getMonitorLabels(monitor)
if len(got) != len(tc.expected) {
t.Fatalf("expected %d labels, got %d (%v)", len(tc.expected), len(got), got)
}
for k, v := range tc.expected {
if gv, ok := got[k]; !ok || gv != v {
t.Errorf("expected label %q=%q, got %q", k, v, gv)
}
}
})
}
}
func TestReconcile_MonitorLabelsPropagatedToPodWorkload(t *testing.T) {
scheme := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
monitor := &cozyv1alpha1.WorkloadMonitor{
ObjectMeta: metav1.ObjectMeta{
Name: "test-monitor",
Namespace: "default",
Labels: map[string]string{
"workloads.cozystack.io/resource-preset": "medium",
"app.kubernetes.io/name": "ignored-not-propagated",
},
},
Spec: cozyv1alpha1.WorkloadMonitorSpec{
Selector: map[string]string{"app": "test"},
Kind: "postgres",
Type: "postgres",
},
}
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod-1",
Namespace: "default",
Labels: map[string]string{
"app": "test",
"app.kubernetes.io/name": "pod-wins-on-conflict",
},
},
Status: corev1.PodStatus{
Conditions: []corev1.PodCondition{
{Type: corev1.PodReady, Status: corev1.ConditionTrue},
},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(monitor, pod).
WithStatusSubresource(monitor).
Build()
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: scheme}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-monitor", Namespace: "default"}}
if _, err := reconciler.Reconcile(context.TODO(), req); err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
workload := &cozyv1alpha1.Workload{}
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: "pod-test-pod-1", Namespace: "default"}, workload); err != nil {
t.Fatalf("Failed to get Workload: %v", err)
}
if got := workload.Labels["workloads.cozystack.io/resource-preset"]; got != "medium" {
t.Errorf("expected monitor label propagated, got %q", got)
}
// Non-workloads.cozystack.io monitor labels must not be copied
if _, ok := workload.Labels["app.kubernetes.io/name"]; !ok {
t.Error("expected pod label to be present on Workload")
}
// Source-object label takes precedence on conflict
if got := workload.Labels["app.kubernetes.io/name"]; got != "pod-wins-on-conflict" {
t.Errorf("expected pod label to win on conflict, got %q", got)
}
// Reserved monitor label is always set from the monitor name
if got := workload.Labels["workloads.cozystack.io/monitor"]; got != "test-monitor" {
t.Errorf("expected monitor-name label, got %q", got)
}
}
func TestReconcile_BackwardCompat_NoMonitorLabels(t *testing.T) {
scheme := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
monitor := &cozyv1alpha1.WorkloadMonitor{
ObjectMeta: metav1.ObjectMeta{
Name: "test-monitor",
Namespace: "default",
},
Spec: cozyv1alpha1.WorkloadMonitorSpec{
Selector: map[string]string{"app": "test"},
},
}
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test-pod-1",
Namespace: "default",
Labels: map[string]string{"app": "test"},
},
Status: corev1.PodStatus{
Conditions: []corev1.PodCondition{
{Type: corev1.PodReady, Status: corev1.ConditionTrue},
},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(monitor, pod).
WithStatusSubresource(monitor).
Build()
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: scheme}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "test-monitor", Namespace: "default"}}
if _, err := reconciler.Reconcile(context.TODO(), req); err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
workload := &cozyv1alpha1.Workload{}
if err := fakeClient.Get(context.TODO(), types.NamespacedName{Name: "pod-test-pod-1", Namespace: "default"}, workload); err != nil {
t.Fatalf("Failed to get Workload: %v", err)
}
for k := range workload.Labels {
if strings.HasPrefix(k, "workloads.cozystack.io/") && k != "workloads.cozystack.io/monitor" {
t.Errorf("unexpected workload label present: %q", k)
}
}
}
func TestReconcile_OperationalTrue_WhenNoMinReplicas(t *testing.T) {
scheme := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(scheme)
@ -330,6 +524,82 @@ func TestReconcileBucketClaimNotReady(t *testing.T) {
}
}
func TestReconcile_MonitorLabelsPropagatedToBucketClaimWorkload(t *testing.T) {
s := newTestScheme()
monitor := &cozyv1alpha1.WorkloadMonitor{
ObjectMeta: metav1.ObjectMeta{
Name: "my-bucket",
Namespace: "tenant-demo",
Labels: map[string]string{
"workloads.cozystack.io/resource-preset": "medium",
"app.kubernetes.io/name": "ignored-not-propagated",
},
},
Spec: cozyv1alpha1.WorkloadMonitorSpec{
Kind: "bucket",
Type: "s3",
Selector: map[string]string{
"app.kubernetes.io/instance": "my-bucket",
},
},
}
bc := &cosiv1alpha1.BucketClaim{
ObjectMeta: metav1.ObjectMeta{
Name: "my-bucket",
Namespace: "tenant-demo",
Labels: map[string]string{
"app.kubernetes.io/instance": "my-bucket",
"app.kubernetes.io/name": "bucket-wins-on-conflict",
},
},
Spec: cosiv1alpha1.BucketClaimSpec{
BucketClassName: "seaweedfs",
Protocols: []cosiv1alpha1.Protocol{cosiv1alpha1.ProtocolS3},
},
Status: cosiv1alpha1.BucketClaimStatus{
BucketReady: true,
BucketName: "cosi-abc123",
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(s).
WithObjects(monitor, bc).
WithStatusSubresource(monitor).
Build()
reconciler := &WorkloadMonitorReconciler{Client: fakeClient, Scheme: s}
req := reconcile.Request{NamespacedName: types.NamespacedName{
Name: "my-bucket",
Namespace: "tenant-demo",
}}
if _, err := reconciler.Reconcile(context.TODO(), req); err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
workload := &cozyv1alpha1.Workload{}
if err := fakeClient.Get(context.TODO(), types.NamespacedName{
Name: "bucket-my-bucket",
Namespace: "tenant-demo",
}, workload); err != nil {
t.Fatalf("Failed to get Workload: %v", err)
}
if got := workload.Labels["workloads.cozystack.io/resource-preset"]; got != "medium" {
t.Errorf("expected monitor label propagated, got %q", got)
}
// Source-object label takes precedence on conflict
if got := workload.Labels["app.kubernetes.io/name"]; got != "bucket-wins-on-conflict" {
t.Errorf("expected bucket claim label to win on conflict, got %q", got)
}
// Reserved monitor label is always set from the monitor name
if got := workload.Labels["workloads.cozystack.io/monitor"]; got != "my-bucket" {
t.Errorf("expected monitor-name label, got %q", got)
}
}
func TestReconcileNoBucketClaimSkips(t *testing.T) {
s := newTestScheme()

View file

@ -3,6 +3,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1
@ -17,6 +19,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-keeper
labels:
workloads.cozystack.io/resource-preset: {{ .Values.clickhouseKeeper.resourcesPreset | quote }}
spec:
replicas: {{ .Values.clickhouseKeeper.replicas }}
minReplicas: 1

View file

@ -8,6 +8,7 @@ metadata:
app.kubernetes.io/name: foundationdb
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.cluster.processCounts.storage }}
minReplicas: {{ include "foundationdb.minReplicas" . }}

View file

@ -158,6 +158,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-core
labels:
workloads.cozystack.io/resource-preset: {{ .Values.core.resourcesPreset | quote }}
spec:
replicas: 1
minReplicas: 1
@ -174,6 +176,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-registry
labels:
workloads.cozystack.io/resource-preset: {{ .Values.registry.resourcesPreset | quote }}
spec:
replicas: 1
minReplicas: 1

View file

@ -3,6 +3,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-haproxy
labels:
workloads.cozystack.io/resource-preset: {{ .Values.haproxy.resourcesPreset | quote }}
spec:
replicas: {{ .Values.haproxy.replicas }}
minReplicas: 1
@ -16,6 +18,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-nginx
labels:
workloads.cozystack.io/resource-preset: {{ .Values.nginx.resourcesPreset | quote }}
spec:
replicas: {{ .Values.nginx.replicas }}
minReplicas: 1

View file

@ -3,6 +3,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.kafka.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1
@ -19,6 +21,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-zookeeper
labels:
workloads.cozystack.io/resource-preset: {{ .Values.zookeeper.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1

View file

@ -3,6 +3,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1

View file

@ -169,6 +169,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ .Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
{{- if .Values.sharding }}
{{- $totalReplicas := 0 }}

View file

@ -3,6 +3,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1

View file

@ -3,6 +3,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1

View file

@ -93,6 +93,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ .Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1

View file

@ -84,6 +84,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1

View file

@ -41,6 +41,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ .Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
minReplicas: 1
replicas: {{ .Values.replicas }}

View file

@ -3,6 +3,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1

View file

@ -75,6 +75,8 @@ kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-redis
namespace: {{ $.Release.Namespace }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
minReplicas: 1
replicas: {{ .Values.replicas }}
@ -90,6 +92,8 @@ kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}-sentinel
namespace: {{ $.Release.Namespace }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
minReplicas: 2
replicas: 3

View file

@ -3,6 +3,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1

View file

@ -2,6 +2,8 @@ apiVersion: cozystack.io/v1alpha1
kind: WorkloadMonitor
metadata:
name: {{ $.Release.Name }}
labels:
workloads.cozystack.io/resource-preset: {{ .Values.resourcesPreset | quote }}
spec:
replicas: {{ .Values.replicas }}
minReplicas: 1