Skip to content

Commit 4ba0355

Browse files
authored
Enhance pre-created SubnetSet validation (#1369)
wcpsvc will validate if there is no SubnetPort on the Subnet when changing vmDefault/podDefault from true to false. NSX Operator will watch the VPCNetworkConfiguration and remove the Subnet from default SubnetSet CR if vmDefault/podDefault is changed from true to false. This PR is to deal with the race condition that SubnetPort is created after wcpsvc updates the VPCNetworkConfiguration and before NSX Operator updates the SubnetSet. In SubnetSet webhook, operator needs to check the following for vm/pod default SubnetSet - Subnet cannot be removed from SubnetSet if it is used by the SubnetPort through the SubnetSet - Default SubnetSet cannot be deleted if it is used by Pod/SubnetPort When vmDefault/podDefault is changed from true to false for a Subnet in VPCNetworkConfiguration, but it is not removed from vm-default or pod-default SubnetSet due to stale SubnetPort, we should - update the SubnetSet Status to show the update is failed - not allow the following-up SubnetPort being created on the Subnet Signed-off-by: Yanjun Zhou <yanjun.zhou@broadcom.com>
1 parent b153b78 commit 4ba0355

File tree

13 files changed

+649
-76
lines changed

13 files changed

+649
-76
lines changed

pkg/apis/vpc/v1alpha1/condition_types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const (
1414
AutoSnatEnabled ConditionType = "AutoSnatEnabled"
1515
ExternalIPBlocksConfigured ConditionType = "ExternalIPBlocksConfigured"
1616
DeleteFailure ConditionType = "DeletionFailed"
17+
UpdateFailure ConditionType = "UpdateFailed"
1718
)
1819

1920
// Condition defines condition of custom resource.

pkg/controllers/common/utils.go

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
v1 "k8s.io/api/core/v1"
1414
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1515
"k8s.io/apimachinery/pkg/types"
16+
"k8s.io/apimachinery/pkg/util/sets"
1617
"k8s.io/client-go/tools/record"
1718
k8sclient "sigs.k8s.io/controller-runtime/pkg/client"
1819

@@ -79,8 +80,36 @@ func GetNSXSubnetsForSubnetSet(client k8sclient.Client, subnetSet *v1alpha1.Subn
7980
return subnets, nil
8081
}
8182

82-
func GetSubnetFromSubnetSet(client k8sclient.Client, subnetSet *v1alpha1.SubnetSet, subnetService servicecommon.SubnetServiceProvider, subnetPortService servicecommon.SubnetPortServiceProvider) (string, error) {
83+
func getSubnetFromVPCNetworkConfiguration(vpcService servicecommon.VPCServiceProvider, ns string, defaultSubnetSetFor string) (sets.Set[string], error) {
84+
subnetPaths := sets.New[string]()
85+
vpcNetworkConfig, err := GetVpcNetworkConfig(vpcService, ns)
86+
if err != nil {
87+
return subnetPaths, err
88+
}
89+
for _, subnet := range vpcNetworkConfig.Spec.Subnets {
90+
if defaultSubnetSetFor == servicecommon.DefaultPodNetwork && subnet.PodDefault {
91+
subnetPaths.Insert(subnet.Path)
92+
} else if defaultSubnetSetFor == servicecommon.DefaultVMNetwork && subnet.VMDefault {
93+
subnetPaths.Insert(subnet.Path)
94+
}
95+
}
96+
return subnetPaths, nil
97+
}
98+
99+
// Get a Subnet with available IPs from the pre-created SubnetSet
100+
func GetSubnetFromSubnetSet(client k8sclient.Client, subnetSet *v1alpha1.SubnetSet, vpcService servicecommon.VPCServiceProvider, subnetService servicecommon.SubnetServiceProvider, subnetPortService servicecommon.SubnetPortServiceProvider) (string, error) {
83101
var errList []error
102+
defaultSubnetSetFor := util.GetSubnetSetKind(subnetSet)
103+
subnetPathsFromConfig := sets.New[string]()
104+
var err error
105+
// Get default Subnets from VPCNetworkConfiguration CR
106+
if len(defaultSubnetSetFor) > 0 {
107+
subnetPathsFromConfig, err = getSubnetFromVPCNetworkConfiguration(vpcService, subnetSet.Namespace, defaultSubnetSetFor)
108+
if err != nil {
109+
log.Error(err, "Failed to get Subnet for default Network in VPCNetworkConfiguration")
110+
return "", err
111+
}
112+
}
84113
for _, subnetName := range *subnetSet.Spec.SubnetNames {
85114
subnetCR := &v1alpha1.Subnet{}
86115
if err := client.Get(context.Background(), types.NamespacedName{Namespace: subnetSet.Namespace, Name: subnetName}, subnetCR); err != nil {
@@ -94,6 +123,16 @@ func GetSubnetFromSubnetSet(client k8sclient.Client, subnetSet *v1alpha1.SubnetS
94123
errList = append(errList, err)
95124
continue
96125
}
126+
// For default network, race between wcpsvc and NSX Operator on removing Subnet from default SubnetSet
127+
// may result in difference between Subnets in VPCNetworkConfiguration and default SubnetSet
128+
// We should not allow new SubnetPort to be created on the Subnet already removed from VPCNetworkConfiguration
129+
// but still exist in SubnetSet CR due to stale SubnetPorts
130+
if len(defaultSubnetSetFor) > 0 {
131+
if !subnetPathsFromConfig.Has(*nsxSubnet.Path) {
132+
log.Info("Shared Subnet for default network is removed from VPCNetworkConfiguration, skipped", "Subnet", *nsxSubnet.Path)
133+
continue
134+
}
135+
}
97136
canAllocate, err := subnetPortService.AllocatePortFromSubnet(nsxSubnet)
98137
if err != nil {
99138
log.Error(err, "Failed to check capacity of NSX Subnet", "Subnet", subnetName, "SubnetSet", subnetSet.Name, "Namespace", subnetSet.Namespace, "NSXSubnet", nsxSubnet.Id)
@@ -138,12 +177,16 @@ func IsNamespaceInTepLessMode(client k8sclient.Client, namespace string) (bool,
138177
return networkInfo.VPCs[0].NetworkStack == v1alpha1.VLANBackedVPC, nil
139178
}
140179

141-
func AllocateSubnetFromSubnetSet(client k8sclient.Client, subnetSet *v1alpha1.SubnetSet, vpcService servicecommon.VPCServiceProvider, subnetService servicecommon.SubnetServiceProvider, subnetPortService servicecommon.SubnetPortServiceProvider) (string, *types.UID, *sync.RWMutex, error) {
180+
func AllocateSubnetFromSubnetSet(client k8sclient.Client, apiReader k8sclient.Reader, subnetSet *v1alpha1.SubnetSet, vpcService servicecommon.VPCServiceProvider, subnetService servicecommon.SubnetServiceProvider, subnetPortService servicecommon.SubnetPortServiceProvider) (string, *types.UID, *sync.RWMutex, error) {
142181
if subnetSet.Spec.SubnetNames != nil {
143182
// Use Read lock to allow SubnetPorts created parallelly on the pre-created SubnetSet
144183
// and block the SubnetPort creation when the SubnetSet is updated
145184
subnetSetLock := RLockSubnetSet(subnetSet.UID)
146-
nsxSubnet, err := GetSubnetFromSubnetSet(client, subnetSet, subnetService, subnetPortService)
185+
// Retrieve the SubnetSet again to avoid it being updated before acquiring the lock
186+
if err := apiReader.Get(context.Background(), types.NamespacedName{Namespace: subnetSet.Namespace, Name: subnetSet.Name}, subnetSet); err != nil {
187+
return "", &subnetSet.UID, subnetSetLock, err
188+
}
189+
nsxSubnet, err := GetSubnetFromSubnetSet(client, subnetSet, vpcService, subnetService, subnetPortService)
147190
return nsxSubnet, &subnetSet.UID, subnetSetLock, err
148191
}
149192
// Use SubnetSet uuid lock to make sure when multiple ports are created on the same SubnetSet, only one Subnet will be created

pkg/controllers/common/utils_test.go

Lines changed: 115 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,22 @@ func TestGetVirtualMachineNameForSubnetPort(t *testing.T) {
8989
func TestAllocateSubnetFromSubnetSet(t *testing.T) {
9090
expectedSubnetPath := "subnet-path-1"
9191
subnetSize := int64(32)
92+
scheme := runtime.NewScheme()
93+
v1alpha1.AddToScheme(scheme)
94+
k8sclient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&v1alpha1.SubnetSet{
95+
ObjectMeta: metav1.ObjectMeta{
96+
Name: "subnetset-1",
97+
Namespace: "ns-1",
98+
},
99+
}).Build()
100+
patches := gomonkey.ApplyFunc(GetSubnetFromSubnetSet, func(client client.Client, subnetSet *v1alpha1.SubnetSet, vpcService servicecommon.VPCServiceProvider, subnetService servicecommon.SubnetServiceProvider, subnetPortService servicecommon.SubnetPortServiceProvider) (string, error) {
101+
return expectedSubnetPath, nil
102+
})
103+
defer patches.Reset()
92104
tests := []struct {
93105
name string
94106
prepareFunc func(*testing.T, servicecommon.VPCServiceProvider, servicecommon.SubnetServiceProvider, servicecommon.SubnetPortServiceProvider)
107+
subnetSet *v1alpha1.SubnetSet
95108
expectedErr string
96109
expectedResult string
97110
}{
@@ -111,6 +124,12 @@ func TestAllocateSubnetFromSubnetSet(t *testing.T) {
111124
spsp.(*pkg_mock.MockSubnetPortServiceProvider).On("AllocatePortFromSubnet", mock.Anything).Return(true, nil)
112125
},
113126
expectedResult: expectedSubnetPath,
127+
subnetSet: &v1alpha1.SubnetSet{
128+
ObjectMeta: metav1.ObjectMeta{
129+
Name: "subnetset-1",
130+
Namespace: "ns-1",
131+
},
132+
},
114133
},
115134
{
116135
name: "ListVPCFailure",
@@ -121,6 +140,12 @@ func TestAllocateSubnetFromSubnetSet(t *testing.T) {
121140
vsp.(*pkg_mock.MockVPCServiceProvider).On("ListVPCInfo", mock.Anything).Return([]servicecommon.VPCResourceInfo{})
122141
},
123142
expectedErr: "no VPC found",
143+
subnetSet: &v1alpha1.SubnetSet{
144+
ObjectMeta: metav1.ObjectMeta{
145+
Name: "subnetset-1",
146+
Namespace: "ns-1",
147+
},
148+
},
124149
},
125150
{
126151
name: "CreateSubnet",
@@ -133,6 +158,28 @@ func TestAllocateSubnetFromSubnetSet(t *testing.T) {
133158
spsp.(*pkg_mock.MockSubnetPortServiceProvider).On("AllocatePortFromSubnet", mock.Anything).Return(true, nil)
134159
},
135160
expectedResult: expectedSubnetPath,
161+
subnetSet: &v1alpha1.SubnetSet{
162+
ObjectMeta: metav1.ObjectMeta{
163+
Name: "subnetset-1",
164+
Namespace: "ns-1",
165+
},
166+
},
167+
},
168+
{
169+
name: "PrecreatedSubnetSet",
170+
prepareFunc: func(t *testing.T, vsp servicecommon.VPCServiceProvider, ssp servicecommon.SubnetServiceProvider, spsp servicecommon.SubnetPortServiceProvider) {
171+
172+
},
173+
expectedResult: expectedSubnetPath,
174+
subnetSet: &v1alpha1.SubnetSet{
175+
ObjectMeta: metav1.ObjectMeta{
176+
Name: "subnetset-1",
177+
Namespace: "ns-1",
178+
},
179+
Spec: v1alpha1.SubnetSetSpec{
180+
SubnetNames: &[]string{"subnet-1"},
181+
},
182+
},
136183
},
137184
}
138185
for _, tt := range tests {
@@ -141,12 +188,10 @@ func TestAllocateSubnetFromSubnetSet(t *testing.T) {
141188
ssp := &pkg_mock.MockSubnetServiceProvider{}
142189
spsp := &pkg_mock.MockSubnetPortServiceProvider{}
143190
tt.prepareFunc(t, vps, ssp, spsp)
144-
subnetPath, _, _, err := AllocateSubnetFromSubnetSet(fake.NewClientBuilder().Build(), &v1alpha1.SubnetSet{
145-
ObjectMeta: metav1.ObjectMeta{
146-
Name: "subnetset-1",
147-
Namespace: "ns-1",
148-
},
149-
}, vps, ssp, spsp)
191+
subnetPath, subnetSetUID, subnetSetLock, err := AllocateSubnetFromSubnetSet(k8sclient, k8sclient, tt.subnetSet, vps, ssp, spsp)
192+
if subnetSetLock != nil {
193+
RUnlockSubnetSet(*subnetSetUID, subnetSetLock)
194+
}
150195
if tt.expectedErr != "" {
151196
assert.Contains(t, err.Error(), tt.expectedErr)
152197
} else {
@@ -655,14 +700,16 @@ func TestGetSubnetFromSubnetSet(t *testing.T) {
655700
_ = v1alpha1.AddToScheme(scheme)
656701

657702
ns := "ns-1"
658-
path := "/orgs/default/projects/default/vpcs/ns-1/subnets/subnet-1"
659-
nsxSubnet := &model.VpcSubnet{Id: servicecommon.String("subnet-1"), Path: &path}
703+
path1 := "/orgs/default/projects/default/vpcs/ns-1/subnets/subnet-1"
704+
nsxSubnet1 := &model.VpcSubnet{Id: servicecommon.String("subnet-1"), Path: &path1}
705+
path2 := "/orgs/default/projects/default/vpcs/ns-1/subnets/subnet-2"
706+
nsxSubnet2 := &model.VpcSubnet{Id: servicecommon.String("subnet-2"), Path: &path2}
660707

661708
tests := []struct {
662709
name string
663710
subnetSet *v1alpha1.SubnetSet
664711
existingObjects []runtime.Object
665-
mockSetup func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider)
712+
mockSetup func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) *gomonkey.Patches
666713
expectedPath string
667714
wantErr bool
668715
errContains string
@@ -676,12 +723,13 @@ func TestGetSubnetFromSubnetSet(t *testing.T) {
676723
existingObjects: []runtime.Object{
677724
&v1alpha1.Subnet{ObjectMeta: metav1.ObjectMeta{Name: "subnet-1", Namespace: ns}},
678725
},
679-
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) {
726+
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) *gomonkey.Patches {
680727
// We use mock.Anything here to avoid pointer address issues
681-
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet, nil)
728+
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet1, nil)
682729
mp.On("AllocatePortFromSubnet", mock.Anything).Return(true, nil)
730+
return nil
683731
},
684-
expectedPath: path,
732+
expectedPath: path1,
685733
wantErr: false,
686734
},
687735
{
@@ -694,13 +742,49 @@ func TestGetSubnetFromSubnetSet(t *testing.T) {
694742
&v1alpha1.Subnet{ObjectMeta: metav1.ObjectMeta{Name: "full-subnet", Namespace: ns}},
695743
&v1alpha1.Subnet{ObjectMeta: metav1.ObjectMeta{Name: "ok-subnet", Namespace: ns}},
696744
},
697-
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) {
698-
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet, nil)
745+
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) *gomonkey.Patches {
746+
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet1, nil)
699747
// First call returns false (full), second call returns true
700748
mp.On("AllocatePortFromSubnet", mock.Anything).Return(false, nil).Once()
701749
mp.On("AllocatePortFromSubnet", mock.Anything).Return(true, nil).Once()
750+
return nil
702751
},
703-
expectedPath: path,
752+
expectedPath: path1,
753+
wantErr: false,
754+
},
755+
{
756+
name: "Success - First subnet is removed from profile",
757+
subnetSet: &v1alpha1.SubnetSet{
758+
ObjectMeta: metav1.ObjectMeta{
759+
Name: "subnetset-2",
760+
Namespace: ns,
761+
Labels: map[string]string{
762+
servicecommon.LabelDefaultNetwork: servicecommon.DefaultPodNetwork,
763+
},
764+
},
765+
Spec: v1alpha1.SubnetSetSpec{SubnetNames: &[]string{"removed-subnet", "ok-subnet"}},
766+
},
767+
existingObjects: []runtime.Object{
768+
&v1alpha1.Subnet{ObjectMeta: metav1.ObjectMeta{Name: "removed-subnet", Namespace: ns}},
769+
&v1alpha1.Subnet{ObjectMeta: metav1.ObjectMeta{Name: "ok-subnet", Namespace: ns}},
770+
},
771+
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) *gomonkey.Patches {
772+
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet1, nil).Once()
773+
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet2, nil).Once()
774+
mp.On("AllocatePortFromSubnet", mock.Anything).Return(true, nil).Once()
775+
patches := gomonkey.ApplyFunc(GetVpcNetworkConfig, func(service servicecommon.VPCServiceProvider, ns string) (*v1alpha1.VPCNetworkConfiguration, error) {
776+
return &v1alpha1.VPCNetworkConfiguration{
777+
Spec: v1alpha1.VPCNetworkConfigurationSpec{
778+
Subnets: []v1alpha1.SharedSubnet{
779+
{Path: path1, PodDefault: false},
780+
{Path: path2, PodDefault: true},
781+
},
782+
},
783+
}, nil
784+
})
785+
return patches
786+
},
787+
expectedPath: path2,
704788
wantErr: false,
705789
},
706790
{
@@ -712,9 +796,10 @@ func TestGetSubnetFromSubnetSet(t *testing.T) {
712796
existingObjects: []runtime.Object{
713797
&v1alpha1.Subnet{ObjectMeta: metav1.ObjectMeta{Name: "subnet-1", Namespace: ns}},
714798
},
715-
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) {
716-
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet, nil)
799+
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) *gomonkey.Patches {
800+
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet1, nil)
717801
mp.On("AllocatePortFromSubnet", mock.Anything).Return(false, nil)
802+
return nil
718803
},
719804
expectedPath: "",
720805
wantErr: true,
@@ -726,7 +811,8 @@ func TestGetSubnetFromSubnetSet(t *testing.T) {
726811
Spec: v1alpha1.SubnetSetSpec{SubnetNames: &[]string{"missing-subnet"}},
727812
},
728813
existingObjects: []runtime.Object{},
729-
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) {
814+
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) *gomonkey.Patches {
815+
return nil
730816
},
731817
wantErr: true,
732818
errContains: "not found",
@@ -740,8 +826,9 @@ func TestGetSubnetFromSubnetSet(t *testing.T) {
740826
existingObjects: []runtime.Object{
741827
&v1alpha1.Subnet{ObjectMeta: metav1.ObjectMeta{Name: "subnet-1", Namespace: ns}},
742828
},
743-
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) {
829+
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) *gomonkey.Patches {
744830
ms.On("GetSubnetByCR", mock.Anything).Return(nil, errors.New("nsx-service-down"))
831+
return nil
745832
},
746833
wantErr: true,
747834
errContains: "nsx-service-down",
@@ -755,9 +842,10 @@ func TestGetSubnetFromSubnetSet(t *testing.T) {
755842
existingObjects: []runtime.Object{
756843
&v1alpha1.Subnet{ObjectMeta: metav1.ObjectMeta{Name: "subnet-1", Namespace: ns}},
757844
},
758-
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) {
759-
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet, nil)
845+
mockSetup: func(ms *pkg_mock.MockSubnetServiceProvider, mp *pkg_mock.MockSubnetPortServiceProvider) *gomonkey.Patches {
846+
ms.On("GetSubnetByCR", mock.Anything).Return(nsxSubnet1, nil)
760847
mp.On("AllocatePortFromSubnet", mock.Anything).Return(false, errors.New("capacity-check-failed"))
848+
return nil
761849
},
762850
wantErr: true,
763851
errContains: "capacity-check-failed",
@@ -770,10 +858,14 @@ func TestGetSubnetFromSubnetSet(t *testing.T) {
770858
client := fake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(tt.existingObjects...).Build()
771859
mockSubnetSvc := new(pkg_mock.MockSubnetServiceProvider)
772860
mockPortSvc := new(pkg_mock.MockSubnetPortServiceProvider)
773-
tt.mockSetup(mockSubnetSvc, mockPortSvc)
861+
mockVpcSvc := new(pkg_mock.MockVPCServiceProvider)
862+
patches := tt.mockSetup(mockSubnetSvc, mockPortSvc)
863+
if patches != nil {
864+
defer patches.Reset()
865+
}
774866

775867
// Execute
776-
result, err := GetSubnetFromSubnetSet(client, tt.subnetSet, mockSubnetSvc, mockPortSvc)
868+
result, err := GetSubnetFromSubnetSet(client, tt.subnetSet, mockVpcSvc, mockSubnetSvc, mockPortSvc)
777869

778870
// Assert
779871
if tt.wantErr {

0 commit comments

Comments
 (0)