Skip to content

Commit 0fea7b3

Browse files
committed
Implement Mixed Mode State Detection
Replace the global boolean cf.CoeConfig.EnableVPCNetwork with namespace-label-driven mixed-mode state: HasT1Namespaces and HasVPCNamespaces. New module: pkg/config/mixed_mode.go - Checks SupervisorCapabilities CRD (iaas.vmware.com/v1alpha1) for supports_per_namespace_network_providers capability. - If supported: scans namespace labels (iaas.vmware.com/network-provider) to derive HasT1Namespaces (nsx-t1) and HasVPCNamespaces (nsx-vpc or vsphere-network). - If not supported (legacy/pre-9.2): falls back to EnableVPCNetwork config flag. - Provides InitializeMixedModeState(), RefreshMixedModeState(), HasT1Namespaces(), HasVPCNamespaces(), GetNamespaceNetworkProvider() APIs. 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.
1 parent 6f7ddad commit 0fea7b3

11 files changed

Lines changed: 840 additions & 56 deletions

File tree

cmd/main.go

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
corev1 "k8s.io/api/core/v1"
1616
"k8s.io/apimachinery/pkg/runtime"
1717
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
18+
"k8s.io/client-go/dynamic"
19+
"k8s.io/client-go/kubernetes"
1820
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
1921
ctrl "sigs.k8s.io/controller-runtime"
2022
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -113,7 +115,7 @@ func init() {
113115

114116
func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) {
115117
// Generate webhook certificates and start refreshing webhook certificates periodically
116-
if cf.CoeConfig.EnableVPCNetwork {
118+
if config.HasVPCNamespaces() {
117119
if err := pkgutil.GenerateWebhookCerts(); err != nil {
118120
log.Error(err, "Failed to generate webhook certificates")
119121
os.Exit(1)
@@ -123,7 +125,7 @@ func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) {
123125
}
124126

125127
// Initialize and start the system health reporter
126-
if cf.CoeConfig.EnableVPCNetwork && cf.EnableInventory && cf.CoeConfig.EnableSha {
128+
if config.HasVPCNamespaces() && cf.EnableInventory && cf.CoeConfig.EnableSha {
127129
health.Start(nsxClient, cf, mgr.GetClient())
128130
}
129131

@@ -136,7 +138,7 @@ func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) {
136138

137139
checkLicense(nsxClient, cf.LicenseValidationInterval)
138140

139-
if cf.K8sConfig.EnableRestore && cf.CoeConfig.EnableVPCNetwork {
141+
if cf.K8sConfig.EnableRestore && config.HasVPCNamespaces() {
140142
var err error
141143
restoreMode, err = pkgutil.CompareNSXRestore(mgr.GetClient(), nsxClient)
142144
if err != nil {
@@ -153,7 +155,7 @@ func startServiceController(mgr manager.Manager, nsxClient *nsx.Client) {
153155
var hookServer webhook.Server
154156
var subnetSetReconcile *subnetset.SubnetSetReconciler
155157

156-
if cf.CoeConfig.EnableVPCNetwork {
158+
if config.HasVPCNamespaces() {
157159
// Check NSX version for VPC networking mode
158160
if !commonService.NSXClient.NSXCheckVersion(nsx.VPC) {
159161
log.Error(nil, "VPC mode cannot be enabled if NSX version is lower than 4.1.1")
@@ -329,6 +331,20 @@ func main() {
329331
os.Exit(1)
330332
}
331333

334+
// Initialize mixed-mode state (has_t1_namespaces / has_vpc_namespaces)
335+
clientset, err := kubernetes.NewForConfig(cfg)
336+
if err != nil {
337+
log.Error(err, "Failed to create kubernetes clientset")
338+
os.Exit(1)
339+
}
340+
dynClient, err := dynamic.NewForConfig(cfg)
341+
if err != nil {
342+
log.Error(err, "Failed to create dynamic kubernetes client")
343+
os.Exit(1)
344+
}
345+
config.InitMixedMode(context.Background(), clientset, dynClient, cf.CoeConfig.EnableVPCNetwork)
346+
util.SetHasVPCNamespacesFunc(config.HasVPCNamespaces)
347+
332348
if cf.HAEnabled() {
333349
go electMaster(mgr, nsxClient)
334350
} else {

pkg/config/mixed_mode.go

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/* Copyright © 2026 VMware, Inc. All Rights Reserved.
2+
SPDX-License-Identifier: Apache-2.0 */
3+
4+
package config
5+
6+
import (
7+
"context"
8+
"sync"
9+
10+
"go.uber.org/zap"
11+
v1 "k8s.io/api/core/v1"
12+
"k8s.io/apimachinery/pkg/api/errors"
13+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
15+
"k8s.io/apimachinery/pkg/runtime/schema"
16+
"k8s.io/client-go/dynamic"
17+
"k8s.io/client-go/kubernetes"
18+
)
19+
20+
const (
21+
NetworkProviderLabel = "iaas.vmware.com/network-provider"
22+
ProviderNSXT1 = "nsx-t1"
23+
ProviderNSXVPC = "nsx-vpc"
24+
ProviderVSphereNetwork = "vsphere-network"
25+
26+
supervisorCapabilitiesName = "supervisor-capabilities"
27+
)
28+
29+
var (
30+
supervisorCapabilitiesGVR = schema.GroupVersionResource{
31+
Group: "iaas.vmware.com",
32+
Version: "v1alpha1",
33+
Resource: "supervisorcapabilities",
34+
}
35+
36+
stateMu sync.RWMutex
37+
hasT1Namespaces bool
38+
hasVPCNamespaces bool
39+
perNamespaceProvidersSupported *bool
40+
stateInitialized bool
41+
stateLog *zap.SugaredLogger
42+
)
43+
44+
func init() {
45+
zapLogger, _ := zap.NewProduction()
46+
stateLog = zapLogger.Sugar()
47+
}
48+
49+
func checkPerNamespaceProvidersSupported(ctx context.Context, dynClient dynamic.Interface) bool {
50+
obj, err := dynClient.Resource(supervisorCapabilitiesGVR).Get(
51+
ctx, supervisorCapabilitiesName, metav1.GetOptions{})
52+
if err != nil {
53+
if errors.IsNotFound(err) {
54+
stateLog.Info("SupervisorCapabilities CRD not found; " +
55+
"falling back to legacy config")
56+
} else {
57+
stateLog.Infof("Failed to get SupervisorCapabilities: %v; "+
58+
"falling back to legacy config", err)
59+
}
60+
return false
61+
}
62+
return extractCapability(obj)
63+
}
64+
65+
func extractCapability(obj *unstructured.Unstructured) bool {
66+
status, found, err := unstructured.NestedMap(obj.Object, "status")
67+
if err != nil || !found {
68+
return false
69+
}
70+
services, found, err := unstructured.NestedMap(status, "services")
71+
if err != nil || !found {
72+
return false
73+
}
74+
for _, svcCaps := range services {
75+
capsMap, ok := svcCaps.(map[string]interface{})
76+
if !ok {
77+
continue
78+
}
79+
cap, ok := capsMap["supports_per_namespace_network_providers"]
80+
if !ok {
81+
continue
82+
}
83+
capMap, ok := cap.(map[string]interface{})
84+
if !ok {
85+
continue
86+
}
87+
activated, ok := capMap["activated"]
88+
if ok {
89+
if b, ok := activated.(bool); ok && b {
90+
return true
91+
}
92+
}
93+
}
94+
return false
95+
}
96+
97+
func scanNamespaceLabels(ctx context.Context, clientset kubernetes.Interface) (hasT1 bool, hasVPC bool) {
98+
nsList, err := clientset.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})
99+
if err != nil {
100+
stateLog.Errorf("Failed to list namespaces for mixed-mode state detection: %v", err)
101+
return false, false
102+
}
103+
for _, ns := range nsList.Items {
104+
provider := ns.Labels[NetworkProviderLabel]
105+
switch provider {
106+
case ProviderNSXT1:
107+
hasT1 = true
108+
case ProviderNSXVPC:
109+
hasVPC = true
110+
case ProviderVSphereNetwork:
111+
hasVPC = true
112+
}
113+
}
114+
return hasT1, hasVPC
115+
}
116+
117+
// InitMixedMode initializes mixed-mode state by checking SupervisorCapabilities
118+
// and scanning namespace labels. If per-namespace providers are not supported,
119+
// falls back to the legacy EnableVPCNetwork flag.
120+
func InitMixedMode(ctx context.Context, clientset kubernetes.Interface, dynClient dynamic.Interface, enableVPCNetwork bool) {
121+
stateMu.Lock()
122+
defer stateMu.Unlock()
123+
124+
supported := checkPerNamespaceProvidersSupported(ctx, dynClient)
125+
perNamespaceProvidersSupported = &supported
126+
127+
if supported {
128+
stateLog.Info("Per-namespace network providers are supported " +
129+
"(SupervisorCapabilities); scanning namespace labels")
130+
hasT1Namespaces, hasVPCNamespaces = scanNamespaceLabels(ctx, clientset)
131+
} else {
132+
stateLog.Infof("Per-namespace network providers not supported; "+
133+
"using legacy EnableVPCNetwork=%v", enableVPCNetwork)
134+
if enableVPCNetwork {
135+
hasT1Namespaces = false
136+
hasVPCNamespaces = true
137+
} else {
138+
hasT1Namespaces = true
139+
hasVPCNamespaces = false
140+
}
141+
}
142+
143+
stateInitialized = true
144+
stateLog.Infof("Mixed-mode state initialized: HasT1Namespaces=%v, "+
145+
"HasVPCNamespaces=%v", hasT1Namespaces, hasVPCNamespaces)
146+
}
147+
148+
// RefreshMixedMode re-scans namespace labels and updates state.
149+
// Returns true if the state changed (caller should consider restarting).
150+
func RefreshMixedMode(ctx context.Context, clientset kubernetes.Interface) bool {
151+
stateMu.Lock()
152+
defer stateMu.Unlock()
153+
154+
if perNamespaceProvidersSupported == nil || !*perNamespaceProvidersSupported {
155+
return false
156+
}
157+
158+
oldT1, oldVPC := hasT1Namespaces, hasVPCNamespaces
159+
hasT1Namespaces, hasVPCNamespaces = scanNamespaceLabels(ctx, clientset)
160+
161+
changed := oldT1 != hasT1Namespaces || oldVPC != hasVPCNamespaces
162+
if changed {
163+
stateLog.Infof("Mixed-mode state changed: HasT1Namespaces=%v->%v, "+
164+
"HasVPCNamespaces=%v->%v", oldT1, hasT1Namespaces, oldVPC, hasVPCNamespaces)
165+
}
166+
return changed
167+
}
168+
169+
// HasT1Namespaces returns true when at least one namespace uses T1 networking.
170+
func HasT1Namespaces() bool {
171+
stateMu.RLock()
172+
defer stateMu.RUnlock()
173+
return hasT1Namespaces
174+
}
175+
176+
// HasVPCNamespaces returns true when at least one namespace uses VPC (or VDS in migration).
177+
func HasVPCNamespaces() bool {
178+
stateMu.RLock()
179+
defer stateMu.RUnlock()
180+
return hasVPCNamespaces
181+
}
182+
183+
// IsMixedModeStateInitialized returns true after InitMixedMode has been called.
184+
func IsMixedModeStateInitialized() bool {
185+
stateMu.RLock()
186+
defer stateMu.RUnlock()
187+
return stateInitialized
188+
}
189+
190+
// SetMixedModeStateForTest sets hasT1Namespaces and hasVPCNamespaces for unit tests.
191+
// Must only be used from test code so production always goes through InitMixedMode.
192+
func SetMixedModeStateForTest(hasT1, hasVPC bool) {
193+
stateMu.Lock()
194+
defer stateMu.Unlock()
195+
hasT1Namespaces = hasT1
196+
hasVPCNamespaces = hasVPC
197+
stateInitialized = true
198+
}
199+
200+
// IsPerNamespaceProvidersSupported returns true when SupervisorCapabilities
201+
// advertises per-namespace network providers.
202+
func IsPerNamespaceProvidersSupported() bool {
203+
stateMu.RLock()
204+
defer stateMu.RUnlock()
205+
return perNamespaceProvidersSupported != nil && *perNamespaceProvidersSupported
206+
}
207+
208+
// GetNamespaceNetworkProvider returns the network provider string from a
209+
// namespace's labels.
210+
func GetNamespaceNetworkProvider(ns *v1.Namespace) string {
211+
if ns == nil || ns.Labels == nil {
212+
return ""
213+
}
214+
return ns.Labels[NetworkProviderLabel]
215+
}

0 commit comments

Comments
 (0)