Browse Source

IBM Provider: enable ESO to pull secrets by name (#2326)

* IBM Provider: enable ESO to pull secrets by name

Signed-off-by: tanishg6@gmail.com <tanishg6@gmail.com>

* document ESO's capability to pull by secret name for IBM provider

Signed-off-by: tanishg6@gmail.com <tanishg6@gmail.com>

* correct the metrics instrumentation

Signed-off-by: tanishg6@gmail.com <tanishg6@gmail.com>

---------

Signed-off-by: tanishg6@gmail.com <tanishg6@gmail.com>
Shanti G 2 years ago
parent
commit
00bc81c8c7

+ 6 - 1
docs/provider/ibm-secrets-manager.md

@@ -191,12 +191,17 @@ data:
 ### Creating external secret
 
 To create a kubernetes secret from the IBM Secrets Manager, a `Kind=ExternalSecret` is needed.
+Below example creates a kubernetes secret based on ID of the secret in Secrets Manager.
 
 ```yaml
 {% include 'ibm-external-secret.yaml' %}
 ```
 
-Currently we can only get the secret by its id and not its name, so something like `565287ce-578f-8d96-a746-9409d531fe2a`.
+Alternatively, secret name can be specified instead of secret ID.
+
+```yaml
+{% include 'ibm-external-secret-by-name.yaml' %}
+```
 
 ### Getting the Kubernetes secret
 The operator will fetch the IBM Secret Manager secret and inject it as a `Kind=Secret`

+ 21 - 0
docs/snippets/ibm-external-secret-by-name.yaml

@@ -0,0 +1,21 @@
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+  name: database-credentials
+spec:
+  refreshInterval: 60m
+  secretStoreRef:
+    name: ibm-store
+    kind: SecretStore
+  target:
+    name: database-credentials
+    creationPolicy: Owner
+  data:
+  - secretKey: username
+    remoteRef:
+      key: username_password/<SECRET_NAME>
+      property: username
+  - secretKey: password
+    remoteRef:
+      key: username_password/<SECRET_NAME>
+      property: password

+ 4 - 2
docs/snippets/ibm-external-secret.yaml

@@ -13,7 +13,9 @@ spec:
   data:
   - secretKey: username
     remoteRef:
-      key: database_user
+      key: username_password/<SECRET_ID>
+      property: username
   - secretKey: password
     remoteRef:
-      key: database_password
+      key: username_password/<SECRET_ID>
+      property: password

+ 1 - 1
go.mod

@@ -135,7 +135,7 @@ require (
 	github.com/goccy/go-json v0.10.2 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
-	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
+	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/gnostic v0.6.9 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect

+ 3 - 2
pkg/constants/constants.go

@@ -67,8 +67,9 @@ const (
 	CallKubernetesUpdateSecret                 = "UpdateSecret"
 	CallKubernetesCreateSelfSubjectRulesReview = "CreateSelfSubjectRulesReview"
 
-	ProviderIBMSM      = "IBM/SecretsManager"
-	CallIBMSMGetSecret = "GetSecret"
+	ProviderIBMSM        = "IBM/SecretsManager"
+	CallIBMSMGetSecret   = "GetSecret"
+	CallIBMSMListSecrets = "ListSecrets"
 
 	ProviderWebhook    = "Webhook"
 	CallWebhookHTTPReq = "HTTPRequest"

+ 72 - 0
pkg/provider/ibm/cache.go

@@ -0,0 +1,72 @@
+/*
+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 ibm
+
+import (
+	"time"
+
+	"github.com/golang/groupcache/lru"
+)
+
+type (
+	cacheIntf interface {
+		GetData(key string) (bool, []byte)
+		PutData(key string, value []byte)
+		DeleteData(key string)
+	}
+
+	lruCache struct {
+		lru *lru.Cache
+		ttl time.Duration
+	}
+
+	cacheObject struct {
+		timeExpires time.Time
+		value       []byte
+	}
+)
+
+func NewCache(maxEntries int, ttl time.Duration) cacheIntf {
+	lruCache := &lruCache{
+		lru: lru.New(maxEntries),
+		ttl: ttl,
+	}
+
+	return lruCache
+}
+
+func (c *lruCache) GetData(key string) (bool, []byte) {
+	v, ok := c.lru.Get(key)
+	if !ok {
+		return false, nil
+	}
+	returnedObj := v.(cacheObject)
+	if time.Now().After(returnedObj.timeExpires) && c.ttl > 0 {
+		c.DeleteData(key)
+		return false, nil
+	}
+	return true, returnedObj.value
+}
+
+func (c *lruCache) PutData(key string, value []byte) {
+	obj := cacheObject{
+		timeExpires: time.Now().Add(c.ttl),
+		value:       value,
+	}
+	c.lru.Add(key, obj)
+}
+
+func (c *lruCache) DeleteData(key string) {
+	c.lru.Remove(key)
+}

+ 92 - 0
pkg/provider/ibm/cache_test.go

@@ -0,0 +1,92 @@
+/*
+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 ibm
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+const maxEntries = 10
+
+func dataCache(t *testing.T, ttl time.Duration, maxEntries int) cacheIntf {
+	t.Helper()
+	return NewCache(maxEntries, ttl)
+}
+
+func TestGetData(t *testing.T) {
+	tests := []struct {
+		name      string
+		ttl       time.Duration
+		key       string
+		value     []byte
+		wantValue []byte
+		wantFound bool
+	}{
+		{
+			name:      "object exists in cache and has not expired",
+			ttl:       30 * time.Second,
+			key:       "testObject",
+			value:     []byte("testValue"),
+			wantValue: []byte("testValue"),
+			wantFound: true,
+		},
+		{
+			name:      "object exists in cache and will never expire",
+			ttl:       0 * time.Second,
+			key:       "testObject",
+			value:     []byte("testValue"),
+			wantValue: []byte("testValue"),
+			wantFound: true,
+		},
+		{
+			name:      "object exists in cache but has expired",
+			ttl:       1 * time.Nanosecond,
+			key:       "testObject",
+			value:     []byte("testValue"),
+			wantFound: false,
+		},
+		{
+			name:      "object not in cache",
+			ttl:       30 * time.Second,
+			key:       "testObject",
+			wantFound: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := NewCache(10, tt.ttl)
+			if tt.value != nil {
+				c.PutData(tt.key, tt.value)
+			}
+			gotFound, gotValue := c.GetData(tt.key)
+			assert.Equal(t, tt.wantFound, gotFound)
+			assert.Equal(t, string(gotValue), string(tt.wantValue))
+		})
+	}
+}
+
+func TestPutData(t *testing.T) {
+	t.Parallel()
+	d := dataCache(t, time.Minute, maxEntries)
+	d.PutData("test-key", []byte("test-value"))
+}
+
+func TestDeleteData(t *testing.T) {
+	t.Parallel()
+	d := dataCache(t, time.Minute, maxEntries)
+	d.DeleteData("test-key")
+}

+ 20 - 4
pkg/provider/ibm/fake/fake.go

@@ -24,19 +24,27 @@ import (
 )
 
 type IBMMockClient struct {
-	getSecretWithContext func(ctx context.Context, getSecretOptions *sm.GetSecretOptions) (result sm.SecretIntf, response *core.DetailedResponse, err error)
+	getSecretWithContext   func(ctx context.Context, getSecretOptions *sm.GetSecretOptions) (result sm.SecretIntf, response *core.DetailedResponse, err error)
+	listSecretsWithContext func(ctx context.Context, listSecretsOptions *sm.ListSecretsOptions) (result *sm.SecretMetadataPaginatedCollection, response *core.DetailedResponse, err error)
 }
 
 type IBMMockClientParams struct {
-	GetSecretOptions *sm.GetSecretOptions
-	GetSecretOutput  sm.SecretIntf
-	GetSecretErr     error
+	GetSecretOptions   *sm.GetSecretOptions
+	GetSecretOutput    sm.SecretIntf
+	GetSecretErr       error
+	ListSecretsOptions *sm.ListSecretsOptions
+	ListSecretsOutput  *sm.SecretMetadataPaginatedCollection
+	ListSecretsErr     error
 }
 
 func (mc *IBMMockClient) GetSecretWithContext(ctx context.Context, getSecretOptions *sm.GetSecretOptions) (result sm.SecretIntf, response *core.DetailedResponse, err error) {
 	return mc.getSecretWithContext(ctx, getSecretOptions)
 }
 
+func (mc *IBMMockClient) ListSecretsWithContext(ctx context.Context, listSecretsOptions *sm.ListSecretsOptions) (result *sm.SecretMetadataPaginatedCollection, response *core.DetailedResponse, err error) {
+	return mc.listSecretsWithContext(ctx, listSecretsOptions)
+}
+
 func (mc *IBMMockClient) WithValue(params IBMMockClientParams) {
 	if mc != nil {
 		mc.getSecretWithContext = func(ctx context.Context, paramReq *sm.GetSecretOptions) (sm.SecretIntf, *core.DetailedResponse, error) {
@@ -47,5 +55,13 @@ func (mc *IBMMockClient) WithValue(params IBMMockClientParams) {
 			}
 			return params.GetSecretOutput, nil, params.GetSecretErr
 		}
+		mc.listSecretsWithContext = func(ctx context.Context, paramReq *sm.ListSecretsOptions) (result *sm.SecretMetadataPaginatedCollection, response *core.DetailedResponse, err error) {
+			// type secretmanagerpb.AccessSecretVersionRequest contains unexported fields
+			// use cmpopts.IgnoreUnexported to ignore all the unexported fields in the cmp.
+			if !cmp.Equal(paramReq, params.ListSecretsOptions, cmpopts.IgnoreUnexported(sm.SecretMetadataPaginatedCollection{})) {
+				return nil, nil, fmt.Errorf("unexpected test argument for ListSecrets: %s, %s", *paramReq.Search, *params.ListSecretsOptions.Search)
+			}
+			return params.ListSecretsOutput, nil, params.ListSecretsErr
+		}
 	}
 }

+ 75 - 0
pkg/provider/ibm/helper.go

@@ -0,0 +1,75 @@
+/*
+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 ibm
+
+import (
+	"fmt"
+
+	sm "github.com/IBM/secrets-manager-go-sdk/v2/secretsmanagerv2"
+)
+
+func extractSecretMetadata(response sm.SecretMetadataIntf, givenName *string, secretType string) (*string, *string, error) {
+	switch secretType {
+	case sm.Secret_SecretType_Arbitrary:
+		metadata, ok := response.(*sm.ArbitrarySecretMetadata)
+		if !ok {
+			return nil, nil, fmt.Errorf(errExtractingSecret, *givenName, sm.Secret_SecretType_Arbitrary, "extractSecretMetadata")
+		}
+		return metadata.ID, metadata.Name, nil
+	case sm.Secret_SecretType_UsernamePassword:
+		metadata, ok := response.(*sm.UsernamePasswordSecretMetadata)
+		if !ok {
+			return nil, nil, fmt.Errorf(errExtractingSecret, *givenName, sm.Secret_SecretType_UsernamePassword, "extractSecretMetadata")
+		}
+		return metadata.ID, metadata.Name, nil
+
+	case sm.Secret_SecretType_IamCredentials:
+		metadata, ok := response.(*sm.IAMCredentialsSecretMetadata)
+		if !ok {
+			return nil, nil, fmt.Errorf(errExtractingSecret, *givenName, sm.Secret_SecretType_IamCredentials, "extractSecretMetadata")
+		}
+		return metadata.ID, metadata.Name, nil
+
+	case sm.Secret_SecretType_ImportedCert:
+		metadata, ok := response.(*sm.ImportedCertificateMetadata)
+		if !ok {
+			return nil, nil, fmt.Errorf(errExtractingSecret, *givenName, sm.Secret_SecretType_ImportedCert, "extractSecretMetadata")
+		}
+		return metadata.ID, metadata.Name, nil
+
+	case sm.Secret_SecretType_PublicCert:
+		metadata, ok := response.(*sm.PublicCertificateMetadata)
+		if !ok {
+			return nil, nil, fmt.Errorf(errExtractingSecret, *givenName, sm.Secret_SecretType_PublicCert, "extractSecretMetadata")
+		}
+		return metadata.ID, metadata.Name, nil
+
+	case sm.Secret_SecretType_PrivateCert:
+		metadata, ok := response.(*sm.PrivateCertificateMetadata)
+		if !ok {
+			return nil, nil, fmt.Errorf(errExtractingSecret, *givenName, sm.Secret_SecretType_PrivateCert, "extractSecretMetadata")
+		}
+		return metadata.ID, metadata.Name, nil
+
+	case sm.Secret_SecretType_Kv:
+		metadata, ok := response.(*sm.KVSecretMetadata)
+		if !ok {
+			return nil, nil, fmt.Errorf(errExtractingSecret, *givenName, sm.Secret_SecretType_Kv, "extractSecretMetadata")
+		}
+		return metadata.ID, metadata.Name, nil
+
+	default:
+		return nil, nil, fmt.Errorf("unknown secret type %s", secretType)
+	}
+}

+ 84 - 23
pkg/provider/ibm/provider.go

@@ -24,6 +24,7 @@ import (
 
 	core "github.com/IBM/go-sdk-core/v5/core"
 	sm "github.com/IBM/secrets-manager-go-sdk/v2/secretsmanagerv2"
+	"github.com/google/uuid"
 	gjson "github.com/tidwall/gjson"
 	corev1 "k8s.io/api/core/v1"
 	types "k8s.io/apimachinery/pkg/types"
@@ -55,7 +56,10 @@ const (
 	errFetchSAKSecret                        = "could not fetch SecretAccessKey secret: %w"
 	errMissingSAK                            = "missing SecretAccessKey"
 	errJSONSecretUnmarshal                   = "unable to unmarshal secret: %w"
-	errExtractingSecret                      = "unable to extract the fetched secret %s of type %s"
+	errExtractingSecret                      = "unable to extract the fetched secret %s of type %s while performing %s"
+
+	defaultCacheSize   = 100
+	defaultCacheExpiry = 1 * time.Hour
 )
 
 var contextTimeout = time.Minute * 2
@@ -66,10 +70,12 @@ var _ esv1beta1.Provider = &providerIBM{}
 
 type SecretManagerClient interface {
 	GetSecretWithContext(ctx context.Context, getSecretOptions *sm.GetSecretOptions) (result sm.SecretIntf, response *core.DetailedResponse, err error)
+	ListSecretsWithContext(ctx context.Context, listSecretsOptions *sm.ListSecretsOptions) (result *sm.SecretMetadataPaginatedCollection, response *core.DetailedResponse, err error)
 }
 
 type providerIBM struct {
 	IBMClient SecretManagerClient
+	cache     cacheIntf
 }
 
 type client struct {
@@ -189,26 +195,26 @@ func (ibm *providerIBM) GetSecret(_ context.Context, ref esv1beta1.ExternalSecre
 }
 
 func getArbitrarySecret(ibm *providerIBM, secretName *string) ([]byte, error) {
-	response, err := getSecretData(ibm, secretName)
+	response, err := getSecretData(ibm, secretName, sm.Secret_SecretType_Arbitrary)
 	if err != nil {
 		return nil, err
 	}
 	secret, ok := response.(*sm.ArbitrarySecret)
 	if !ok {
-		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_Arbitrary)
+		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_Arbitrary, "getArbitrarySecret")
 	}
 
 	return []byte(*secret.Payload), nil
 }
 
 func getImportCertSecret(ibm *providerIBM, secretName *string, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	response, err := getSecretData(ibm, secretName)
+	response, err := getSecretData(ibm, secretName, sm.Secret_SecretType_ImportedCert)
 	if err != nil {
 		return nil, err
 	}
 	secret, ok := response.(*sm.ImportedCertificate)
 	if !ok {
-		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_ImportedCert)
+		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_ImportedCert, "getImportCertSecret")
 	}
 	switch ref.Property {
 	case certificateConst:
@@ -223,13 +229,13 @@ func getImportCertSecret(ibm *providerIBM, secretName *string, ref esv1beta1.Ext
 }
 
 func getPublicCertSecret(ibm *providerIBM, secretName *string, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	response, err := getSecretData(ibm, secretName)
+	response, err := getSecretData(ibm, secretName, sm.Secret_SecretType_PublicCert)
 	if err != nil {
 		return nil, err
 	}
 	secret, ok := response.(*sm.PublicCertificate)
 	if !ok {
-		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_PublicCert)
+		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_PublicCert, "getPublicCertSecret")
 	}
 
 	switch ref.Property {
@@ -245,13 +251,13 @@ func getPublicCertSecret(ibm *providerIBM, secretName *string, ref esv1beta1.Ext
 }
 
 func getPrivateCertSecret(ibm *providerIBM, secretName *string, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	response, err := getSecretData(ibm, secretName)
+	response, err := getSecretData(ibm, secretName, sm.Secret_SecretType_PrivateCert)
 	if err != nil {
 		return nil, err
 	}
 	secret, ok := response.(*sm.PrivateCertificate)
 	if !ok {
-		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_PrivateCert)
+		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_PrivateCert, "getPrivateCertSecret")
 	}
 	switch ref.Property {
 	case certificateConst:
@@ -264,25 +270,25 @@ func getPrivateCertSecret(ibm *providerIBM, secretName *string, ref esv1beta1.Ex
 }
 
 func getIamCredentialsSecret(ibm *providerIBM, secretName *string) ([]byte, error) {
-	response, err := getSecretData(ibm, secretName)
+	response, err := getSecretData(ibm, secretName, sm.Secret_SecretType_IamCredentials)
 	if err != nil {
 		return nil, err
 	}
 	secret, ok := response.(*sm.IAMCredentialsSecret)
 	if !ok {
-		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_IamCredentials)
+		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_IamCredentials, "getIamCredentialsSecret")
 	}
 	return []byte(*secret.ApiKey), nil
 }
 
 func getUsernamePasswordSecret(ibm *providerIBM, secretName *string, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	response, err := getSecretData(ibm, secretName)
+	response, err := getSecretData(ibm, secretName, sm.Secret_SecretType_UsernamePassword)
 	if err != nil {
 		return nil, err
 	}
 	secret, ok := response.(*sm.UsernamePasswordSecret)
 	if !ok {
-		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_UsernamePassword)
+		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_UsernamePassword, "getUsernamePasswordSecret")
 	}
 	switch ref.Property {
 	case "username":
@@ -296,13 +302,13 @@ func getUsernamePasswordSecret(ibm *providerIBM, secretName *string, ref esv1bet
 
 // Returns a secret of type kv and supports json path.
 func getKVSecret(ibm *providerIBM, secretName *string, ref esv1beta1.ExternalSecretDataRemoteRef) ([]byte, error) {
-	response, err := getSecretData(ibm, secretName)
+	response, err := getSecretData(ibm, secretName, sm.Secret_SecretType_Kv)
 	if err != nil {
 		return nil, err
 	}
 	secret, ok := response.(*sm.KVSecret)
 	if !ok {
-		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_Kv)
+		return nil, fmt.Errorf(errExtractingSecret, *secretName, sm.Secret_SecretType_Kv, "getKVSecret")
 	}
 	payloadJSONByte, err := json.Marshal(secret.Data)
 	if err != nil {
@@ -346,7 +352,29 @@ func getKVSecret(ibm *providerIBM, secretName *string, ref esv1beta1.ExternalSec
 	return nil, fmt.Errorf("no property provided for secret %s", ref.Key)
 }
 
-func getSecretData(ibm *providerIBM, secretName *string) (sm.SecretIntf, error) {
+func getSecretData(ibm *providerIBM, secretName *string, secretType string) (sm.SecretIntf, error) {
+	var givenName *string
+	var cachedKey string
+
+	// parse given secretName for a uuid or a secret name
+	_, err := uuid.Parse(*secretName)
+	if err != nil {
+		givenName = secretName
+		cachedKey = fmt.Sprintf("%s/%s", secretType, *givenName)
+		isCached, cacheData := ibm.cache.GetData(cachedKey)
+		tmp := string(cacheData)
+		cachedName := &tmp
+		if isCached && *cachedName != "" {
+			secretName = cachedName
+		} else {
+			secretName, err = findSecretByName(ibm, givenName, secretType)
+			if err != nil {
+				return nil, err
+			}
+			ibm.cache.PutData(cachedKey, []byte(*secretName))
+		}
+	}
+
 	ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
 	defer cancel()
 	response, _, err := ibm.IBMClient.GetSecretWithContext(
@@ -361,6 +389,38 @@ func getSecretData(ibm *providerIBM, secretName *string) (sm.SecretIntf, error)
 	return response, nil
 }
 
+func findSecretByName(ibm *providerIBM, secretName *string, secretType string) (*string, error) {
+	var secretID *string
+	ctx, cancel := context.WithTimeout(context.Background(), contextTimeout)
+	defer cancel()
+	response, _, err := ibm.IBMClient.ListSecretsWithContext(ctx,
+		&sm.ListSecretsOptions{
+			Search: secretName,
+		})
+	metrics.ObserveAPICall(constants.ProviderIBMSM, constants.CallIBMSMListSecrets, err)
+	if err != nil {
+		return nil, err
+	}
+
+	found := 0
+	for _, r := range response.Secrets {
+		foundsecretID, foundSecretName, err := extractSecretMetadata(r, secretName, secretType)
+		if err == nil {
+			if *foundSecretName == *secretName {
+				found++
+				secretID = foundsecretID
+			}
+		}
+	}
+	if found == 0 {
+		return nil, fmt.Errorf("failed to find a secret for the given secretName %s", *secretName)
+	}
+	if found > 1 {
+		return nil, fmt.Errorf("found more than one secret matching for the given secretName %s, cannot proceed further", *secretName)
+	}
+	return secretID, nil
+}
+
 func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
 	if utils.IsNil(ibm.IBMClient) {
 		return nil, fmt.Errorf(errUninitalizedIBMProvider)
@@ -376,7 +436,7 @@ func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSe
 	}
 
 	secretMap := make(map[string][]byte)
-	response, err := getSecretData(ibm, &secretName)
+	response, err := getSecretData(ibm, &secretName, secretType)
 	if err != nil {
 		return nil, err
 	}
@@ -385,7 +445,7 @@ func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSe
 	case sm.Secret_SecretType_Arbitrary:
 		secretData, ok := response.(*sm.ArbitrarySecret)
 		if !ok {
-			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_Arbitrary)
+			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_Arbitrary, "GetSecretMap")
 		}
 		secretMap[arbitraryConst] = []byte(*secretData.Payload)
 		return secretMap, nil
@@ -393,7 +453,7 @@ func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSe
 	case sm.Secret_SecretType_UsernamePassword:
 		secretData, ok := response.(*sm.UsernamePasswordSecret)
 		if !ok {
-			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_UsernamePassword)
+			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_UsernamePassword, "GetSecretMap")
 		}
 		secretMap[usernameConst] = []byte(*secretData.Username)
 		secretMap[passwordConst] = []byte(*secretData.Password)
@@ -403,7 +463,7 @@ func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSe
 	case sm.Secret_SecretType_IamCredentials:
 		secretData, ok := response.(*sm.IAMCredentialsSecret)
 		if !ok {
-			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_IamCredentials)
+			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_IamCredentials, "GetSecretMap")
 		}
 		secretMap[apikeyConst] = []byte(*secretData.ApiKey)
 
@@ -412,7 +472,7 @@ func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSe
 	case sm.Secret_SecretType_ImportedCert:
 		secretData, ok := response.(*sm.ImportedCertificate)
 		if !ok {
-			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_ImportedCert)
+			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_ImportedCert, "GetSecretMap")
 		}
 		secretMap[certificateConst] = []byte(*secretData.Certificate)
 		secretMap[intermediateConst] = []byte(*secretData.Intermediate)
@@ -423,7 +483,7 @@ func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSe
 	case sm.Secret_SecretType_PublicCert:
 		secretData, ok := response.(*sm.PublicCertificate)
 		if !ok {
-			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_PublicCert)
+			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_PublicCert, "GetSecretMap")
 		}
 		secretMap[certificateConst] = []byte(*secretData.Certificate)
 		secretMap[intermediateConst] = []byte(*secretData.Intermediate)
@@ -434,7 +494,7 @@ func (ibm *providerIBM) GetSecretMap(_ context.Context, ref esv1beta1.ExternalSe
 	case sm.Secret_SecretType_PrivateCert:
 		secretData, ok := response.(*sm.PrivateCertificate)
 		if !ok {
-			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_PrivateCert)
+			return nil, fmt.Errorf(errExtractingSecret, secretName, sm.Secret_SecretType_PrivateCert, "GetSecretMap")
 		}
 		secretMap[certificateConst] = []byte(*secretData.Certificate)
 		secretMap[privateKeyConst] = []byte(*secretData.PrivateKey)
@@ -626,6 +686,7 @@ func (ibm *providerIBM) NewClient(ctx context.Context, store esv1beta1.GenericSt
 	}
 
 	ibm.IBMClient = secretsManager
+	ibm.cache = NewCache(defaultCacheSize, defaultCacheExpiry)
 	return ibm, nil
 }
 

+ 88 - 33
pkg/provider/ibm/provider_test.go

@@ -20,6 +20,7 @@ import (
 	"reflect"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/IBM/go-sdk-core/v5/core"
 	sm "github.com/IBM/secrets-manager-go-sdk/v2/secretsmanagerv2"
@@ -44,6 +45,9 @@ type secretManagerTestCase struct {
 	mockClient     *fakesm.IBMMockClient
 	apiInput       *sm.GetSecretOptions
 	apiOutput      sm.SecretIntf
+	listInput      *sm.ListSecretsOptions
+	listOutput     *sm.SecretMetadataPaginatedCollection
+	listError      error
 	ref            *esv1beta1.ExternalSecretDataRemoteRef
 	serviceURL     *string
 	apiErr         error
@@ -59,6 +63,9 @@ func makeValidSecretManagerTestCase() *secretManagerTestCase {
 		apiInput:       makeValidAPIInput(),
 		ref:            makeValidRef(),
 		apiOutput:      makeValidAPIOutput(),
+		listInput:      makeValidListInput(),
+		listOutput:     makeValidListSecretsOutput(),
+		listError:      nil,
 		serviceURL:     nil,
 		apiErr:         nil,
 		expectError:    "",
@@ -66,9 +73,12 @@ func makeValidSecretManagerTestCase() *secretManagerTestCase {
 		expectedData:   map[string][]byte{},
 	}
 	mcParams := fakesm.IBMMockClientParams{
-		GetSecretOptions: smtc.apiInput,
-		GetSecretOutput:  smtc.apiOutput,
-		GetSecretErr:     smtc.apiErr,
+		GetSecretOptions:   smtc.apiInput,
+		GetSecretOutput:    smtc.apiOutput,
+		GetSecretErr:       smtc.apiErr,
+		ListSecretsOptions: smtc.listInput,
+		ListSecretsOutput:  smtc.listOutput,
+		ListSecretsErr:     smtc.listError,
 	}
 	smtc.mockClient.WithValue(mcParams)
 	return &smtc
@@ -97,15 +107,28 @@ func makeValidAPIOutput() sm.SecretIntf {
 	return i
 }
 
+func makeValidListSecretsOutput() *sm.SecretMetadataPaginatedCollection {
+	list := sm.SecretMetadataPaginatedCollection{}
+	return &list
+}
+
+func makeValidListInput() *sm.ListSecretsOptions {
+	listOpt := sm.ListSecretsOptions{}
+	return &listOpt
+}
+
 func makeValidSecretManagerTestCaseCustom(tweaks ...func(smtc *secretManagerTestCase)) *secretManagerTestCase {
 	smtc := makeValidSecretManagerTestCase()
 	for _, fn := range tweaks {
 		fn(smtc)
 	}
 	mcParams := fakesm.IBMMockClientParams{
-		GetSecretOptions: smtc.apiInput,
-		GetSecretOutput:  smtc.apiOutput,
-		GetSecretErr:     smtc.apiErr,
+		GetSecretOptions:   smtc.apiInput,
+		GetSecretOutput:    smtc.apiOutput,
+		GetSecretErr:       smtc.apiErr,
+		ListSecretsOptions: smtc.listInput,
+		ListSecretsOutput:  smtc.listOutput,
+		ListSecretsErr:     smtc.listError,
 	}
 	smtc.mockClient.WithValue(mcParams)
 	return smtc
@@ -229,37 +252,65 @@ func TestIBMSecretManagerGetSecret(t *testing.T) {
 	}
 
 	// good case: username_password type with property
-	setSecretUserPass := func(smtc *secretManagerTestCase) {
-		secret := &sm.UsernamePasswordSecret{
-			SecretType: utilpointer.String(sm.Secret_SecretType_UsernamePassword),
-			Name:       utilpointer.String("testyname"),
-			ID:         utilpointer.String(secretUUID),
-			Username:   &secretUsername,
-			Password:   &secretPassword,
+	funcSetUserPass := func(secretName, property, name string) func(smtc *secretManagerTestCase) {
+		return func(smtc *secretManagerTestCase) {
+			secret := &sm.UsernamePasswordSecret{
+				SecretType: utilpointer.String(sm.Secret_SecretType_UsernamePassword),
+				Name:       utilpointer.String("testyname"),
+				ID:         utilpointer.String(secretUUID),
+				Username:   &secretUsername,
+				Password:   &secretPassword,
+			}
+			secretMetadata := &sm.UsernamePasswordSecretMetadata{
+				Name: utilpointer.String("testyname"),
+				ID:   utilpointer.String(secretUUID),
+			}
+			smtc.name = name
+			smtc.apiInput.ID = utilpointer.String(secretUUID)
+			smtc.apiOutput = secret
+			smtc.listInput.Search = utilpointer.String("testyname")
+			smtc.listOutput.Secrets = make([]sm.SecretMetadataIntf, 1)
+			smtc.listOutput.Secrets[0] = secretMetadata
+			smtc.ref.Key = "username_password/" + secretName
+			smtc.ref.Property = property
+			if property == "username" {
+				smtc.expectedSecret = secretUsername
+			} else {
+				smtc.expectedSecret = secretPassword
+			}
 		}
-		smtc.name = "good case: username_password type with property"
-		smtc.apiInput.ID = utilpointer.String(secretUUID)
-		smtc.apiOutput = secret
-		smtc.ref.Key = secretUserPass
-		smtc.ref.Property = "password"
-		smtc.expectedSecret = secretPassword
 	}
+	setSecretUserPassByID := funcSetUserPass(secretUUID, "username", "good case: username_password type - get username by ID")
+	setSecretUserPassUsername := funcSetUserPass("testyname", "username", "good case: username_password type - get username by secret name")
+	setSecretUserPassPassword := funcSetUserPass("testyname", "password", "good case: username_password type - get password by secret name")
 
-	// good case: iam_credenatials type
-	setSecretIam := func(smtc *secretManagerTestCase) {
-		secret := &sm.IAMCredentialsSecret{
-			SecretType: utilpointer.String(sm.Secret_SecretType_IamCredentials),
-			Name:       utilpointer.String("testyname"),
-			ID:         utilpointer.String(secretUUID),
-			ApiKey:     utilpointer.String(secretAPIKey),
+	// good case: iam_credentials type
+	funcSetSecretIam := func(secretName, name string) func(*secretManagerTestCase) {
+		return func(smtc *secretManagerTestCase) {
+			secret := &sm.IAMCredentialsSecret{
+				SecretType: utilpointer.String(sm.Secret_SecretType_IamCredentials),
+				Name:       utilpointer.String("testyname"),
+				ID:         utilpointer.String(secretUUID),
+				ApiKey:     utilpointer.String(secretAPIKey),
+			}
+			secretMetadata := &sm.IAMCredentialsSecretMetadata{
+				Name: utilpointer.String("testyname"),
+				ID:   utilpointer.String(secretUUID),
+			}
+			smtc.apiInput.ID = utilpointer.String(secretUUID)
+			smtc.name = name
+			smtc.apiOutput = secret
+			smtc.listInput.Search = utilpointer.String("testyname")
+			smtc.listOutput.Secrets = make([]sm.SecretMetadataIntf, 1)
+			smtc.listOutput.Secrets[0] = secretMetadata
+			smtc.ref.Key = "iam_credentials/" + secretName
+			smtc.expectedSecret = secretAPIKey
 		}
-		smtc.apiInput.ID = utilpointer.String(secretUUID)
-		smtc.name = "good case: iam_credenatials type"
-		smtc.apiOutput = secret
-		smtc.ref.Key = "iam_credentials/" + secretUUID
-		smtc.expectedSecret = secretAPIKey
 	}
 
+	setSecretIamByID := funcSetSecretIam(secretUUID, "good case: iam_credenatials type - get API Key by ID")
+	setSecretIamByName := funcSetSecretIam("testyname", "good case: iam_credenatials type - get API Key by name")
+
 	funcSetCertSecretTest := func(secret sm.SecretIntf, name, certType string, good bool) func(*secretManagerTestCase) {
 		return func(smtc *secretManagerTestCase) {
 			smtc.name = name
@@ -426,8 +477,11 @@ func TestIBMSecretManagerGetSecret(t *testing.T) {
 		makeValidSecretManagerTestCaseCustom(setAPIErr),
 		makeValidSecretManagerTestCaseCustom(setNilMockClient),
 		makeValidSecretManagerTestCaseCustom(badSecretUserPass),
-		makeValidSecretManagerTestCaseCustom(setSecretUserPass),
-		makeValidSecretManagerTestCaseCustom(setSecretIam),
+		makeValidSecretManagerTestCaseCustom(setSecretUserPassByID),
+		makeValidSecretManagerTestCaseCustom(setSecretUserPassUsername),
+		makeValidSecretManagerTestCaseCustom(setSecretUserPassPassword),
+		makeValidSecretManagerTestCaseCustom(setSecretIamByID),
+		makeValidSecretManagerTestCaseCustom(setSecretIamByName),
 		makeValidSecretManagerTestCaseCustom(setSecretCert),
 		makeValidSecretManagerTestCaseCustom(setSecretKV),
 		makeValidSecretManagerTestCaseCustom(setSecretKVWithKey),
@@ -446,6 +500,7 @@ func TestIBMSecretManagerGetSecret(t *testing.T) {
 	for k, v := range successCases {
 		t.Run(v.name, func(t *testing.T) {
 			sm.IBMClient = v.mockClient
+			sm.cache = NewCache(10, 1*time.Minute)
 			out, err := sm.GetSecret(context.Background(), *v.ref)
 			if !ErrorContains(err, v.expectError) {
 				t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError)