Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions pkg/webhook/preflight/nutanix/cache.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nutanix
Copy link
Contributor

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]


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()
}
244 changes: 204 additions & 40 deletions pkg/webhook/preflight/nutanix/clients.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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))
Copy link
Contributor

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 🐶
convergedc.Users undefined (type *"github.com/nutanix-cloud-native/prism-go-client/converged/v4".Client has no field or method Users)

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)
Copy link
Contributor

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 🐶
convergedc.DomainManager undefined (type *"github.com/nutanix-cloud-native/prism-go-client/converged/v4".Client has no field or method DomainManager) (typecheck)

},
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(
Expand Down
4 changes: 2 additions & 2 deletions pkg/webhook/preflight/nutanix/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,9 @@ func newCredentialsCheck(
}

// Validate the credentials using an API call.
_, err = nclient.GetCurrentLoggedInUser(ctx)
err = nclient.ValidateCredentials(ctx)
if err == nil {
// We initialized both clients, and verified the credentials using the v3 client.
// We initialized the converged client and verified the credentials using the Users API.
cd.nclient = nclient
return credentialsCheck
}
Expand Down
Loading
Loading