Skip to content

Commit 058e687

Browse files
committed
feat: add strict ordering
1 parent 4d23724 commit 058e687

File tree

8 files changed

+289
-62
lines changed

8 files changed

+289
-62
lines changed

operator/api/v1alpha1/skyhook_types.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ type SkyhookSpec struct {
7171
// This skyhook is required to have been completed before any workloads can start
7272
//+kubebuilder:default=false
7373
RuntimeRequired bool `json:"runtimeRequired,omitempty"`
74+
75+
// Priority determines the order in which skyhooks are applied. Lower values are applied first.
76+
//+kubebuilder:validation:Minimum=0
77+
//+kubebuilder:default=0
78+
Priority int `json:"priority,omitempty"`
7479
}
7580

7681
// BuildGraph turns packages in the a graph of dependencies
@@ -659,3 +664,23 @@ func init() {
659664
func (s *Skyhook) WasUpdated() bool {
660665
return s.Generation > 1 && s.Generation > s.Status.ObservedGeneration
661666
}
667+
668+
func (s *Skyhook) IsPaused() bool {
669+
if s.Annotations == nil {
670+
return false
671+
}
672+
if val, ok := s.Annotations[fmt.Sprintf("%s/pause", METADATA_PREFIX)]; ok {
673+
return val == "true"
674+
}
675+
return false
676+
}
677+
678+
func (s *Skyhook) IsDisabled() bool {
679+
if s.Annotations == nil {
680+
return false
681+
}
682+
if val, ok := s.Annotations[fmt.Sprintf("%s/disable", METADATA_PREFIX)]; ok {
683+
return val == "true"
684+
}
685+
return false
686+
}

operator/api/v1alpha1/skyhook_types_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ package v1alpha1
2323
import (
2424
. "github.com/onsi/ginkgo/v2"
2525
. "github.com/onsi/gomega"
26+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2627
)
2728

2829
var _ = Describe("Skyhook Types", func() {
@@ -552,4 +553,52 @@ var _ = Describe("Skyhook Types", func() {
552553
Expect(nodeState.IsPackageComplete(packages["bar"], interrupts, configUpdates)).To(BeEquivalentTo(false))
553554
})
554555

556+
It("Should detect IsPaused correctly", func() {
557+
s := &Skyhook{
558+
ObjectMeta: metav1.ObjectMeta{},
559+
}
560+
// Case 1: Annotations is nil
561+
Expect(s.IsPaused()).To(BeFalse())
562+
563+
// Case 2: Annotations is empty map
564+
s.Annotations = map[string]string{}
565+
Expect(s.IsPaused()).To(BeFalse())
566+
567+
// Case 3: Key not present
568+
s.Annotations = map[string]string{"other": "true"}
569+
Expect(s.IsPaused()).To(BeFalse())
570+
571+
// Case 4: Key present with value "true"
572+
s.Annotations = map[string]string{METADATA_PREFIX + "/pause": "true"}
573+
Expect(s.IsPaused()).To(BeTrue())
574+
575+
// Case 5: Key present with value "false"
576+
s.Annotations = map[string]string{METADATA_PREFIX + "/pause": "false"}
577+
Expect(s.IsPaused()).To(BeFalse())
578+
})
579+
580+
It("Should detect IsDisabled correctly", func() {
581+
s := &Skyhook{
582+
ObjectMeta: metav1.ObjectMeta{},
583+
}
584+
// Case 1: Annotations is nil
585+
Expect(s.IsDisabled()).To(BeFalse())
586+
587+
// Case 2: Annotations is empty map
588+
s.Annotations = map[string]string{}
589+
Expect(s.IsDisabled()).To(BeFalse())
590+
591+
// Case 3: Key not present
592+
s.Annotations = map[string]string{"other": "true"}
593+
Expect(s.IsDisabled()).To(BeFalse())
594+
595+
// Case 4: Key present with value "true"
596+
s.Annotations = map[string]string{METADATA_PREFIX + "/disable": "true"}
597+
Expect(s.IsDisabled()).To(BeTrue())
598+
599+
// Case 5: Key present with value "false"
600+
s.Annotations = map[string]string{METADATA_PREFIX + "/disable": "false"}
601+
Expect(s.IsDisabled()).To(BeFalse())
602+
})
603+
555604
})

operator/config/crd/bases/skyhook.nvidia.com_skyhooks.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,12 @@ spec:
488488
type: object
489489
type: object
490490
x-kubernetes-map-type: atomic
491+
priority:
492+
default: 0
493+
description: Priority determines the order in which skyhooks are applied.
494+
Lower values are applied first.
495+
minimum: 0
496+
type: integer
491497
runtimeRequired:
492498
default: false
493499
description: This skyhook is required to have been completed before

operator/internal/controller/cluster_state_v2.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,13 @@ func BuildState(skyhooks *v1alpha1.SkyhookList, nodes *corev1.NodeList) (*cluste
104104
}
105105
}
106106

