Skip to content

Commit ba122d5

Browse files
committed
fix: tighten stored gvk review feedback
Signed-off-by: Alex Jun <aljun@nvidia.com>
1 parent 50e7673 commit ba122d5

10 files changed

Lines changed: 179 additions & 45 deletions

File tree

fault-remediation/pkg/annotation/annotation.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ func (m *NodeAnnotationManager) GetRemediationState(
9696
// UpdateRemediationState updates the node annotation with new remediation state
9797
func (m *NodeAnnotationManager) UpdateRemediationState(ctx context.Context, nodeName string,
9898
group string, crName string, actionName string, resourceRef MaintenanceResourceReference) error {
99+
if err := validateMaintenanceResourceReference(resourceRef); err != nil {
100+
return fmt.Errorf("invalid maintenance resource reference for node %s group %s: %w", nodeName, group, err)
101+
}
102+
99103
err := retry.RetryOnConflict(conflictBackoff, func() error {
100104
// Get current state
101105
state, node, err := m.GetRemediationState(ctx, nodeName)
@@ -111,7 +115,7 @@ func (m *NodeAnnotationManager) UpdateRemediationState(ctx context.Context, node
111115
ActionName: actionName,
112116
Namespace: resourceRef.Namespace,
113117
Version: resourceRef.Version,
114-
ApiGroup: resourceRef.ApiGroup,
118+
APIGroup: resourceRef.APIGroup,
115119
Kind: resourceRef.Kind,
116120
}
117121

@@ -147,11 +151,12 @@ func (m *NodeAnnotationManager) UpdateRemediationState(ctx context.Context, node
147151
}
148152

149153
// EnsureRemediationStateGVK backfills concrete resource identity for legacy
150-
// annotation entries using the resourceRefs map, if and only if the GVK is not already set.
154+
// annotation entries using resourceRefsByAction, if and only if the GVK is not already set.
155+
// The map must be keyed by ActionName.
151156
func (m *NodeAnnotationManager) EnsureRemediationStateGVK(
152157
ctx context.Context,
153158
nodeName string,
154-
resourceRefs map[string]MaintenanceResourceReference,
159+
resourceRefsByAction map[string]MaintenanceResourceReference,
155160
) error {
156161
err := retry.RetryOnConflict(conflictBackoff, func() error {
157162
state, node, err := m.GetRemediationState(ctx, nodeName)
@@ -163,7 +168,7 @@ func (m *NodeAnnotationManager) EnsureRemediationStateGVK(
163168
changed := false
164169

165170
for group, groupState := range state.EquivalenceGroups {
166-
resourceRef, exists := resourceRefs[groupState.ActionName]
171+
resourceRef, exists := resourceRefsByAction[groupState.ActionName]
167172
if !exists {
168173
continue
169174
}
@@ -207,14 +212,22 @@ func (m *NodeAnnotationManager) EnsureRemediationStateGVK(
207212
return nil
208213
}
209214

215+
func validateMaintenanceResourceReference(resourceRef MaintenanceResourceReference) error {
216+
if resourceRef.APIGroup == "" || resourceRef.Version == "" || resourceRef.Kind == "" {
217+
return fmt.Errorf("apiGroup, version, and kind must be non-empty")
218+
}
219+
220+
return nil
221+
}
222+
210223
func backfillResourceReference(
211224
groupState *EquivalenceGroupState,
212225
resourceRef MaintenanceResourceReference,
213226
) bool {
214227
changed := false
215228

216-
if groupState.ApiGroup == "" && resourceRef.ApiGroup != "" {
217-
groupState.ApiGroup = resourceRef.ApiGroup
229+
if groupState.APIGroup == "" && resourceRef.APIGroup != "" {
230+
groupState.APIGroup = resourceRef.APIGroup
218231
changed = true
219232
}
220233

fault-remediation/pkg/annotation/annotation_interface.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,12 @@ type NodeAnnotationManagerInterface interface {
3737
actionName string,
3838
resourceRef MaintenanceResourceReference,
3939
) error
40+
// EnsureRemediationStateGVK backfills legacy annotation entries using
41+
// resource references keyed by ActionName.
4042
EnsureRemediationStateGVK(
4143
ctx context.Context,
4244
nodeName string,
43-
resourceRefs map[string]MaintenanceResourceReference,
45+
resourceRefsByAction map[string]MaintenanceResourceReference,
4446
) error
4547
ClearRemediationState(ctx context.Context, nodeName string) error
4648
RemoveGroupsFromState(ctx context.Context, nodeName string, groups []string) error
@@ -56,7 +58,7 @@ type RemediationStateAnnotation struct {
5658
type MaintenanceResourceReference struct {
5759
Namespace string
5860
Version string
59-
ApiGroup string
61+
APIGroup string
6062
Kind string
6163
}
6264

@@ -72,6 +74,6 @@ type EquivalenceGroupState struct {
7274
// Concrete resource identity for the CR that was created.
7375
Namespace string `json:"namespace,omitempty"`
7476
Version string `json:"version,omitempty"`
75-
ApiGroup string `json:"apiGroup,omitempty"`
77+
APIGroup string `json:"apiGroup,omitempty"`
7678
Kind string `json:"kind,omitempty"`
7779
}

fault-remediation/pkg/annotation/annotation_test.go

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ import (
2929
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3030
)
3131

32+
func testMaintenanceResourceReference() MaintenanceResourceReference {
33+
return MaintenanceResourceReference{
34+
Version: "v1alpha1",
35+
APIGroup: "janitor.dgxc.nvidia.com",
36+
Kind: "RebootNode",
37+
}
38+
}
39+
3240
func TestGetRemediationState(t *testing.T) {
3341
ctx := context.Background()
3442
nodeName := "test-node"
@@ -144,7 +152,7 @@ func TestUpdateRemediationState(t *testing.T) {
144152
resourceRef := MaintenanceResourceReference{
145153
Namespace: "dgxc-janitor",
146154
Version: "v1alpha1",
147-
ApiGroup: "janitor.dgxc.nvidia.com",
155+
APIGroup: "janitor.dgxc.nvidia.com",
148156
Kind: "RebootNode",
149157
}
150158
node := &corev1.Node{
@@ -168,10 +176,63 @@ func TestUpdateRemediationState(t *testing.T) {
168176
assert.Equal(t, actionName, state.EquivalenceGroups[group].ActionName)
169177
assert.Equal(t, resourceRef.Namespace, state.EquivalenceGroups[group].Namespace)
170178
assert.Equal(t, resourceRef.Version, state.EquivalenceGroups[group].Version)
171-
assert.Equal(t, resourceRef.ApiGroup, state.EquivalenceGroups[group].ApiGroup)
179+
assert.Equal(t, resourceRef.APIGroup, state.EquivalenceGroups[group].APIGroup)
172180
assert.Equal(t, resourceRef.Kind, state.EquivalenceGroups[group].Kind)
173181
}
174182

183+
func TestUpdateRemediationStateRejectsIncompleteReference(t *testing.T) {
184+
nodeName := "node"
185+
node := &corev1.Node{
186+
ObjectMeta: metav1.ObjectMeta{
187+
Name: nodeName,
188+
Annotations: map[string]string{},
189+
},
190+
}
191+
client := fake.NewClientBuilder().WithObjects(node).Build()
192+
annotationManager := NodeAnnotationManager{client: client}
193+
194+
err := annotationManager.UpdateRemediationState(
195+
context.TODO(),
196+
nodeName,
197+
"test",
198+
"reboot",
199+
"reboot-action",
200+
MaintenanceResourceReference{Version: "v1alpha1", Kind: "RebootNode"},
201+
)
202+
require.Error(t, err)
203+
assert.Contains(t, err.Error(), "apiGroup, version, and kind must be non-empty")
204+
}
205+
206+
func TestUpdateRemediationStateAllowsClusterScopedReference(t *testing.T) {
207+
group := "test"
208+
nodeName := "node"
209+
resourceRef := MaintenanceResourceReference{
210+
Version: "v1alpha1",
211+
APIGroup: "janitor.dgxc.nvidia.com",
212+
Kind: "RebootNode",
213+
}
214+
node := &corev1.Node{
215+
ObjectMeta: metav1.ObjectMeta{
216+
Name: nodeName,
217+
Annotations: map[string]string{},
218+
},
219+
}
220+
client := fake.NewClientBuilder().WithObjects(node).Build()
221+
annotationManager := NodeAnnotationManager{client: client}
222+
223+
err := annotationManager.UpdateRemediationState(context.TODO(), nodeName, group, "reboot", "reboot-action", resourceRef)
224+
require.NoError(t, err)
225+
226+
updatedNode := &corev1.Node{}
227+
require.NoError(t, client.Get(context.TODO(), types.NamespacedName{Name: nodeName}, updatedNode))
228+
assert.Contains(t, updatedNode.Annotations[AnnotationKey], `"apiGroup":"janitor.dgxc.nvidia.com"`)
229+
230+
state, _, err := annotationManager.GetRemediationState(context.TODO(), nodeName)
231+
require.NoError(t, err)
232+
assert.Empty(t, state.EquivalenceGroups[group].Namespace)
233+
assert.Equal(t, resourceRef.APIGroup, state.EquivalenceGroups[group].APIGroup)
234+
}
235+
175236
func TestEnsureRemediationStateGVKBackfillsLegacyState(t *testing.T) {
176237
nodeName := "node"
177238
createdAt := time.Now().Add(-time.Hour).UTC()
@@ -195,7 +256,7 @@ func TestEnsureRemediationStateGVKBackfillsLegacyState(t *testing.T) {
195256
resourceRef := MaintenanceResourceReference{
196257
Namespace: "dgxc-janitor",
197258
Version: "v1alpha1",
198-
ApiGroup: "janitor.dgxc.nvidia.com",
259+
APIGroup: "janitor.dgxc.nvidia.com",
199260
Kind: "RebootNode",
200261
}
201262
client := fake.NewClientBuilder().WithObjects(node).Build()
@@ -215,7 +276,7 @@ func TestEnsureRemediationStateGVKBackfillsLegacyState(t *testing.T) {
215276
assert.Equal(t, createdAt.Unix(), groupState.CreatedAt.Unix())
216277
assert.Equal(t, resourceRef.Namespace, groupState.Namespace)
217278
assert.Equal(t, resourceRef.Version, groupState.Version)
218-
assert.Equal(t, resourceRef.ApiGroup, groupState.ApiGroup)
279+
assert.Equal(t, resourceRef.APIGroup, groupState.APIGroup)
219280
assert.Equal(t, resourceRef.Kind, groupState.Kind)
220281
}
221282

@@ -248,7 +309,7 @@ func TestEnsureRemediationStateGVKDoesNotOverwriteExistingReference(t *testing.T
248309
"RESTART_BM": {
249310
Namespace: "new-namespace",
250311
Version: "v2",
251-
ApiGroup: "new.example.com",
312+
APIGroup: "new.example.com",
252313
Kind: "NewKind",
253314
},
254315
})
@@ -259,7 +320,7 @@ func TestEnsureRemediationStateGVKDoesNotOverwriteExistingReference(t *testing.T
259320

260321
groupState := state.EquivalenceGroups["restart"]
261322
assert.Equal(t, "old-namespace", groupState.Namespace)
262-
assert.Equal(t, "old.example.com", groupState.ApiGroup)
323+
assert.Equal(t, "old.example.com", groupState.APIGroup)
263324
assert.Equal(t, "v1", groupState.Version)
264325
assert.Equal(t, "OldKind", groupState.Kind)
265326
}
@@ -394,10 +455,10 @@ func TestConcurrentUpdateAndRemoveGroupsFromState(t *testing.T) {
394455
for i := 0; i < iterations; i++ {
395456
// Reset state each iteration
396457
err := annotationManager.UpdateRemediationState(context.TODO(), nodeName, "existing-group-1", "old-cr-1",
397-
"RESTART_BM", MaintenanceResourceReference{})
458+
"RESTART_BM", testMaintenanceResourceReference())
398459
require.NoError(t, err)
399460
err = annotationManager.UpdateRemediationState(context.TODO(), nodeName, "existing-group-2", "old-cr-2",
400-
"COMPONENT_RESET", MaintenanceResourceReference{})
461+
"COMPONENT_RESET", testMaintenanceResourceReference())
401462
require.NoError(t, err)
402463

403464
var wg sync.WaitGroup
@@ -411,7 +472,7 @@ func TestConcurrentUpdateAndRemoveGroupsFromState(t *testing.T) {
411472
go func() {
412473
defer wg.Done()
413474
_ = annotationManager.UpdateRemediationState(context.TODO(), nodeName, "new-group", "new-cr",
414-
"COMPONENT_RESET", MaintenanceResourceReference{})
475+
"COMPONENT_RESET", testMaintenanceResourceReference())
415476
}()
416477

417478
wg.Wait()

fault-remediation/pkg/crstatus/checker.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"context"
1919
"log/slog"
2020

21+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2122
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2223
"k8s.io/apimachinery/pkg/runtime/schema"
2324
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -49,6 +50,7 @@ func NewCRStatusChecker(
4950
}
5051
}
5152

53+
// GetCRStateForReference fetches the stored maintenance CR reference and returns its current remediation state.
5254
func (c *CRStatusChecker) GetCRStateForReference(
5355
ctx context.Context,
5456
crName string,
@@ -61,7 +63,7 @@ func (c *CRStatusChecker) GetCRStateForReference(
6163
}
6264

6365
gvk := schema.GroupVersionKind{
64-
Group: resourceRef.ApiGroup,
66+
Group: resourceRef.APIGroup,
6567
Version: resourceRef.Version,
6668
Kind: resourceRef.Kind,
6769
}
@@ -76,8 +78,14 @@ func (c *CRStatusChecker) GetCRStateForReference(
7678
key := client.ObjectKey{Name: crName, Namespace: resourceRef.Namespace}
7779

7880
if err := c.client.Get(ctx, key, obj); err != nil {
79-
slog.WarnContext(ctx, "Failed to get CR, allowing create", "crName", crName, "gvk", gvk.String(), "error", err)
80-
return CRStateNotFound
81+
if apierrors.IsNotFound(err) {
82+
slog.InfoContext(ctx, "Stored CR was not found", "crName", crName, "gvk", gvk.String())
83+
return CRStateNotFound
84+
}
85+
86+
slog.ErrorContext(ctx, "Failed to get CR, keeping create blocked",
87+
"crName", crName, "gvk", gvk.String(), "error", err)
88+
return CRStateInProgress
8189
}
8290

8391
return c.checkConditionType(obj, completeConditionType)

fault-remediation/pkg/crstatus/crstatus_test.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@ package crstatus
1616

1717
import (
1818
"context"
19+
"fmt"
1920
"testing"
2021

2122
"github.com/stretchr/testify/assert"
2223
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
24+
"sigs.k8s.io/controller-runtime/pkg/client"
2325
"sigs.k8s.io/controller-runtime/pkg/client/fake"
26+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
2427

2528
"github.com/nvidia/nvsentinel/fault-remediation/pkg/annotation"
2629
)
@@ -142,11 +145,35 @@ func TestGetCRStateForReferenceUsesStoredReference(t *testing.T) {
142145
checker := NewCRStatusChecker(fakeClient, false)
143146

144147
state := checker.GetCRStateForReference(context.Background(), "stored-cr", annotation.MaintenanceResourceReference{
145-
ApiGroup: "stored.example.com",
148+
APIGroup: "stored.example.com",
146149
Version: "v9",
147150
Kind: "StoredMaintenance",
148151
Namespace: "stored-namespace",
149152
}, "NodeReady")
150153

151154
assert.Equal(t, CRStateSucceeded, state)
152155
}
156+
157+
func TestGetCRStateForReferenceBlocksOnGetError(t *testing.T) {
158+
fakeClient := fake.NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{
159+
Get: func(
160+
_ context.Context,
161+
_ client.WithWatch,
162+
_ client.ObjectKey,
163+
_ client.Object,
164+
_ ...client.GetOption,
165+
) error {
166+
return fmt.Errorf("api server unavailable")
167+
},
168+
}).Build()
169+
checker := NewCRStatusChecker(fakeClient, false)
170+
171+
state := checker.GetCRStateForReference(context.Background(), "stored-cr", annotation.MaintenanceResourceReference{
172+
APIGroup: "stored.example.com",
173+
Version: "v9",
174+
Kind: "StoredMaintenance",
175+
Namespace: "stored-namespace",
176+
}, "NodeReady")
177+
178+
assert.Equal(t, CRStateInProgress, state)
179+
}

fault-remediation/pkg/reconciler/reconciler.go

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -527,28 +527,28 @@ func (r *FaultRemediationReconciler) ensureRemediationStateGVK(ctx context.Conte
527527
return nil
528528
}
529529

530-
resourceRefs := maintenanceResourceReferences(remediationConfig.RemediationActions)
531-
if len(resourceRefs) == 0 {
530+
resourceRefsByAction := maintenanceResourceReferences(remediationConfig.RemediationActions)
531+
if len(resourceRefsByAction) == 0 {
532532
return nil
533533
}
534534

535-
return r.annotationManager.EnsureRemediationStateGVK(ctx, nodeName, resourceRefs)
535+
return r.annotationManager.EnsureRemediationStateGVK(ctx, nodeName, resourceRefsByAction)
536536
}
537537

538538
func maintenanceResourceReferences(
539539
remediationActions map[string]config.MaintenanceResource,
540540
) map[string]annotation.MaintenanceResourceReference {
541-
resourceRefs := make(map[string]annotation.MaintenanceResourceReference, len(remediationActions))
541+
resourceRefsByAction := make(map[string]annotation.MaintenanceResourceReference, len(remediationActions))
542542
for actionName, resource := range remediationActions {
543-
resourceRefs[actionName] = annotation.MaintenanceResourceReference{
543+
resourceRefsByAction[actionName] = annotation.MaintenanceResourceReference{
544544
Namespace: resource.Namespace,
545545
Version: resource.Version,
546-
ApiGroup: resource.ApiGroup,
546+
APIGroup: resource.ApiGroup,
547547
Kind: resource.Kind,
548548
}
549549
}
550550

551-
return resourceRefs
551+
return resourceRefsByAction
552552
}
553553

554554
// trySkipEvent returns (result, err, true) when the event should be skipped; otherwise (zero, nil, false).
@@ -1044,10 +1044,10 @@ func resourceReferenceFromState(
10441044
resourceRef := annotation.MaintenanceResourceReference{
10451045
Namespace: groupState.Namespace,
10461046
Version: groupState.Version,
1047-
ApiGroup: groupState.ApiGroup,
1047+
APIGroup: groupState.APIGroup,
10481048
Kind: groupState.Kind,
10491049
}
1050-
if resourceRef.ApiGroup == "" || resourceRef.Version == "" || resourceRef.Kind == "" {
1050+
if resourceRef.APIGroup == "" || resourceRef.Version == "" || resourceRef.Kind == "" {
10511051
return annotation.MaintenanceResourceReference{}, false
10521052
}
10531053

@@ -1070,6 +1070,9 @@ func (r *FaultRemediationReconciler) completeConditionTypeForStoredState(
10701070
if !exists {
10711071
return "", false
10721072
}
1073+
if strings.TrimSpace(resource.CompleteConditionType) == "" {
1074+
return "", false
1075+
}
10731076

10741077
return resource.CompleteConditionType, true
10751078
}

0 commit comments

Comments
 (0)