Skip to content

Commit fac5b21

Browse files
Introduce a nutanix prism client cache (#415)
* Introduce a nutanix prism client cache The cache stores a prismgoclient.V3 client instance for each NutanixCluster instance. The cache is shared between nutanixcluster and nutanixmachine controllers. * Address review comments
1 parent 8228f42 commit fac5b21

17 files changed

+668
-541
lines changed

Makefile

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,10 @@ mocks: ## Generate mocks for the project
306306
mockgen -destination=mocks/ctlclient/cache_mock.go -package=mockctlclient sigs.k8s.io/controller-runtime/pkg/cache Cache
307307
mockgen -destination=mocks/k8sclient/cm_informer.go -package=mockk8sclient k8s.io/client-go/informers/core/v1 ConfigMapInformer
308308
mockgen -destination=mocks/k8sclient/secret_informer.go -package=mockk8sclient k8s.io/client-go/informers/core/v1 SecretInformer
309+
mockgen -destination=mocks/k8sclient/secret_lister.go -package=mockk8sclient k8s.io/client-go/listers/core/v1 SecretLister
310+
mockgen -destination=mocks/k8sclient/secret_namespace_lister.go -package=mockk8sclient k8s.io/client-go/listers/core/v1 SecretNamespaceLister
309311

310-
GOTESTPKGS = $(shell go list ./... | grep -v /mocks | grep -v /templates)
312+
GOTESTPKGS = $(shell go list ./... | grep -v /mocks | grep -v /templates | grep -v /v1alpha4)
311313

312314
.PHONY: unit-test
313315
unit-test: mocks ## Run unit tests.

api/v1beta1/nutanixcluster_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
"cmp"
2021
"fmt"
2122

2223
credentialTypes "github.com/nutanix-cloud-native/prism-go-client/environment/credentials"
24+
corev1 "k8s.io/api/core/v1"
2325
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2426
capiv1 "sigs.k8s.io/cluster-api/api/v1beta1"
2527
"sigs.k8s.io/cluster-api/errors"
@@ -162,6 +164,12 @@ func (ncl *NutanixCluster) GetPrismCentralCredentialRef() (*credentialTypes.Nuta
162164
return prismCentralInfo.CredentialRef, nil
163165
}
164166

167+
// GetNamespacedName returns the namespaced name of the NutanixCluster.
168+
func (ncl *NutanixCluster) GetNamespacedName() string {
169+
namespace := cmp.Or(ncl.Namespace, corev1.NamespaceDefault)
170+
return fmt.Sprintf("%s/%s", namespace, ncl.Name)
171+
}
172+
165173
// +kubebuilder:object:root=true
166174

167175
// NutanixClusterList contains a list of NutanixCluster

api/v1beta1/nutanixcluster_types_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,3 +112,39 @@ func TestGetCredentialRefForCluster(t *testing.T) {
112112
})
113113
}
114114
}
115+
116+
func TestGetNamespacedName(t *testing.T) {
117+
t.Parallel()
118+
tests := []struct {
119+
name string
120+
nutanixCluster *NutanixCluster
121+
expectedFullName string
122+
}{
123+
{
124+
name: "namespace and name are set",
125+
nutanixCluster: &NutanixCluster{
126+
ObjectMeta: metav1.ObjectMeta{
127+
Name: "test",
128+
Namespace: "test-namespace",
129+
},
130+
},
131+
expectedFullName: "test-namespace/test",
132+
},
133+
{
134+
name: "namespace is not set, should use default",
135+
nutanixCluster: &NutanixCluster{
136+
ObjectMeta: metav1.ObjectMeta{
137+
Name: "test",
138+
},
139+
},
140+
expectedFullName: "default/test",
141+
},
142+
}
143+
144+
for _, tt := range tests {
145+
t.Run(tt.name, func(t *testing.T) {
146+
fullName := tt.nutanixCluster.GetNamespacedName()
147+
assert.Equal(t, tt.expectedFullName, fullName)
148+
})
149+
}
150+
}

