From 3e29dcd2624586c687cb17dc44662fec594e4552 Mon Sep 17 00:00:00 2001 From: Qian Sun Date: Tue, 17 Mar 2026 16:23:24 +0800 Subject: [PATCH 1/2] Implement Mixed Mode State Detection Replace the global boolean cf.CoeConfig.EnableVPCNetwork with namespace-driven mixed-mode state: HasT1Namespaces and HasVPCNamespaces. New module: pkg/config/mixed_mode.go - Checks SupervisorCapabilities CRD for supports_per_namespace_network_providers capability. - If supported: scans namespace annotations `nsx.vmware.com/vpc_network_config` to derive HasVPCNamespaces and HasT1Namespaces. - If not supported (legacy/pre-9.2): falls back to EnableVPCNetwork config flag. This enables NSX Operator to run in mixed mode where both T1 and VPC namespaces coexist, as required for VDS->VPC and T1->VPC migration. NOTE: This patch only ensures that the existing pure T1 or pure VPC envs preserve existing behaviours. The full functionality will be implemented in the follow-up patches. 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/ --- cmd/main.go | 29 +- pkg/config/mixed_mode.go | 319 ++++++++++ pkg/config/mixed_mode_test.go | 588 ++++++++++++++++++ .../securitypolicy_controller_test.go | 1 + pkg/nsx/client.go | 5 +- pkg/nsx/client_test.go | 3 - .../services/securitypolicy/builder_test.go | 61 +- .../services/securitypolicy/expand_test.go | 1 + .../services/securitypolicy/firewall_test.go | 9 + pkg/nsx/services/securitypolicy/parse.go | 5 +- pkg/nsx/util/license.go | 34 +- pkg/nsx/util/license_test.go | 55 +- 12 files changed, 1038 insertions(+), 72 deletions(-) create mode 100644 pkg/config/mixed_mode.go create mode 100644 pkg/config/mixed_mode_test.go diff --git a/cmd/main.go b/cmd/main.go index f848b61c7..7f8f4fc5e 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -113,7 +113,7 @@ func init() { func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) { // Generate webhook certificates and start refreshing webhook certificates periodically - if cf.CoeConfig.EnableVPCNetwork { + if config.HasVPCNamespaces() { if err := pkgutil.GenerateWebhookCerts(); err != nil { log.Error(err, "Failed to generate webhook certificates") os.Exit(1) @@ -123,7 +123,7 @@ func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) { } // Initialize and start the system health reporter - if cf.CoeConfig.EnableVPCNetwork && cf.EnableInventory && cf.CoeConfig.EnableSha { + if config.HasVPCNamespaces() && cf.EnableInventory && cf.CoeConfig.EnableSha { health.Start(nsxClient, cf, mgr.GetClient()) } @@ -136,7 +136,7 @@ func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) { checkLicense(nsxClient, cf.LicenseValidationInterval) - if cf.K8sConfig.EnableRestore && cf.CoeConfig.EnableVPCNetwork { + if cf.K8sConfig.EnableRestore && config.HasVPCNamespaces() { var err error restoreMode, err = pkgutil.CompareNSXRestore(mgr.GetClient(), nsxClient) if err != nil { @@ -153,7 +153,7 @@ func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) { var hookServer webhook.Server var subnetSetReconcile *subnetset.SubnetSetReconciler - if cf.CoeConfig.EnableVPCNetwork { + if config.HasVPCNamespaces() { // Check NSX version for VPC networking mode if !commonService.NSXClient.NSXCheckVersion(nsx.VPC) { log.Error(nil, "VPC mode cannot be enabled if NSX version is lower than 4.1.1") @@ -284,6 +284,21 @@ func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) { log.Error(err, "Failed to update Pod labels") panic(err) } + + // Watch for mixed-mode state changes (e.g. T1-only → T1+VPC when the migration starts). + // If the state changes, exit so the operator restarts with the new configuration + // — this is simpler and safer than hot-initializing VPC services after startup. + config.SetMixedModeNamespaceRefreshReader(mgr.GetClient()) + go func() { + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for range ticker.C { + if config.RefreshMixedModeState(context.Background()) { + log.Info("Mixed-mode state changed; restarting NSX Operator to pick up new configuration") + os.Exit(0) + } + } + }() } func electMaster(mgr manager.Manager, nsxClient *nsx.Client) { @@ -329,6 +344,12 @@ func main() { os.Exit(1) } + if err := config.InitMixedMode(context.Background(), cfg, cf.CoeConfig.EnableVPCNetwork); err != nil { + log.Error(err, "Failed to initialize mixed mode state") + os.Exit(1) + } + util.SetHasVPCNamespacesFunc(config.HasVPCNamespaces) + if cf.HAEnabled() { go electMaster(mgr, nsxClient) } else { diff --git a/pkg/config/mixed_mode.go b/pkg/config/mixed_mode.go new file mode 100644 index 000000000..668db44b9 --- /dev/null +++ b/pkg/config/mixed_mode.go @@ -0,0 +1,319 @@ +/* Copyright © 2026 VMware, Inc. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 */ + +package config + +import ( + "context" + "strings" + "sync" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware-tanzu/nsx-operator/pkg/logger" +) + +const ( + VPCNetworkConfigAnnotation = "nsx.vmware.com/vpc_network_config" + + supervisorCapabilitiesName = "supervisor-capabilities" +) + +var ( + supervisorCapabilitiesGVR = schema.GroupVersionResource{ + Group: "iaas.vmware.com", + Version: "v1alpha1", + Resource: "supervisorcapabilities", + } + + stateMu sync.RWMutex + hasT1Namespaces bool + hasVPCNamespaces bool + perNamespaceProvidersSupported *bool + stateInitialized bool + + // retryInitialInterval and retryMaxInterval control the exponential + // backoff used when a transient error prevents reading + // SupervisorCapabilities or listing namespaces. Overridable in tests. + retryInitialInterval = 2 * time.Second + retryMaxInterval = 30 * time.Second + + // storedClientset is kept from InitMixedMode so that RefreshMixedModeState + // can re-scan without requiring the caller to pass it each time. + storedClientset kubernetes.Interface + + // namespaceRefreshReader, when non-nil, is used by RefreshMixedModeState to list + // namespaces from the controller-runtime cache (mgr.GetClient()) instead of + // a direct API list on storedClientset — reducing apiserver load on the 30s + // refresh ticker. Set via SetMixedModeNamespaceRefreshReader from cmd after + // controllers are registered on the manager. + namespaceRefreshReader client.Reader + refreshReaderMu sync.RWMutex +) + +var log = logger.Log + +// checkPerNamespaceProvidersSupported fetches the SupervisorCapabilities object and +// returns whether per-namespace network providers are activated. It retries +// all errors with exponential backoff (starting at retryInitialInterval, +// doubling each attempt, capped at retryMaxInterval). The SupervisorCapabilities +// CR is guaranteed to exist; all failures are treated as transient (e.g. API +// server not yet ready at operator startup). Returns false only when the +// context is cancelled. +func checkPerNamespaceProvidersSupported(ctx context.Context, dynClient dynamic.Interface) bool { + interval := retryInitialInterval + for { + obj, err := dynClient.Resource(supervisorCapabilitiesGVR).Get( + ctx, supervisorCapabilitiesName, metav1.GetOptions{}) + if err == nil { + return extractCapability(obj) + } + log.Info("Failed to get SupervisorCapabilities, will retry", "error", err, "retryIn", interval) + select { + case <-ctx.Done(): + log.Info("Context cancelled while waiting for SupervisorCapabilities, falling back to legacy config") + return false + case <-time.After(interval): + } + interval = min(interval*2, retryMaxInterval) + } +} + +func extractCapability(obj *unstructured.Unstructured) bool { + status, found, err := unstructured.NestedMap(obj.Object, "status") + if err != nil || !found { + return false + } + services, found, err := unstructured.NestedMap(status, "services") + if err != nil || !found { + return false + } + for _, svcCaps := range services { + capsMap, ok := svcCaps.(map[string]interface{}) + if !ok { + continue + } + cap, ok := capsMap["supports_per_namespace_network_providers"] + if !ok { + continue + } + capMap, ok := cap.(map[string]interface{}) + if !ok { + continue + } + activated, ok := capMap["activated"] + if ok { + if b, ok := activated.(bool); ok && b { + return true + } + } + } + return false +} + +func namespaceHasVPCNetworkConfig(ns *v1.Namespace) bool { + if ns == nil { + return false + } + v := strings.TrimSpace(ns.Annotations[VPCNetworkConfigAnnotation]) + return v != "" +} + +func accumulateMixedModeFlagsFromNamespaces(items []v1.Namespace) (hasT1 bool, hasVPC bool) { + for i := range items { + if namespaceHasVPCNetworkConfig(&items[i]) { + hasVPC = true + } else { + hasT1 = true + } + } + return hasT1, hasVPC +} + +func scanNamespaceProviders(ctx context.Context, clientset kubernetes.Interface) (hasT1 bool, hasVPC bool, err error) { + nsList, err := clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{}) + if err != nil { + return false, false, err + } + hasT1, hasVPC = accumulateMixedModeFlagsFromNamespaces(nsList.Items) + return hasT1, hasVPC, nil +} + +func scanNamespaceProvidersWithClient(ctx context.Context, reader client.Reader) (hasT1 bool, hasVPC bool, err error) { + nsList := &v1.NamespaceList{} + if err := reader.List(ctx, nsList); err != nil { + return false, false, err + } + hasT1, hasVPC = accumulateMixedModeFlagsFromNamespaces(nsList.Items) + return hasT1, hasVPC, nil +} + +// SetMixedModeNamespaceRefreshReader registers a cache-backed client.Reader +// (typically mgr.GetClient()) for periodic mixed-mode rescans. When nil, +// RefreshMixedModeState keeps using the kubernetes.Interface from InitMixedMode. +// Call once from cmd after controllers are set up on the manager. +func SetMixedModeNamespaceRefreshReader(r client.Reader) { + refreshReaderMu.Lock() + defer refreshReaderMu.Unlock() + namespaceRefreshReader = r +} + +func currentNamespaceRefreshReader() client.Reader { + refreshReaderMu.RLock() + defer refreshReaderMu.RUnlock() + return namespaceRefreshReader +} + +// waitForNamespaceProviders retries scanNamespaceProviders with exponential +// backoff on transient errors (e.g. API server not yet ready at operator startup). +func waitForNamespaceProviders(ctx context.Context, clientset kubernetes.Interface) (bool, bool) { + interval := retryInitialInterval + for { + hasT1, hasVPC, err := scanNamespaceProviders(ctx, clientset) + if err == nil { + return hasT1, hasVPC + } + log.Warn("Failed to list namespaces for mixed-mode scan, will retry", "error", err, "retryIn", interval) + select { + case <-ctx.Done(): + log.Info("Context cancelled during mixed-mode namespace scan, returning empty state") + return false, false + case <-time.After(interval): + } + interval = min(interval*2, retryMaxInterval) + } +} + +// InitMixedMode initializes mixed-mode state by checking SupervisorCapabilities +// and scanning namespaces (non-empty nsx.vmware.com/vpc_network_config to VPC, +// otherwise T1 for mixed-mode aggregation). If per-namespace providers are not +// activated, falls back to the legacy EnableVPCNetwork flag. +// +// The SupervisorCapabilities lookup is performed outside the state mutex so +// that transient API errors can be retried without blocking readers for an +// extended period. +func InitMixedMode(ctx context.Context, cfg *rest.Config, enableVPCNetwork bool) error { + clientset, err := kubernetes.NewForConfig(cfg) + if err != nil { + return err + } + dynClient, err := dynamic.NewForConfig(cfg) + if err != nil { + return err + } + initMixedModeWithClients(ctx, clientset, dynClient, enableVPCNetwork) + return nil +} + +func initMixedModeWithClients(ctx context.Context, clientset kubernetes.Interface, dynClient dynamic.Interface, enableVPCNetwork bool) { + // checkPerNamespaceProvidersSupported retries on transient errors; runs outside + // the mutex to avoid holding the lock during potentially many retries. + supported := checkPerNamespaceProvidersSupported(ctx, dynClient) + + var t1, vpc bool + if supported { + log.Info("Per-namespace network providers are supported, scanning namespaces for mixed-mode") + t1, vpc = waitForNamespaceProviders(ctx, clientset) + } else { + log.Info("Per-namespace network providers not supported, using legacy config", "enableVPCNetwork", enableVPCNetwork) + if enableVPCNetwork { + t1, vpc = false, true + } else { + t1, vpc = true, false + } + } + stateMu.Lock() + defer stateMu.Unlock() + storedClientset = clientset + perNamespaceProvidersSupported = &supported + hasT1Namespaces = t1 + hasVPCNamespaces = vpc + stateInitialized = true + log.Info("Mixed-mode state initialized", "hasT1Namespaces", t1, "hasVPCNamespaces", vpc) +} + +// RefreshMixedModeState re-scans namespaces using the clientset stored during +// InitMixedMode and updates the global state. Returns true if the state +// changed; the caller should then restart the operator so that VPC services +// and controllers are initialized for the new mode. +func RefreshMixedModeState(ctx context.Context) bool { + stateMu.Lock() + defer stateMu.Unlock() + + if perNamespaceProvidersSupported == nil || !*perNamespaceProvidersSupported { + return false + } + if storedClientset == nil { + return false + } + + oldT1, oldVPC := hasT1Namespaces, hasVPCNamespaces + var newT1, newVPC bool + var err error + if r := currentNamespaceRefreshReader(); r != nil { + newT1, newVPC, err = scanNamespaceProvidersWithClient(ctx, r) + } else { + newT1, newVPC, err = scanNamespaceProviders(ctx, storedClientset) + } + if err != nil { + log.Warn("Failed to scan namespaces during mixed-mode refresh, keeping current state", "error", err) + return false + } + hasT1Namespaces = newT1 + hasVPCNamespaces = newVPC + + changed := oldT1 != hasT1Namespaces || oldVPC != hasVPCNamespaces + if changed { + log.Info("Mixed-mode state changed", + "oldHasT1Namespaces", oldT1, "hasT1Namespaces", hasT1Namespaces, + "oldHasVPCNamespaces", oldVPC, "hasVPCNamespaces", hasVPCNamespaces) + } + return changed +} + +// HasT1Namespaces returns true when at least one namespace uses T1 networking. +func HasT1Namespaces() bool { + stateMu.RLock() + defer stateMu.RUnlock() + return hasT1Namespaces +} + +// HasVPCNamespaces returns true when at least one namespace uses VPC (or VDS in migration). +func HasVPCNamespaces() bool { + stateMu.RLock() + defer stateMu.RUnlock() + return hasVPCNamespaces +} + +// IsMixedModeStateInitialized returns true after InitMixedMode has been called. +func IsMixedModeStateInitialized() bool { + stateMu.RLock() + defer stateMu.RUnlock() + return stateInitialized +} + +// SetMixedModeStateForTest sets hasT1Namespaces and hasVPCNamespaces for unit tests. +// Must only be used from test code so production always goes through InitMixedMode. +func SetMixedModeStateForTest(hasT1, hasVPC bool) { + stateMu.Lock() + defer stateMu.Unlock() + hasT1Namespaces = hasT1 + hasVPCNamespaces = hasVPC + stateInitialized = true +} + +// IsPerNamespaceProvidersSupported returns true when SupervisorCapabilities +// advertises per-namespace network providers. +func IsPerNamespaceProvidersSupported() bool { + stateMu.RLock() + defer stateMu.RUnlock() + return perNamespaceProvidersSupported != nil && *perNamespaceProvidersSupported +} diff --git a/pkg/config/mixed_mode_test.go b/pkg/config/mixed_mode_test.go new file mode 100644 index 000000000..76da6e2aa --- /dev/null +++ b/pkg/config/mixed_mode_test.go @@ -0,0 +1,588 @@ +/* Copyright © 2026 VMware, Inc. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 */ + +package config + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + dynamicfake "k8s.io/client-go/dynamic/fake" + kubefake "k8s.io/client-go/kubernetes/fake" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + k8stesting "k8s.io/client-go/testing" + crfake "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +// resetMixedModeState resets all global mixed-mode state for test isolation. +func resetMixedModeState() { + refreshReaderMu.Lock() + namespaceRefreshReader = nil + refreshReaderMu.Unlock() + stateMu.Lock() + defer stateMu.Unlock() + hasT1Namespaces = false + hasVPCNamespaces = false + perNamespaceProvidersSupported = nil + stateInitialized = false + storedClientset = nil +} + +// makeCapabilitiesObj builds an unstructured SupervisorCapabilities object +// with the given supports_per_namespace_network_providers.activated value. +func makeCapabilitiesObj(activated bool) *unstructured.Unstructured { + return &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "iaas.vmware.com/v1alpha1", + "kind": "SupervisorCapabilities", + "metadata": map[string]interface{}{ + "name": supervisorCapabilitiesName, + }, + "status": map[string]interface{}{ + "services": map[string]interface{}{ + "wcp": map[string]interface{}{ + "supports_per_namespace_network_providers": map[string]interface{}{ + "activated": activated, + }, + }, + }, + }, + }, + } +} + +// makeDynClientWith returns a fake dynamic client pre-seeded with a +// SupervisorCapabilities object whose activated flag is set as specified. +func makeDynClientWith(activated bool) *dynamicfake.FakeDynamicClient { + scheme := runtime.NewScheme() + fc := dynamicfake.NewSimpleDynamicClient(scheme) + obj := makeCapabilitiesObj(activated) + if err := fc.Tracker().Create(supervisorCapabilitiesGVR, obj, ""); err != nil { + panic(fmt.Sprintf("test setup: could not seed capabilities object: %v", err)) + } + return fc +} + +// makeNamespace creates a Namespace. If vpcNetworkConfigValue is non-empty +// (after trim), the namespace is treated as VPC for mixed-mode discovery; +// otherwise it counts as T1. +func makeNamespace(name, vpcNetworkConfigValue string) *v1.Namespace { + ns := &v1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}} + if strings.TrimSpace(vpcNetworkConfigValue) != "" { + ns.Annotations = map[string]string{ + VPCNetworkConfigAnnotation: vpcNetworkConfigValue, + } + } + return ns +} + +// ---------- extractCapability ---------- + +func TestExtractCapability(t *testing.T) { + tests := []struct { + name string + obj *unstructured.Unstructured + expect bool + }{ + { + name: "no status field", + obj: &unstructured.Unstructured{Object: map[string]interface{}{}}, + expect: false, + }, + { + name: "status without services", + obj: &unstructured.Unstructured{Object: map[string]interface{}{ + "status": map[string]interface{}{}, + }}, + expect: false, + }, + { + name: "services map is empty", + obj: &unstructured.Unstructured{Object: map[string]interface{}{ + "status": map[string]interface{}{ + "services": map[string]interface{}{}, + }, + }}, + expect: false, + }, + { + name: "service has no matching capability key", + obj: &unstructured.Unstructured{Object: map[string]interface{}{ + "status": map[string]interface{}{ + "services": map[string]interface{}{ + "wcp": map[string]interface{}{ + "other_capability": map[string]interface{}{"activated": true}, + }, + }, + }, + }}, + expect: false, + }, + { + name: "capability activated=false", + obj: makeCapabilitiesObj(false), + expect: false, + }, + { + name: "capability activated=true", + obj: makeCapabilitiesObj(true), + expect: true, + }, + { + name: "activated is not a bool", + obj: &unstructured.Unstructured{Object: map[string]interface{}{ + "status": map[string]interface{}{ + "services": map[string]interface{}{ + "wcp": map[string]interface{}{ + "supports_per_namespace_network_providers": map[string]interface{}{ + "activated": "yes", + }, + }, + }, + }, + }}, + expect: false, + }, + { + name: "capability field is not a map", + obj: &unstructured.Unstructured{Object: map[string]interface{}{ + "status": map[string]interface{}{ + "services": map[string]interface{}{ + "wcp": map[string]interface{}{ + "supports_per_namespace_network_providers": "true", + }, + }, + }, + }}, + expect: false, + }, + { + name: "service entry is not a map", + obj: &unstructured.Unstructured{Object: map[string]interface{}{ + "status": map[string]interface{}{ + "services": map[string]interface{}{ + "wcp": "not-a-map", + }, + }, + }}, + expect: false, + }, + { + name: "multiple services, second has activated=true", + obj: &unstructured.Unstructured{Object: map[string]interface{}{ + "status": map[string]interface{}{ + "services": map[string]interface{}{ + "svc-a": map[string]interface{}{ + "supports_per_namespace_network_providers": map[string]interface{}{ + "activated": false, + }, + }, + "svc-b": map[string]interface{}{ + "supports_per_namespace_network_providers": map[string]interface{}{ + "activated": true, + }, + }, + }, + }, + }}, + expect: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expect, extractCapability(tt.obj)) + }) + } +} + +// ---------- checkPerNamespaceProvidersSupported ---------- + +func TestCheckPerNamespaceProvidersSupported(t *testing.T) { + ctx := context.Background() + + // Speed up retries so tests complete in milliseconds. + origInit, origMax := retryInitialInterval, retryMaxInterval + retryInitialInterval = 1 * time.Millisecond + retryMaxInterval = 4 * time.Millisecond + defer func() { retryInitialInterval, retryMaxInterval = origInit, origMax }() + + t.Run("returns immediately on success (activated=true)", func(t *testing.T) { + assert.True(t, checkPerNamespaceProvidersSupported(ctx, makeDynClientWith(true))) + }) + + t.Run("returns immediately on success (activated=false)", func(t *testing.T) { + assert.False(t, checkPerNamespaceProvidersSupported(ctx, makeDynClientWith(false))) + }) + + t.Run("retries on transient error and eventually succeeds", func(t *testing.T) { + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + callCount := 0 + dynClient.PrependReactor("get", "*", func(_ k8stesting.Action) (bool, runtime.Object, error) { + callCount++ + if callCount < 3 { + return true, nil, fmt.Errorf("transient error %d", callCount) + } + return true, makeCapabilitiesObj(true), nil + }) + result := checkPerNamespaceProvidersSupported(ctx, dynClient) + assert.True(t, result) + assert.Equal(t, 3, callCount) + }) + + t.Run("returns false on context cancellation during retry", func(t *testing.T) { + cancelCtx, cancel := context.WithCancel(context.Background()) + scheme := runtime.NewScheme() + dynClient := dynamicfake.NewSimpleDynamicClient(scheme) + dynClient.PrependReactor("get", "*", func(_ k8stesting.Action) (bool, runtime.Object, error) { + cancel() + return true, nil, fmt.Errorf("still failing") + }) + result := checkPerNamespaceProvidersSupported(cancelCtx, dynClient) + assert.False(t, result) + }) +} + +// ---------- waitForNamespaceProviders ---------- + +func TestWaitForNamespaceProviders(t *testing.T) { + ctx := context.Background() + + // Speed up retries so tests complete in milliseconds. + origInit, origMax := retryInitialInterval, retryMaxInterval + retryInitialInterval = 1 * time.Millisecond + retryMaxInterval = 4 * time.Millisecond + defer func() { retryInitialInterval, retryMaxInterval = origInit, origMax }() + + t.Run("returns immediately on success", func(t *testing.T) { + cs := kubefake.NewClientset( + makeNamespace("ns-t1", ""), + makeNamespace("ns-vpc", "default"), + ) + hasT1, hasVPC := waitForNamespaceProviders(ctx, cs) + assert.True(t, hasT1) + assert.True(t, hasVPC) + }) + + t.Run("retries on transient error and eventually succeeds", func(t *testing.T) { + cs := kubefake.NewClientset(makeNamespace("ns-t1", "")) + callCount := 0 + cs.PrependReactor("list", "namespaces", func(_ k8stesting.Action) (bool, runtime.Object, error) { + callCount++ + if callCount < 3 { + return true, nil, fmt.Errorf("list failed %d", callCount) + } + return false, nil, nil // fall through to real clientset + }) + hasT1, hasVPC := waitForNamespaceProviders(ctx, cs) + assert.True(t, hasT1) + assert.False(t, hasVPC) + assert.Equal(t, 3, callCount) + }) + + t.Run("returns false false on context cancellation", func(t *testing.T) { + cancelCtx, cancel := context.WithCancel(context.Background()) + cs := kubefake.NewClientset() + cs.PrependReactor("list", "namespaces", func(_ k8stesting.Action) (bool, runtime.Object, error) { + cancel() + return true, nil, fmt.Errorf("still failing") + }) + hasT1, hasVPC := waitForNamespaceProviders(cancelCtx, cs) + assert.False(t, hasT1) + assert.False(t, hasVPC) + }) +} + +// ---------- scanNamespaceProviders ---------- + +func TestScanNamespaceProviders(t *testing.T) { + ctx := context.Background() + + t.Run("error listing namespaces returns error", func(t *testing.T) { + cs := kubefake.NewClientset() + cs.PrependReactor("list", "namespaces", func(_ k8stesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("apiserver unavailable") + }) + hasT1, hasVPC, err := scanNamespaceProviders(ctx, cs) + assert.False(t, hasT1) + assert.False(t, hasVPC) + assert.Error(t, err) + }) + + t.Run("empty namespace list returns false false no error", func(t *testing.T) { + hasT1, hasVPC, err := scanNamespaceProviders(ctx, kubefake.NewClientset()) + assert.False(t, hasT1) + assert.False(t, hasVPC) + assert.NoError(t, err) + }) + + t.Run("T1-only namespace", func(t *testing.T) { + cs := kubefake.NewClientset(makeNamespace("ns-t1", "")) + hasT1, hasVPC, err := scanNamespaceProviders(ctx, cs) + assert.True(t, hasT1) + assert.False(t, hasVPC) + assert.NoError(t, err) + }) + + t.Run("VPC-only namespace", func(t *testing.T) { + cs := kubefake.NewClientset(makeNamespace("ns-vpc", "default")) + hasT1, hasVPC, err := scanNamespaceProviders(ctx, cs) + assert.False(t, hasT1) + assert.True(t, hasVPC) + assert.NoError(t, err) + }) + + t.Run("vsphere-style namespace without vpc annotation counts as T1", func(t *testing.T) { + cs := kubefake.NewClientset(makeNamespace("ns-vsphere", "")) + hasT1, hasVPC, err := scanNamespaceProviders(ctx, cs) + assert.True(t, hasT1) + assert.False(t, hasVPC) + assert.NoError(t, err) + }) + + t.Run("mixed T1 and VPC namespaces", func(t *testing.T) { + cs := kubefake.NewClientset( + makeNamespace("ns-t1", ""), + makeNamespace("ns-vpc", "default"), + ) + hasT1, hasVPC, err := scanNamespaceProviders(ctx, cs) + assert.True(t, hasT1) + assert.True(t, hasVPC) + assert.NoError(t, err) + }) + + t.Run("plain namespace without vpc annotation counts as T1", func(t *testing.T) { + cs := kubefake.NewClientset(makeNamespace("ns-plain", "")) + hasT1, hasVPC, err := scanNamespaceProviders(ctx, cs) + assert.True(t, hasT1) + assert.False(t, hasVPC) + assert.NoError(t, err) + }) +} + +// ---------- scanNamespaceProvidersWithClient ---------- + +func TestScanNamespaceProvidersWithClient(t *testing.T) { + ctx := context.Background() + cl := crfake.NewClientBuilder().WithScheme(clientgoscheme.Scheme).WithObjects( + makeNamespace("ns-vpc", "default"), + ).Build() + hasT1, hasVPC, err := scanNamespaceProvidersWithClient(ctx, cl) + assert.NoError(t, err) + assert.False(t, hasT1) + assert.True(t, hasVPC) +} + +// ---------- InitMixedMode ---------- + +func TestInitMixedModeWithClients(t *testing.T) { + ctx := context.Background() + + t.Run("capability not activated enableVPCNetwork=true uses legacy config", func(t *testing.T) { + resetMixedModeState() + initMixedModeWithClients(ctx, kubefake.NewClientset(), makeDynClientWith(false), true) + assert.True(t, IsMixedModeStateInitialized()) + assert.False(t, IsPerNamespaceProvidersSupported()) + assert.True(t, HasVPCNamespaces()) + assert.False(t, HasT1Namespaces()) + }) + + t.Run("capability not activated enableVPCNetwork=false uses legacy config", func(t *testing.T) { + resetMixedModeState() + initMixedModeWithClients(ctx, kubefake.NewClientset(), makeDynClientWith(false), false) + assert.True(t, IsMixedModeStateInitialized()) + assert.False(t, IsPerNamespaceProvidersSupported()) + assert.False(t, HasVPCNamespaces()) + assert.True(t, HasT1Namespaces()) + }) + + t.Run("per-namespace supported scans namespaces for mixed-mode", func(t *testing.T) { + resetMixedModeState() + cs := kubefake.NewClientset( + makeNamespace("ns-t1", ""), + makeNamespace("ns-vpc", "default"), + ) + initMixedModeWithClients(ctx, cs, makeDynClientWith(true), false) + assert.True(t, IsMixedModeStateInitialized()) + assert.True(t, IsPerNamespaceProvidersSupported()) + assert.True(t, HasT1Namespaces()) + assert.True(t, HasVPCNamespaces()) + }) + + t.Run("per-namespace supported but no namespaces", func(t *testing.T) { + resetMixedModeState() + initMixedModeWithClients(ctx, kubefake.NewClientset(), makeDynClientWith(true), true) + assert.True(t, IsMixedModeStateInitialized()) + assert.True(t, IsPerNamespaceProvidersSupported()) + assert.False(t, HasT1Namespaces()) + assert.False(t, HasVPCNamespaces()) + }) +} + +// ---------- RefreshMixedModeState ---------- + +func TestRefreshMixedModeState(t *testing.T) { + ctx := context.Background() + + t.Run("returns false when perNamespaceProvidersSupported is nil", func(t *testing.T) { + resetMixedModeState() + storedClientset = kubefake.NewClientset() + assert.False(t, RefreshMixedModeState(ctx)) + }) + + t.Run("returns false when storedClientset is nil", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + stateMu.Unlock() + assert.False(t, RefreshMixedModeState(ctx)) + }) + + t.Run("returns false when per-namespace providers not supported", func(t *testing.T) { + resetMixedModeState() + supported := false + stateMu.Lock() + perNamespaceProvidersSupported = &supported + storedClientset = kubefake.NewClientset() + stateMu.Unlock() + assert.False(t, RefreshMixedModeState(ctx)) + }) + + t.Run("returns false when state is unchanged", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + hasT1Namespaces = true + hasVPCNamespaces = false + storedClientset = kubefake.NewClientset(makeNamespace("ns-t1", "")) + stateMu.Unlock() + assert.False(t, RefreshMixedModeState(ctx)) + assert.True(t, HasT1Namespaces()) + assert.False(t, HasVPCNamespaces()) + }) + + t.Run("returns true when state changes", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + hasT1Namespaces = true + hasVPCNamespaces = false + storedClientset = kubefake.NewClientset(makeNamespace("ns-vpc", "default")) + stateMu.Unlock() + assert.True(t, RefreshMixedModeState(ctx)) + assert.False(t, HasT1Namespaces()) + assert.True(t, HasVPCNamespaces()) + }) + + t.Run("returns true when new namespace added and state grows", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + storedClientset = kubefake.NewClientset( + makeNamespace("ns-t1", ""), + makeNamespace("ns-vpc", "default"), + ) + stateMu.Unlock() + assert.True(t, RefreshMixedModeState(ctx)) + assert.True(t, HasT1Namespaces()) + assert.True(t, HasVPCNamespaces()) + }) + + t.Run("namespace list error preserves old state and returns false", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + hasT1Namespaces = true + hasVPCNamespaces = false + cs := kubefake.NewClientset() + cs.PrependReactor("list", "namespaces", func(_ k8stesting.Action) (bool, runtime.Object, error) { + return true, nil, fmt.Errorf("list failed") + }) + storedClientset = cs + stateMu.Unlock() + assert.False(t, RefreshMixedModeState(ctx)) + // State must be preserved despite the error. + assert.True(t, HasT1Namespaces()) + assert.False(t, HasVPCNamespaces()) + }) + + t.Run("uses cache-backed reader when set (clientset would miss the namespace)", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + hasT1Namespaces = true + hasVPCNamespaces = false + // Empty clientset: only the cache reader sees the test namespace. + storedClientset = kubefake.NewClientset() + stateMu.Unlock() + cl := crfake.NewClientBuilder().WithScheme(clientgoscheme.Scheme).WithObjects( + makeNamespace("ns-vpc", "default"), + ).Build() + SetMixedModeNamespaceRefreshReader(cl) + t.Cleanup(func() { SetMixedModeNamespaceRefreshReader(nil) }) + assert.True(t, RefreshMixedModeState(ctx)) + assert.False(t, HasT1Namespaces()) + assert.True(t, HasVPCNamespaces()) + }) +} + +// ---------- Getters and SetMixedModeStateForTest ---------- + +func TestGettersAndSetMixedModeStateForTest(t *testing.T) { + t.Run("SetMixedModeStateForTest sets T1=true VPC=false", func(t *testing.T) { + SetMixedModeStateForTest(true, false) + assert.True(t, HasT1Namespaces()) + assert.False(t, HasVPCNamespaces()) + assert.True(t, IsMixedModeStateInitialized()) + }) + + t.Run("SetMixedModeStateForTest sets T1=false VPC=true", func(t *testing.T) { + SetMixedModeStateForTest(false, true) + assert.False(t, HasT1Namespaces()) + assert.True(t, HasVPCNamespaces()) + assert.True(t, IsMixedModeStateInitialized()) + }) + + t.Run("IsPerNamespaceProvidersSupported false when nil", func(t *testing.T) { + resetMixedModeState() + assert.False(t, IsPerNamespaceProvidersSupported()) + }) + + t.Run("IsPerNamespaceProvidersSupported false when explicitly false", func(t *testing.T) { + resetMixedModeState() + supported := false + stateMu.Lock() + perNamespaceProvidersSupported = &supported + stateMu.Unlock() + assert.False(t, IsPerNamespaceProvidersSupported()) + }) + + t.Run("IsPerNamespaceProvidersSupported true when set", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + stateMu.Unlock() + assert.True(t, IsPerNamespaceProvidersSupported()) + }) + + t.Run("IsMixedModeStateInitialized false before init", func(t *testing.T) { + resetMixedModeState() + assert.False(t, IsMixedModeStateInitialized()) + }) +} diff --git a/pkg/controllers/securitypolicy/securitypolicy_controller_test.go b/pkg/controllers/securitypolicy/securitypolicy_controller_test.go index 842ca1237..73222630f 100644 --- a/pkg/controllers/securitypolicy/securitypolicy_controller_test.go +++ b/pkg/controllers/securitypolicy/securitypolicy_controller_test.go @@ -615,6 +615,7 @@ func TestReconcileSecurityPolicy(t *testing.T) { } func TestSecurityPolicyReconciler_listSecurityPolciyCRIDsForVPC(t *testing.T) { + config.SetMixedModeStateForTest(false, true) mockCtl := gomock.NewController(t) defer mockCtl.Finish() k8sClient := mock_client.NewMockClient(mockCtl) diff --git a/pkg/nsx/client.go b/pkg/nsx/client.go index 1dbbae17d..d740cea15 100644 --- a/pkg/nsx/client.go +++ b/pkg/nsx/client.go @@ -398,9 +398,6 @@ func (client *Client) FeatureEnabled(feature int) bool { // once license updated, operator will restart // if FeatureContainer license is false, operatore will restart func (client *Client) ValidateLicense(init bool) error { - if init { - util.SetEnableVpcNetwork(client.NsxConfig.EnableVPCNetwork) - } log.Info("Checking NSX license") oldContainerLicense := util.IsLicensed(util.FeatureContainer) oldDfwLicense := util.GetDFWLicense() @@ -413,7 +410,7 @@ func (client *Client) ValidateLicense(init bool) error { log.Error(err, "Container license is not supported") return err } - if client.NsxConfig.EnableVPCNetwork { + if config.HasVPCNamespaces() { if !util.IsLicensed(util.FeatureVPC) { err = errors.New("NSX license check failed") log.Error(err, "VPC license is not supported") diff --git a/pkg/nsx/client_test.go b/pkg/nsx/client_test.go index 87a4051fb..fc10537f2 100644 --- a/pkg/nsx/client_test.go +++ b/pkg/nsx/client_test.go @@ -340,9 +340,6 @@ func TestValidateLicense(t *testing.T) { }) defer patchDfw.Reset() - patchSetVpc := gomonkey.ApplyFunc(util.SetEnableVpcNetwork, func(enable bool) {}) - defer patchSetVpc.Reset() - client := &Client{ NsxConfig: &cf, NSXChecker: NSXHealthChecker{ diff --git a/pkg/nsx/services/securitypolicy/builder_test.go b/pkg/nsx/services/securitypolicy/builder_test.go index 0e371781a..75f725b7f 100644 --- a/pkg/nsx/services/securitypolicy/builder_test.go +++ b/pkg/nsx/services/securitypolicy/builder_test.go @@ -185,6 +185,7 @@ func Test_BuildSecurityPolicyForVPC(t *testing.T) { VPCInfo1[0].ProjectID = "default" VPCInfo1[0].VPCID = "vpc1" + config.SetMixedModeStateForTest(false, true) fakeService := fakeSecurityPolicyService() fakeService.NSXConfig.EnableVPCNetwork = true mockVPCService := mock.MockVPCServiceProvider{} @@ -436,6 +437,7 @@ func Test_BuildSecurityPolicyForVPC(t *testing.T) { } func Test_BuildPolicyGroupForT1(t *testing.T) { + config.SetMixedModeStateForTest(true, false) tests := []struct { name string inputPolicy *v1alpha1.SecurityPolicy @@ -452,8 +454,16 @@ func Test_BuildPolicyGroupForT1(t *testing.T) { }, } s := &SecurityPolicyService{ - Service: common.Service{}, + Service: common.Service{ + NSXConfig: &config.NSXOperatorConfig{ + CoeConfig: &config.CoeConfig{ + Cluster: "k8scl-one", + EnableVPCNetwork: false, + }, + }, + }, } + s.setUpStore(common.TagValueScopeSecurityPolicyUID, false) patches := gomonkey.ApplyMethod(reflect.TypeOf(&s.Service), "GetNamespaceUID", func(s *common.Service, ns string) types.UID { return types.UID(tagValueNSUID) @@ -461,7 +471,7 @@ func Test_BuildPolicyGroupForT1(t *testing.T) { defer patches.Reset() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - observedGroup, observedGroupPath, _ := service.buildPolicyGroup(tt.inputPolicy, common.ResourceTypeSecurityPolicy, nil) + observedGroup, observedGroupPath, _ := s.buildPolicyGroup(tt.inputPolicy, common.ResourceTypeSecurityPolicy, nil) assert.Equal(t, tt.expectedPolicyGroupID, observedGroup.Id) assert.Equal(t, tt.expectedPolicyGroupName, observedGroup.DisplayName) assert.Equal(t, tt.expectedPolicyGroupPath, observedGroupPath) @@ -470,6 +480,7 @@ func Test_BuildPolicyGroupForT1(t *testing.T) { } func Test_BuildPolicyGroupForVPC(t *testing.T) { + config.SetMixedModeStateForTest(false, true) VPCInfo := make([]common.VPCResourceInfo, 1) VPCInfo[0].OrgID = "default" VPCInfo[0].ProjectID = "project1" @@ -518,10 +529,19 @@ func Test_BuildPolicyGroupForVPC(t *testing.T) { } func Test_BuildTargetTags(t *testing.T) { + config.SetMixedModeStateForTest(true, false) common.TagValueScopeSecurityPolicyName = common.TagScopeSecurityPolicyCRName common.TagValueScopeSecurityPolicyUID = common.TagScopeSecurityPolicyCRUID - ruleTagID0 := service.buildRuleID(&spWithPodSelector, 0, common.ResourceTypeSecurityPolicy) + svc := &SecurityPolicyService{ + Service: common.Service{ + NSXConfig: &config.NSXOperatorConfig{ + CoeConfig: &config.CoeConfig{Cluster: "k8scl-one", EnableVPCNetwork: false}, + }, + }, + } + svc.setUpStore(common.TagValueScopeSecurityPolicyUID, false) + ruleTagID0 := svc.buildRuleID(&spWithPodSelector, 0, common.ResourceTypeSecurityPolicy) tests := []struct { name string inputPolicy *v1alpha1.SecurityPolicy @@ -590,24 +610,30 @@ func Test_BuildTargetTags(t *testing.T) { }, }, } - s := &SecurityPolicyService{ - Service: common.Service{}, - } - patches := gomonkey.ApplyMethod(reflect.TypeOf(&s.Service), "GetNamespaceUID", + patches := gomonkey.ApplyMethod(reflect.TypeOf(&svc.Service), "GetNamespaceUID", func(s *common.Service, ns string) types.UID { return types.UID(tagValueNSUID) }) defer patches.Reset() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - ruleBaseID := service.buildRuleID(tt.inputPolicy, tt.inputIndex, common.ResourceTypeSecurityPolicy) - assert.ElementsMatch(t, tt.expectedTags, service.buildTargetTags(tt.inputPolicy, tt.inputTargets, ruleBaseID, common.ResourceTypeSecurityPolicy)) + ruleBaseID := svc.buildRuleID(tt.inputPolicy, tt.inputIndex, common.ResourceTypeSecurityPolicy) + assert.ElementsMatch(t, tt.expectedTags, svc.buildTargetTags(tt.inputPolicy, tt.inputTargets, ruleBaseID, common.ResourceTypeSecurityPolicy)) }) } } func Test_BuildPeerTags(t *testing.T) { - ruleTagID0 := service.buildRuleID(&spWithPodSelector, 0, common.ResourceTypeSecurityPolicy) + config.SetMixedModeStateForTest(true, false) + svc := &SecurityPolicyService{ + Service: common.Service{ + NSXConfig: &config.NSXOperatorConfig{ + CoeConfig: &config.CoeConfig{Cluster: "k8scl-one", EnableVPCNetwork: false}, + }, + }, + } + svc.setUpStore(common.TagValueScopeSecurityPolicyUID, false) + ruleTagID0 := svc.buildRuleID(&spWithPodSelector, 0, common.ResourceTypeSecurityPolicy) tests := []struct { name string inputPolicy *v1alpha1.SecurityPolicy @@ -656,17 +682,14 @@ func Test_BuildPeerTags(t *testing.T) { }, }, } - s := &SecurityPolicyService{ - Service: common.Service{}, - } - patches := gomonkey.ApplyMethod(reflect.TypeOf(&s.Service), "GetNamespaceUID", + patches := gomonkey.ApplyMethod(reflect.TypeOf(&svc.Service), "GetNamespaceUID", func(s *common.Service, ns string) types.UID { return types.UID(tagValueNSUID) }) defer patches.Reset() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - assert.ElementsMatch(t, tt.expectedTags, service.buildPeerTags(tt.inputPolicy, &tt.inputPolicy.Spec.Rules[0], ruleTagID0, true, VPCScopeGroup, common.ResourceTypeSecurityPolicy)) + assert.ElementsMatch(t, tt.expectedTags, svc.buildPeerTags(tt.inputPolicy, &tt.inputPolicy.Spec.Rules[0], ruleTagID0, true, VPCScopeGroup, common.ResourceTypeSecurityPolicy)) }) } } @@ -1357,6 +1380,7 @@ func Test_BuildExpandedRuleID(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + config.SetMixedModeStateForTest(!tt.vpcEnabled, tt.vpcEnabled) svc.NSXConfig.EnableVPCNetwork = tt.vpcEnabled ruleBaseID := svc.buildRuleID(tt.inputSecurityPolicy, tt.ruleIdx, common.ResourceTypeSecurityPolicy) observedRuleID := svc.buildExpandedRuleID(tt.inputSecurityPolicy, tt.ruleIdx, ruleBaseID, tt.namedPort) @@ -1515,6 +1539,7 @@ func Test_BuildSecurityPolicyIDAndName(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + config.SetMixedModeStateForTest(!tc.vpcEnabled, tc.vpcEnabled) svc.setUpStore(common.TagValueScopeSecurityPolicyUID, false) svc.NSXConfig.EnableVPCNetwork = tc.vpcEnabled if tc.existingSecurityPolicy != nil { @@ -1611,6 +1636,7 @@ func Test_BuildGroupIDAndName(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + config.SetMixedModeStateForTest(!tc.enableVPC, tc.enableVPC) svc.NSXConfig.EnableVPCNetwork = tc.enableVPC dispName := svc.buildRulePeerGroupName(obj, tc.ruleIdx, tc.isSource) assert.Equal(t, tc.expName, dispName) @@ -1673,6 +1699,7 @@ func Test_BuildGroupIDAndName(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + config.SetMixedModeStateForTest(!tc.enableVPC, tc.enableVPC) svc.NSXConfig.EnableVPCNetwork = tc.enableVPC id, dispName := svc.buildAppliedGroupIDAndName(obj, tc.ruleIdx, tc.ruleBasedID, common.ResourceTypeNetworkPolicy) assert.Equal(t, tc.expId, id) @@ -1878,6 +1905,7 @@ func Test_dedupBlocks(t *testing.T) { } func Test_getAppliedGroupByRuleID(t *testing.T) { + config.SetMixedModeStateForTest(false, true) common.TagValueScopeSecurityPolicyName = common.TagScopeSecurityPolicyName common.TagValueScopeSecurityPolicyUID = common.TagScopeSecurityPolicyUID @@ -1971,6 +1999,7 @@ func Test_getAppliedGroupByRuleID(t *testing.T) { } func Test_getPeerGroupByRuleID(t *testing.T) { + config.SetMixedModeStateForTest(false, true) common.TagValueScopeSecurityPolicyName = common.TagScopeSecurityPolicyName common.TagValueScopeSecurityPolicyUID = common.TagScopeSecurityPolicyUID @@ -2101,6 +2130,7 @@ func Test_getPeerGroupByRuleID(t *testing.T) { } func Test_getRuleIDByUUIDAndRuleHash(t *testing.T) { + config.SetMixedModeStateForTest(false, true) common.TagValueScopeSecurityPolicyName = common.TagScopeSecurityPolicyName common.TagValueScopeSecurityPolicyUID = common.TagScopeSecurityPolicyUID @@ -2287,6 +2317,7 @@ func Test_buildRuleID(t *testing.T) { for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { + config.SetMixedModeStateForTest(!tt.enableVPC, tt.enableVPC) service := fakeSecurityPolicyService() service.NSXConfig.EnableVPCNetwork = tt.enableVPC diff --git a/pkg/nsx/services/securitypolicy/expand_test.go b/pkg/nsx/services/securitypolicy/expand_test.go index 74e9e6061..2278bb7ac 100644 --- a/pkg/nsx/services/securitypolicy/expand_test.go +++ b/pkg/nsx/services/securitypolicy/expand_test.go @@ -540,6 +540,7 @@ func Test_ExpandRule(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { + config.SetMixedModeStateForTest(!tc.vpcEnabled, tc.vpcEnabled) // Initial the security policy related tags. This is executed in `InitializeSecurityPolicy` in // function logic. if tc.vpcEnabled { diff --git a/pkg/nsx/services/securitypolicy/firewall_test.go b/pkg/nsx/services/securitypolicy/firewall_test.go index b5d9a3fc0..3e2d490a7 100644 --- a/pkg/nsx/services/securitypolicy/firewall_test.go +++ b/pkg/nsx/services/securitypolicy/firewall_test.go @@ -508,6 +508,7 @@ var ( ) func Test_GetSecurityService(t *testing.T) { + config.SetMixedModeStateForTest(false, true) fakeService := fakeSecurityPolicyService() fakeService.NSXConfig.EnableVPCNetwork = true commonService := fakeService.Service @@ -527,6 +528,7 @@ func Test_GetSecurityService(t *testing.T) { } func Test_InitializeSecurityPolicy(t *testing.T) { + config.SetMixedModeStateForTest(false, true) fakeService := fakeSecurityPolicyService() fakeService.NSXConfig.EnableVPCNetwork = true commonService := fakeService.Service @@ -699,6 +701,7 @@ func Test_createOrUpdateGroups(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + config.SetMixedModeStateForTest(false, true) common.TagValueScopeSecurityPolicyName = common.TagScopeSecurityPolicyName common.TagValueScopeSecurityPolicyUID = common.TagScopeSecurityPolicyUID @@ -2389,6 +2392,7 @@ func Test_CreateOrUpdateSecurityPolicyFromNetworkPolicy(t *testing.T) { } func Test_createOrUpdateT1SecurityPolicy(t *testing.T) { + config.SetMixedModeStateForTest(true, false) fakeService := fakeSecurityPolicyService() fakeService.NSXConfig.EnableVPCNetwork = false @@ -2533,6 +2537,7 @@ func Test_createOrUpdateT1SecurityPolicy(t *testing.T) { } func Test_createOrUpdateVPCSecurityPolicy(t *testing.T) { + config.SetMixedModeStateForTest(false, true) VPCInfo := make([]common.VPCResourceInfo, 1) VPCInfo[0].OrgID = "default" VPCInfo[0].ProjectID = "projectQuality" @@ -2704,6 +2709,7 @@ func Test_createOrUpdateVPCSecurityPolicy(t *testing.T) { } func Test_createOrUpdateVPCSecurityPolicyInDefaultProject(t *testing.T) { + config.SetMixedModeStateForTest(false, true) VPCInfo := make([]common.VPCResourceInfo, 1) VPCInfo[0].OrgID = "default" VPCInfo[0].ProjectID = "default" @@ -2884,6 +2890,7 @@ func Test_createOrUpdateVPCSecurityPolicyInDefaultProject(t *testing.T) { } func Test_GetFinalSecurityPolicyResourceForT1(t *testing.T) { + config.SetMixedModeStateForTest(true, false) fakeService := fakeSecurityPolicyService() type args struct { @@ -2973,6 +2980,7 @@ func Test_GetFinalSecurityPolicyResourceForT1(t *testing.T) { } func Test_GetFinalSecurityPolicyResourceForVPC(t *testing.T) { + config.SetMixedModeStateForTest(false, true) VPCInfo := make([]common.VPCResourceInfo, 1) VPCInfo[0].OrgID = "default" VPCInfo[0].ProjectID = "projectQuality" @@ -3228,6 +3236,7 @@ func Test_ConvertNetworkPolicyToInternalSecurityPolicies(t *testing.T) { } func Test_GetFinalSecurityPolicyResourceFromNetworkPolicy(t *testing.T) { + config.SetMixedModeStateForTest(false, true) VPCInfo := make([]common.VPCResourceInfo, 1) VPCInfo[0].OrgID = "default" VPCInfo[0].ProjectID = "projectQuality" diff --git a/pkg/nsx/services/securitypolicy/parse.go b/pkg/nsx/services/securitypolicy/parse.go index 27a4e55b5..902d29e07 100644 --- a/pkg/nsx/services/securitypolicy/parse.go +++ b/pkg/nsx/services/securitypolicy/parse.go @@ -7,6 +7,7 @@ import ( "errors" "github.com/vmware-tanzu/nsx-operator/pkg/apis/legacy/v1alpha1" + "github.com/vmware-tanzu/nsx-operator/pkg/config" "github.com/vmware-tanzu/nsx-operator/pkg/nsx/services/common" "github.com/vmware-tanzu/nsx-operator/pkg/util" ) @@ -62,8 +63,10 @@ func getDefaultProjectDomain() string { return "default" } +// IsVPCEnabled returns whether VPC namespaces exist. Callers must ensure mixed-mode +// state has been initialized (InitMixedMode in main; SetMixedModeStateForTest in tests). func IsVPCEnabled(service *SecurityPolicyService) bool { - return service.NSXConfig.EnableVPCNetwork + return config.HasVPCNamespaces() } func getScopeCluserTag(service *SecurityPolicyService) string { diff --git a/pkg/nsx/util/license.go b/pkg/nsx/util/license.go index 2a57b0957..ee663fcf7 100644 --- a/pkg/nsx/util/license.go +++ b/pkg/nsx/util/license.go @@ -17,10 +17,11 @@ const ( ) var ( - licenseMutex sync.Mutex - licenseMap = map[string]bool{} - FeaturesToCheck = []string{} - FeatureLicenseMap = map[string][]string{ + licenseMutex sync.Mutex + licenseMap = map[string]bool{} + hasVPCNamespacesFunc func() bool // set from cmd/main after mixed-mode init to avoid import cycle + FeaturesToCheck = []string{} + FeatureLicenseMap = map[string][]string{ FeatureContainer: { LicenseContainerNetwork, LicenseContainer, @@ -29,7 +30,6 @@ var ( FeatureVPCSecurity: {LicenseVPCSecurity}, FeatureVPC: {LicenseVPCNetworking}, } - enableVpcNetwork bool ) func init() { @@ -47,16 +47,28 @@ type NsxLicense struct { ResultCount int `json:"result_count"` } +// SetHasVPCNamespacesFunc sets the callback used by GetDFWLicense/UpdateDFWLicense. +// Must be called from cmd/main after mixed-mode init to avoid config->util import cycle. +func SetHasVPCNamespacesFunc(f func() bool) { + hasVPCNamespacesFunc = f +} + +func hasVPCNamespaces() bool { + if hasVPCNamespacesFunc != nil { + return hasVPCNamespacesFunc() + } + return false +} + func GetDFWLicense() bool { - if enableVpcNetwork { + if hasVPCNamespaces() { return IsLicensed(LicenseVPCSecurity) - } else { - return IsLicensed(LicenseDFW) } + return IsLicensed(LicenseDFW) } func UpdateDFWLicense(isLicensed bool) { - if enableVpcNetwork { + if hasVPCNamespaces() { UpdateLicense(LicenseVPCSecurity, isLicensed) } else { UpdateLicense(LicenseDFW, isLicensed) @@ -69,10 +81,6 @@ func IsLicensed(feature string) bool { return licenseMap[feature] } -func SetEnableVpcNetwork(vpcNetwork bool) { - enableVpcNetwork = vpcNetwork -} - func UpdateLicense(feature string, isLicensed bool) { licenseMutex.Lock() licenseMap[feature] = isLicensed diff --git a/pkg/nsx/util/license_test.go b/pkg/nsx/util/license_test.go index fb077925c..fcefa2d44 100644 --- a/pkg/nsx/util/license_test.go +++ b/pkg/nsx/util/license_test.go @@ -93,6 +93,8 @@ func TestSearchLicense(t *testing.T) { } func TestUpdateFeatureLicense(t *testing.T) { + t.Cleanup(func() { SetHasVPCNamespacesFunc(nil) }) + SetHasVPCNamespacesFunc(nil) // Normal case licenses := &NsxLicense{ @@ -131,7 +133,8 @@ func TestUpdateFeatureLicense(t *testing.T) { assert.False(t, IsLicensed(FeatureContainer)) assert.False(t, IsLicensed(FeatureVPC)) - SetEnableVpcNetwork(true) + // Equivalent to legacy SetEnableVpcNetwork(true): cluster has VPC namespaces. + SetHasVPCNamespacesFunc(func() bool { return true }) licenses = &NsxLicense{ Results: []struct { FeatureName string `json:"feature_name"` @@ -148,8 +151,8 @@ func TestUpdateFeatureLicense(t *testing.T) { assert.False(t, IsLicensed(FeatureContainer)) assert.True(t, IsLicensed(FeatureVPC)) - // enable vpc network: true; LicenseVPCSecurity: true, GetDFWLicense: true - SetEnableVpcNetwork(true) + // Has VPC namespaces; LicenseVPCSecurity: true, GetDFWLicense: true (mirrors main SetEnableVpcNetwork(true)) + SetHasVPCNamespacesFunc(func() bool { return true }) licenses = &NsxLicense{ Results: []struct { FeatureName string `json:"feature_name"` @@ -167,8 +170,8 @@ func TestUpdateFeatureLicense(t *testing.T) { assert.False(t, IsLicensed(FeatureContainer)) assert.True(t, IsLicensed(FeatureVPC)) - // enable vpc network: false; LicenseDFW: false, GetDFWLicense: false - SetEnableVpcNetwork(false) + // Equivalent to legacy SetEnableVpcNetwork(false): no VPC namespaces, use DFW license for GetDFWLicense. + SetHasVPCNamespacesFunc(nil) licenses = &NsxLicense{ Results: []struct { FeatureName string `json:"feature_name"` @@ -187,60 +190,48 @@ func TestUpdateFeatureLicense(t *testing.T) { assert.True(t, IsLicensed(FeatureVPC)) } -func TestSetEnableVpcNetwork(t *testing.T) { - // Test enabling VPC network - SetEnableVpcNetwork(true) - assert.Equal(t, []string{LicenseVPCSecurity}, FeatureLicenseMap[FeatureVPCSecurity]) - - // Test disabling VPC network - SetEnableVpcNetwork(false) - assert.Equal(t, []string{LicenseDFW}, FeatureLicenseMap[FeatureDFW]) - - // Test toggling back to enabled - SetEnableVpcNetwork(true) - assert.Equal(t, []string{LicenseVPCSecurity}, FeatureLicenseMap[FeatureVPCSecurity]) -} func TestGetSecurityPolicyLicense(t *testing.T) { - // Test with VPC network disabled, DFW licensed - SetEnableVpcNetwork(false) + // Test with VPC namespaces disabled (callback nil or returns false), DFW licensed + SetHasVPCNamespacesFunc(nil) UpdateLicense(LicenseDFW, true) assert.True(t, GetDFWLicense()) - // Test with VPC network disabled, DFW not licensed + // Test with VPC namespaces disabled, DFW not licensed UpdateLicense(LicenseDFW, false) assert.False(t, GetDFWLicense()) - // Test with VPC network enabled, VPC security licensed - SetEnableVpcNetwork(true) + // Test with VPC namespaces enabled (callback returns true), VPC security licensed + SetHasVPCNamespacesFunc(func() bool { return true }) UpdateLicense(LicenseVPCSecurity, true) assert.True(t, GetDFWLicense()) - // Test with VPC network enabled, VPC security not licensed + // Test with VPC namespaces enabled, VPC security not licensed UpdateLicense(LicenseVPCSecurity, false) assert.False(t, GetDFWLicense()) // Clean up - SetEnableVpcNetwork(false) + SetHasVPCNamespacesFunc(nil) } + func TestUpdateDFWLicense(t *testing.T) { - // Test with VPC network disabled, updating to licensed - SetEnableVpcNetwork(false) + // Test with VPC namespaces disabled, updating to licensed + SetHasVPCNamespacesFunc(nil) UpdateDFWLicense(true) assert.True(t, licenseMap[LicenseDFW]) - // Test with VPC network disabled, updating to not licensed + // Test with VPC namespaces disabled, updating to not licensed UpdateDFWLicense(false) assert.False(t, licenseMap[LicenseDFW]) - // Test with VPC network enabled, updating to licensed - SetEnableVpcNetwork(true) + // Test with VPC namespaces enabled, updating to licensed + SetHasVPCNamespacesFunc(func() bool { return true }) UpdateDFWLicense(true) assert.True(t, licenseMap[LicenseVPCSecurity]) - // Test with VPC network enabled, updating to not licensed + // Test with VPC namespaces enabled, updating to not licensed UpdateDFWLicense(false) assert.False(t, licenseMap[LicenseVPCSecurity]) // Clean up - SetEnableVpcNetwork(false) + SetHasVPCNamespacesFunc(nil) } From b4fd5bffa5978eb69153efc8025347c0e73fc3c8 Mon Sep 17 00:00:00 2001 From: Qian Sun Date: Mon, 6 Apr 2026 09:23:40 +0000 Subject: [PATCH 2/2] 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/ --- pkg/config/mixed_mode.go | 12 ++++ pkg/config/mixed_mode_test.go | 44 ++++++++++++ pkg/controllers/common/namespace_filter.go | 69 +++++++++++++++++++ .../ipaddressallocation_controller.go | 1 + .../namespace/namespace_controller.go | 11 ++- .../namespace/namespace_controller_test.go | 12 +++- .../networkinfo/networkinfo_controller.go | 1 + .../networkpolicy/networkpolicy_controller.go | 1 + pkg/controllers/pod/pod_controller.go | 1 + .../service/service_lb_controller.go | 1 + .../staticroute/staticroute_controller.go | 1 + pkg/controllers/subnet/subnet_controller.go | 1 + .../subnetbinding/subnetbinding_controller.go | 1 + .../subnetipreservation_controller.go | 1 + .../subnetport/subnetport_controller.go | 1 + .../subnetset/subnetset_controller.go | 1 + 16 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 pkg/controllers/common/namespace_filter.go diff --git a/pkg/config/mixed_mode.go b/pkg/config/mixed_mode.go index 668db44b9..7ebf7570d 100644 --- a/pkg/config/mixed_mode.go +++ b/pkg/config/mixed_mode.go @@ -317,3 +317,15 @@ func IsPerNamespaceProvidersSupported() bool { defer stateMu.RUnlock() return perNamespaceProvidersSupported != nil && *perNamespaceProvidersSupported } + +// IsVPCNamespace reports whether ns should be treated as a VPC namespace. +// In legacy mode (pre-9.2, per-namespace providers not supported) the whole +// cluster runs a single provider, so the cluster-level HasVPCNamespaces flag +// (derived from EnableVPCNetwork) is returned regardless of the namespace. +// In mixed mode, non-empty VPCNetworkConfigAnnotation marks a VPC namespace. +func IsVPCNamespace(ns *v1.Namespace) bool { + if !IsPerNamespaceProvidersSupported() { + return HasVPCNamespaces() + } + return namespaceHasVPCNetworkConfig(ns) +} diff --git a/pkg/config/mixed_mode_test.go b/pkg/config/mixed_mode_test.go index 76da6e2aa..1630306ed 100644 --- a/pkg/config/mixed_mode_test.go +++ b/pkg/config/mixed_mode_test.go @@ -541,6 +541,50 @@ func TestRefreshMixedModeState(t *testing.T) { }) } +func TestIsVPCNamespace(t *testing.T) { + t.Run("nil namespace", func(t *testing.T) { + resetMixedModeState() + assert.False(t, IsVPCNamespace(nil)) + }) + + t.Run("per-namespace on vpc annotation", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + stateMu.Unlock() + ns := makeNamespace("x", "default") + assert.True(t, IsVPCNamespace(ns)) + }) + + t.Run("per-namespace on no annotation counts as T1", func(t *testing.T) { + resetMixedModeState() + supported := true + stateMu.Lock() + perNamespaceProvidersSupported = &supported + stateMu.Unlock() + ns := makeNamespace("y", "") + assert.False(t, IsVPCNamespace(ns)) + }) + + t.Run("per-namespace off IsVPCNamespace uses cluster flags", func(t *testing.T) { + resetMixedModeState() + supported := false + stateMu.Lock() + perNamespaceProvidersSupported = &supported + hasVPCNamespaces = true + hasT1Namespaces = false + stateMu.Unlock() + ns := makeNamespace("any", "") + assert.True(t, IsVPCNamespace(ns)) + stateMu.Lock() + hasVPCNamespaces = false + hasT1Namespaces = true + stateMu.Unlock() + assert.False(t, IsVPCNamespace(ns)) + }) +} + // ---------- Getters and SetMixedModeStateForTest ---------- func TestGettersAndSetMixedModeStateForTest(t *testing.T) { diff --git a/pkg/controllers/common/namespace_filter.go b/pkg/controllers/common/namespace_filter.go new file mode 100644 index 000000000..d0a947161 --- /dev/null +++ b/pkg/controllers/common/namespace_filter.go @@ -0,0 +1,69 @@ +/* Copyright © 2026 Broadcom, Inc. All Rights Reserved. + SPDX-License-Identifier: Apache-2.0 */ + +package common + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + "github.com/vmware-tanzu/nsx-operator/pkg/config" +) + +// isVPCNamespaceByName fetches the Namespace by name and calls config.IsVPCNamespace. +// Returns true when the namespace cannot be fetched (transient error or already +// gone) so the Reconcile loop can decide what to do. +func isVPCNamespaceByName(c client.Reader, ns string) bool { + namespace := &corev1.Namespace{} + if err := c.Get(context.Background(), types.NamespacedName{Name: ns}, namespace); err != nil { + if !apierrors.IsNotFound(err) { + log.Error(err, "Failed to get Namespace for VPC predicate; allowing event through", "namespace", ns) + } + return true + } + return config.IsVPCNamespace(namespace) +} + +// VPCNamespacePredicate returns a predicate that filters events for VPC-only +// controllers. Events are passed when config.IsVPCNamespace reports true for +// the resource's namespace. +// +// Behaviour by event type: +// - Create / Update / Generic: allowed only for VPC namespaces. +// - Delete: always allowed so the controller can clean up any existing NSX +// resources even if the namespace is already gone. +// +// The namespace check is skipped for cluster-scoped resources (empty namespace), +// which are always allowed through. +func VPCNamespacePredicate(c client.Reader) predicate.Funcs { + isVPCNs := func(ns string) bool { + if ns == "" { + // Cluster-scoped resource: no per-namespace filtering. + return true + } + return isVPCNamespaceByName(c, ns) + } + + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return isVPCNs(e.Object.GetNamespace()) + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return isVPCNs(e.ObjectNew.GetNamespace()) + }, + // Always allow Delete events so the controller can clean up NSX + // resources regardless of the current namespace network metadata. + DeleteFunc: func(e event.DeleteEvent) bool { + return true + }, + GenericFunc: func(e event.GenericEvent) bool { + return isVPCNs(e.Object.GetNamespace()) + }, + } +} diff --git a/pkg/controllers/ipaddressallocation/ipaddressallocation_controller.go b/pkg/controllers/ipaddressallocation/ipaddressallocation_controller.go index edcc9030e..9d5cb93cf 100644 --- a/pkg/controllers/ipaddressallocation/ipaddressallocation_controller.go +++ b/pkg/controllers/ipaddressallocation/ipaddressallocation_controller.go @@ -132,6 +132,7 @@ func (r *IPAddressAllocationReconciler) handleDeletion(req ctrl.Request, obj *v1 func (r *IPAddressAllocationReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.IPAddressAllocation{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions( controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), diff --git a/pkg/controllers/namespace/namespace_controller.go b/pkg/controllers/namespace/namespace_controller.go index 26d9fba81..b71c377ef 100644 --- a/pkg/controllers/namespace/namespace_controller.go +++ b/pkg/controllers/namespace/namespace_controller.go @@ -253,8 +253,17 @@ func (r *NamespaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) ( return common.ResultNormal, client.IgnoreNotFound(err) } - // processing create/update event ns := obj.GetName() + + // Only process VPC namespaces. Migration destination is VPC strictly, so a non-VPC + // namespace never carries VPC infra and can safely be skipped on both create + // and delete. + if !config.IsVPCNamespace(obj) { + log.Info("Skipping Namespace: not a VPC namespace", "Namespace", ns) + return common.ResultNormal, nil + } + + // processing create/update event if obj.ObjectMeta.DeletionTimestamp.IsZero() { metrics.CounterInc(r.NSXConfig, metrics.ControllerUpdateTotal, common.MetricResTypeNamespace) log.Info("Start processing Namespace create/update event", "Namespace", ns) diff --git a/pkg/controllers/namespace/namespace_controller_test.go b/pkg/controllers/namespace/namespace_controller_test.go index 6b43979a4..ec167816c 100644 --- a/pkg/controllers/namespace/namespace_controller_test.go +++ b/pkg/controllers/namespace/namespace_controller_test.go @@ -128,6 +128,12 @@ func TestGetDefaultNetworkConfigName(t *testing.T) { } func TestNamespaceReconciler_Reconcile(t *testing.T) { + // Simulate a legacy VPC cluster (EnableVPCNetwork=true, per-namespace + // providers not supported) so that all namespaces are treated as VPC + // namespaces by IsVPCNamespace. + config.SetMixedModeStateForTest(false, true) + t.Cleanup(func() { config.SetMixedModeStateForTest(false, false) }) + nc := v1alpha1.VPCNetworkConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: "fake-VPCNetworkConfig", @@ -374,11 +380,11 @@ func TestCreateDefaultSubnetSet(t *testing.T) { expectedSubnetSets: 1, // VM networkStack: v1alpha1.FullStackVPC, nameSpaceType: ctlcommon.SystemNs, + // Stub NSXCheckVersion so getSystemNsDefaultSize does not call a nil cluster (empty Client in createNameSpaceReconciler). setupMocks: func(r *NamespaceReconciler) *gomonkey.Patches { - patches := gomonkey.ApplyPrivateMethod(reflect.TypeOf(r), "getSystemNsDefaultSize", func(_ *NamespaceReconciler) int { - return 8 + return gomonkey.ApplyMethod(reflect.TypeOf(&nsx.Client{}), "NSXCheckVersion", func(_ *nsx.Client, _ int) bool { + return true // treat as NSX >= 9.1 => MinSubnetSizeV91 (8) }) - return patches }, }, { diff --git a/pkg/controllers/networkinfo/networkinfo_controller.go b/pkg/controllers/networkinfo/networkinfo_controller.go index 76c0f5342..4a26b64b5 100644 --- a/pkg/controllers/networkinfo/networkinfo_controller.go +++ b/pkg/controllers/networkinfo/networkinfo_controller.go @@ -469,6 +469,7 @@ func (r *NetworkInfoReconciler) getNSXLBSNATIP(nc *v1alpha1.VPCNetworkConfigurat func (r *NetworkInfoReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.NetworkInfo{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions( controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), diff --git a/pkg/controllers/networkpolicy/networkpolicy_controller.go b/pkg/controllers/networkpolicy/networkpolicy_controller.go index f663d7ddc..8e129406c 100644 --- a/pkg/controllers/networkpolicy/networkpolicy_controller.go +++ b/pkg/controllers/networkpolicy/networkpolicy_controller.go @@ -140,6 +140,7 @@ func (r *NetworkPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Reques func (r *NetworkPolicyReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&networkingv1.NetworkPolicy{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). Watches( &v1.Pod{}, &EnqueueRequestForPod{ diff --git a/pkg/controllers/pod/pod_controller.go b/pkg/controllers/pod/pod_controller.go index 2499c4d8d..a1543e5ef 100644 --- a/pkg/controllers/pod/pod_controller.go +++ b/pkg/controllers/pod/pod_controller.go @@ -257,6 +257,7 @@ func (r *PodReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1.Pod{}). WithEventFilter(PredicateFuncsPod). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions( controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), diff --git a/pkg/controllers/service/service_lb_controller.go b/pkg/controllers/service/service_lb_controller.go index de35aee0a..b5e7b33c5 100644 --- a/pkg/controllers/service/service_lb_controller.go +++ b/pkg/controllers/service/service_lb_controller.go @@ -121,6 +121,7 @@ func (r *ServiceLbReconciler) setServiceLbStatus(ctx context.Context, lbService func (r *ServiceLbReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1.Service{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions( controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), diff --git a/pkg/controllers/staticroute/staticroute_controller.go b/pkg/controllers/staticroute/staticroute_controller.go index 40c26390a..5b95a2da2 100644 --- a/pkg/controllers/staticroute/staticroute_controller.go +++ b/pkg/controllers/staticroute/staticroute_controller.go @@ -172,6 +172,7 @@ func getExistingConditionOfType(conditionType v1alpha1.StaticRouteStatusConditio func (r *StaticRouteReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.StaticRoute{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions( controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), diff --git a/pkg/controllers/subnet/subnet_controller.go b/pkg/controllers/subnet/subnet_controller.go index 981cd4b8b..75b38c537 100644 --- a/pkg/controllers/subnet/subnet_controller.go +++ b/pkg/controllers/subnet/subnet_controller.go @@ -523,6 +523,7 @@ func (r *SubnetReconciler) start(mgr ctrl.Manager, hookServer webhook.Server) er func (r *SubnetReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.Subnet{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions( controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), diff --git a/pkg/controllers/subnetbinding/subnetbinding_controller.go b/pkg/controllers/subnetbinding/subnetbinding_controller.go index 45fc38037..58a6b96ef 100644 --- a/pkg/controllers/subnetbinding/subnetbinding_controller.go +++ b/pkg/controllers/subnetbinding/subnetbinding_controller.go @@ -170,6 +170,7 @@ var PredicateFuncsForBindingMaps = predicate.Funcs{ func (r *Reconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.SubnetConnectionBindingMap{}, builder.WithPredicates(PredicateFuncsForBindingMaps)). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions(controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), }). diff --git a/pkg/controllers/subnetipreservation/subnetipreservation_controller.go b/pkg/controllers/subnetipreservation/subnetipreservation_controller.go index a35ad8b6a..c164ea91e 100644 --- a/pkg/controllers/subnetipreservation/subnetipreservation_controller.go +++ b/pkg/controllers/subnetipreservation/subnetipreservation_controller.go @@ -101,6 +101,7 @@ func subnetIPReservationSubnetNameIndexFunc(obj client.Object) []string { func (r *Reconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.SubnetIPReservation{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions(controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), }). diff --git a/pkg/controllers/subnetport/subnetport_controller.go b/pkg/controllers/subnetport/subnetport_controller.go index 72ada7a59..2a15edd23 100644 --- a/pkg/controllers/subnetport/subnetport_controller.go +++ b/pkg/controllers/subnetport/subnetport_controller.go @@ -403,6 +403,7 @@ func (r *SubnetPortReconciler) deleteSubnetPortByName(ctx context.Context, ns st func (r *SubnetPortReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.SubnetPort{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions( controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), diff --git a/pkg/controllers/subnetset/subnetset_controller.go b/pkg/controllers/subnetset/subnetset_controller.go index d96f6fa34..11d211087 100644 --- a/pkg/controllers/subnetset/subnetset_controller.go +++ b/pkg/controllers/subnetset/subnetset_controller.go @@ -385,6 +385,7 @@ func getExistingConditionOfType(conditionType v1alpha1.ConditionType, existingCo func (r *SubnetSetReconciler) setupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&v1alpha1.SubnetSet{}). + WithEventFilter(common.VPCNamespacePredicate(r.Client)). WithOptions(controller.Options{ MaxConcurrentReconciles: common.NumReconcile(), }).