Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/go-logr/logr v1.4.4
github.com/go-logr/zapr v1.3.0
github.com/google/go-cmp v0.7.0
github.com/onsi/ginkgo/v2 v2.32.0
github.com/onsi/gomega v1.42.1
github.com/openshift/api v0.0.0-20260727141720-967cc4c36c9b
Expand Down Expand Up @@ -72,7 +73,6 @@ require (
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/huandu/xstrings v1.5.0 // indirect
Expand Down
232 changes: 232 additions & 0 deletions internal/conditions/clusterpolicy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/**
# Copyright (c) NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
**/

package conditions

import (
"context"
"errors"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"

nvidiav1 "github.com/NVIDIA/gpu-operator/api/nvidia/v1"
)

func clusterPolicyScheme(t *testing.T) *runtime.Scheme {
t.Helper()
s := runtime.NewScheme()
require.NoError(t, nvidiav1.AddToScheme(s))
return s
}

func newClusterPolicyClient(t *testing.T, objs ...client.Object) client.Client {
t.Helper()
b := fake.NewClientBuilder().WithScheme(clusterPolicyScheme(t))
if len(objs) > 0 {
b = b.WithObjects(objs...).WithStatusSubresource(objs...)
}
return b.Build()
}

func newClusterPolicy(name string) *nvidiav1.ClusterPolicy {
return &nvidiav1.ClusterPolicy{ObjectMeta: metav1.ObjectMeta{Name: name}}
}

func TestNewClusterPolicyUpdater(t *testing.T) {
u := NewClusterPolicyUpdater(newClusterPolicyClient(t))
assert.NotNil(t, u)
assert.IsType(t, &clusterPolicyUpdater{}, u)
}

func TestClusterPolicyUpdater_SetConditionsReady(t *testing.T) {
clusterPolicy := newClusterPolicy("cluster-policy")
c := newClusterPolicyClient(t, clusterPolicy)
u := NewClusterPolicyUpdater(c)

err := u.SetConditionsReady(context.Background(), clusterPolicy, Reconciled, "all resources reconciled")
require.NoError(t, err)

got := &nvidiav1.ClusterPolicy{}
require.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: clusterPolicy.Name}, got))

want := []metav1.Condition{
{Type: Ready, Status: metav1.ConditionTrue, Reason: Reconciled, Message: "all resources reconciled"},
{Type: Error, Status: metav1.ConditionFalse, Reason: Ready},
}
diff := cmp.Diff(want, got.Status.Conditions,
cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime", "ObservedGeneration"))
assert.Empty(t, diff, "unexpected conditions (-want +got):\n%s", diff)
}

func TestClusterPolicyUpdater_SetConditionsError(t *testing.T) {
clusterPolicy := newClusterPolicy("cluster-policy")
c := newClusterPolicyClient(t, clusterPolicy)
u := NewClusterPolicyUpdater(c)

err := u.SetConditionsError(context.Background(), clusterPolicy, ReconcileFailed, "reconciliation failed")
require.NoError(t, err)

got := &nvidiav1.ClusterPolicy{}
require.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: clusterPolicy.Name}, got))

want := []metav1.Condition{
{Type: Ready, Status: metav1.ConditionFalse, Reason: Error},
{Type: Error, Status: metav1.ConditionTrue, Reason: ReconcileFailed, Message: "reconciliation failed"},
}
diff := cmp.Diff(want, got.Status.Conditions,
cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime", "ObservedGeneration"))
assert.Empty(t, diff, "unexpected conditions (-want +got):\n%s", diff)
}

func TestClusterPolicyUpdater_ReadyThenError(t *testing.T) {
clusterPolicy := newClusterPolicy("cluster-policy")
c := newClusterPolicyClient(t, clusterPolicy)
u := NewClusterPolicyUpdater(c)
ctx := context.Background()

require.NoError(t, u.SetConditionsReady(ctx, clusterPolicy, Reconciled, "ok"))
require.NoError(t, u.SetConditionsError(ctx, clusterPolicy, DriverNotReady, "driver down"))

got := &nvidiav1.ClusterPolicy{}
require.NoError(t, c.Get(ctx, types.NamespacedName{Name: clusterPolicy.Name}, got))

want := []metav1.Condition{
{Type: Ready, Status: metav1.ConditionFalse, Reason: Error},
{Type: Error, Status: metav1.ConditionTrue, Reason: DriverNotReady, Message: "driver down"},
}
diff := cmp.Diff(want, got.Status.Conditions,
cmpopts.IgnoreFields(metav1.Condition{}, "LastTransitionTime", "ObservedGeneration"))
assert.Empty(t, diff, "unexpected conditions (-want +got):\n%s", diff)
}