107+
// Sort by priority (ascending), then by name (ascending) if priorities are equal
107108
sort.Slice(ret.skyhooks, func(i, j int) bool {
109+
pi := ret.skyhooks[i].skyhook.Spec.Priority
110+
pj := ret.skyhooks[j].skyhook.Spec.Priority
111+
if pi != pj {
112+
return pi < pj
113+
}
108114
return ret.skyhooks[i].skyhook.Name < ret.skyhooks[j].skyhook.Name
109115
})
110116

@@ -117,13 +123,26 @@ func BuildState(skyhooks *v1alpha1.SkyhookList, nodes *corev1.NodeList) (*cluste
117123
return ret, nil
118124
}
119125

126+
func GetNextSkyhook(skyhooks []*skyhookNodes) *skyhookNodes {
127+
for _, skyhook := range skyhooks {
128+
// Move skip and pause to be annotations
129+
if skyhook.IsComplete() || skyhook.skyhook.IsDisabled() {
130+
continue
131+
}
132+
return skyhook
133+
}
134+
return nil
135+
}
136+
120137
// SkyhookNodes wraps the skyhook and nodes that it pertains too
121138
type SkyhookNodes interface {
122139
CollectNodeStatus() v1alpha1.Status
123140
GetSkyhook() *wrapper.Skyhook
124141
GetNodes() []wrapper.SkyhookNode
125142
GetNode(name string) (v1alpha1.Status, wrapper.SkyhookNode)
126143
IsComplete() bool
144+
IsDisabled() bool
145+
IsPaused() bool
127146
NodeCount() int
128147
SetStatus(status v1alpha1.Status)
129148
Status() v1alpha1.Status
@@ -168,6 +187,14 @@ func (s *skyhookNodes) IsComplete() bool {
168187
return true
169188
}
170189

190+
func (s *skyhookNodes) IsDisabled() bool {
191+
return s.skyhook.IsDisabled()
192+
}
193+
194+
func (s *skyhookNodes) IsPaused() bool {
195+
return s.skyhook.IsPaused()
196+
}
197+
171198
func (s *skyhookNodes) Status() v1alpha1.Status {
172199
return s.skyhook.Status.Status
173200
}

operator/internal/controller/cluster_state_v2_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,11 @@ import (
2424
. "github.com/onsi/ginkgo/v2"
2525
. "github.com/onsi/gomega"
2626

27+
"github.com/NVIDIA/skyhook/api/v1alpha1"
28+
skyhookNodesMock "github.com/NVIDIA/skyhook/internal/controller/mock"
29+
"github.com/NVIDIA/skyhook/internal/wrapper"
2730
corev1 "k8s.io/api/core/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2832
)
2933

3034
var _ = Describe("cluster state v2 tests", func() {
@@ -106,3 +110,82 @@ var _ = Describe("cluster state v2 tests", func() {
106110
Expect(CheckTaintToleration(tolerations, taints)).To(BeTrue())
107111
})
108112
})
113+
114+
// --- Add GetNextSkyhook tests ---
115+
116+
var _ = Describe("GetNextSkyhook", func() {
117+
It("returns the first not-complete, not-disabled skyhook", func() {
118+
// Helper to make a skyhookNodes with given complete/disabled
119+
makeSkyhookNodes := func(name string, complete bool, disabled bool, paused bool) SkyhookNodes {
120+
sn_mock := skyhookNodesMock.MockSkyhookNodes{}
121+
sn_mock.EXPECT().IsComplete().Return(complete)
122+
sn_mock.EXPECT().IsDisabled().Return(disabled)
123+
sn_mock.EXPECT().IsPaused().Return(paused)
124+
return &sn_mock
125+
}
126+
127+
// Not complete, not disabled
128+
n1 := makeSkyhookNodes("n1", false, false, false)
129+
// Complete
130+
n2 := makeSkyhookNodes("n2", true, false, false)
131+
// Disabled
132+
n3 := makeSkyhookNodes("n3", false, true, false)
133+
134+
// Should return n1
135+
result := GetNextSkyhook([]SkyhookNodes{n1, n2, n3})
136+
Expect(result).To(Equal(n1))
137+
138+
// Should skip n1 if complete, return nil if all complete/disabled
139+
n1.skyhook.Status.Status = v1alpha1.StatusComplete
140+
result = GetNextSkyhook([]*skyhookNodes{n1, n2, n3})
141+
Expect(result).To(BeNil())
142+
143+
// Should skip disabled, return next not-complete, not-disabled
144+
n1.skyhook.Status.Status = v1alpha1.StatusUnknown
145+
n1.skyhook.Annotations[v1alpha1.METADATA_PREFIX+"/disable"] = "true"
146+
n2.skyhook.Status.Status = v1alpha1.StatusUnknown
147+
n2.skyhook.Annotations[v1alpha1.METADATA_PREFIX+"/disable"] = "false"
148+
result = GetNextSkyhook([]*skyhookNodes{n1, n2, n3})
149+
Expect(result).To(Equal(n2))
150+
})
151+
152+
It("returns nil if all skyhooks are complete or disabled", func() {
153+
n1 := &skyhookNodes{skyhook: wrapper.NewSkyhookWrapper(&v1alpha1.Skyhook{
154+
ObjectMeta: metav1.ObjectMeta{Name: "n1", Annotations: map[string]string{v1alpha1.METADATA_PREFIX + "/disable": "true"}},
155+
Status: v1alpha1.SkyhookStatus{Status: v1alpha1.StatusComplete},
156+
})}
157+
n2 := &skyhookNodes{skyhook: wrapper.NewSkyhookWrapper(&v1alpha1.Skyhook{
158+
ObjectMeta: metav1.ObjectMeta{Name: "n2"},
159+
Status: v1alpha1.SkyhookStatus{Status: v1alpha1.StatusComplete},
160+
})}
161+
result := GetNextSkyhook([]*skyhookNodes{n1, n2})
162+
Expect(result).To(BeNil())
163+
})
164+
})
165+
166+
var _ = Describe("BuildState ordering", func() {
167+
It("orders skyhooks by priority and name", func() {
168+
priorityKey := v1alpha1.METADATA_PREFIX + "/priority"
169+
skyhooks := &v1alpha1.SkyhookList{
170+
Items: []v1alpha1.Skyhook{
171+
{
172+
ObjectMeta: metav1.ObjectMeta{Name: "b", Annotations: map[string]string{priorityKey: "2"}},
173+
},
174+
{
175+
ObjectMeta: metav1.ObjectMeta{Name: "a", Annotations: map[string]string{priorityKey: "1"}},
176+
},
177+
{
178+
ObjectMeta: metav1.ObjectMeta{Name: "c", Annotations: map[string]string{priorityKey: "2"}},
179+
},
180+
},
181+
}
182+
nodes := &corev1.NodeList{Items: []corev1.Node{}}
183+
clusterState, err := BuildState(skyhooks, nodes)
184+
Expect(err).ToNot(HaveOccurred())
185+
ordered := clusterState.skyhooks
186+
// Should be: a (priority 1), b (priority 2, name b), c (priority 2, name c)
187+
Expect(ordered[0].skyhook.Name).To(Equal("a"))
188+
Expect(ordered[1].skyhook.Name).To(Equal("b"))
189+
Expect(ordered[2].skyhook.Name).To(Equal("c"))
190+
})
191+
})

operator/internal/controller/mock/SkyhookNodes.go

Lines changed: 90 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)