Skip to content

Commit f04226a

Browse files
committed
Add per-namespace VPC filtering for VPC-only controllers
Introduce config.IsVPCNamespace() to decide whether a namespace is served by VPC-only controllers: in mixed mode it checks the namespace annotation for ProviderNSXVPC; in legacy mode (when per-namespace providers are not supported) it uses the cluster-wide HasVPCNamespaces flag set by InitMixedMode from EnableVPCNetwork. controllers/common: add VPCNamespacePredicate and register it with WithEventFilter on VPC-only controllers so non-VPC namespace creates and updates are dropped before the work queue; Delete events stay allowed for cleanup controllers/namespace: Reconcile skips non-VPC namespaces Testing done: https://jenkins-vcf-wcp-dev.devops.broadcom.net/job/dev-integ-nsxt/5639/ https://jenkins-vcf-wcp-dev.devops.broadcom.net/job/dev-nsxvpc/16738/
1 parent 3e29dcd commit f04226a

16 files changed

Lines changed: 155 additions & 4 deletions

pkg/config/mixed_mode.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,3 +317,15 @@ func IsPerNamespaceProvidersSupported() bool {
317317
defer stateMu.RUnlock()
318318
return perNamespaceProvidersSupported != nil && *perNamespaceProvidersSupported
319319
}
320+
321+
// IsVPCNamespace reports whether ns should be treated as a VPC namespace.
322+
// In legacy mode (pre-9.2, per-namespace providers not supported) the whole
323+
// cluster runs a single provider, so the cluster-level HasVPCNamespaces flag
324+
// (derived from EnableVPCNetwork) is returned regardless of the namespace.
325+
// In mixed mode, non-empty VPCNetworkConfigAnnotation marks a VPC namespace.
326+
func IsVPCNamespace(ns *v1.Namespace) bool {
327+
if !IsPerNamespaceProvidersSupported() {
328+
return HasVPCNamespaces()
329+
}
330+
return namespaceHasVPCNetworkConfig(ns)
331+
}

pkg/config/mixed_mode_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -541,6 +541,50 @@ func TestRefreshMixedModeState(t *testing.T) {
541541
})
542542
}
543543

544+
func TestIsVPCNamespace(t *testing.T) {
545+
t.Run("nil namespace", func(t *testing.T) {
546+
resetMixedModeState()
547+
assert.False(t, IsVPCNamespace(nil))
548+
})
549+
550+
t.Run("per-namespace on vpc annotation", func(t *testing.T) {
551+
resetMixedModeState()
552+
supported := true
553+
stateMu.Lock()
554+
perNamespaceProvidersSupported = &supported
555+
stateMu.Unlock()
556+
ns := makeNamespace("x", "default")
557+
assert.True(t, IsVPCNamespace(ns))
558+
})
559+
560+
t.Run("per-namespace on no annotation counts as T1", func(t *testing.T) {
561+
resetMixedModeState()
562+
supported := true
563+
stateMu.Lock()
564+
perNamespaceProvidersSupported = &supported
565+
stateMu.Unlock()
566+
ns := makeNamespace("y", "")
567+
assert.False(t, IsVPCNamespace(ns))
568+
})
569+
570+
t.Run("per-namespace off IsVPCNamespace uses cluster flags", func(t *testing.T) {
571+
resetMixedModeState()
572+
supported := false
573+
stateMu.Lock()
574+
perNamespaceProvidersSupported = &supported
575+
hasVPCNamespaces = true
576+
hasT1Namespaces = false
577+
stateMu.Unlock()
578+
ns := makeNamespace("any", "")
579+
assert.True(t, IsVPCNamespace(ns))
580+
stateMu.Lock()
581+
hasVPCNamespaces = false
582+
hasT1Namespaces = true
583+
stateMu.Unlock()
584+
assert.False(t, IsVPCNamespace(ns))
585+
})
586+
}
587+
544588
// ---------- Getters and SetMixedModeStateForTest ----------
545589

