@@ -10,6 +10,7 @@ import (
1010 "testing"
1111 "time"
1212
13+ "github.com/simplyblock/atlas/ptr"
1314 simplyblockv1alpha1 "github.com/simplyblock/simplyblock-operator/api/v1alpha1"
1415 "github.com/simplyblock/simplyblock-operator/internal/utils"
1516 "github.com/simplyblock/simplyblock-operator/internal/webapi"
@@ -146,6 +147,184 @@ func TestReconcileActivateInitializesObservedGeneration(t *testing.T) {
146147 }
147148}
148149
150+ func TestFdActivationDomainCountViolation (t * testing.T ) {
151+ hosts := func (domainCounts ... int32 ) map [string ]int32 {
152+ m := map [string ]int32 {}
153+ for i , fd := range domainCounts {
154+ m [fmt .Sprintf ("10.0.0.%d" , i )] = fd
155+ }
156+ return m
157+ }
158+
159+ cases := []struct {
160+ name string
161+ npcs int
162+ hosts map [string ]int32
163+ wantErr bool
164+ }{
165+ {"empty" , 1 , map [string ]int32 {}, true },
166+ {"npcs1 two domains violates" , 1 , hosts (0 , 0 , 1 , 1 ), true },
167+ {"npcs1 three domains ok" , 1 , hosts (0 , 1 , 2 ), false },
168+ {"npcs1 three domains unequal violates" , 1 , hosts (0 , 1 , 1 , 2 ), true },
169+ {"npcs2 two domains violates" , 2 , hosts (0 , 0 , 1 , 1 ), true },
170+ {"npcs2 three domains violates" , 2 , hosts (0 , 1 , 2 ), true },
171+ {"npcs2 four domains ok" , 2 , hosts (0 , 1 , 2 , 3 ), false },
172+ {"npcs2 four domains unequal violates" , 2 , hosts (0 , 0 , 1 , 2 , 3 ), true },
173+ }
174+ for _ , tc := range cases {
175+ t .Run (tc .name , func (t * testing.T ) {
176+ reason := fdActivationDomainCountViolation (tc .npcs , tc .hosts )
177+ if tc .wantErr && reason == "" {
178+ t .Fatalf ("expected a violation reason, got none" )
179+ }
180+ if ! tc .wantErr && reason != "" {
181+ t .Fatalf ("expected no violation, got: %s" , reason )
182+ }
183+ })
184+ }
185+ }
186+
187+ func TestClusterFailureDomainHosts (t * testing.T ) {
188+ fd := func (v int32 ) * int32 { return & v }
189+
190+ nsA := & simplyblockv1alpha1.StorageNodeSet {
191+ ObjectMeta : metav1.ObjectMeta {Name : "set-a" , Namespace : "default" },
192+ Spec : simplyblockv1alpha1.StorageNodeSetSpec {ClusterName : "cluster-x" },
193+ Status : simplyblockv1alpha1.StorageNodeSetStatus {
194+ Nodes : []simplyblockv1alpha1.NodeStatus {
195+ {Hostname : "w0" , MgmtIp : "10.0.0.1" , FailureDomain : fd (0 )},
196+ {Hostname : "w1" , MgmtIp : "10.0.0.2" , FailureDomain : fd (1 )},
197+ {Hostname : "w2" , MgmtIp : "10.0.0.3" }, // no domain yet, must be skipped
198+ },
199+ },
200+ }
201+ nsB := & simplyblockv1alpha1.StorageNodeSet {
202+ ObjectMeta : metav1.ObjectMeta {Name : "set-b" , Namespace : "default" },
203+ Spec : simplyblockv1alpha1.StorageNodeSetSpec {ClusterName : "cluster-x" },
204+ Status : simplyblockv1alpha1.StorageNodeSetStatus {
205+ Nodes : []simplyblockv1alpha1.NodeStatus {
206+ {Hostname : "w3" , MgmtIp : "10.0.0.4" , FailureDomain : fd (2 )},
207+ },
208+ },
209+ }
210+ nsOther := & simplyblockv1alpha1.StorageNodeSet {
211+ ObjectMeta : metav1.ObjectMeta {Name : "set-other" , Namespace : "default" },
212+ Spec : simplyblockv1alpha1.StorageNodeSetSpec {ClusterName : "cluster-y" },
213+ Status : simplyblockv1alpha1.StorageNodeSetStatus {
214+ Nodes : []simplyblockv1alpha1.NodeStatus {
215+ {Hostname : "y0" , MgmtIp : "10.0.0.9" , FailureDomain : fd (0 )},
216+ },
217+ },
218+ }
219+
220+ r := newClusterStateTestReconciler (t , nsA , nsB , nsOther )
221+
222+ got , err := clusterFailureDomainHosts (context .Background (), r .Client , "default" , "cluster-x" )
223+ if err != nil {
224+ t .Fatalf ("clusterFailureDomainHosts returned error: %v" , err )
225+ }
226+ want := map [string ]int32 {"10.0.0.1" : 0 , "10.0.0.2" : 1 , "10.0.0.4" : 2 }
227+ if len (got ) != len (want ) {
228+ t .Fatalf ("expected %v, got %v" , want , got )
229+ }
230+ for ip , fdVal := range want {
231+ if got [ip ] != fdVal {
232+ t .Fatalf ("expected %s -> domain %d, got %v" , ip , fdVal , got )
233+ }
234+ }
235+ }
236+
237+ func TestReconcileActivateWaitsForFailureDomainReadiness (t * testing.T ) {
238+ fd := func (v int32 ) * int32 { return & v }
239+
240+ cluster := & simplyblockv1alpha1.StorageCluster {
241+ ObjectMeta : metav1.ObjectMeta {
242+ Name : "cluster-fd-wait" ,
243+ Namespace : "default" ,
244+ },
245+ Spec : simplyblockv1alpha1.StorageClusterSpec {
246+ Action : utils .ClusterActionActivate ,
247+ EnableFailureDomains : ptr .To (true ),
248+ StripeSpec : & simplyblockv1alpha1.StripeSpec {
249+ ParityChunks : ptr .To (int32 (2 )),
250+ },
251+ },
252+ }
253+ // Only 2 distinct domains for npcs=2 -- must NOT be allowed through
254+ // (requires npcs+2 = 4).
255+ nodeSet := & simplyblockv1alpha1.StorageNodeSet {
256+ ObjectMeta : metav1.ObjectMeta {Name : "set-fd-wait" , Namespace : "default" },
257+ Spec : simplyblockv1alpha1.StorageNodeSetSpec {ClusterName : "cluster-fd-wait" },
258+ Status : simplyblockv1alpha1.StorageNodeSetStatus {
259+ Nodes : []simplyblockv1alpha1.NodeStatus {
260+ {Hostname : "w0" , MgmtIp : "10.0.0.1" , FailureDomain : fd (0 )},
261+ {Hostname : "w1" , MgmtIp : "10.0.0.2" , FailureDomain : fd (1 )},
262+ },
263+ },
264+ }
265+
266+ r := newClusterStateTestReconciler (t , cluster , nodeSet )
267+
268+ res , err := r .reconcileActivate (context .Background (), cluster )
269+ if err != nil {
270+ t .Fatalf ("reconcileActivate returned error: %v" , err )
271+ }
272+ if res .RequeueAfter == 0 {
273+ t .Fatalf ("expected a requeue while waiting on failure-domain readiness" )
274+ }
275+ if cluster .Status .ActionStatus != nil {
276+ t .Fatalf ("expected ActionStatus to stay untouched while not ready, got %#v" , cluster .Status .ActionStatus )
277+ }
278+ }
279+
280+ func TestReconcileActivateProceedsOnceFailureDomainsAreReady (t * testing.T ) {
281+ fd := func (v int32 ) * int32 { return & v }
282+
283+ cluster := & simplyblockv1alpha1.StorageCluster {
284+ ObjectMeta : metav1.ObjectMeta {
285+ Name : "cluster-fd-ready" ,
286+ Namespace : "default" ,
287+ },
288+ Spec : simplyblockv1alpha1.StorageClusterSpec {
289+ Action : utils .ClusterActionActivate ,
290+ EnableFailureDomains : ptr .To (true ),
291+ StripeSpec : & simplyblockv1alpha1.StripeSpec {
292+ ParityChunks : ptr .To (int32 (2 )),
293+ },
294+ },
295+ }
296+ // 4 distinct, equally-sized domains for npcs=2 -- satisfies npcs+2 = 4,
297+ // so the gate must let this through to the normal init-action path.
298+ nodeSet := & simplyblockv1alpha1.StorageNodeSet {
299+ ObjectMeta : metav1.ObjectMeta {Name : "set-fd-ready" , Namespace : "default" },
300+ Spec : simplyblockv1alpha1.StorageNodeSetSpec {ClusterName : "cluster-fd-ready" },
301+ Status : simplyblockv1alpha1.StorageNodeSetStatus {
302+ Nodes : []simplyblockv1alpha1.NodeStatus {
303+ {Hostname : "w0" , MgmtIp : "10.0.0.1" , FailureDomain : fd (0 )},
304+ {Hostname : "w1" , MgmtIp : "10.0.0.2" , FailureDomain : fd (1 )},
305+ {Hostname : "w2" , MgmtIp : "10.0.0.3" , FailureDomain : fd (2 )},
306+ {Hostname : "w3" , MgmtIp : "10.0.0.4" , FailureDomain : fd (3 )},
307+ },
308+ },
309+ }
310+
311+ r := newClusterStateTestReconciler (t , cluster , nodeSet )
312+
313+ res , err := r .reconcileActivate (context .Background (), cluster )
314+ if err != nil {
315+ t .Fatalf ("reconcileActivate returned error: %v" , err )
316+ }
317+ if cluster .Status .ActionStatus == nil {
318+ t .Fatalf ("expected ActionStatus to be initialized once failure-domain readiness is satisfied" )
319+ }
320+ if cluster .Status .ActionStatus .State != utils .ActionStateRunning {
321+ t .Fatalf ("expected Running state, got %#v" , cluster .Status .ActionStatus )
322+ }
323+ if res .Requeue != true {
324+ t .Fatalf ("expected immediate requeue for the init-action step, got %+v" , res )
325+ }
326+ }
327+
149328func TestReconcileExpandTransitions (t * testing.T ) {
150329 t .Run ("initializes running status for expand with observed generation" , func (t * testing.T ) {
151330 cluster := & simplyblockv1alpha1.StorageCluster {
0 commit comments