controllers/helpers.go

Lines changed: 68 additions & 49 deletions
Large diffs are not rendered by default.

controllers/helpers_test.go

Lines changed: 113 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,24 @@ package controllers
1818

1919
import (
2020
"context"
21+
"encoding/json"
22+
"errors"
2123
"testing"
2224

23-
credentialTypes "github.com/nutanix-cloud-native/prism-go-client/environment/credentials"
25+
"github.com/golang/mock/gomock"
26+
credentialtypes "github.com/nutanix-cloud-native/prism-go-client/environment/credentials"
27+
prismclientv3 "github.com/nutanix-cloud-native/prism-go-client/v3"
28+
. "github.com/onsi/ginkgo/v2"
29+
. "github.com/onsi/gomega"
30+
"github.com/stretchr/testify/assert"
31+
"github.com/stretchr/testify/require"
32+
corev1 "k8s.io/api/core/v1"
2433
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2534
"sigs.k8s.io/cluster-api/util"
2635

2736
infrav1 "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1"
28-
29-
. "github.com/onsi/ginkgo/v2"
30-
. "github.com/onsi/gomega"
37+
mockk8sclient "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/mocks/k8sclient"
38+
nutanixclient "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/client"
3139
)
3240

3341
func TestControllerHelpers(t *testing.T) {
@@ -52,7 +60,7 @@ func TestControllerHelpers(t *testing.T) {
5260
Namespace: "default",
5361
},
5462
Spec: infrav1.NutanixClusterSpec{
55-
PrismCentral: &credentialTypes.NutanixPrismEndpoint{
63+
PrismCentral: &credentialtypes.NutanixPrismEndpoint{
5664
// Adding port info to override default value (0)
5765
Port: 9440,
5866
},
@@ -121,3 +129,103 @@ func TestControllerHelpers(t *testing.T) {
121129
})
122130
})
123131
}
132+
133+
func TestGetPrismCentralClientForCluster(t *testing.T) {
134+
ctx := context.Background()
135+
cluster := &infrav1.NutanixCluster{
136+
Spec: infrav1.NutanixClusterSpec{
137+
PrismCentral: &credentialtypes.NutanixPrismEndpoint{
138+
Address: "prismcentral.nutanix.com",
139+
Port: 9440,
140+
CredentialRef: &credentialtypes.NutanixCredentialReference{
141+
Kind: credentialtypes.SecretKind,
142+
Name: "test-credential",
143+
Namespace: "test-ns",
144+
},
145+
},
146+
},
147+
}
148+
149+
t.Run("BuildManagementEndpoint Fails", func(t *testing.T) {
150+
ctrl := gomock.NewController(t)
151+
152+
secretNamespaceLister := mockk8sclient.NewMockSecretNamespaceLister(ctrl)
153+
secretNamespaceLister.EXPECT().Get("test-credential").Return(nil, errors.New("failed to get secret"))
154+
secretLister := mockk8sclient.NewMockSecretLister(ctrl)
155+
secretLister.EXPECT().Secrets("test-ns").Return(secretNamespaceLister)
156+
secretInformer := mockk8sclient.NewMockSecretInformer(ctrl)
157+
mapInformer := mockk8sclient.NewMockConfigMapInformer(ctrl)
158+
secretInformer.EXPECT().Lister().Return(secretLister)
159+
160+
_, err := getPrismCentralClientForCluster(ctx, cluster, secretInformer, mapInformer)
161+
assert.Error(t, err)
162+
})
163+
164+
t.Run("GetOrCreate Fails", func(t *testing.T) {
165+
ctrl := gomock.NewController(t)
166+
167+
creds := []credentialtypes.Credential{
168+
{
169+
Type: credentialtypes.BasicAuthCredentialType,
170+
Data: []byte(`{"prismCentral":{"username":"user","password":"password"}}`),
171+
},
172+
}
173+
credsMarshal, err := json.Marshal(creds)
174+
require.NoError(t, err)
175+
176+
secret := &corev1.Secret{
177+
Data: map[string][]byte{
178+
credentialtypes.KeyName: credsMarshal,
179+
},
180+
}
181+
182+
secretNamespaceLister := mockk8sclient.NewMockSecretNamespaceLister(ctrl)
183+
secretNamespaceLister.EXPECT().Get("test-credential").Return(secret, nil)
184+
secretLister := mockk8sclient.NewMockSecretLister(ctrl)
185+
secretLister.EXPECT().Secrets("test-ns").Return(secretNamespaceLister)
186+
secretInformer := mockk8sclient.NewMockSecretInformer(ctrl)
187+
mapInformer := mockk8sclient.NewMockConfigMapInformer(ctrl)
188+
secretInformer.EXPECT().Lister().Return(secretLister)
189+
190+
_, err = getPrismCentralClientForCluster(ctx, cluster, secretInformer, mapInformer)
191+
assert.Error(t, err)
192+
})
193+
194+
t.Run("GetOrCreate succeeds", func(t *testing.T) {
195+
ctrl := gomock.NewController(t)
196+
197+
oldNutanixClientCache := nutanixclient.NutanixClientCache
198+
defer func() {
199+
nutanixclient.NutanixClientCache = oldNutanixClientCache
200+
}()
201+
202+
// Create a new client cache with session auth disabled to avoid network calls in tests
203+
nutanixclient.NutanixClientCache = prismclientv3.NewClientCache()
204+
205+
creds := []credentialtypes.Credential{
206+
{
207+
Type: credentialtypes.BasicAuthCredentialType,
208+
Data: []byte(`{"prismCentral":{"username":"user","password":"password"}}`),
209+
},
210+
}
211+
212+
credsMarshal, err := json.Marshal(creds)
213+
require.NoError(t, err)
214+
secret := &corev1.Secret{
215+
Data: map[string][]byte{
216+
credentialtypes.KeyName: credsMarshal,
217+
},
218+
}
219+
220+
secretNamespaceLister := mockk8sclient.NewMockSecretNamespaceLister(ctrl)
221+
secretNamespaceLister.EXPECT().Get("test-credential").Return(secret, nil)
222+
secretLister := mockk8sclient.NewMockSecretLister(ctrl)
223+
secretLister.EXPECT().Secrets("test-ns").Return(secretNamespaceLister)
224+
secretInformer := mockk8sclient.NewMockSecretInformer(ctrl)
225+
mapInformer := mockk8sclient.NewMockConfigMapInformer(ctrl)
226+
secretInformer.EXPECT().Lister().Return(secretLister)
227+
228+
_, err = getPrismCentralClientForCluster(ctx, cluster, secretInformer, mapInformer)
229+
assert.NoError(t, err)
230+
})
231+
}

