|
20 | 20 | package cloudstack |
21 | 21 |
|
22 | 22 | import ( |
| 23 | + "reflect" |
23 | 24 | "sort" |
| 25 | + "strings" |
24 | 26 | "testing" |
25 | 27 |
|
26 | 28 | "github.com/apache/cloudstack-go/v2/cloudstack" |
| 29 | + "github.com/blang/semver/v4" |
| 30 | + "go.uber.org/mock/gomock" |
27 | 31 | corev1 "k8s.io/api/core/v1" |
28 | 32 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
29 | 33 | ) |
@@ -478,3 +482,285 @@ func TestGetBoolFromServiceAnnotation(t *testing.T) { |
478 | 482 | }) |
479 | 483 | } |
480 | 484 | } |
| 485 | + |
| 486 | +func TestGetCIDRList(t *testing.T) { |
| 487 | + tests := []struct { |
| 488 | + name string |
| 489 | + annotations map[string]string |
| 490 | + want []string |
| 491 | + wantErr bool |
| 492 | + errContains string |
| 493 | + expectEmpty bool |
| 494 | + }{ |
| 495 | + { |
| 496 | + name: "defaults to allow all when annotation missing", |
| 497 | + annotations: nil, |
| 498 | + want: []string{defaultAllowedCIDR}, |
| 499 | + }, |
| 500 | + { |
| 501 | + name: "trims and splits cidrs", |
| 502 | + annotations: map[string]string{ |
| 503 | + ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8, 192.168.0.0/16", |
| 504 | + }, |
| 505 | + want: []string{"10.0.0.0/8", "192.168.0.0/16"}, |
| 506 | + }, |
| 507 | + { |
| 508 | + name: "empty annotation returns empty list", |
| 509 | + annotations: map[string]string{ |
| 510 | + ServiceAnnotationLoadBalancerSourceCidrs: "", |
| 511 | + }, |
| 512 | + expectEmpty: true, |
| 513 | + }, |
| 514 | + { |
| 515 | + name: "invalid cidr returns error", |
| 516 | + annotations: map[string]string{ |
| 517 | + ServiceAnnotationLoadBalancerSourceCidrs: "invalid-cidr", |
| 518 | + }, |
| 519 | + wantErr: true, |
| 520 | + errContains: "invalid CIDR", |
| 521 | + }, |
| 522 | + } |
| 523 | + |
| 524 | + for _, tt := range tests { |
| 525 | + t.Run(tt.name, func(t *testing.T) { |
| 526 | + lb := &loadBalancer{} |
| 527 | + svc := &corev1.Service{ |
| 528 | + ObjectMeta: metav1.ObjectMeta{ |
| 529 | + Name: "svc", |
| 530 | + Namespace: "default", |
| 531 | + Annotations: tt.annotations, |
| 532 | + }, |
| 533 | + } |
| 534 | + |
| 535 | + got, err := lb.getCIDRList(svc) |
| 536 | + if tt.wantErr { |
| 537 | + if err == nil { |
| 538 | + t.Fatalf("expected error, got nil") |
| 539 | + } |
| 540 | + if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) { |
| 541 | + t.Fatalf("error = %v, expected to contain %q", err, tt.errContains) |
| 542 | + } |
| 543 | + return |
| 544 | + } |
| 545 | + |
| 546 | + if err != nil { |
| 547 | + t.Fatalf("unexpected error: %v", err) |
| 548 | + } |
| 549 | + |
| 550 | + if tt.expectEmpty { |
| 551 | + if len(got) != 0 { |
| 552 | + t.Fatalf("expected empty CIDR list, got %v", got) |
| 553 | + } |
| 554 | + return |
| 555 | + } |
| 556 | + |
| 557 | + if !reflect.DeepEqual(got, tt.want) { |
| 558 | + t.Fatalf("getCIDRList() = %v, want %v", got, tt.want) |
| 559 | + } |
| 560 | + }) |
| 561 | + } |
| 562 | +} |
| 563 | + |
| 564 | +func TestCheckLoadBalancerRule(t *testing.T) { |
| 565 | + t.Run("rule not present returns nil", func(t *testing.T) { |
| 566 | + lb := &loadBalancer{ |
| 567 | + rules: map[string]*cloudstack.LoadBalancerRule{}, |
| 568 | + } |
| 569 | + port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP} |
| 570 | + service := &corev1.Service{} |
| 571 | + |
| 572 | + rule, needsUpdate, err := lb.checkLoadBalancerRule("missing", port, LoadBalancerProtocolTCP, service, semver.Version{}) |
| 573 | + if err != nil { |
| 574 | + t.Fatalf("unexpected error: %v", err) |
| 575 | + } |
| 576 | + if rule != nil { |
| 577 | + t.Fatalf("expected nil rule, got %v", rule) |
| 578 | + } |
| 579 | + if needsUpdate { |
| 580 | + t.Fatalf("expected needsUpdate to be false") |
| 581 | + } |
| 582 | + }) |
| 583 | + |
| 584 | + t.Run("basic property mismatch deletes rule", func(t *testing.T) { |
| 585 | + ctrl := gomock.NewController(t) |
| 586 | + t.Cleanup(ctrl.Finish) |
| 587 | + |
| 588 | + mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl) |
| 589 | + deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{} |
| 590 | + |
| 591 | + gomock.InOrder( |
| 592 | + mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-id").Return(deleteParams), |
| 593 | + mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(&cloudstack.DeleteLoadBalancerRuleResponse{}, nil), |
| 594 | + ) |
| 595 | + |
| 596 | + lb := &loadBalancer{ |
| 597 | + CloudStackClient: &cloudstack.CloudStackClient{ |
| 598 | + LoadBalancer: mockLB, |
| 599 | + }, |
| 600 | + ipAddr: "1.1.1.1", |
| 601 | + rules: map[string]*cloudstack.LoadBalancerRule{ |
| 602 | + "rule": { |
| 603 | + Id: "rule-id", |
| 604 | + Name: "rule", |
| 605 | + Publicip: "2.2.2.2", |
| 606 | + Privateport: "30000", |
| 607 | + Publicport: "80", |
| 608 | + Cidrlist: defaultAllowedCIDR, |
| 609 | + Algorithm: "roundrobin", |
| 610 | + Protocol: LoadBalancerProtocolTCP.CSProtocol(), |
| 611 | + }, |
| 612 | + }, |
| 613 | + } |
| 614 | + port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP} |
| 615 | + service := &corev1.Service{} |
| 616 | + |
| 617 | + rule, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 21, Patch: 0}) |
| 618 | + if err != nil { |
| 619 | + t.Fatalf("unexpected error: %v", err) |
| 620 | + } |
| 621 | + if rule != nil { |
| 622 | + t.Fatalf("expected nil rule after deletion, got %v", rule) |
| 623 | + } |
| 624 | + if needsUpdate { |
| 625 | + t.Fatalf("expected needsUpdate to be false") |
| 626 | + } |
| 627 | + if _, exists := lb.rules["rule"]; exists { |
| 628 | + t.Fatalf("expected rule entry to be removed from map") |
| 629 | + } |
| 630 | + }) |
| 631 | + |
| 632 | + t.Run("cidr change triggers update on supported version", func(t *testing.T) { |
| 633 | + ctrl := gomock.NewController(t) |
| 634 | + t.Cleanup(ctrl.Finish) |
| 635 | + |
| 636 | + // No expectations on the mock; any delete call would fail the test. |
| 637 | + mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl) |
| 638 | + |
| 639 | + lbRule := &cloudstack.LoadBalancerRule{ |
| 640 | + Id: "rule-id", |
| 641 | + Name: "rule", |
| 642 | + Publicip: "1.1.1.1", |
| 643 | + Privateport: "30000", |
| 644 | + Publicport: "80", |
| 645 | + Cidrlist: "10.0.0.0/8", |
| 646 | + Algorithm: "roundrobin", |
| 647 | + Protocol: LoadBalancerProtocolTCP.CSProtocol(), |
| 648 | + } |
| 649 | + |
| 650 | + lb := &loadBalancer{ |
| 651 | + CloudStackClient: &cloudstack.CloudStackClient{ |
| 652 | + LoadBalancer: mockLB, |
| 653 | + }, |
| 654 | + ipAddr: "1.1.1.1", |
| 655 | + algorithm: "roundrobin", |
| 656 | + rules: map[string]*cloudstack.LoadBalancerRule{ |
| 657 | + "rule": lbRule, |
| 658 | + }, |
| 659 | + } |
| 660 | + port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP} |
| 661 | + service := &corev1.Service{ |
| 662 | + ObjectMeta: metav1.ObjectMeta{ |
| 663 | + Annotations: map[string]string{ |
| 664 | + ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8,192.168.0.0/16", |
| 665 | + }, |
| 666 | + }, |
| 667 | + } |
| 668 | + |
| 669 | + rule, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0}) |
| 670 | + if err != nil { |
| 671 | + t.Fatalf("unexpected error: %v", err) |
| 672 | + } |
| 673 | + if rule != lbRule { |
| 674 | + t.Fatalf("expected existing rule to be returned") |
| 675 | + } |
| 676 | + if !needsUpdate { |
| 677 | + t.Fatalf("expected needsUpdate to be true due to CIDR change") |
| 678 | + } |
| 679 | + }) |
| 680 | + |
| 681 | + t.Run("cidr change triggers delete with older version", func(t *testing.T) { |
| 682 | + ctrl := gomock.NewController(t) |
| 683 | + t.Cleanup(ctrl.Finish) |
| 684 | + |
| 685 | + // No expectations on the mock; any delete or create call would fail the test. |
| 686 | + mockLB := cloudstack.NewMockLoadBalancerServiceIface(ctrl) |
| 687 | + |
| 688 | + deleteParams := &cloudstack.DeleteLoadBalancerRuleParams{} |
| 689 | + |
| 690 | + gomock.InOrder( |
| 691 | + mockLB.EXPECT().NewDeleteLoadBalancerRuleParams("rule-id").Return(deleteParams), |
| 692 | + mockLB.EXPECT().DeleteLoadBalancerRule(deleteParams).Return(&cloudstack.DeleteLoadBalancerRuleResponse{}, nil), |
| 693 | + ) |
| 694 | + |
| 695 | + lbRule := &cloudstack.LoadBalancerRule{ |
| 696 | + Id: "rule-id", |
| 697 | + Name: "rule", |
| 698 | + Publicip: "1.1.1.1", |
| 699 | + Privateport: "30000", |
| 700 | + Publicport: "80", |
| 701 | + Cidrlist: "10.0.0.0/8", |
| 702 | + Algorithm: "roundrobin", |
| 703 | + Protocol: LoadBalancerProtocolTCP.CSProtocol(), |
| 704 | + } |
| 705 | + |
| 706 | + lb := &loadBalancer{ |
| 707 | + CloudStackClient: &cloudstack.CloudStackClient{ |
| 708 | + LoadBalancer: mockLB, |
| 709 | + }, |
| 710 | + ipAddr: "1.1.1.1", |
| 711 | + algorithm: "roundrobin", |
| 712 | + rules: map[string]*cloudstack.LoadBalancerRule{ |
| 713 | + "rule": lbRule, |
| 714 | + }, |
| 715 | + } |
| 716 | + port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP} |
| 717 | + service := &corev1.Service{ |
| 718 | + ObjectMeta: metav1.ObjectMeta{ |
| 719 | + Annotations: map[string]string{ |
| 720 | + ServiceAnnotationLoadBalancerSourceCidrs: "10.0.0.0/8,192.168.0.0/16", |
| 721 | + }, |
| 722 | + }, |
| 723 | + } |
| 724 | + |
| 725 | + rule, needsUpdate, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 12, Patch: 0}) |
| 726 | + if err != nil { |
| 727 | + t.Fatalf("unexpected error: %v", err) |
| 728 | + } |
| 729 | + if rule != nil { |
| 730 | + t.Fatalf("expected nil rule after deletion, got %v", rule) |
| 731 | + } |
| 732 | + if needsUpdate { |
| 733 | + t.Fatalf("expected needsUpdate to be false due to CIDR change with older version") |
| 734 | + } |
| 735 | + }) |
| 736 | + |
| 737 | + t.Run("invalid cidr returns error", func(t *testing.T) { |
| 738 | + lb := &loadBalancer{ |
| 739 | + rules: map[string]*cloudstack.LoadBalancerRule{ |
| 740 | + "rule": { |
| 741 | + Id: "rule-id", |
| 742 | + Name: "rule", |
| 743 | + Publicip: "1.1.1.1", |
| 744 | + Privateport: "30000", |
| 745 | + Publicport: "80", |
| 746 | + Cidrlist: defaultAllowedCIDR, |
| 747 | + Algorithm: "roundrobin", |
| 748 | + Protocol: LoadBalancerProtocolTCP.CSProtocol(), |
| 749 | + }, |
| 750 | + }, |
| 751 | + } |
| 752 | + port := corev1.ServicePort{Port: 80, NodePort: 30000, Protocol: corev1.ProtocolTCP} |
| 753 | + service := &corev1.Service{ |
| 754 | + ObjectMeta: metav1.ObjectMeta{ |
| 755 | + Annotations: map[string]string{ |
| 756 | + ServiceAnnotationLoadBalancerSourceCidrs: "bad-cidr", |
| 757 | + }, |
| 758 | + }, |
| 759 | + } |
| 760 | + |
| 761 | + _, _, err := lb.checkLoadBalancerRule("rule", port, LoadBalancerProtocolTCP, service, semver.Version{Major: 4, Minor: 22, Patch: 0}) |
| 762 | + if err == nil { |
| 763 | + t.Fatalf("expected error for invalid CIDR") |
| 764 | + } |
| 765 | + }) |
| 766 | +} |
0 commit comments