546590
func TestGettersAndSetMixedModeStateForTest(t *testing.T) {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/* Copyright © 2026 Broadcom, Inc. All Rights Reserved.
2+
SPDX-License-Identifier: Apache-2.0 */
3+
4+
package common
5+
6+
import (
7+
"context"
8+
9+
corev1 "k8s.io/api/core/v1"
10+
apierrors "k8s.io/apimachinery/pkg/api/errors"
11+
"k8s.io/apimachinery/pkg/types"
12+
"sigs.k8s.io/controller-runtime/pkg/client"
13+
"sigs.k8s.io/controller-runtime/pkg/event"
14+
"sigs.k8s.io/controller-runtime/pkg/predicate"
15+
16+
"github.com/vmware-tanzu/nsx-operator/pkg/config"
17+
)
18+
19+
// isVPCNamespaceByName fetches the Namespace by name and calls config.IsVPCNamespace.
20+
// Returns true when the namespace cannot be fetched (transient error or already
21+
// gone) so the Reconcile loop can decide what to do.
22+
func isVPCNamespaceByName(c client.Reader, ns string) bool {
23+
namespace := &corev1.Namespace{}
24+
if err := c.Get(context.Background(), types.NamespacedName{Name: ns}, namespace); err != nil {
25+
if !apierrors.IsNotFound(err) {
26+
log.Error(err, "Failed to get Namespace for VPC predicate; allowing event through", "namespace", ns)
27+
}
28+
return true
29+
}
30+
return config.IsVPCNamespace(namespace)
31+
}
32+
33+
// VPCNamespacePredicate returns a predicate that filters events for VPC-only
34+
// controllers. Events are passed when config.IsVPCNamespace reports true for
35+
// the resource's namespace.
36+
//
37+
// Behaviour by event type:
38+
// - Create / Update / Generic: allowed only for VPC namespaces.
39+
// - Delete: always allowed so the controller can clean up any existing NSX
40+
// resources even if the namespace is already gone.
41+
//
42+
// The namespace check is skipped for cluster-scoped resources (empty namespace),
43+
// which are always allowed through.
44+
func VPCNamespacePredicate(c client.Reader) predicate.Funcs {
45+
isVPCNs := func(ns string) bool {
46+
if ns == "" {
47+
// Cluster-scoped resource: no per-namespace filtering.
48+
return true
49+
}
50+
return isVPCNamespaceByName(c, ns)
51+
}
52+
53+
return predicate.Funcs{
54+
CreateFunc: func(e event.CreateEvent) bool {
55+
return isVPCNs(e.Object.GetNamespace())
56+
},
57+
UpdateFunc: func(e event.UpdateEvent) bool {
58+
return isVPCNs(e.ObjectNew.GetNamespace())
59+
},
60+
// Always allow Delete events so the controller can clean up NSX
61+
// resources regardless of the current namespace network metadata.
62+
DeleteFunc: func(e event.DeleteEvent) bool {
63+
return true
64+
},
65+
GenericFunc: func(e event.GenericEvent) bool {
66+
return isVPCNs(e.Object.GetNamespace())
67+
},
68+
}
69+
}

pkg/controllers/ipaddressallocation/ipaddressallocation_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ func (r *IPAddressAllocationReconciler) handleDeletion(req ctrl.Request, obj *v1
132132
func (r *IPAddressAllocationReconciler) setupWithManager(mgr ctrl.Manager) error {
133133
return ctrl.NewControllerManagedBy(mgr).
134134
For(&v1alpha1.IPAddressAllocation{}).
135+
WithEventFilter(common.VPCNamespacePredicate(r.Client)).
135136
WithOptions(
136137
controller.Options{
137138
MaxConcurrentReconciles: common.NumReconcile(),

pkg/controllers/namespace/namespace_controller.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,17 @@ func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (
253253
return common.ResultNormal, client.IgnoreNotFound(err)
254254
}
255255

256-
// processing create/update event
257256
ns := obj.GetName()
257+
258+
// Only process VPC namespaces. Migration destination is VPC strictly, so a non-VPC
259+
// namespace never carries VPC infra and can safely be skipped on both create
260+
// and delete.
261+
if !config.IsVPCNamespace(obj) {
262+
log.Info("Skipping Namespace: not a VPC namespace", "Namespace", ns)
263+
return common.ResultNormal, nil
264+
}
265+
266+
// processing create/update event
258267
if obj.ObjectMeta.DeletionTimestamp.IsZero() {
259268
metrics.CounterInc(r.NSXConfig, metrics.ControllerUpdateTotal, common.MetricResTypeNamespace)
260269
log.Info("Start processing Namespace create/update event", "Namespace", ns)

pkg/controllers/namespace/namespace_controller_test.go

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,12 @@ func TestGetDefaultNetworkConfigName(t *testing.T) {
128128
}
129129

130130
func TestNamespaceReconciler_Reconcile(t *testing.T) {
131+
// Simulate a legacy VPC cluster (EnableVPCNetwork=true, per-namespace
132+
// providers not supported) so that all namespaces are treated as VPC
133+
// namespaces by IsVPCNamespace.
134+
config.SetMixedModeStateForTest(false, true)
135+
t.Cleanup(func() { config.SetMixedModeStateForTest(false, false) })
136+
131137
nc := v1alpha1.VPCNetworkConfiguration{
132138
ObjectMeta: metav1.ObjectMeta{
133139
Name: "fake-VPCNetworkConfig",
@@ -374,11 +380,11 @@ func TestCreateDefaultSubnetSet(t *testing.T) {
374380
expectedSubnetSets: 1, // VM
375381
networkStack: v1alpha1.FullStackVPC,
376382
nameSpaceType: ctlcommon.SystemNs,
383+
// Stub NSXCheckVersion so getSystemNsDefaultSize does not call a nil cluster (empty Client in createNameSpaceReconciler).
377384
setupMocks: func(r *NamespaceReconciler) *gomonkey.Patches {
378-
patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSystemNsDefaultSize", func(_ *NamespaceReconciler) int {
379-
return 8
385+
return gomonkey.ApplyMethod(reflect.TypeOf(&nsx.Client{}), "NSXCheckVersion", func(_ *nsx.Client, _ int) bool {
386+
return true // treat as NSX >= 9.1 => MinSubnetSizeV91 (8)
380387
})
381-
return patches
382388
},
383389
},
384390
{

pkg/controllers/networkinfo/networkinfo_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ func (r *NetworkInfoReconciler) getNSXLBSNATIP(nc *v1alpha1.VPCNetworkConfigurat
469469
func (r *NetworkInfoReconciler) setupWithManager(mgr ctrl.Manager) error {
470470
return ctrl.NewControllerManagedBy(mgr).
471471
For(&v1alpha1.NetworkInfo{}).
472+
WithEventFilter(common.VPCNamespacePredicate(r.Client)).
472473
WithOptions(
473474
controller.Options{
474475
MaxConcurrentReconciles: common.NumReconcile(),

pkg/controllers/networkpolicy/networkpolicy_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func (r *NetworkPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Reques
140140
func (r *NetworkPolicyReconciler) setupWithManager(mgr ctrl.Manager) error {
141141
return ctrl.NewControllerManagedBy(mgr).
142142
For(&networkingv1.NetworkPolicy{}).
143+
WithEventFilter(common.VPCNamespacePredicate(r.Client)).
143144
Watches(
144145
&v1.Pod{},
145146
&EnqueueRequestForPod{

pkg/controllers/pod/pod_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ func (r *PodReconciler) setupWithManager(mgr ctrl.Manager) error {
257257
return ctrl.NewControllerManagedBy(mgr).
258258
For(&v1.Pod{}).
259259
WithEventFilter(PredicateFuncsPod).
260+
WithEventFilter(common.VPCNamespacePredicate(r.Client)).
260261
WithOptions(
261262
controller.Options{
262263
MaxConcurrentReconciles: common.NumReconcile(),

pkg/controllers/service/service_lb_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func (r *ServiceLbReconciler) setServiceLbStatus(ctx context.Context, lbService
121121
func (r *ServiceLbReconciler) setupWithManager(mgr ctrl.Manager) error {
122122
return ctrl.NewControllerManagedBy(mgr).
123123
For(&v1.Service{}).
124+
WithEventFilter(common.VPCNamespacePredicate(r.Client)).
124125
WithOptions(
125126
controller.Options{
126127
MaxConcurrentReconciles: common.NumReconcile(),

0 commit comments

Comments
 (0)