-
Notifications
You must be signed in to change notification settings - Fork 9
refactor: migration nutanix API to v4 #1444
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package nutanix | ||
|
|
||
| import ( | ||
| v4Converged "github.com/nutanix-cloud-native/prism-go-client/converged/v4" | ||
| "github.com/nutanix-cloud-native/prism-go-client/environment/types" | ||
| v3 "github.com/nutanix-cloud-native/prism-go-client/v3" | ||
| ) | ||
|
|
||
| // NutanixClientCache is the cache of prism clients to be shared across the different controllers. | ||
| // | ||
| //nolint:gochecknoglobals // Client cache must be a package-level singleton for connection pooling | ||
| var NutanixClientCache = v3.NewClientCache(v3.WithSessionAuth(true)) | ||
|
|
||
| // NutanixConvergedClientV4Cache is the cache of prism clients to be shared across the different controllers. | ||
| // | ||
| //nolint:gochecknoglobals // Client cache must be a package-level singleton for connection pooling | ||
| var NutanixConvergedClientV4Cache = v4Converged.NewClientCache() | ||
|
|
||
| // CacheParams is the struct that implements ClientCacheParams interface from prism-go-client. | ||
| type CacheParams struct { | ||
| PrismManagementEndpoint *types.ManagementEndpoint | ||
| } | ||
|
|
||
| // ManagementEndpoint returns the management endpoint of the NutanixCluster CR. | ||
| func (c *CacheParams) ManagementEndpoint() types.ManagementEndpoint { | ||
| return *c.PrismManagementEndpoint | ||
| } | ||
|
|
||
| // Key returns a unique key for the client cache based on the management endpoint. | ||
| func (c *CacheParams) Key() string { | ||
| return c.PrismManagementEndpoint.Address.String() | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,25 +5,27 @@ package nutanix | |
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "fmt" | ||
| "net/url" | ||
| "strings" | ||
|
|
||
| clustermgmtv4 "github.com/nutanix/ntnx-api-golang-clients/clustermgmt-go-client/v4/models/clustermgmt/v4/config" | ||
| netv4 "github.com/nutanix/ntnx-api-golang-clients/networking-go-client/v4/models/networking/v4/config" | ||
| vmmv4 "github.com/nutanix/ntnx-api-golang-clients/vmm-go-client/v4/models/vmm/v4/content" | ||
|
|
||
| prismgoclient "github.com/nutanix-cloud-native/prism-go-client" | ||
| prismv3 "github.com/nutanix-cloud-native/prism-go-client/v3" | ||
| prismv4 "github.com/nutanix-cloud-native/prism-go-client/v4" | ||
| "github.com/nutanix-cloud-native/prism-go-client/converged" | ||
| "github.com/nutanix-cloud-native/prism-go-client/environment/types" | ||
| ) | ||
|
|
||
| // client contains methods to interact with Nutanix Prism v3 and v4 APIs. | ||
| // client contains methods to interact with Nutanix Prism converged v4 client. | ||
| type client interface { | ||
| GetCurrentLoggedInUser( | ||
| // ValidateCredentials validates credentials by making a lightweight API call. | ||
| // This replaces the V3 GetCurrentLoggedInUser() for credential validation. | ||
| ValidateCredentials( | ||
| ctx context.Context, | ||
| ) ( | ||
| *prismv3.UserIntentResponse, | ||
| error, | ||
| ) | ||
| ) error | ||
|
|
||
| GetPrismCentralVersion( | ||
| ctx context.Context, | ||
|
|
@@ -110,13 +112,11 @@ type client interface { | |
| ) | ||
| } | ||
|
|
||
| // clientWrapper implements the client interface and wraps both v3 and v4 clients. | ||
| // clientWrapper implements the client interface and wraps converged v4 client. | ||
| type clientWrapper struct { | ||
| GetCurrentLoggedInUserFunc func( | ||
| ValidateCredentialsFunc func( | ||
| ctx context.Context, | ||
| ) ( | ||
| *prismv3.UserIntentResponse, error, | ||
| ) | ||
| ) error | ||
|
|
||
| GetPrismCentralVersionFunc func( | ||
| ctx context.Context, | ||
|
|
@@ -196,60 +196,224 @@ type clientWrapper struct { | |
|
|
||
| var _ = client(&clientWrapper{}) | ||
|
|
||
| // ErrEmptyHostInURL is returned when the parsed URL has an empty host. | ||
| var ErrEmptyHostInURL = errors.New("parsed URL has empty host") | ||
|
|
||
| // buildODataOptions converts pointer-based OData parameters to functional options. | ||
| func buildODataOptions(page, limit *int, filter, orderby, selectFields *string) []converged.ODataOption { | ||
| var opts []converged.ODataOption | ||
| if page != nil { | ||
| opts = append(opts, converged.WithPage(*page)) | ||
| } | ||
| if limit != nil { | ||
| opts = append(opts, converged.WithLimit(*limit)) | ||
| } | ||
| if filter != nil && *filter != "" { | ||
| opts = append(opts, converged.WithFilter(*filter)) | ||
| } | ||
| if orderby != nil && *orderby != "" { | ||
| opts = append(opts, converged.WithOrderBy(*orderby)) | ||
| } | ||
| if selectFields != nil && *selectFields != "" { | ||
| opts = append(opts, converged.WithSelect(*selectFields)) | ||
| } | ||
| return opts | ||
| } | ||
|
|
||
| // buildManagementEndpoint creates a ManagementEndpoint from credentials and trust bundle. | ||
| func buildManagementEndpoint(credentials *prismgoclient.Credentials) (*types.ManagementEndpoint, error) { | ||
| urlStr := credentials.URL | ||
|
|
||
| // Prepend https:// if no scheme is present | ||
| // Nutanix Prism Central URLs may be provided as "host:port" without scheme | ||
| if !strings.HasPrefix(urlStr, "http://") && !strings.HasPrefix(urlStr, "https://") { | ||
| urlStr = "https://" + urlStr | ||
| } | ||
|
|
||
| // Parse URL - preserve existing scheme if present (e.g., for test servers) | ||
| parsedURL, err := url.Parse(urlStr) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to parse URL %q: %w", urlStr, err) | ||
| } | ||
|
|
||
| // Validate that we have a host after parsing | ||
| if parsedURL.Host == "" { | ||
| return nil, fmt.Errorf("invalid URL %q: %w", credentials.URL, ErrEmptyHostInURL) | ||
| } | ||
|
|
||
| return &types.ManagementEndpoint{ | ||
| Address: parsedURL, | ||
| Insecure: credentials.Insecure, | ||
| ApiCredentials: types.ApiCredentials{ | ||
| Username: credentials.Username, | ||
| Password: credentials.Password, | ||
| }, | ||
| }, nil | ||
| } | ||
|
|
||
| func newClient( | ||
| credentials prismgoclient.Credentials, //nolint:gocritic // hugeParam is fine | ||
| ) (client, error) { | ||
| v3c, err := prismv3.NewV3Client(credentials) | ||
| endpoint, err := buildManagementEndpoint(&credentials) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create v3 client: %w", err) | ||
| return nil, fmt.Errorf("failed to build management endpoint: %w", err) | ||
| } | ||
| cacheParams := &CacheParams{ | ||
| PrismManagementEndpoint: endpoint, | ||
| } | ||
|
|
||
| v4c, err := prismv4.NewV4Client(credentials) | ||
| convergedc, err := NutanixConvergedClientV4Cache.GetOrCreate(cacheParams) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("failed to create v4 client: %w", err) | ||
| return nil, fmt.Errorf("failed to create converged client: %w", err) | ||
| } | ||
|
|
||
| return &clientWrapper{ | ||
| GetCurrentLoggedInUserFunc: v3c.V3.GetCurrentLoggedInUser, | ||
| ValidateCredentialsFunc: func(ctx context.Context) error { | ||
| // Use Users.List() as a lightweight API call to validate credentials. | ||
| // This is available to all users and serves the same purpose as V3's GetCurrentLoggedInUser. | ||
| _, err := convergedc.Users.List(ctx, converged.WithLimit(1)) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚫 [golangci-lint] reported by reviewdog 🐶 |
||
| return err | ||
| }, | ||
| GetPrismCentralVersionFunc: func(ctx context.Context) (string, error) { | ||
| pcInfo, err := v3c.V3.GetPrismCentral(ctx) | ||
| // Use DomainManager.GetPrismCentralVersion() as V4 equivalent to V3's GetPrismCentral(). | ||
| return convergedc.DomainManager.GetPrismCentralVersion(ctx) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚫 [golangci-lint] reported by reviewdog 🐶 |
||
| }, | ||
| GetImageByIdFunc: func(uuid *string, args ...map[string]interface{}) (*vmmv4.GetImageApiResponse, error) { | ||
| if uuid == nil { | ||
| return nil, fmt.Errorf("uuid cannot be nil") | ||
| } | ||
| image, err := convergedc.Images.Get(context.Background(), *uuid) | ||
| if err != nil { | ||
| return "", err | ||
| return nil, err | ||
| } | ||
|
|
||
| if pcInfo == nil || pcInfo.Resources == nil || pcInfo.Resources.Version == nil { | ||
| return "", fmt.Errorf("failed to get Prism Central version: API did not return the PC version") | ||
| resp := &vmmv4.GetImageApiResponse{} | ||
| resp.Data = vmmv4.NewOneOfGetImageApiResponseData() | ||
| if err := resp.Data.SetValue(image); err != nil { | ||
| return nil, fmt.Errorf("failed to set image response data: %w", err) | ||
| } | ||
|
|
||
| return *pcInfo.Resources.Version, nil | ||
| return resp, nil | ||
| }, | ||
| ListImagesFunc: func( | ||
| page_ *int, | ||
| limit_ *int, | ||
| filter_ *string, | ||
| orderby_ *string, | ||
| select_ *string, | ||
| args ...map[string]interface{}, | ||
| ) (*vmmv4.ListImagesApiResponse, error) { | ||
| opts := buildODataOptions(page_, limit_, filter_, orderby_, select_) | ||
| images, err := convergedc.Images.List(context.Background(), opts...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp := &vmmv4.ListImagesApiResponse{} | ||
| resp.Data = vmmv4.NewOneOfListImagesApiResponseData() | ||
| if err := resp.Data.SetValue(images); err != nil { | ||
| return nil, fmt.Errorf("failed to set images response data: %w", err) | ||
| } | ||
| return resp, nil | ||
| }, | ||
| GetImageByIdFunc: v4c.ImagesApiInstance.GetImageById, | ||
| ListImagesFunc: v4c.ImagesApiInstance.ListImages, | ||
| GetClusterByIdFunc: func(uuid *string, args ...map[string]interface{}) (*clustermgmtv4.GetClusterApiResponse, error) { | ||
| return v4c.ClustersApiInstance.GetClusterById(uuid, nil, args...) | ||
| if uuid == nil { | ||
| return nil, fmt.Errorf("uuid cannot be nil") | ||
| } | ||
| cluster, err := convergedc.Clusters.Get(context.Background(), *uuid) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp := &clustermgmtv4.GetClusterApiResponse{} | ||
| resp.Data = clustermgmtv4.NewOneOfGetClusterApiResponseData() | ||
| if err := resp.Data.SetValue(cluster); err != nil { | ||
| return nil, fmt.Errorf("failed to set cluster response data: %w", err) | ||
| } | ||
| return resp, nil | ||
| }, | ||
| ListClustersFunc: func( | ||
| page_, limit_ *int, | ||
| filter_, orderby_, apply_, select_ *string, | ||
| args ...map[string]interface{}, | ||
| ) (*clustermgmtv4.ListClustersApiResponse, error) { | ||
| return v4c.ClustersApiInstance.ListClusters( | ||
| page_, limit_, filter_, orderby_, apply_, nil, select_, args..., | ||
| ) | ||
| opts := buildODataOptions(page_, limit_, filter_, orderby_, select_) | ||
| if apply_ != nil && *apply_ != "" { | ||
| opts = append(opts, converged.WithApply(*apply_)) | ||
| } | ||
| clusters, err := convergedc.Clusters.List(context.Background(), opts...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp := &clustermgmtv4.ListClustersApiResponse{} | ||
| resp.Data = clustermgmtv4.NewOneOfListClustersApiResponseData() | ||
| if err := resp.Data.SetValue(clusters); err != nil { | ||
| return nil, fmt.Errorf("failed to set clusters response data: %w", err) | ||
| } | ||
| return resp, nil | ||
| }, | ||
| ListStorageContainersFunc: func( | ||
| page_ *int, | ||
| limit_ *int, | ||
| filter_ *string, | ||
| orderby_ *string, | ||
| select_ *string, | ||
| args ...map[string]interface{}, | ||
| ) (*clustermgmtv4.ListStorageContainersApiResponse, error) { | ||
| opts := buildODataOptions(page_, limit_, filter_, orderby_, select_) | ||
| containers, err := convergedc.StorageContainers.List(context.Background(), opts...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp := &clustermgmtv4.ListStorageContainersApiResponse{} | ||
| resp.Data = clustermgmtv4.NewOneOfListStorageContainersApiResponseData() | ||
| if err := resp.Data.SetValue(containers); err != nil { | ||
| return nil, fmt.Errorf("failed to set storage containers response data: %w", err) | ||
| } | ||
| return resp, nil | ||
| }, | ||
| GetSubnetByIdFunc: func(uuid *string, args ...map[string]interface{}) (*netv4.GetSubnetApiResponse, error) { | ||
| if uuid == nil { | ||
| return nil, fmt.Errorf("uuid cannot be nil") | ||
| } | ||
| subnet, err := convergedc.Subnets.Get(context.Background(), *uuid) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp := &netv4.GetSubnetApiResponse{} | ||
| resp.Data = netv4.NewOneOfGetSubnetApiResponseData() | ||
| if err := resp.Data.SetValue(subnet); err != nil { | ||
| return nil, fmt.Errorf("failed to set subnet response data: %w", err) | ||
| } | ||
| return resp, nil | ||
| }, | ||
| ListSubnetsFunc: func( | ||
| page_ *int, | ||
| limit_ *int, | ||
| filter_ *string, | ||
| orderby_ *string, | ||
| expand_ *string, | ||
| select_ *string, | ||
| args ...map[string]interface{}, | ||
| ) (*netv4.ListSubnetsApiResponse, error) { | ||
| opts := buildODataOptions(page_, limit_, filter_, orderby_, select_) | ||
| if expand_ != nil && *expand_ != "" { | ||
| opts = append(opts, converged.WithExpand(*expand_)) | ||
| } | ||
| subnets, err := convergedc.Subnets.List(context.Background(), opts...) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| resp := &netv4.ListSubnetsApiResponse{} | ||
| resp.Data = netv4.NewOneOfListSubnetsApiResponseData() | ||
| if err := resp.Data.SetValue(subnets); err != nil { | ||
| return nil, fmt.Errorf("failed to set subnets response data: %w", err) | ||
| } | ||
| return resp, nil | ||
| }, | ||
| ListStorageContainersFunc: v4c.StorageContainerAPI.ListStorageContainers, | ||
| GetSubnetByIdFunc: v4c.SubnetsApiInstance.GetSubnetById, | ||
| ListSubnetsFunc: v4c.SubnetsApiInstance.ListSubnets, | ||
| }, nil | ||
| } | ||
|
|
||
| func (c *clientWrapper) GetCurrentLoggedInUser( | ||
| func (c *clientWrapper) ValidateCredentials( | ||
| ctx context.Context, | ||
| ) ( | ||
| *prismv3.UserIntentResponse, | ||
| error, | ||
| ) { | ||
| return c.GetCurrentLoggedInUserFunc(ctx) | ||
| ) error { | ||
| return c.ValidateCredentialsFunc(ctx) | ||
| } | ||
|
|
||
| func (c *clientWrapper) GetPrismCentralVersion( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🚫 [golangci-lint] reported by reviewdog 🐶
: # github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/nutanix [github.com/nutanix-cloud-native/cluster-api-runtime-extensions-nutanix/pkg/webhook/preflight/nutanix.test]