|
9 | 9 | appsv1 "k8s.io/api/apps/v1" |
10 | 10 | corev1 "k8s.io/api/core/v1" |
11 | 11 | apierrors "k8s.io/apimachinery/pkg/api/errors" |
| 12 | + "k8s.io/apimachinery/pkg/api/meta" |
12 | 13 | "k8s.io/apimachinery/pkg/api/resource" |
13 | 14 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
14 | 15 | "k8s.io/apimachinery/pkg/runtime" |
@@ -206,6 +207,15 @@ func TestMultigresClusterReconciler_Reconcile_Success(t *testing.T) { |
206 | 207 | if got, want := tg.Spec.Images.ImagePullPolicy, corev1.PullAlways; got != want { |
207 | 208 | t.Errorf("TableGroup pull policy mismatch got %q, want %q", got, want) |
208 | 209 | } |
| 210 | + |
| 211 | + // Verify MultiAdmin Image |
| 212 | + deploy := &appsv1.Deployment{} |
| 213 | + if err := c.Get(ctx, types.NamespacedName{Name: clusterName + "-multiadmin", Namespace: namespace}, deploy); err != nil { |
| 214 | + t.Fatal("Expected MultiAdmin deployment to exist") |
| 215 | + } |
| 216 | + if got, want := deploy.Spec.Template.Spec.Containers[0].Image, "admin:latest"; got != want { |
| 217 | + t.Errorf("MultiAdmin image mismatch got %q, want %q", got, want) |
| 218 | + } |
209 | 219 | }, |
210 | 220 | }, |
211 | 221 | "Create: Ultra-Minimalist (Shard Injection)": { |
@@ -332,6 +342,157 @@ func TestMultigresClusterReconciler_Reconcile_Success(t *testing.T) { |
332 | 342 | } |
333 | 343 | }, |
334 | 344 | }, |
| 345 | + "Reconcile: Prune Orphaned Resources": { |
| 346 | + existingObjects: []client.Object{ |
| 347 | + coreTpl, cellTpl, shardTpl, |
| 348 | + // Orphaned Cell (not in spec) |
| 349 | + &multigresv1alpha1.Cell{ |
| 350 | + ObjectMeta: metav1.ObjectMeta{ |
| 351 | + Name: clusterName + "-orphaned-cell", |
| 352 | + Namespace: namespace, |
| 353 | + Labels: map[string]string{"multigres.com/cluster": clusterName}, |
| 354 | + }, |
| 355 | + Spec: multigresv1alpha1.CellSpec{Name: "orphaned-cell"}, |
| 356 | + }, |
| 357 | + // Orphaned TableGroup (not in spec) |
| 358 | + &multigresv1alpha1.TableGroup{ |
| 359 | + ObjectMeta: metav1.ObjectMeta{ |
| 360 | + Name: clusterName + "-orphaned-tg", |
| 361 | + Namespace: namespace, |
| 362 | + Labels: map[string]string{"multigres.com/cluster": clusterName}, |
| 363 | + }, |
| 364 | + }, |
| 365 | + }, |
| 366 | + validate: func(t testing.TB, c client.Client) { |
| 367 | + ctx := t.Context() |
| 368 | + cell := &multigresv1alpha1.Cell{} |
| 369 | + err := c.Get( |
| 370 | + ctx, |
| 371 | + types.NamespacedName{ |
| 372 | + Name: clusterName + "-orphaned-cell", |
| 373 | + Namespace: namespace, |
| 374 | + }, |
| 375 | + cell, |
| 376 | + ) |
| 377 | + if !apierrors.IsNotFound(err) { |
| 378 | + t.Errorf("Expected orphaned cell to be deleted, got error: %v", err) |
| 379 | + } |
| 380 | + |
| 381 | + tg := &multigresv1alpha1.TableGroup{} |
| 382 | + err = c.Get( |
| 383 | + ctx, |
| 384 | + types.NamespacedName{Name: clusterName + "-orphaned-tg", Namespace: namespace}, |
| 385 | + tg, |
| 386 | + ) |
| 387 | + if !apierrors.IsNotFound(err) { |
| 388 | + t.Errorf("Expected orphaned tablegroup to be deleted, got error: %v", err) |
| 389 | + } |
| 390 | + }, |
| 391 | + }, |
| 392 | + "Reconcile: Status Available (All Ready)": { |
| 393 | + existingObjects: []client.Object{ |
| 394 | + coreTpl, cellTpl, shardTpl, |
| 395 | + // Existing Cell that is Ready (Mocking status from child controller) |
| 396 | + &multigresv1alpha1.Cell{ |
| 397 | + ObjectMeta: metav1.ObjectMeta{ |
| 398 | + Name: clusterName + "-zone-a", |
| 399 | + Namespace: namespace, |
| 400 | + Labels: map[string]string{"multigres.com/cluster": clusterName}, |
| 401 | + }, |
| 402 | + Spec: multigresv1alpha1.CellSpec{Name: "zone-a"}, |
| 403 | + Status: multigresv1alpha1.CellStatus{ |
| 404 | + Conditions: []metav1.Condition{ |
| 405 | + {Type: "Available", Status: metav1.ConditionTrue}, |
| 406 | + }, |
| 407 | + GatewayReplicas: 2, |
| 408 | + }, |
| 409 | + }, |
| 410 | + }, |
| 411 | + validate: func(t testing.TB, c client.Client) { |
| 412 | + ctx := t.Context() |
| 413 | + cluster := &multigresv1alpha1.MultigresCluster{} |
| 414 | + if err := c.Get(ctx, types.NamespacedName{Name: clusterName, Namespace: namespace}, cluster); err != nil { |
| 415 | + t.Fatal(err) |
| 416 | + } |
| 417 | + |
| 418 | + // Verify Aggregated Status |
| 419 | + cond := meta.FindStatusCondition(cluster.Status.Conditions, "Available") |
| 420 | + if cond == nil { |
| 421 | + t.Fatal("Available condition not found") |
| 422 | + return // explicit return to satisfy linter |
| 423 | + } |
| 424 | + if cond.Status != metav1.ConditionTrue { |
| 425 | + t.Errorf("Expected Available=True, got %s", cond.Status) |
| 426 | + } |
| 427 | + |
| 428 | + // Verify Cell Summary |
| 429 | + summary, ok := cluster.Status.Cells["zone-a"] |
| 430 | + if !ok { |
| 431 | + t.Fatal("Cell zone-a summary missing") |
| 432 | + } |
| 433 | + if !summary.Ready { |
| 434 | + t.Error("Expected Cell summary Ready=true") |
| 435 | + } |
| 436 | + if summary.GatewayReplicas != 2 { |
| 437 | + t.Errorf("Expected GatewayReplicas=2, got %d", summary.GatewayReplicas) |
| 438 | + } |
| 439 | + }, |
| 440 | + }, |
| 441 | + "Reconcile: Implicit Cell Sorting": { |
| 442 | + preReconcileUpdate: func(t testing.TB, c *multigresv1alpha1.MultigresCluster) { |
| 443 | + // Define 2 cells to force multi-cell expansion |
| 444 | + c.Spec.Cells = []multigresv1alpha1.CellConfig{ |
| 445 | + {Name: "zone-b", Zone: "us-east-1b"}, // b comes after a |
| 446 | + {Name: "zone-a", Zone: "us-east-1a"}, |
| 447 | + } |
| 448 | + // We do NOT set Shard.Spec.MultiOrch.Cells explicitly. |
| 449 | + }, |
| 450 | + // FIX: Use a ShardTemplate that HAS explicit pool cells, so the controller finds them |
| 451 | + // and populates MultiOrch.Cells, triggering the sort logic. |
| 452 | + existingObjects: []client.Object{ |
| 453 | + coreTpl, cellTpl, |
| 454 | + &multigresv1alpha1.ShardTemplate{ |
| 455 | + ObjectMeta: metav1.ObjectMeta{Name: "default-shard", Namespace: namespace}, |
| 456 | + Spec: multigresv1alpha1.ShardTemplateSpec{ |
| 457 | + MultiOrch: &multigresv1alpha1.MultiOrchSpec{ |
| 458 | + StatelessSpec: multigresv1alpha1.StatelessSpec{ |
| 459 | + Replicas: ptr.To(int32(3)), |
| 460 | + }, |
| 461 | + // Cells EMPTY to trigger defaulting logic |
| 462 | + }, |
| 463 | + Pools: map[string]multigresv1alpha1.PoolSpec{ |
| 464 | + "primary": { |
| 465 | + ReplicasPerCell: ptr.To(int32(2)), |
| 466 | + Type: "readWrite", |
| 467 | + // Explicit cells to be aggregated |
| 468 | + Cells: []multigresv1alpha1.CellName{"zone-b", "zone-a"}, |
| 469 | + }, |
| 470 | + }, |
| 471 | + }, |
| 472 | + }, |
| 473 | + }, |
| 474 | + validate: func(t testing.TB, c client.Client) { |
| 475 | + ctx := t.Context() |
| 476 | + tg := &multigresv1alpha1.TableGroup{} |
| 477 | + tgName := clusterName + "-db1-tg1" |
| 478 | + if err := c.Get(ctx, types.NamespacedName{Name: tgName, Namespace: namespace}, tg); err != nil { |
| 479 | + t.Fatal(err) |
| 480 | + } |
| 481 | + |
| 482 | + shards := tg.Spec.Shards |
| 483 | + if len(shards) == 0 { |
| 484 | + t.Fatal("No shards found") |
| 485 | + } |
| 486 | + cells := shards[0].MultiOrch.Cells |
| 487 | + if len(cells) != 2 { |
| 488 | + t.Fatalf("Expected 2 cells, got %d", len(cells)) |
| 489 | + } |
| 490 | + // Verify Order (Must be sorted alphabetically: zone-a, zone-b) |
| 491 | + if cells[0] != "zone-a" || cells[1] != "zone-b" { |
| 492 | + t.Errorf("Cells not sorted: %v", cells) |
| 493 | + } |
| 494 | + }, |
| 495 | + }, |
335 | 496 | "Delete: Allow Finalization if Children Gone": { |
336 | 497 | preReconcileUpdate: func(t testing.TB, c *multigresv1alpha1.MultigresCluster) { |
337 | 498 | now := metav1.Now() |
|
0 commit comments