From 2591f82b2e8e3dcc151c56a9131f654fb9e0343f Mon Sep 17 00:00:00 2001 From: Sid Shukla Date: Thu, 11 Apr 2024 15:48:03 +0200 Subject: [PATCH] Cherry-pick PR #360 and #403 to release-v1.3 (#409) * refactor: client.go file helper methods (#360) * refactor: client.go file helper methods Refactored the existing methods and functions to be unit testable. Also made some methods that do not use the struct as generic functions. The changes were primarily an effort to add unit test coverage. * refactor: more testable read file function * test: new nutanixcluster types unit tests * test: additional test cases for errors * fixup! refactor: client.go file helper methods * fixup! refactor: client.go file helper methods * fixup! refactor: more testable read file function * fixup! refactor: client.go file helper methods * Ensure fallback config is only read when prismCentral is absent (#403) Skip reading fallback config file from /etc/nutanix/config/prismCentral if NutanixCluster has prismCentral set. Co-authored-by: Sid Shukla * Update build-dev.yaml add codecov token --------- Co-authored-by: Dimitri Koshkin Co-authored-by: Deepak Muley --- .github/workflows/build-dev.yaml | 2 + api/v1beta1/nutanixcluster_types.go | 31 +- api/v1beta1/nutanixcluster_types_test.go | 114 +++ controllers/helpers.go | 15 +- controllers/nutanixcluster_controller.go | 25 +- pkg/client/client.go | 262 ++++--- pkg/client/client_test.go | 687 +++++++++++++++++- ...invalidTestConfigMissingCredentialRef.json | 4 + pkg/client/testdata/validTestCA.pem | 18 + pkg/client/testdata/validTestConfig.json | 9 + pkg/client/testdata/validTestCredentials.json | 11 + pkg/client/testdata/validTestManagerCA.pem | 21 + .../testdata/validTestManagerCredentials.json | 11 + test/e2e/nutanix_client.go | 7 +- 14 files changed, 1074 insertions(+), 143 deletions(-) create mode 100644 api/v1beta1/nutanixcluster_types_test.go create mode 100644 pkg/client/testdata/invalidTestConfigMissingCredentialRef.json create mode 100644 pkg/client/testdata/validTestCA.pem create mode 100644 pkg/client/testdata/validTestConfig.json create mode 100644 pkg/client/testdata/validTestCredentials.json create mode 100644 pkg/client/testdata/validTestManagerCA.pem create mode 100644 pkg/client/testdata/validTestManagerCredentials.json diff --git a/.github/workflows/build-dev.yaml b/.github/workflows/build-dev.yaml index 73231d0093..5933d526ce 100644 --- a/.github/workflows/build-dev.yaml +++ b/.github/workflows/build-dev.yaml @@ -36,6 +36,8 @@ jobs: - name: Codecov uses: codecov/codecov-action@v3.1.4 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: file: ./coverage.xml # Replace with the path to your coverage report fail_ci_if_error: true diff --git a/api/v1beta1/nutanixcluster_types.go b/api/v1beta1/nutanixcluster_types.go index b08fcb1212..daf954f2a9 100644 --- a/api/v1beta1/nutanixcluster_types.go +++ b/api/v1beta1/nutanixcluster_types.go @@ -17,6 +17,8 @@ limitations under the License. package v1beta1 import ( + "fmt" + credentialTypes "github.com/nutanix-cloud-native/prism-go-client/environment/credentials" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" capiv1 "sigs.k8s.io/cluster-api/api/v1beta1" @@ -86,12 +88,12 @@ type NutanixClusterStatus struct { FailureMessage *string `json:"failureMessage,omitempty"` } -//+kubebuilder:object:root=true -//+kubebuilder:resource:path=nutanixclusters,shortName=ncl,scope=Namespaced,categories=cluster-api -//+kubebuilder:subresource:status -//+kubebuilder:storageversion -//+kubebuilder:printcolumn:name="ControlplaneEndpoint",type="string",JSONPath=".spec.controlPlaneEndpoint.host",description="ControlplaneEndpoint" -//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="in ready status" +// +kubebuilder:object:root=true +// +kubebuilder:resource:path=nutanixclusters,shortName=ncl,scope=Namespaced,categories=cluster-api +// +kubebuilder:subresource:status +// +kubebuilder:storageversion +// +kubebuilder:printcolumn:name="ControlplaneEndpoint",type="string",JSONPath=".spec.controlPlaneEndpoint.host",description="ControlplaneEndpoint" +// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.ready",description="in ready status" // NutanixCluster is the Schema for the nutanixclusters API type NutanixCluster struct { @@ -145,7 +147,22 @@ func (ncl *NutanixCluster) SetConditions(conditions capiv1.Conditions) { ncl.Status.Conditions = conditions } -//+kubebuilder:object:root=true +func (ncl *NutanixCluster) GetPrismCentralCredentialRef() (*credentialTypes.NutanixCredentialReference, error) { + prismCentralInfo := ncl.Spec.PrismCentral + if prismCentralInfo == nil { + return nil, nil + } + if prismCentralInfo.CredentialRef == nil { + return nil, fmt.Errorf("credentialRef must be set on prismCentral attribute for cluster %s in namespace %s", ncl.Name, ncl.Namespace) + } + if prismCentralInfo.CredentialRef.Kind != credentialTypes.SecretKind { + return nil, nil + } + + return prismCentralInfo.CredentialRef, nil +} + +// +kubebuilder:object:root=true // NutanixClusterList contains a list of NutanixCluster type NutanixClusterList struct { diff --git a/api/v1beta1/nutanixcluster_types_test.go b/api/v1beta1/nutanixcluster_types_test.go new file mode 100644 index 0000000000..59a74b3298 --- /dev/null +++ b/api/v1beta1/nutanixcluster_types_test.go @@ -0,0 +1,114 @@ +/* +Copyright 2024 Nutanix + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1beta1 + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/nutanix-cloud-native/prism-go-client/environment/credentials" +) + +func TestGetCredentialRefForCluster(t *testing.T) { + t.Parallel() + tests := []struct { + name string + nutanixCluster *NutanixCluster + expectedCredentialsRef *credentials.NutanixCredentialReference + expectedErr error + }{ + { + name: "all info is set", + nutanixCluster: &NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: corev1.NamespaceDefault, + }, + Spec: NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Address: "address", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: corev1.NamespaceDefault, + }, + }, + }, + }, + expectedCredentialsRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: corev1.NamespaceDefault, + }, + }, + { + name: "prismCentralInfo is nil, should not fail", + nutanixCluster: &NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: corev1.NamespaceDefault, + }, + Spec: NutanixClusterSpec{}, + }, + }, + { + name: "CredentialRef kind is not kind Secret, should not fail", + nutanixCluster: &NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: corev1.NamespaceDefault, + }, + Spec: NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: "unknown", + }, + }, + }, + }, + }, + { + name: "prismCentralInfo is not nil but CredentialRef is nil, should fail", + nutanixCluster: &NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: corev1.NamespaceDefault, + }, + Spec: NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Address: "address", + }, + }, + }, + expectedErr: fmt.Errorf("credentialRef must be set on prismCentral attribute for cluster test in namespace default"), + }, + } + for _, tt := range tests { + tt := tt // Capture range variable. + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ref, err := tt.nutanixCluster.GetPrismCentralCredentialRef() + assert.Equal(t, tt.expectedCredentialsRef, ref) + assert.Equal(t, tt.expectedErr, err) + }) + } +} diff --git a/controllers/helpers.go b/controllers/helpers.go index 70e6e2fca9..1ab81cae7b 100644 --- a/controllers/helpers.go +++ b/controllers/helpers.go @@ -23,13 +23,14 @@ import ( "strings" "github.com/google/uuid" - infrav1 "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1" - nutanixClientHelper "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/client" "github.com/nutanix-cloud-native/prism-go-client/utils" nutanixClientV3 "github.com/nutanix-cloud-native/prism-go-client/v3" "k8s.io/apimachinery/pkg/api/resource" coreinformers "k8s.io/client-go/informers/core/v1" ctrl "sigs.k8s.io/controller-runtime" + + infrav1 "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1" + nutanixClient "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/client" ) const ( @@ -47,12 +48,8 @@ const ( func CreateNutanixClient(ctx context.Context, secretInformer coreinformers.SecretInformer, cmInformer coreinformers.ConfigMapInformer, nutanixCluster *infrav1.NutanixCluster) (*nutanixClientV3.Client, error) { log := ctrl.LoggerFrom(ctx) log.V(1).Info("creating nutanix client") - helper, err := nutanixClientHelper.NewNutanixClientHelper(secretInformer, cmInformer) - if err != nil { - log.Error(err, "error creating nutanix client helper") - return nil, err - } - return helper.GetClientFromEnvironment(ctx, nutanixCluster) + helper := nutanixClient.NewHelper(secretInformer, cmInformer) + return helper.BuildClientForNutanixClusterWithFallback(ctx, nutanixCluster) } // DeleteVM deletes a VM and is invoked by the NutanixMachineReconciler @@ -346,7 +343,7 @@ func GetImageUUID(ctx context.Context, client *nutanixClientV3.Client, imageName // HasTaskInProgress returns true if the given task is in progress func HasTaskInProgress(ctx context.Context, client *nutanixClientV3.Client, taskUUID string) (bool, error) { log := ctrl.LoggerFrom(ctx) - taskStatus, err := nutanixClientHelper.GetTaskStatus(ctx, client, taskUUID) + taskStatus, err := nutanixClient.GetTaskStatus(ctx, client, taskUUID) if err != nil { return false, err } diff --git a/controllers/nutanixcluster_controller.go b/controllers/nutanixcluster_controller.go index 963b3d1853..9c12bc2f5f 100644 --- a/controllers/nutanixcluster_controller.go +++ b/controllers/nutanixcluster_controller.go @@ -21,6 +21,7 @@ import ( "fmt" "time" + credentialTypes "github.com/nutanix-cloud-native/prism-go-client/environment/credentials" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -43,7 +44,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/source" infrav1 "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1" - nutanixClient "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/client" nctx "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/context" ) @@ -101,11 +101,11 @@ func (r *NutanixClusterReconciler) SetupWithManager(ctx context.Context, mgr ctr return nil } -//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;update;delete -//+kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch -//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=nutanixclusters,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=nutanixclusters/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=nutanixclusters/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;update;delete +// +kubebuilder:rbac:groups=cluster.x-k8s.io,resources=clusters;clusters/status,verbs=get;list;watch +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=nutanixclusters,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=nutanixclusters/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=nutanixclusters/finalizers,verbs=update // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. @@ -331,7 +331,7 @@ func (r *NutanixClusterReconciler) reconcileCategoriesDelete(rctx *nctx.ClusterC func (r *NutanixClusterReconciler) reconcileCredentialRefDelete(ctx context.Context, nutanixCluster *infrav1.NutanixCluster) error { log := ctrl.LoggerFrom(ctx) - credentialRef, err := nutanixClient.GetCredentialRefForCluster(nutanixCluster) + credentialRef, err := getPrismCentralCredentialRefForCluster(nutanixCluster) if err != nil { log.Error(err, fmt.Sprintf("error occurred while getting credential ref for cluster %s", nutanixCluster.Name)) return err @@ -372,7 +372,7 @@ func (r *NutanixClusterReconciler) reconcileCredentialRefDelete(ctx context.Cont func (r *NutanixClusterReconciler) reconcileCredentialRef(ctx context.Context, nutanixCluster *infrav1.NutanixCluster) error { log := ctrl.LoggerFrom(ctx) - credentialRef, err := nutanixClient.GetCredentialRefForCluster(nutanixCluster) + credentialRef, err := getPrismCentralCredentialRefForCluster(nutanixCluster) if err != nil { return err } @@ -418,3 +418,12 @@ func (r *NutanixClusterReconciler) reconcileCredentialRef(ctx context.Context, n } return nil } + +// getPrismCentralCredentialRefForCluster calls nutanixCluster.GetPrismCentralCredentialRef() function +// and returns an error if nutanixCluster is nil +func getPrismCentralCredentialRefForCluster(nutanixCluster *infrav1.NutanixCluster) (*credentialTypes.NutanixCredentialReference, error) { + if nutanixCluster == nil { + return nil, fmt.Errorf("cannot get credential reference if nutanix cluster object is nil") + } + return nutanixCluster.GetPrismCentralCredentialRef() +} diff --git a/pkg/client/client.go b/pkg/client/client.go index 35dc3bb017..fbb70ba842 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -21,11 +21,10 @@ import ( "encoding/json" "fmt" "os" - "path/filepath" prismgoclient "github.com/nutanix-cloud-native/prism-go-client" "github.com/nutanix-cloud-native/prism-go-client/environment" - credentialTypes "github.com/nutanix-cloud-native/prism-go-client/environment/credentials" + "github.com/nutanix-cloud-native/prism-go-client/environment/credentials" kubernetesEnv "github.com/nutanix-cloud-native/prism-go-client/environment/providers/kubernetes" envTypes "github.com/nutanix-cloud-native/prism-go-client/environment/types" nutanixClientV3 "github.com/nutanix-cloud-native/prism-go-client/v3" @@ -37,66 +36,138 @@ import ( const ( defaultEndpointPort = "9440" - ProviderName = "nutanix" - configPath = "/etc/nutanix/config" - endpointKey = "prismCentral" - capxNamespaceKey = "POD_NAMESPACE" + + capxNamespaceKey = "POD_NAMESPACE" +) + +const ( + configPath = "/etc/nutanix/config/prismCentral" ) -var ErrPrismAddressNotSet = fmt.Errorf("cannot get credentials if Prism Address is not set") -var ErrPrismPortNotSet = fmt.Errorf("cannot get credentials if Prism Port is not set") +var ( + ErrPrismAddressNotSet = fmt.Errorf("cannot get credentials if Prism Address is not set") + ErrPrismPortNotSet = fmt.Errorf("cannot get credentials if Prism Port is not set") + + ErrPrismIUsernameNotSet = fmt.Errorf("could not create client because username was not set") + ErrPrismIPasswordNotSet = fmt.Errorf("could not create client because password was not set") + + ErrCredentialRefNotSet = fmt.Errorf("credentialRef must be set on CAPX manager") +) type NutanixClientHelper struct { secretInformer coreinformers.SecretInformer configMapInformer coreinformers.ConfigMapInformer + + managerNutanixPrismEndpointReader func() (*credentials.NutanixPrismEndpoint, error) } -func NewNutanixClientHelper(secretInformer coreinformers.SecretInformer, cmInformer coreinformers.ConfigMapInformer) (*NutanixClientHelper, error) { +func NewHelper(secretInformer coreinformers.SecretInformer, cmInformer coreinformers.ConfigMapInformer) *NutanixClientHelper { return &NutanixClientHelper{ - secretInformer: secretInformer, - configMapInformer: cmInformer, - }, nil + secretInformer: secretInformer, + configMapInformer: cmInformer, + managerNutanixPrismEndpointReader: readManagerNutanixPrismEndpointFromDefaultFile, + } +} + +func (n *NutanixClientHelper) withCustomNutanixPrismEndpointReader(getter func() (*credentials.NutanixPrismEndpoint, error)) *NutanixClientHelper { + n.managerNutanixPrismEndpointReader = getter + return n +} + +// BuildClientForNutanixClusterWithFallback builds a Nutanix Client from the information provided in nutanixCluster. +func (n *NutanixClientHelper) BuildClientForNutanixClusterWithFallback(ctx context.Context, nutanixCluster *infrav1.NutanixCluster) (*nutanixClientV3.Client, error) { + me, err := n.buildManagementEndpoint(ctx, nutanixCluster) + if err != nil { + return nil, err + } + creds := prismgoclient.Credentials{ + URL: me.Address.Host, + Endpoint: me.Address.Host, + Insecure: me.Insecure, + Username: me.ApiCredentials.Username, + Password: me.ApiCredentials.Password, + } + return Build(creds, me.AdditionalTrustBundle) } -func (n *NutanixClientHelper) GetClientFromEnvironment(ctx context.Context, nutanixCluster *infrav1.NutanixCluster) (*nutanixClientV3.Client, error) { +// buildManagementEndpoint takes in a NutanixCluster and constructs a ManagementEndpoint with all the information provided. +// If required information is not set, it will fallback to using information from /etc/nutanix/config/prismCentral, +// which is expected to be mounted in the Pod. +func (n *NutanixClientHelper) buildManagementEndpoint(ctx context.Context, nutanixCluster *infrav1.NutanixCluster) (*envTypes.ManagementEndpoint, error) { log := ctrl.LoggerFrom(ctx) - // Create a list of env providers + + // Create an empty list of env providers providers := make([]envTypes.Provider, 0) - // If PrismCentral is set, add the required env provider - prismCentralInfo := nutanixCluster.Spec.PrismCentral - if prismCentralInfo != nil { - if prismCentralInfo.Address == "" { - return nil, ErrPrismAddressNotSet - } - if prismCentralInfo.Port == 0 { - return nil, ErrPrismPortNotSet - } - credentialRef, err := GetCredentialRefForCluster(nutanixCluster) + // Attempt to build a provider from the NutanixCluster object + providerForNutanixCluster, err := n.buildProviderFromNutanixCluster(nutanixCluster) + if err != nil { + return nil, fmt.Errorf("error building an environment provider from NutanixCluster: %w", err) + } + if providerForNutanixCluster != nil { + providers = append(providers, providerForNutanixCluster) + } else { + log.Info(fmt.Sprintf("[WARNING] prismCentral attribute was not set on NutanixCluster %s in namespace %s. Defaulting to CAPX manager credentials", nutanixCluster.Name, nutanixCluster.Namespace)) + // Fallback to building a provider using prism central information from the CAPX management cluster + // using information from /etc/nutanix/config/prismCentral + providerForLocalFile, err := n.buildProviderFromFile() if err != nil { - //nolint:wrapcheck // error is alredy wrapped - return nil, err + return nil, fmt.Errorf("error building an environment provider from file: %w", err) } - // If namespace is empty, use the cluster namespace - if credentialRef.Namespace == "" { - credentialRef.Namespace = nutanixCluster.Namespace + if providerForLocalFile != nil { + providers = append(providers, providerForLocalFile) } - additionalTrustBundleRef := prismCentralInfo.AdditionalTrustBundle - if additionalTrustBundleRef != nil && - additionalTrustBundleRef.Kind == credentialTypes.NutanixTrustBundleKindConfigMap && - additionalTrustBundleRef.Namespace == "" { - additionalTrustBundleRef.Namespace = nutanixCluster.Namespace - } - providers = append(providers, kubernetesEnv.NewProvider( - *nutanixCluster.Spec.PrismCentral, - n.secretInformer, - n.configMapInformer)) - } else { - log.Info(fmt.Sprintf("[WARNING] prismCentral attribute was not set on NutanixCluster %s in namespace %s. Defaulting to CAPX manager credentials", nutanixCluster.Name, nutanixCluster.Namespace)) } - // Add env provider for CAPX manager - npe, err := n.getManagerNutanixPrismEndpoint() + // Initialize environment with providers + env := environment.NewEnvironment(providers...) + // GetManagementEndpoint will return the first valid endpoint from the list of providers + me, err := env.GetManagementEndpoint(envTypes.Topology{}) + if err != nil { + return nil, fmt.Errorf("failed to get management endpoint object: %w", err) + } + return me, nil +} + +// buildProviderFromNutanixCluster will return an envTypes.Provider with info from the provided NutanixCluster. +// It will return nil if nutanixCluster.Spec.PrismCentral is nil. +// It will return an error if required information is missing. +func (n *NutanixClientHelper) buildProviderFromNutanixCluster(nutanixCluster *infrav1.NutanixCluster) (envTypes.Provider, error) { + prismCentralInfo := nutanixCluster.Spec.PrismCentral + if prismCentralInfo == nil { + return nil, nil + } + + // PrismCentral is set, build a provider and fixup missing information + if prismCentralInfo.Address == "" { + return nil, ErrPrismAddressNotSet + } + if prismCentralInfo.Port == 0 { + return nil, ErrPrismPortNotSet + } + credentialRef, err := nutanixCluster.GetPrismCentralCredentialRef() + if err != nil { + //nolint:wrapcheck // error is already wrapped + return nil, err + } + // If namespace is empty, use the cluster namespace + if credentialRef.Namespace == "" { + credentialRef.Namespace = nutanixCluster.Namespace + } + additionalTrustBundleRef := prismCentralInfo.AdditionalTrustBundle + if additionalTrustBundleRef != nil && + additionalTrustBundleRef.Kind == credentials.NutanixTrustBundleKindConfigMap && + additionalTrustBundleRef.Namespace == "" { + additionalTrustBundleRef.Namespace = nutanixCluster.Namespace + } + + return kubernetesEnv.NewProvider(*prismCentralInfo, n.secretInformer, n.configMapInformer), nil +} + +// buildProviderFromFile will return an envTypes.Provider with info from the provided file. +// It will return an error if required information is missing. +func (n *NutanixClientHelper) buildProviderFromFile() (envTypes.Provider, error) { + npe, err := n.managerNutanixPrismEndpointReader() if err != nil { return nil, fmt.Errorf("failed to create prism endpoint: %w", err) } @@ -115,99 +186,70 @@ func (n *NutanixClientHelper) GetClientFromEnvironment(ctx context.Context, nuta } npe.AdditionalTrustBundle.Namespace = capxNamespace } - providers = append(providers, kubernetesEnv.NewProvider( - *npe, - n.secretInformer, - n.configMapInformer)) - // init env with providers - env := environment.NewEnvironment( - providers..., - ) - // fetch endpoint details - me, err := env.GetManagementEndpoint(envTypes.Topology{}) + + return kubernetesEnv.NewProvider(*npe, n.secretInformer, n.configMapInformer), nil +} + +func Build(creds prismgoclient.Credentials, additionalTrustBundle string) (*nutanixClientV3.Client, error) { + cli, err := buildClientFromCredentials(creds, additionalTrustBundle) if err != nil { - return nil, fmt.Errorf("failed to get management endpoint object: %w", err) + return nil, err } - creds := prismgoclient.Credentials{ - URL: me.Address.Host, - Endpoint: me.Address.Host, - Insecure: me.Insecure, - Username: me.ApiCredentials.Username, - Password: me.ApiCredentials.Password, + // Check if the client is working + _, err = cli.V3.GetCurrentLoggedInUser(context.Background()) + if err != nil { + return nil, fmt.Errorf("failed to get current logged in user with client: %w", err) } - - return n.GetClient(creds, me.AdditionalTrustBundle) + return cli, nil } -func (n *NutanixClientHelper) GetClient(cred prismgoclient.Credentials, additionalTrustBundle string) (*nutanixClientV3.Client, error) { - if cred.Username == "" { - return nil, fmt.Errorf("could not create client because username was not set") +func buildClientFromCredentials(creds prismgoclient.Credentials, additionalTrustBundle string) (*nutanixClientV3.Client, error) { + if creds.Username == "" { + return nil, ErrPrismIUsernameNotSet } - if cred.Password == "" { - return nil, fmt.Errorf("could not create client because password was not set") + if creds.Password == "" { + return nil, ErrPrismIPasswordNotSet } - if cred.Port == "" { - cred.Port = defaultEndpointPort + if creds.Port == "" { + creds.Port = defaultEndpointPort } - if cred.URL == "" { - cred.URL = fmt.Sprintf("%s:%s", cred.Endpoint, cred.Port) + if creds.URL == "" { + creds.URL = fmt.Sprintf("%s:%s", creds.Endpoint, creds.Port) } + clientOpts := make([]nutanixClientV3.ClientOption, 0) if additionalTrustBundle != "" { clientOpts = append(clientOpts, nutanixClientV3.WithPEMEncodedCertBundle([]byte(additionalTrustBundle))) } - cli, err := nutanixClientV3.NewV3Client(cred, clientOpts...) + // Build the client with the creds and possibly an additional TrustBundle + cli, err := nutanixClientV3.NewV3Client(creds, clientOpts...) if err != nil { return nil, fmt.Errorf("failed to create new nutanix client: %w", err) } - // Check if the client is working - _, err = cli.V3.GetCurrentLoggedInUser(context.Background()) - if err != nil { - return nil, fmt.Errorf("failed to get current logged in user with client: %w", err) - } return cli, nil } -func (n *NutanixClientHelper) getManagerNutanixPrismEndpoint() (*credentialTypes.NutanixPrismEndpoint, error) { - npe := &credentialTypes.NutanixPrismEndpoint{} - config, err := n.readEndpointConfig() +// readManagerNutanixPrismEndpoint reads the default config file and unmarshalls it into NutanixPrismEndpoint. +// Returns an error if the file does not exist and other read or unmarshalling errors. +func readManagerNutanixPrismEndpointFromDefaultFile() (*credentials.NutanixPrismEndpoint, error) { + return readManagerNutanixPrismEndpointFromFile(configPath) +} + +// this function is primarily here to make writing unit tests simpler +// readManagerNutanixPrismEndpointFromDefaultFile should be used outside of tests +func readManagerNutanixPrismEndpointFromFile(configFile string) (*credentials.NutanixPrismEndpoint, error) { + // fail on all errors including NotExist error + config, err := os.ReadFile(configFile) if err != nil { - return nil, fmt.Errorf("failed to read config: %w", err) + return nil, fmt.Errorf("failed to read prism config in manager: %w", err) } + npe := &credentials.NutanixPrismEndpoint{} if err = json.Unmarshal(config, npe); err != nil { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } if npe.CredentialRef == nil { - return nil, fmt.Errorf("credentialRef must be set on CAPX manager") + return nil, ErrCredentialRefNotSet } return npe, nil } - -func (n *NutanixClientHelper) readEndpointConfig() ([]byte, error) { - if b, err := os.ReadFile(filepath.Join(configPath, endpointKey)); err == nil { - return b, err - } else if os.IsNotExist(err) { - return []byte{}, nil - } else { - return []byte{}, err - } -} - -func GetCredentialRefForCluster(nutanixCluster *infrav1.NutanixCluster) (*credentialTypes.NutanixCredentialReference, error) { - if nutanixCluster == nil { - return nil, fmt.Errorf("cannot get credential reference if nutanix cluster object is nil") - } - prismCentralInfo := nutanixCluster.Spec.PrismCentral - if prismCentralInfo == nil { - return nil, nil - } - if prismCentralInfo.CredentialRef == nil { - return nil, fmt.Errorf("credentialRef must be set on prismCentral attribute for cluster %s in namespace %s", nutanixCluster.Name, nutanixCluster.Namespace) - } - if prismCentralInfo.CredentialRef.Kind != credentialTypes.SecretKind { - return nil, nil - } - - return prismCentralInfo.CredentialRef, nil -} diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 3930c7502b..65286ebea3 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -1,12 +1,689 @@ +/* +Copyright 2024 Nutanix + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + package client import ( - "github.com/stretchr/testify/assert" + "context" + _ "embed" + "fmt" + "net/url" + "os" "testing" + "time" + + prismgoclient "github.com/nutanix-cloud-native/prism-go-client" + "github.com/nutanix-cloud-native/prism-go-client/environment/credentials" + envTypes "github.com/nutanix-cloud-native/prism-go-client/environment/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" + "k8s.io/client-go/kubernetes/fake" + "k8s.io/client-go/tools/cache" + ctrl "sigs.k8s.io/controller-runtime" + + infrav1 "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1" ) -func TestClient(t *testing.T) { - clientHelper, err := NewNutanixClientHelper(nil, nil) - assert.NoError(t, err) - assert.NotNil(t, clientHelper) +var ( + //go:embed testdata/validTestCredentials.json + validTestCredentials string + //go:embed testdata/validTestManagerCredentials.json + validTestManagerCredentials string + + certBundleKey = "ca.crt" + //go:embed testdata/validTestCA.pem + validTestCA string + //go:embed testdata/validTestManagerCA.pem + validTestManagerCA string + + //go:embed testdata/validTestConfig.json + validTestConfig string + //go:embed testdata/invalidTestConfigMissingCredentialRef.json + invalidTestConfigMissingCredentialRef string +) + +var ( + testSecrets = []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "creds", + Namespace: "test", + }, + Data: map[string][]byte{ + credentials.KeyName: []byte(validTestCredentials), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "capx-nutanix-creds", + Namespace: "capx-system", + }, + Data: map[string][]byte{ + credentials.KeyName: []byte(validTestManagerCredentials), + }, + }, + } + testConfigMaps = []corev1.ConfigMap{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cm", + Namespace: "test", + }, + BinaryData: map[string][]byte{ + certBundleKey: []byte(validTestCA), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "cm", + Namespace: "capx-system", + }, + BinaryData: map[string][]byte{ + certBundleKey: []byte(validTestManagerCA), + }, + }, + } +) + +// setup a single controller context to be shared in unit tests +// otherwise it gets prematurely closed +var controllerCtx = ctrl.SetupSignalHandler() + +func Test_buildManagementEndpoint(t *testing.T) { + t.Parallel() + tests := []struct { + name string + helper *NutanixClientHelper + nutanixCluster *infrav1.NutanixCluster + expectedManagementEndpoint *envTypes.ManagementEndpoint + expectedErr error + }{ + { + name: "all information set in NutanixCluster, should not fallback to management", + // asserting that not being able to read the file will not result in an error + helper: testHelperWithFakedInformers(testSecrets, testConfigMaps).withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return nil, fmt.Errorf("could not read config") + }, + ), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Address: "cluster-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: "test", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "test", + }, + }, + }, + }, + expectedManagementEndpoint: &envTypes.ManagementEndpoint{ + ApiCredentials: envTypes.ApiCredentials{ + Username: "user", + Password: "password", + }, + Address: &url.URL{ + Scheme: "https", + Host: "cluster-endpoint:9440", + }, + AdditionalTrustBundle: validTestCA, + }, + }, + { + name: "information missing in NutanixCluster, fallback to management", + helper: testHelperWithFakedInformers(testSecrets, testConfigMaps).withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return &credentials.NutanixPrismEndpoint{ + Address: "manager-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "capx-nutanix-creds", + Namespace: "capx-system", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "capx-system", + }, + }, nil + }, + ), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{}, + }, + expectedManagementEndpoint: &envTypes.ManagementEndpoint{ + ApiCredentials: envTypes.ApiCredentials{ + Username: "admin", + Password: "adminpassword", + }, + Address: &url.URL{ + Scheme: "https", + Host: "manager-endpoint:9440", + }, + AdditionalTrustBundle: validTestManagerCA, + }, + }, + { + name: "NutanixCluster missing required information, should fail", + helper: testHelperWithFakedInformers(testSecrets, testConfigMaps).withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return &credentials.NutanixPrismEndpoint{ + Address: "manager-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "capx-nutanix-creds", + Namespace: "capx-system", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "capx-system", + }, + }, nil + }, + ), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: "test", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "test", + }, + }, + }, + }, + expectedErr: fmt.Errorf("error building an environment provider from NutanixCluster: %w", ErrPrismAddressNotSet), + }, + { + name: "could not read management configuration, should fail", + helper: testHelperWithFakedInformers(testSecrets, testConfigMaps).withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return nil, fmt.Errorf("could not read config") + }, + ), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{}, + }, + expectedErr: fmt.Errorf("error building an environment provider from file: %w", fmt.Errorf("failed to create prism endpoint: %w", fmt.Errorf("could not read config"))), + }, + } + for _, tt := range tests { + tt := tt // Capture range variable. + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + me, err := tt.helper.buildManagementEndpoint(context.TODO(), tt.nutanixCluster) + assert.Equal(t, tt.expectedManagementEndpoint, me) + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func Test_buildClientFromCredentials(t *testing.T) { + t.Parallel() + tests := []struct { + name string + creds prismgoclient.Credentials + additionalTrustBundle string + expectClientToBeNil bool + expectedErr error + }{ + { + name: "all information set", + creds: prismgoclient.Credentials{ + Endpoint: "cluster-endpoint", + Port: "9440", + Username: "user", + Password: "password", + }, + additionalTrustBundle: validTestCA, + }, + { + name: "some information set, expect defaults", + creds: prismgoclient.Credentials{ + Endpoint: "cluster-endpoint", + Username: "user", + Password: "password", + }, + }, + { + name: "missing username", + creds: prismgoclient.Credentials{ + Endpoint: "cluster-endpoint", + Port: "9440", + Password: "password", + }, + additionalTrustBundle: validTestCA, + expectClientToBeNil: true, + expectedErr: ErrPrismIUsernameNotSet, + }, + { + name: "missing password", + creds: prismgoclient.Credentials{ + Endpoint: "cluster-endpoint", + Port: "9440", + Username: "user", + }, + additionalTrustBundle: validTestCA, + expectClientToBeNil: true, + expectedErr: ErrPrismIPasswordNotSet, + }, + } + for _, tt := range tests { + tt := tt // Capture range variable. + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + client, err := buildClientFromCredentials(tt.creds, tt.additionalTrustBundle) + if tt.expectClientToBeNil { + assert.Nil(t, client) + } else { + assert.NotNil(t, client) + } + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func Test_buildProviderFromNutanixCluster(t *testing.T) { + t.Parallel() + tests := []struct { + name string + helper *NutanixClientHelper + nutanixCluster *infrav1.NutanixCluster + expectProviderToBeNil bool + expectedErr error + }{ + { + name: "all information set", + helper: testHelper(), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Address: "cluster-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: "test", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "test", + }, + }, + }, + }, + expectProviderToBeNil: false, + }, + { + name: "namespace not set, should not fail", + helper: testHelper(), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Address: "cluster-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + }, + }, + }, + }, + expectProviderToBeNil: false, + }, + { + name: "address is empty, should fail", + helper: testHelper(), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Address: "", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: "test", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "test", + }, + }, + }, + }, + expectProviderToBeNil: true, + expectedErr: ErrPrismAddressNotSet, + }, + { + name: "port is not set, should fail", + helper: testHelper(), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Address: "cluster-endpoint", + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: "test", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "test", + }, + }, + }, + }, + expectProviderToBeNil: true, + expectedErr: ErrPrismPortNotSet, + }, + { + name: "CredentialRef is not set, should fail", + helper: testHelper(), + nutanixCluster: &infrav1.NutanixCluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: corev1.NamespaceDefault, + }, + Spec: infrav1.NutanixClusterSpec{ + PrismCentral: &credentials.NutanixPrismEndpoint{ + Address: "cluster-endpoint", + Port: 9440, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "test", + }, + }, + }, + }, + expectProviderToBeNil: true, + expectedErr: fmt.Errorf("credentialRef must be set on prismCentral attribute for cluster %s in namespace %s", "test", corev1.NamespaceDefault), + }, + } + for _, tt := range tests { + tt := tt // Capture range variable. + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + provider, err := tt.helper.buildProviderFromNutanixCluster(tt.nutanixCluster) + if tt.expectProviderToBeNil { + assert.Nil(t, provider) + } else { + assert.NotNil(t, provider) + } + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func Test_buildProviderFromFile(t *testing.T) { + tests := []struct { + name string + helper *NutanixClientHelper + expectProviderToBeNil bool + envs map[string]string + expectedErr error + }{ + { + name: "all information set", + helper: testHelper().withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return &credentials.NutanixPrismEndpoint{ + Address: "manager-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "capx-nutanix-creds", + Namespace: "capx-system", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "capx-system", + }, + }, nil + }, + ), + expectProviderToBeNil: false, + }, + { + name: "namespace not set with env, should not fail", + helper: testHelper().withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return &credentials.NutanixPrismEndpoint{ + Address: "manager-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "capx-nutanix-creds", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + }, + }, nil + }, + ), + envs: map[string]string{capxNamespaceKey: "test"}, + expectProviderToBeNil: false, + }, + { + name: "credentialRef namespace not set, should fail", + helper: testHelper().withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return &credentials.NutanixPrismEndpoint{ + Address: "cluster-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "capx-nutanix-creds", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + Namespace: "capx-system", + }, + }, nil + }, + ), + expectProviderToBeNil: true, + expectedErr: fmt.Errorf("failed to retrieve capx-namespace. Make sure %s env variable is set", capxNamespaceKey), + }, + { + name: "additionalTrustBundle namespace not set, should fail", + helper: testHelper().withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return &credentials.NutanixPrismEndpoint{ + Address: "cluster-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: "capx-system", + }, + AdditionalTrustBundle: &credentials.NutanixTrustBundleReference{ + Kind: credentials.NutanixTrustBundleKindConfigMap, + Name: "cm", + }, + }, nil + }, + ), + expectProviderToBeNil: true, + expectedErr: fmt.Errorf("failed to retrieve capx-namespace. Make sure %s env variable is set", capxNamespaceKey), + }, + { + name: "reader returns an error, should fail", + helper: testHelper().withCustomNutanixPrismEndpointReader( + func() (*credentials.NutanixPrismEndpoint, error) { + return nil, fmt.Errorf("could not read config") + }, + ), + expectProviderToBeNil: true, + expectedErr: fmt.Errorf("failed to create prism endpoint: %w", fmt.Errorf("could not read config")), + }, + } + for _, tt := range tests { + tt := tt // Capture range variable. + t.Run(tt.name, func(t *testing.T) { + for k, v := range tt.envs { + t.Setenv(k, v) + } + provider, err := tt.helper.buildProviderFromFile() + if tt.expectProviderToBeNil { + assert.Nil(t, provider) + } else { + assert.NotNil(t, provider) + } + assert.Equal(t, tt.expectedErr, err) + }) + } +} + +func Test_readManagerNutanixPrismEndpointFromFile(t *testing.T) { + t.Parallel() + tests := []struct { + name string + config []byte + expectedEndpoint *credentials.NutanixPrismEndpoint + expectedErrString string + }{ + { + name: "valid config", + config: []byte(validTestConfig), + expectedEndpoint: &credentials.NutanixPrismEndpoint{ + Address: "cluster-endpoint", + Port: 9440, + CredentialRef: &credentials.NutanixCredentialReference{ + Kind: credentials.SecretKind, + Name: "creds", + Namespace: "test", + }, + }, + }, + { + name: "invalid config, expect an error", + config: []byte("{{}"), + expectedErrString: "failed to unmarshal config: invalid character '{' looking for beginning of object key string", + }, + { + name: "missing CredentialRef, expect an error", + config: []byte(invalidTestConfigMissingCredentialRef), + expectedErrString: "credentialRef must be set on CAPX manager", + }, + } + for _, tt := range tests { + tt := tt // Capture range variable. + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + f, err := os.CreateTemp(t.TempDir(), "config-*.json") + require.NoError(t, err, "error creating temp file") + _, err = f.Write(tt.config) + require.NoError(t, err, "error writing temp file") + + endpoint, err := readManagerNutanixPrismEndpointFromFile(f.Name()) + if err != nil { + assert.ErrorContains(t, err, tt.expectedErrString) + } + assert.Equal(t, tt.expectedEndpoint, endpoint) + }) + } +} + +func Test_readManagerNutanixPrismEndpointFromFile_IsNotExist(t *testing.T) { + _, err := readManagerNutanixPrismEndpointFromFile("filedoesnotexist.json") + assert.ErrorIs(t, err, os.ErrNotExist) +} + +func testHelper() *NutanixClientHelper { + helper := NutanixClientHelper{} + return &helper +} + +func testHelperWithFakedInformers(secrets []corev1.Secret, configMaps []corev1.ConfigMap) *NutanixClientHelper { + objects := make([]runtime.Object, 0) + for i := range secrets { + secret := secrets[i] + objects = append(objects, &secret) + } + for i := range configMaps { + cm := configMaps[i] + objects = append(objects, &cm) + } + + fakeClient := fake.NewSimpleClientset(objects...) + informerFactory := informers.NewSharedInformerFactory(fakeClient, time.Minute) + secretInformer := informerFactory.Core().V1().Secrets() + informer := secretInformer.Informer() + go informer.Run(controllerCtx.Done()) + cache.WaitForCacheSync(controllerCtx.Done(), informer.HasSynced) + + configMapInformer := informerFactory.Core().V1().ConfigMaps() + informer = configMapInformer.Informer() + go informer.Run(controllerCtx.Done()) + cache.WaitForCacheSync(controllerCtx.Done(), informer.HasSynced) + + return NewHelper(secretInformer, configMapInformer) } diff --git a/pkg/client/testdata/invalidTestConfigMissingCredentialRef.json b/pkg/client/testdata/invalidTestConfigMissingCredentialRef.json new file mode 100644 index 0000000000..ae1a540481 --- /dev/null +++ b/pkg/client/testdata/invalidTestConfigMissingCredentialRef.json @@ -0,0 +1,4 @@ +{ + "address": "cluster-endpoint", + "port": 9440 +} \ No newline at end of file diff --git a/pkg/client/testdata/validTestCA.pem b/pkg/client/testdata/validTestCA.pem new file mode 100644 index 0000000000..471f3c553b --- /dev/null +++ b/pkg/client/testdata/validTestCA.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkGA1UEBhMCQkUx +GTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jvb3QgQ0ExGzAZBgNVBAMTEkds +b2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAwMDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYD +VQQDExJHbG9iYWxTaWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDa +DuaZjc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavpxy0Sy6sc +THAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp1Wrjsok6Vjk4bwY8iGlb +Kk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdGsnUOhugZitVtbNV4FpWi6cgKOOvyJBNP +c1STE4U6G7weNLWLBYy5d4ux2x8gkasJU26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrX +gzT/LCrBbBlDSgeF59N89iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0BAQUF +AAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOzyj1hTdNGCbM+w6Dj +Y1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE38NflNUVyRRBnMRddWQVDf9VMOyG +j/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymPAbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhH +hm4qxFYxldBniYUr+WymXUadDKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveC +X4XSQRjbgbMEHMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/pkg/client/testdata/validTestConfig.json b/pkg/client/testdata/validTestConfig.json new file mode 100644 index 0000000000..f61e1b0a73 --- /dev/null +++ b/pkg/client/testdata/validTestConfig.json @@ -0,0 +1,9 @@ +{ + "address": "cluster-endpoint", + "port": 9440, + "credentialRef": { + "kind": "Secret", + "name": "creds", + "namespace": "test" + } +} \ No newline at end of file diff --git a/pkg/client/testdata/validTestCredentials.json b/pkg/client/testdata/validTestCredentials.json new file mode 100644 index 0000000000..ea5c8fb8cc --- /dev/null +++ b/pkg/client/testdata/validTestCredentials.json @@ -0,0 +1,11 @@ +[ + { + "type": "basic_auth", + "data": { + "prismCentral":{ + "username": "user", + "password": "password" + } + } + } +] \ No newline at end of file diff --git a/pkg/client/testdata/validTestManagerCA.pem b/pkg/client/testdata/validTestManagerCA.pem new file mode 100644 index 0000000000..4fdfa0b60a --- /dev/null +++ b/pkg/client/testdata/validTestManagerCA.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIEKjCCAxKgAwIBAgIEOGPe+DANBgkqhkiG9w0BAQUFADCBtDEUMBIGA1UEChMLRW50cnVzdC5u +ZXQxQDA+BgNVBAsUN3d3dy5lbnRydXN0Lm5ldC9DUFNfMjA0OCBpbmNvcnAuIGJ5IHJlZi4gKGxp +bWl0cyBsaWFiLikxJTAjBgNVBAsTHChjKSAxOTk5IEVudHJ1c3QubmV0IExpbWl0ZWQxMzAxBgNV +BAMTKkVudHJ1c3QubmV0IENlcnRpZmljYXRpb24gQXV0aG9yaXR5ICgyMDQ4KTAeFw05OTEyMjQx +NzUwNTFaFw0yOTA3MjQxNDE1MTJaMIG0MRQwEgYDVQQKEwtFbnRydXN0Lm5ldDFAMD4GA1UECxQ3 +d3d3LmVudHJ1c3QubmV0L0NQU18yMDQ4IGluY29ycC4gYnkgcmVmLiAobGltaXRzIGxpYWIuKTEl +MCMGA1UECxMcKGMpIDE5OTkgRW50cnVzdC5uZXQgTGltaXRlZDEzMDEGA1UEAxMqRW50cnVzdC5u +ZXQgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgKDIwNDgpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEArU1LqRKGsuqjIAcVFmQqK0vRvwtKTY7tgHalZ7d4QMBzQshowNtTK91euHaYNZOL +Gp18EzoOH1u3Hs/lJBQesYGpjX24zGtLA/ECDNyrpUAkAH90lKGdCCmziAv1h3edVc3kw37XamSr +hRSGlVuXMlBvPci6Zgzj/L24ScF2iUkZ/cCovYmjZy/Gn7xxGWC4LeksyZB2ZnuU4q941mVTXTzW +nLLPKQP5L6RQstRIzgUyVYr9smRMDuSYB3Xbf9+5CFVghTAp+XtIpGmG4zU/HoZdenoVve8AjhUi +VBcAkCaTvA5JaJG/+EfTnZVCwQ5N328mz8MYIWJmQ3DW1cAH4QIDAQABo0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVeSB0RGAvtiJuQijMfmhJAkWuXAwDQYJ +KoZIhvcNAQEFBQADggEBADubj1abMOdTmXx6eadNl9cZlZD7Bh/KM3xGY4+WZiT6QBshJ8rmcnPy +T/4xmf3IDExoU8aAghOY+rat2l098c5u9hURlIIM7j+VrxGrD9cv3h8Dj1csHsm7mhpElesYT6Yf +zX1XEC+bBAlahLVu2B064dae0Wx5XnkcFMXj0EyTO2U87d89vqbllRrDtRnDvV5bu/8j72gZyxKT +J1wDLW8w0B62GqzeWvfRqqgnpv55gcR5mTNXuhKwqeBCbJPKVt7+bYQLCIt+jerXmCHG8+c8eS9e +nNFMFY3h7CI3zJpDC5fcgJCNs2ebb0gIFVbPv/ErfF6adulZkMV8gzURZVE= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/pkg/client/testdata/validTestManagerCredentials.json b/pkg/client/testdata/validTestManagerCredentials.json new file mode 100644 index 0000000000..e9a28975cc --- /dev/null +++ b/pkg/client/testdata/validTestManagerCredentials.json @@ -0,0 +1,11 @@ +[ + { + "type": "basic_auth", + "data": { + "prismCentral":{ + "username": "admin", + "password": "adminpassword" + } + } + } +] \ No newline at end of file diff --git a/test/e2e/nutanix_client.go b/test/e2e/nutanix_client.go index 09b05c6426..73deb010dd 100644 --- a/test/e2e/nutanix_client.go +++ b/test/e2e/nutanix_client.go @@ -30,7 +30,7 @@ import ( . "github.com/onsi/gomega" "sigs.k8s.io/cluster-api/test/framework/clusterctl" - nutanixClientHelper "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/client" + nutanixClient "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/client" ) const ( @@ -127,11 +127,10 @@ func initNutanixClient(e2eConfig clusterctl.E2EConfig) (*prismGoClientV3.Client, } trustBundle = string(decodedCert) } - nch := nutanixClientHelper.NutanixClientHelper{} - nutanixClient, err := nch.GetClient(*creds, trustBundle) + client, err := nutanixClient.Build(*creds, trustBundle) if err != nil { return nil, err } - return nutanixClient, nil + return client, nil }