From fe8bb50339c7280cac231047de4cd1f97673f050 Mon Sep 17 00:00:00 2001 From: zhengxiexie Date: Wed, 11 Mar 2026 14:09:24 +0800 Subject: [PATCH] Fix IsDefaultNSXProject always returning false after SDK field rename Use Cluster.HttpGet to read the "default" field directly from the NSX REST API JSON response, bypassing the SDK struct deserialization which cannot populate the unexported model.Project._Default field. Co-Authored-By: Oz Change-Id: I79cfd117c95abe17ef7032525e69cd1695e8822e --- pkg/nsx/services/vpc/vpc.go | 43 +++++++-------- pkg/nsx/services/vpc/vpc_test.go | 91 +++++++++++--------------------- 2 files changed, 54 insertions(+), 80 deletions(-) diff --git a/pkg/nsx/services/vpc/vpc.go b/pkg/nsx/services/vpc/vpc.go index ae0ded874..87844de9a 100644 --- a/pkg/nsx/services/vpc/vpc.go +++ b/pkg/nsx/services/vpc/vpc.go @@ -4,10 +4,8 @@ import ( "context" "errors" "fmt" - "reflect" "strings" "sync" - "unsafe" stderrors "github.com/vmware/vsphere-automation-sdk-go/lib/vapi/std/errors" "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" @@ -54,8 +52,9 @@ var ( type VPCService struct { common.Service - VpcStore *VPCStore - LbsStore *LBSStore + VpcStore *VPCStore + LbsStore *LBSStore + defaultProjectCache sync.Map // cache for IsDefaultNSXProject results, key: "orgID/projectID", value: bool } func (s *VPCService) GetDefaultNetworkConfig() (*v1alpha1.VPCNetworkConfiguration, error) { @@ -1168,27 +1167,29 @@ func IsPreCreatedVPC(nc *v1alpha1.VPCNetworkConfiguration) bool { return nc.Spec.VPC != "" } -func getProjectDefault(p *model.Project) *bool { - v := reflect.ValueOf(p).Elem() - f := v.FieldByName("_Default") - if !f.IsValid() { - return nil +// IsDefaultNSXProject checks if the given project is a NSX default project. +// It queries the NSX Policy API directly and reads the "default" field from the +// JSON response, bypassing the Go SDK struct deserialization which cannot populate +// the unexported model.Project._Default field (CanSet=false for unexported fields). +// Results are cached since a project's default status does not change at runtime. +func (s *VPCService) IsDefaultNSXProject(orgID, projectID string) (bool, error) { + cacheKey := orgID + "/" + projectID + if val, ok := s.defaultProjectCache.Load(cacheKey); ok { + return val.(bool), nil } - ptr := unsafe.Pointer(f.UnsafeAddr()) - return *(**bool)(ptr) // read *bool safely -} - -// IsDefaultNSXProject checks if the given project is a default project -func (s *VPCService) IsDefaultNSXProject(orgID, projectID string) (bool, error) { - proj, err := s.NSXClient.ProjectClient.Get(orgID, projectID, nil) + url := fmt.Sprintf("policy/api/v1/orgs/%s/projects/%s", orgID, projectID) + resp, err := s.NSXClient.Cluster.HttpGet(url) if err != nil { - log.Error(err, "Failed to get project", "ProjectID", projectID) + log.Error(err, "Failed to get project from NSX", "OrgID", orgID, "ProjectID", projectID) return false, err } - defaultVal := getProjectDefault(&proj) - if defaultVal != nil && *defaultVal { - return true, nil + isDefault := false + if defaultVal, ok := resp["default"]; ok { + if boolVal, ok := defaultVal.(bool); ok { + isDefault = boolVal + } } - return false, nil + s.defaultProjectCache.Store(cacheKey, isDefault) + return isDefault, nil } diff --git a/pkg/nsx/services/vpc/vpc_test.go b/pkg/nsx/services/vpc/vpc_test.go index a4bfb64e2..a929f4d5e 100644 --- a/pkg/nsx/services/vpc/vpc_test.go +++ b/pkg/nsx/services/vpc/vpc_test.go @@ -7,16 +7,13 @@ import ( "reflect" "strings" "testing" - "unsafe" "github.com/agiledragon/gomonkey/v2" "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/vmware/vsphere-automation-sdk-go/runtime/data" - nsx_client "github.com/vmware/vsphere-automation-sdk-go/runtime/protocol/client" "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/model" - "github.com/vmware/vsphere-automation-sdk-go/services/nsxt/orgs" v1 "k8s.io/api/core/v1" k8sapierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -2921,83 +2918,59 @@ func Test_isNamespaceReady(t *testing.T) { assert.False(t, isNamespaceReady(nsUnready)) } -func SetProjectDefault(p *model.Project, b bool) { - v := reflect.ValueOf(p).Elem() - f := v.FieldByName("_Default") - if !f.IsValid() { - return - } - - val := b // bool value - ptr := unsafe.Pointer(f.UnsafeAddr()) - realPtr := (*unsafe.Pointer)(ptr) - *realPtr = unsafe.Pointer(&val) -} - func TestIsDefaultNSXProject(t *testing.T) { - connector := nsx_client.NewConnector("localhost") - projectClient := orgs.NewProjectsClient(connector) tests := []struct { name string orgID string projectID string - prepareFunc func(*VPCService) *gomonkey.Patches + httpGetResp map[string]interface{} + httpGetErr error expectedResult bool - expectedErrStr string + expectedErr bool }{ { - name: "Project is default", - orgID: "default", - projectID: "project-1", - prepareFunc: func(service *VPCService) *gomonkey.Patches { - patches := gomonkey.ApplyMethod(reflect.TypeOf(projectClient), "Get", func(_ orgs.ProjectsClient, _ string, _ string, _ *bool) (model.Project, error) { - pro := model.Project{} - SetProjectDefault(&pro, true) - return pro, nil - }) - return patches - }, + name: "project is default", + orgID: "default", + projectID: "default", + httpGetResp: map[string]interface{}{"default": true, "id": "default"}, expectedResult: true, }, { - name: "Project is not default", - orgID: "default", - projectID: "project-2", - prepareFunc: func(service *VPCService) *gomonkey.Patches { - patches := gomonkey.ApplyMethod(reflect.TypeOf(projectClient), "Get", func(_ orgs.ProjectsClient, _ string, _ string, _ *bool) (model.Project, error) { - pro := model.Project{} - return pro, nil - }) - return patches - }, + name: "project is not default", + orgID: "default", + projectID: "customer-project", + httpGetResp: map[string]interface{}{"default": false, "id": "customer-project"}, expectedResult: false, }, { - name: "Get project error", - orgID: "default", - projectID: "project-3", - prepareFunc: func(service *VPCService) *gomonkey.Patches { - patches := gomonkey.ApplyMethod(reflect.TypeOf(projectClient), "Get", func(_ orgs.ProjectsClient, _ string, _ string, _ *bool) (model.Project, error) { - return model.Project{}, fmt.Errorf("failed to get project") - }) - return patches - }, - expectedErrStr: "failed to get project", + name: "response missing default field", + orgID: "default", + projectID: "some-project", + httpGetResp: map[string]interface{}{"id": "some-project"}, + expectedResult: false, + }, + { + name: "HTTP error", + orgID: "default", + projectID: "default", + httpGetErr: fmt.Errorf("connection refused"), + expectedErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { service, _, _, _, _ := createService(t) - if tt.prepareFunc != nil { - patches := tt.prepareFunc(service) - defer patches.Reset() - } - service.NSXClient.ProjectClient = projectClient - result, err := service.IsDefaultNSXProject(tt.orgID, tt.projectID) + service.NSXClient.Cluster = &nsx.Cluster{} + patches := gomonkey.ApplyMethod(reflect.TypeOf(service.NSXClient.Cluster), "HttpGet", + func(_ *nsx.Cluster, _ string) (map[string]interface{}, error) { + return tt.httpGetResp, tt.httpGetErr + }) + defer patches.Reset() - if tt.expectedErrStr != "" { - assert.ErrorContains(t, err, tt.expectedErrStr) + result, err := service.IsDefaultNSXProject(tt.orgID, tt.projectID) + if tt.expectedErr { + assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, tt.expectedResult, result)