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)