controllers/nutanixcluster_controller.go

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"sigs.k8s.io/controller-runtime/pkg/source"
4545

4646
infrav1 "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/api/v1beta1"
47+
nutanixClient "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/client"
4748
nctx "github.com/nutanix-cloud-native/cluster-api-provider-nutanix/pkg/context"
4849
)
4950

@@ -170,20 +171,18 @@ func (r *NutanixClusterReconciler) Reconcile(ctx context.Context, req ctrl.Reque
170171
log.V(1).Info(fmt.Sprintf("Patched NutanixCluster. Status: %+v", cluster.Status))
171172
}()
172173

173-
err = r.reconcileCredentialRef(ctx, cluster)
174-
if err != nil {
174+
if err := r.reconcileCredentialRef(ctx, cluster); err != nil {
175175
log.Error(err, fmt.Sprintf("error occurred while reconciling credential ref for cluster %s", capiCluster.Name))
176176
conditions.MarkFalse(cluster, infrav1.CredentialRefSecretOwnerSetCondition, infrav1.CredentialRefSecretOwnerSetFailed, capiv1.ConditionSeverityError, err.Error())
177177
return reconcile.Result{}, err
178178
}
179179
conditions.MarkTrue(cluster, infrav1.CredentialRefSecretOwnerSetCondition)
180180

181-
v3Client, err := CreateNutanixClient(ctx, r.SecretInformer, r.ConfigMapInformer, cluster)
181+
v3Client, err := getPrismCentralClientForCluster(ctx, cluster, r.SecretInformer, r.ConfigMapInformer)
182182
if err != nil {
183-
conditions.MarkFalse(cluster, infrav1.PrismCentralClientCondition, infrav1.PrismCentralClientInitializationFailed, capiv1.ConditionSeverityError, err.Error())
184-
return ctrl.Result{Requeue: true}, fmt.Errorf("nutanix client error: %v", err)
183+
log.Error(err, "error occurred while fetching prism central client")
184+
return reconcile.Result{}, err
185185
}
186-
conditions.MarkTrue(cluster, infrav1.PrismCentralClientCondition)
187186

188187
rctx := &nctx.ClusterContext{
189188
Context: ctx,
@@ -224,6 +223,10 @@ func (r *NutanixClusterReconciler) reconcileDelete(rctx *nctx.ClusterContext) (r
224223
return reconcile.Result{}, err
225224
}
226225

226+
// delete the client from the cache
227+
log.Info(fmt.Sprintf("deleting nutanix prism client for cluster %s from cache", rctx.NutanixCluster.GetNamespacedName()))
228+
nutanixClient.NutanixClientCache.Delete(&nutanixClient.CacheParams{NutanixCluster: rctx.NutanixCluster})
229+
227230
err = r.reconcileCredentialRefDelete(rctx.Context, rctx.NutanixCluster)
228231
if err != nil {
229232
log.Error(err, fmt.Sprintf("error occurred while reconciling credential ref deletion for cluster %s", rctx.Cluster.Name))
@@ -376,21 +379,24 @@ func (r *NutanixClusterReconciler) reconcileCredentialRef(ctx context.Context, n
376379
if err != nil {
377380
return err
378381
}
382+
383+
secret := &corev1.Secret{}
379384
if credentialRef == nil {
380385
return nil
381386
}
387+
382388
log.V(1).Info(fmt.Sprintf("credential ref is kind Secret for cluster %s", nutanixCluster.Name))
383-
secret := &corev1.Secret{}
384389
secretKey := client.ObjectKey{
385390
Namespace: nutanixCluster.Namespace,
386391
Name: credentialRef.Name,
387392
}
388-
err = r.Client.Get(ctx, secretKey, secret)
389-
if err != nil {
393+
394+
if err := r.Client.Get(ctx, secretKey, secret); err != nil {
390395
errorMsg := fmt.Errorf("error occurred while fetching cluster %s secret for credential ref: %v", nutanixCluster.Name, err)
391396
log.Error(errorMsg, "error occurred fetching cluster")
392397
return errorMsg
393398
}
399+
394400
// Check if ownerRef is already set on nutanixCluster object
395401
if !capiutil.IsOwnedByObject(secret, nutanixCluster) {
396402
// Check if another nutanixCluster already has set ownerRef. Secret can only be owned by one nutanixCluster object
@@ -407,15 +413,18 @@ func (r *NutanixClusterReconciler) reconcileCredentialRef(ctx context.Context, n
407413
Name: nutanixCluster.Name,
408414
})
409415
}
416+
410417
if !ctrlutil.ContainsFinalizer(secret, infrav1.NutanixClusterCredentialFinalizer) {
411418
ctrlutil.AddFinalizer(secret, infrav1.NutanixClusterCredentialFinalizer)
412419
}
420+
413421
err = r.Client.Update(ctx, secret)
414422
if err != nil {
415423
errorMsg := fmt.Errorf("failed to update secret for cluster %s: %v", nutanixCluster.Name, err)
416424
log.Error(errorMsg, "failed to update secret")
417425
return errorMsg
418426
}
427+
419428
return nil
420429
}
421430

0 commit comments

Comments
 (0)