func TestClusterPolicyUpdater_WrongObjectType(t *testing.T) {
c := newClusterPolicyClient(t)
u := NewClusterPolicyUpdater(c)
ctx := context.Background()

methods := []struct {
name string
call func(any) error
}{
{"SetConditionsReady", func(o any) error { return u.SetConditionsReady(ctx, o, Reconciled, "m") }},
{"SetConditionsError", func(o any) error { return u.SetConditionsError(ctx, o, ReconcileFailed, "m") }},
}
wrongObjects := []struct {
name string
obj any
}{
{"string", "not-a-cluster-policy"},
{"untyped nil", nil},
{"unrelated pointer", &metav1.ObjectMeta{}},
{"clusterpolicy list", &nvidiav1.ClusterPolicyList{}},
}

for _, m := range methods {
for _, w := range wrongObjects {
t.Run(m.name+"/"+w.name, func(t *testing.T) {
err := m.call(w.obj)
require.Error(t, err)
assert.ErrorContains(t, err, "provided object is not a *nvidiav1.ClusterPolicy")
})
}
}
}

func TestClusterPolicyUpdater_GetError(t *testing.T) {
c := newClusterPolicyClient(t)
u := NewClusterPolicyUpdater(c)

clusterPolicy := newClusterPolicy("missing")
err := u.SetConditionsReady(context.Background(), clusterPolicy, Reconciled, "m")
require.Error(t, err)
assert.ErrorContains(t, err, "failed to get ClusterPolicy instance for status update")
}

// The default branch is reachable only through the unexported setConditions.
func TestClusterPolicyUpdater_UnknownStatusType(t *testing.T) {
clusterPolicy := newClusterPolicy("cluster-policy")
c := newClusterPolicyClient(t, clusterPolicy)
u := &clusterPolicyUpdater{client: c}

err := u.setConditions(context.Background(), clusterPolicy, "BogusStatus", "reason", "message")
require.Error(t, err)
assert.ErrorContains(t, err, "unknown status type provided: BogusStatus")
}

func TestClusterPolicyUpdater_RetryOnConflict(t *testing.T) {
clusterPolicy := newClusterPolicy("cluster-policy")

var updateCalls int
c := fake.NewClientBuilder().
WithScheme(clusterPolicyScheme(t)).
WithObjects(clusterPolicy).
WithStatusSubresource(clusterPolicy).
WithInterceptorFuncs(interceptor.Funcs{
SubResourceUpdate: func(ctx context.Context, cl client.Client, subResourceName string, obj client.Object, opts ...client.SubResourceUpdateOption) error {
updateCalls++
if updateCalls == 1 {
return apierrors.NewConflict(
schema.GroupResource{Group: "nvidia.com", Resource: "clusterpolicies"},
obj.GetName(),
errors.New("the object has been modified"),
)
}
return cl.SubResource(subResourceName).Update(ctx, obj, opts...)
},
}).
Build()
u := NewClusterPolicyUpdater(c)

err := u.SetConditionsReady(context.Background(), clusterPolicy, Reconciled, "m")
require.NoError(t, err)
assert.Equal(t, 2, updateCalls, "expected exactly one retry after the conflict")

got := &nvidiav1.ClusterPolicy{}
require.NoError(t, c.Get(context.Background(), types.NamespacedName{Name: clusterPolicy.Name}, got))
assert.Len(t, got.Status.Conditions, 2)
}

func TestClusterPolicyUpdater_UpdateError(t *testing.T) {
clusterPolicy := newClusterPolicy("cluster-policy")

c := fake.NewClientBuilder().
WithScheme(clusterPolicyScheme(t)).
WithObjects(clusterPolicy).
WithStatusSubresource(clusterPolicy).
WithInterceptorFuncs(interceptor.Funcs{
SubResourceUpdate: func(ctx context.Context, cl client.Client, subResourceName string, obj client.Object, opts ...client.SubResourceUpdateOption) error {
return errors.New("status update boom")
},
}).
Build()
u := NewClusterPolicyUpdater(c)

err := u.SetConditionsError(context.Background(), clusterPolicy, ReconcileFailed, "m")
require.Error(t, err)
assert.ErrorContains(t, err, "status update boom")
}
Loading
Loading