Files
cozystack/internal/controller/workload_controller_test.go
Aleksei Sviridkin 2a7f6cd7bc feat(cozystack-controller): stop counting device GPUs on virt-launcher pods
Each GPUDevice now has a usage record of its own, so the virt-launcher
pod of a VMI with device references would report the same GPUs a
second time. The units the VMI's attached devices hold are subtracted
after the resources annotation is merged, since that annotation may
name the GPU too; names the devices do not cover, such as a NIC next to
the GPU, stay reported.

The Workload controller already deletes a record only when its monitor
is gone, which is what lets GPU records live under a monitor that
selects nothing; a test now pins that.

Assisted-by: LLM
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-10-21 05:24:04 +03:00

152 lines
4.9 KiB
Go

package controller
import (
"context"
"testing"
cozyv1alpha1 "github.com/cozystack/cozystack/api/v1alpha1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
func TestUnprefixedMonitoredObjectReturnsNil(t *testing.T) {
w := &cozyv1alpha1.Workload{}
w.Name = "unprefixed-name"
obj := getMonitoredObject(w)
if obj != nil {
t.Errorf(`getMonitoredObject(&Workload{Name: "%s"}) == %v, want nil`, w.Name, obj)
}
}
func TestPodMonitoredObject(t *testing.T) {
w := &cozyv1alpha1.Workload{}
w.Name = "pod-mypod"
obj := getMonitoredObject(w)
if pod, ok := obj.(*corev1.Pod); !ok || pod.Name != "mypod" {
t.Errorf(`getMonitoredObject(&Workload{Name: "%s"}) == %v, want &Pod{Name: "mypod"}`, w.Name, obj)
}
}
func TestWorkloadReconciler_DeletesOnMissingMonitor(t *testing.T) {
scheme := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Workload with a non-existent monitor
w := &cozyv1alpha1.Workload{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-foo",
Namespace: "default",
Labels: map[string]string{
"workloads.cozystack.io/monitor": "missing-monitor",
},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(w).
Build()
reconciler := &WorkloadReconciler{Client: fakeClient}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "pod-foo", Namespace: "default"}}
if _, err := reconciler.Reconcile(context.TODO(), req); err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
// Expect workload to be deleted
err := fakeClient.Get(context.TODO(), req.NamespacedName, &cozyv1alpha1.Workload{})
if !apierrors.IsNotFound(err) {
t.Errorf("expected workload to be deleted, got: %v", err)
}
}
func TestWorkloadReconciler_KeepsWhenAllExist(t *testing.T) {
scheme := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
// Create a monitor and its backing Pod
monitor := &cozyv1alpha1.WorkloadMonitor{
ObjectMeta: metav1.ObjectMeta{
Name: "mon",
Namespace: "default",
},
}
pod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
Namespace: "default",
},
}
w := &cozyv1alpha1.Workload{
ObjectMeta: metav1.ObjectMeta{
Name: "pod-foo",
Namespace: "default",
Labels: map[string]string{
"workloads.cozystack.io/monitor": "mon",
},
},
}
fakeClient := fake.NewClientBuilder().
WithScheme(scheme).
WithObjects(monitor, pod, w).
Build()
reconciler := &WorkloadReconciler{Client: fakeClient}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "pod-foo", Namespace: "default"}}
if _, err := reconciler.Reconcile(context.TODO(), req); err != nil {
t.Fatalf("Reconcile returned error: %v", err)
}
// Expect workload to remain
err := fakeClient.Get(context.TODO(), req.NamespacedName, &cozyv1alpha1.Workload{})
if err != nil {
t.Errorf("expected workload to persist, got error: %v", err)
}
}
// A GPU usage record has no monitored object and sits under a monitor whose
// selector matches nothing: it stays exactly while its monitor exists,
// whatever the selector does or does not match.
func TestWorkloadReconciler_KeepsARecordWhoseMonitorSelectsNothing(t *testing.T) {
scheme := runtime.NewScheme()
_ = cozyv1alpha1.AddToScheme(scheme)
_ = corev1.AddToScheme(scheme)
monitor := &cozyv1alpha1.WorkloadMonitor{
ObjectMeta: metav1.ObjectMeta{Name: "gpu-devices", Namespace: "default"},
Spec: cozyv1alpha1.WorkloadMonitorSpec{Selector: map[string]string{"gpu.cozystack.io/usage-monitor": "true"}},
}
record := func() *cozyv1alpha1.Workload {
return &cozyv1alpha1.Workload{
ObjectMeta: metav1.ObjectMeta{Name: "gpu-train-0", Namespace: "default",
Labels: map[string]string{"workloads.cozystack.io/monitor": "gpu-devices"}},
Status: cozyv1alpha1.WorkloadStatus{Kind: "gpu-device"},
}
}
req := reconcile.Request{NamespacedName: types.NamespacedName{Name: "gpu-train-0", Namespace: "default"}}
kept := fake.NewClientBuilder().WithScheme(scheme).WithObjects(monitor, record()).Build()
if _, err := (&WorkloadReconciler{Client: kept}).Reconcile(context.TODO(), req); err != nil {
t.Fatalf("Reconcile: %v", err)
}
if err := kept.Get(context.TODO(), req.NamespacedName, &cozyv1alpha1.Workload{}); err != nil {
t.Errorf("record under an existing monitor was deleted: %v", err)
}
orphaned := fake.NewClientBuilder().WithScheme(scheme).WithObjects(record()).Build()
if _, err := (&WorkloadReconciler{Client: orphaned}).Reconcile(context.TODO(), req); err != nil {
t.Fatalf("Reconcile: %v", err)
}
if err := orphaned.Get(context.TODO(), req.NamespacedName, &cozyv1alpha1.Workload{}); !apierrors.IsNotFound(err) {
t.Errorf("record whose monitor is gone was kept: %v", err)
}
}