diff --git a/controllers/btpoperator_controller.go b/controllers/btpoperator_controller.go index e13300e6f..e6fd7839c 100644 --- a/controllers/btpoperator_controller.go +++ b/controllers/btpoperator_controller.go @@ -107,7 +107,7 @@ var ( Version: btpOperatorApiVer, Kind: btpOperatorServiceBinding, } - instanceGvk = schema.GroupVersionKind{ + InstanceGvk = schema.GroupVersionKind{ Group: btpOperatorGroup, Version: btpOperatorApiVer, Kind: btpOperatorServiceInstance, @@ -751,7 +751,7 @@ func (r *BtpOperatorReconciler) handleDeleting(ctx context.Context, cr *v1alpha1 if err != nil { return err } - numberOfInstances, err := r.numberOfResources(ctx, instanceGvk) + numberOfInstances, err := r.numberOfResources(ctx, InstanceGvk) if err != nil { return err } @@ -799,7 +799,7 @@ func (r *BtpOperatorReconciler) handleDeprovisioning(ctx context.Context, cr *v1 if err != nil { return err } - numberOfInstances, err := r.numberOfResources(ctx, instanceGvk) + numberOfInstances, err := r.numberOfResources(ctx, InstanceGvk) if err != nil { return err } @@ -896,13 +896,13 @@ func (r *BtpOperatorReconciler) handleHardDelete(ctx context.Context, namespaces } } - siCrdExists, err := r.crdExists(ctx, instanceGvk) + siCrdExists, err := r.crdExists(ctx, InstanceGvk) if err != nil { - logger.Error(err, "while checking CRD existence", "GVK", instanceGvk.String()) + logger.Error(err, "while checking CRD existence", "GVK", InstanceGvk.String()) errs = append(errs, err) } if siCrdExists { - if err := r.hardDelete(ctx, instanceGvk, namespaces); err != nil { + if err := r.hardDelete(ctx, InstanceGvk, namespaces); err != nil { logger.Error(err, "while deleting Service Instances") if !errors.Is(err, context.DeadlineExceeded) { errs = append(errs, err) @@ -933,7 +933,7 @@ func (r *BtpOperatorReconciler) handleHardDelete(ctx context.Context, namespaces } if siCrdExists { - siResourcesLeft, err = r.resourcesExist(ctx, namespaces, instanceGvk) + siResourcesLeft, err = r.resourcesExist(ctx, namespaces, InstanceGvk) if err != nil { logger.Error(err, "ServiceInstance leftover resources check failed") hardDeleteSucceededCh <- false @@ -1089,9 +1089,9 @@ func (r *BtpOperatorReconciler) handleSoftDelete(ctx context.Context, namespaces return err } - siCrdExists, err := r.crdExists(ctx, instanceGvk) + siCrdExists, err := r.crdExists(ctx, InstanceGvk) if err != nil { - logger.Error(err, "while checking CRD existence", "GVK", instanceGvk.String()) + logger.Error(err, "while checking CRD existence", "GVK", InstanceGvk.String()) return err } @@ -1109,11 +1109,11 @@ func (r *BtpOperatorReconciler) handleSoftDelete(ctx context.Context, namespaces if siCrdExists { logger.Info("Removing finalizers in Service Instances") - if err := r.softDelete(ctx, instanceGvk); err != nil { + if err := r.softDelete(ctx, InstanceGvk); err != nil { logger.Error(err, "while deleting Service Instances") return err } - if err := r.ensureResourcesDontExist(ctx, instanceGvk); err != nil { + if err := r.ensureResourcesDontExist(ctx, InstanceGvk); err != nil { logger.Error(err, "Service Instances still exist") return err } diff --git a/controllers/btpoperator_controller_cr_rotation_test.go b/controllers/btpoperator_controller_cr_rotation_test.go index 31a78ca69..ce461a9c5 100644 --- a/controllers/btpoperator_controller_cr_rotation_test.go +++ b/controllers/btpoperator_controller_cr_rotation_test.go @@ -110,8 +110,8 @@ var _ = Describe("BTP Operator CR leader replacement", func() { Eventually(func() error { return k8sClient.Create(ctx, btpOperator1) }).WithTimeout(k8sOpsTimeout).WithPolling(k8sOpsPollingInterval).Should(Succeed()) Eventually(updateCh).Should(Receive(matchState(v1alpha1.StateReady))) - siUnstructured := createResource(instanceGvk, kymaNamespace, instanceName) - ensureResourceExists(instanceGvk) + siUnstructured := createResource(InstanceGvk, kymaNamespace, instanceName) + ensureResourceExists(InstanceGvk) sbUnstructured := createResource(bindingGvk, kymaNamespace, bindingName) ensureResourceExists(bindingGvk) diff --git a/controllers/btpoperator_controller_deprovisioning_test.go b/controllers/btpoperator_controller_deprovisioning_test.go index 9abd1c14b..d123fbac2 100644 --- a/controllers/btpoperator_controller_deprovisioning_test.go +++ b/controllers/btpoperator_controller_deprovisioning_test.go @@ -49,8 +49,8 @@ var _ = Describe("BTP Operator controller - deprovisioning", func() { }) It("Delete should fail because of existing instances and bindings", func() { - _ = createResource(instanceGvk, kymaNamespace, instanceName) - ensureResourceExists(instanceGvk) + _ = createResource(InstanceGvk, kymaNamespace, instanceName) + ensureResourceExists(InstanceGvk) _ = createResource(bindingGvk, kymaNamespace, bindingName) ensureResourceExists(bindingGvk) @@ -78,8 +78,8 @@ var _ = Describe("BTP Operator controller - deprovisioning", func() { btpServiceOperatorDeployment := &appsv1.Deployment{} Expect(k8sClient.Get(ctx, client.ObjectKey{Name: DeploymentName, Namespace: kymaNamespace}, btpServiceOperatorDeployment)).Should(Succeed()) - siUnstructured = createResource(instanceGvk, kymaNamespace, instanceName) - ensureResourceExists(instanceGvk) + siUnstructured = createResource(InstanceGvk, kymaNamespace, instanceName) + ensureResourceExists(InstanceGvk) sbUnstructured = createResource(bindingGvk, kymaNamespace, bindingName) ensureResourceExists(bindingGvk) diff --git a/controllers/serviceinstance_controller.go b/controllers/serviceinstance_controller.go index c49a60f90..7c706f138 100644 --- a/controllers/serviceinstance_controller.go +++ b/controllers/serviceinstance_controller.go @@ -42,7 +42,7 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ logger.Info("SI reconcile triggered") list := &unstructured.UnstructuredList{} - list.SetGroupVersionKind(instanceGvk) + list.SetGroupVersionKind(InstanceGvk) err := r.List(ctx, list, client.InNamespace(corev1.NamespaceAll)) if err != nil { return ctrl.Result{}, err @@ -89,7 +89,7 @@ func (r *ServiceInstanceReconciler) SetupWithManager(mgr ctrl.Manager) error { r.Config = mgr.GetConfig() si := &unstructured.Unstructured{} - si.SetGroupVersionKind(instanceGvk) + si.SetGroupVersionKind(InstanceGvk) sb := &unstructured.Unstructured{} sb.SetGroupVersionKind(bindingGvk) diff --git a/controllers/serviceinstance_controller_test.go b/controllers/serviceinstance_controller_test.go index 6328be94f..bc64148dc 100644 --- a/controllers/serviceinstance_controller_test.go +++ b/controllers/serviceinstance_controller_test.go @@ -65,8 +65,8 @@ var _ = Describe("Service Instance and Bindings controller", Ordered, func() { Eventually(updateCh).Should(Receive(matchState(v1alpha1.StateReady))) // - create Service Instance - siUnstructured := createResource(instanceGvk, kymaNamespace, serviceInstanceName) - ensureResourceExists(instanceGvk) + siUnstructured := createResource(InstanceGvk, kymaNamespace, serviceInstanceName) + ensureResourceExists(InstanceGvk) // - trigger BTP operator deletion Expect(k8sClient.Delete(ctx, btpOperatorResource)).To(Succeed()) diff --git a/controllers/utils_test.go b/controllers/utils_test.go index ae163aab1..f3676e2d6 100644 --- a/controllers/utils_test.go +++ b/controllers/utils_test.go @@ -77,7 +77,7 @@ func newTimeoutK8sClient(c client.Client) *timeoutK8sClient { func (c *timeoutK8sClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { kind := obj.GetObjectKind().GroupVersionKind().Kind - if kind == instanceGvk.Kind || kind == bindingGvk.Kind { + if kind == InstanceGvk.Kind || kind == bindingGvk.Kind { deleteAllOfCtx, cancel := context.WithTimeout(ctx, time.Millisecond*100) defer cancel() return c.Client.DeleteAllOf(deleteAllOfCtx, obj, opts...) @@ -96,7 +96,7 @@ func newErrorK8sClient(c client.Client) *errorK8sClient { func (c *errorK8sClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { kind := obj.GetObjectKind().GroupVersionKind().Kind - if kind == instanceGvk.Kind || kind == bindingGvk.Kind { + if kind == InstanceGvk.Kind || kind == bindingGvk.Kind { deleteAllOfCtx, cancel := context.WithTimeout(ctx, time.Millisecond*100) defer cancel() _ = c.Client.DeleteAllOf(deleteAllOfCtx, obj, opts...) @@ -467,7 +467,7 @@ func createResource(gvk schema.GroupVersionKind, namespace string, name string) object.SetNamespace(namespace) object.SetName(name) kind := object.GetObjectKind().GroupVersionKind().Kind - if kind == instanceGvk.Kind { + if kind == InstanceGvk.Kind { populateServiceInstanceFields(object) } else if kind == bindingGvk.Kind { populateServiceBindingFields(object) diff --git a/internal/cluster-object/namespace_provider.go b/internal/cluster-object/namespace_provider.go new file mode 100644 index 000000000..f16a2fa98 --- /dev/null +++ b/internal/cluster-object/namespace_provider.go @@ -0,0 +1,44 @@ +package clusterobject + +import ( + "context" + "errors" + "log/slog" + + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const namespaceProviderName = "NamespaceProvider" + +type NamespaceProvider struct { + client.Reader + logger *slog.Logger +} + +func NewNamespaceProvider(reader client.Reader, logger *slog.Logger) *NamespaceProvider { + logger = logger.With(logComponentNameKey, namespaceProviderName) + + return &NamespaceProvider{ + Reader: reader, + logger: logger, + } +} + +func (p *NamespaceProvider) All(ctx context.Context) (*v1.NamespaceList, error) { + p.logger.Info("fetching all namespaces") + + namespaces := &v1.NamespaceList{} + if err := p.Reader.List(ctx, namespaces); err != nil { + p.logger.Error("failed to fetch all namespaces", "error", err) + return nil, err + } + + if len(namespaces.Items) == 0 { + err := errors.New("no namespaces found") + p.logger.Error(err.Error()) + return nil, err + } + + return namespaces, nil +} diff --git a/internal/cluster-object/namespace_provider_test.go b/internal/cluster-object/namespace_provider_test.go new file mode 100644 index 000000000..c8d840386 --- /dev/null +++ b/internal/cluster-object/namespace_provider_test.go @@ -0,0 +1,69 @@ +package clusterobject + +import ( + "context" + "log/slog" + "os" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestNamespaceProvider(t *testing.T) { + // given + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + t.Run("should fetch all namespaces", func(t *testing.T) { + // given + namespaces := initNamespaces() + k8sClient := fake.NewClientBuilder().WithLists(namespaces).Build() + nsProvider := NewNamespaceProvider(k8sClient, logger) + + // when + nsList, err := nsProvider.All(context.TODO()) + + // then + if err != nil { + t.Errorf("Error while fetching namespaces: %s", err) + } + assert.Len(t, nsList.Items, 3) + }) + + t.Run("should return error when no namespaces found", func(t *testing.T) { + // given + k8sClient := fake.NewClientBuilder().Build() + nsProvider := NewNamespaceProvider(k8sClient, logger) + + // when + _, err := nsProvider.All(context.TODO()) + + // then + require.Error(t, err) + }) +} + +func initNamespaces() *corev1.NamespaceList { + return &corev1.NamespaceList{ + Items: []corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "kube-system", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + }, + }, + }, + } +} diff --git a/internal/cluster-object/object_provider.go b/internal/cluster-object/object_provider.go new file mode 100644 index 000000000..e916e896f --- /dev/null +++ b/internal/cluster-object/object_provider.go @@ -0,0 +1,17 @@ +package clusterobject + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const logComponentNameKey = "component" + +type ClusterScopedProvider[T client.ObjectList] interface { + All(ctx context.Context) (T, error) +} + +type NamespacedProvider[T client.Object] interface { + GetByNameAndNamespace(ctx context.Context, name, namespace string) (T, error) +} diff --git a/internal/cluster-object/secret_provider.go b/internal/cluster-object/secret_provider.go new file mode 100644 index 000000000..a31e4feb4 --- /dev/null +++ b/internal/cluster-object/secret_provider.go @@ -0,0 +1,143 @@ +package clusterobject + +import ( + "context" + "fmt" + "log/slog" + + "github.com/kyma-project/btp-manager/controllers" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + secretProviderName = "SecretProvider" + btpServiceOperatorSecretName = "sap-btp-service-operator" +) + +type SecretProvider struct { + client.Reader + namespaceProvider *NamespaceProvider + serviceInstanceProvider *ServiceInstanceProvider + logger *slog.Logger +} + +func NewSecretProvider(reader client.Reader, nsProvider *NamespaceProvider, siProvider *ServiceInstanceProvider, logger *slog.Logger) *SecretProvider { + logger = logger.With(logComponentNameKey, secretProviderName) + + return &SecretProvider{ + Reader: reader, + namespaceProvider: nsProvider, + serviceInstanceProvider: siProvider, + logger: logger, + } +} + +func (p *SecretProvider) All(ctx context.Context) (*corev1.SecretList, error) { + p.logger.Info("fetching all btp operator secrets") + secrets := &corev1.SecretList{} + if err := p.getAllSapBtpServiceOperatorNamedSecrets(ctx, secrets); err != nil { + return nil, err + } + + namespaces, err := p.namespaceProvider.All(ctx) + if err != nil { + p.logger.Error("while fetching namespaces", "error", err) + return nil, err + } + + nsnames := p.getNamespacesNames(namespaces) + if err := p.getAllSecretsWithNamespaceNamePrefix(ctx, secrets, nsnames); err != nil { + return nil, err + } + + siList, err := p.serviceInstanceProvider.AllWithSecretRef(ctx) + if err != nil { + p.logger.Error("while fetching service instances with secret ref", "error", err) + return nil, err + } + + if err := p.getSecretsFromRefInServiceInstances(ctx, siList, secrets); err != nil { + return nil, err + } + + if len(secrets.Items) == 0 { + p.logger.Warn(fmt.Sprintf("no btp operator secrets found")) + return nil, err + } + + return secrets, err +} + +func (p *SecretProvider) getAllSapBtpServiceOperatorNamedSecrets(ctx context.Context, secrets *corev1.SecretList) error { + if err := p.Reader.List(ctx, secrets, client.MatchingFields{"metadata.name": btpServiceOperatorSecretName}); err != nil { + p.logger.Error(fmt.Sprintf("failed to fetch all \"%s\" secrets", btpServiceOperatorSecretName), "error", err) + return err + } + return nil +} + +func (p *SecretProvider) getNamespacesNames(namespaces *corev1.NamespaceList) []string { + names := make([]string, len(namespaces.Items)) + for i, ns := range namespaces.Items { + names[i] = ns.Name + } + return names +} + +func (p *SecretProvider) getAllSecretsWithNamespaceNamePrefix(ctx context.Context, secrets *corev1.SecretList, nsnames []string) error { + for _, nsname := range nsnames { + secret := &corev1.Secret{} + secretName := fmt.Sprintf("%s-%s", nsname, btpServiceOperatorSecretName) + if err := p.Get(ctx, client.ObjectKey{Namespace: controllers.ChartNamespace, Name: secretName}, secret); err != nil { + if k8serrors.IsNotFound(err) { + p.logger.Info(fmt.Sprintf("secret \"%s\" not found in \"%s\" namespace", secretName, controllers.ChartNamespace)) + continue + } + p.logger.Error(fmt.Sprintf("failed to fetch \"%s\" secret", secretName), "error", err) + return err + } + secrets.Items = append(secrets.Items, *secret) + } + + return nil +} + +func (p *SecretProvider) getSecretsFromRefInServiceInstances(ctx context.Context, siList *unstructured.UnstructuredList, secrets *corev1.SecretList) error { + for _, item := range siList.Items { + secretRef, found, err := unstructured.NestedString(item.Object, "spec", secretRefKey) + if err != nil { + p.logger.Error(fmt.Sprintf("while traversing \"%s\" unstructured object to find \"%s\" key", item.GetName(), secretRefKey), "error", err) + return err + } else if !found { + p.logger.Warn(fmt.Sprintf("expected secret ref not found in \"%s\" service instance", item.GetName())) + continue + } + secret := &corev1.Secret{} + if err := p.Get(ctx, client.ObjectKey{Namespace: controllers.ChartNamespace, Name: secretRef}, secret); err != nil { + if k8serrors.IsNotFound(err) { + p.logger.Warn(fmt.Sprintf("secret \"%s\" not found in \"%s\" namespace", secretRef, controllers.ChartNamespace)) + continue + } + p.logger.Error(fmt.Sprintf("failed to fetch \"%s\" secret", secretRef), "error", err) + return err + } + if p.secretExistsInList(secret, secrets) { + continue + } + secrets.Items = append(secrets.Items, *secret) + } + + return nil +} + +func (p *SecretProvider) secretExistsInList(secret *corev1.Secret, secrets *corev1.SecretList) bool { + for _, s := range secrets.Items { + if s.Name == secret.Name { + return true + } + } + return false +} diff --git a/internal/cluster-object/secret_provider_test.go b/internal/cluster-object/secret_provider_test.go new file mode 100644 index 000000000..8bab99180 --- /dev/null +++ b/internal/cluster-object/secret_provider_test.go @@ -0,0 +1,310 @@ +package clusterobject + +import ( + "context" + "fmt" + "log/slog" + "os" + "testing" + + "github.com/kyma-project/btp-manager/controllers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestSecretProvider(t *testing.T) { + // given + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + t.Run("should fetch all secrets - from the module's namespace, with a namespace prefix, with an arbitrary name", func(t *testing.T) { + // given + ns := initNamespaces() + sis := initServiceInstances(t) + additionalNamespaces := createAdditionalNamespaces() + expectedSecrets := []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: btpServiceOperatorSecretName, + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: btpServiceOperatorSecretName, + Namespace: "test1", + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("test2-%s", btpServiceOperatorSecretName), + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "secret1", + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "secret2", + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + } + secrets := &corev1.SecretList{Items: expectedSecrets} + ns.Items = append(ns.Items, additionalNamespaces...) + + k8sClient := fake.NewClientBuilder(). + WithLists(ns, sis, secrets). + WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer). + Build() + nsProvider := NewNamespaceProvider(k8sClient, logger) + siProvider := NewServiceInstanceProvider(k8sClient, logger) + secretProvider := NewSecretProvider(k8sClient, nsProvider, siProvider, logger) + + // when + actualSecrets, err := secretProvider.All(context.TODO()) + require.NoError(t, err) + + // then + compareSecretSlices(t, expectedSecrets, actualSecrets.Items) + }) + + t.Run("should fetch module's secret only", func(t *testing.T) { + // given + ns := initNamespaces() + sis := initServiceInstances(t) + additionalNamespaces := createAdditionalNamespaces() + expectedSecrets := []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: btpServiceOperatorSecretName, + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + } + additionalSecrets := createAdditionalSecrets() + secrets := &corev1.SecretList{Items: expectedSecrets} + secrets.Items = append(secrets.Items, additionalSecrets...) + ns.Items = append(ns.Items, additionalNamespaces...) + + k8sClient := fake.NewClientBuilder(). + WithLists(ns, sis, secrets). + WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer). + Build() + nsProvider := NewNamespaceProvider(k8sClient, logger) + siProvider := NewServiceInstanceProvider(k8sClient, logger) + secretProvider := NewSecretProvider(k8sClient, nsProvider, siProvider, logger) + + // when + actualSecrets, err := secretProvider.All(context.TODO()) + require.NoError(t, err) + + // then + compareSecretSlices(t, expectedSecrets, actualSecrets.Items) + }) + + t.Run("should fetch namespace prefixed secret only", func(t *testing.T) { + // given + ns := initNamespaces() + sis := initServiceInstances(t) + additionalNamespaces := createAdditionalNamespaces() + expectedSecrets := []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("test2-%s", btpServiceOperatorSecretName), + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + } + additionalSecrets := createAdditionalSecrets() + secrets := &corev1.SecretList{Items: expectedSecrets} + secrets.Items = append(secrets.Items, additionalSecrets...) + ns.Items = append(ns.Items, additionalNamespaces...) + + k8sClient := fake.NewClientBuilder(). + WithLists(ns, sis, secrets). + WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer). + Build() + nsProvider := NewNamespaceProvider(k8sClient, logger) + siProvider := NewServiceInstanceProvider(k8sClient, logger) + secretProvider := NewSecretProvider(k8sClient, nsProvider, siProvider, logger) + + // when + actualSecrets, err := secretProvider.All(context.TODO()) + require.NoError(t, err) + + // then + compareSecretSlices(t, expectedSecrets, actualSecrets.Items) + }) + + t.Run("should fetch only secrets referenced in service instances", func(t *testing.T) { + // given + ns := initNamespaces() + sis := initServiceInstances(t) + additionalNamespaces := createAdditionalNamespaces() + expectedSecrets := []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "secret1", + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "secret2", + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + } + additionalSecrets := createAdditionalSecrets() + secrets := &corev1.SecretList{Items: expectedSecrets} + secrets.Items = append(secrets.Items, additionalSecrets...) + ns.Items = append(ns.Items, additionalNamespaces...) + + k8sClient := fake.NewClientBuilder(). + WithLists(ns, sis, secrets). + WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer). + Build() + nsProvider := NewNamespaceProvider(k8sClient, logger) + siProvider := NewServiceInstanceProvider(k8sClient, logger) + secretProvider := NewSecretProvider(k8sClient, nsProvider, siProvider, logger) + + // when + actualSecrets, err := secretProvider.All(context.TODO()) + require.NoError(t, err) + + // then + compareSecretSlices(t, expectedSecrets, actualSecrets.Items) + }) + + t.Run("should return nil when there are no btp operator secrets", func(t *testing.T) { + // given + ns := initNamespaces() + sis := initServiceInstances(t) + additionalNamespaces := createAdditionalNamespaces() + additionalSecrets := createAdditionalSecrets() + secrets := &corev1.SecretList{Items: additionalSecrets} + ns.Items = append(ns.Items, additionalNamespaces...) + + k8sClient := fake.NewClientBuilder(). + WithLists(ns, sis, secrets). + WithIndex(&corev1.Secret{}, "metadata.name", secretNameIndexer). + Build() + nsProvider := NewNamespaceProvider(k8sClient, logger) + siProvider := NewServiceInstanceProvider(k8sClient, logger) + secretProvider := NewSecretProvider(k8sClient, nsProvider, siProvider, logger) + + // when + actualSecrets, err := secretProvider.All(context.TODO()) + require.NoError(t, err) + + // then + assert.Nil(t, actualSecrets) + }) +} + +func createAdditionalNamespaces() []corev1.Namespace { + return []corev1.Namespace{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: controllers.ChartNamespace, + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test1", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test2", + }, + }, + } +} + +func createAdditionalSecrets() []corev1.Secret { + return []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "unrelated-additional-secret1", + Namespace: controllers.ChartNamespace, + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "unrelated-additional-secret2", + Namespace: "test1", + }, + StringData: map[string]string{ + "foo": "bar", + }, + }, + } +} + +func secretNameIndexer(obj client.Object) []string { + secret, ok := obj.(*corev1.Secret) + if !ok { + panic(fmt.Errorf("indexer function for type %T's metadata.name field received"+ + " object of type %T, this should never happen", corev1.Secret{}, obj)) + } + + return []string{secret.Name} +} + +func compareSecretSlices(t *testing.T, expected, actual []corev1.Secret) { + assert.Equal(t, len(expected), len(actual)) + + for _, expectedSecret := range expected { + if !containsSecret(actual, expectedSecret) { + t.Errorf("Expected secret %s not found in the actual list", expectedSecret.Name) + } + } +} + +func containsSecret(secrets []corev1.Secret, secret corev1.Secret) bool { + for _, s := range secrets { + if s.Name == secret.Name && s.Namespace == secret.Namespace { + return true + } + } + return false +} diff --git a/internal/cluster-object/service_instance_provider.go b/internal/cluster-object/service_instance_provider.go new file mode 100644 index 000000000..ba2ae1c39 --- /dev/null +++ b/internal/cluster-object/service_instance_provider.go @@ -0,0 +1,86 @@ +package clusterobject + +import ( + "context" + "fmt" + "log/slog" + + "github.com/kyma-project/btp-manager/controllers" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +const ( + serviceInstanceProviderName = "ServiceInstanceProvider" + secretRefKey = "btpAccessCredentialsSecret" +) + +type ServiceInstanceProvider struct { + client.Reader + logger *slog.Logger +} + +func NewServiceInstanceProvider(reader client.Reader, logger *slog.Logger) *ServiceInstanceProvider { + logger = logger.With(logComponentNameKey, serviceInstanceProviderName) + + return &ServiceInstanceProvider{ + Reader: reader, + logger: logger, + } +} + +func (p *ServiceInstanceProvider) AllWithSecretRef(ctx context.Context) (*unstructured.UnstructuredList, error) { + filtered, err := p.All(ctx) + if err != nil { + p.logger.Error("while fetching filtered service instances", "error", err) + return nil, err + } + + if err := p.filterBySecretRef(filtered); err != nil { + p.logger.Error("while filtering service instances by secret ref", "error", err) + return nil, err + } + + return filtered, nil +} + +func (p *ServiceInstanceProvider) All(ctx context.Context) (*unstructured.UnstructuredList, error) { + list := &unstructured.UnstructuredList{} + list.SetGroupVersionKind(controllers.InstanceGvk) + if err := p.List(ctx, list); err != nil { + p.logger.Error("failed to list all service instances", "error", err) + return nil, err + } + if len(list.Items) == 0 { + p.logger.Info("no service instances found") + return list, nil + } + + return list, nil +} + +func (p *ServiceInstanceProvider) filterBySecretRef(all *unstructured.UnstructuredList) error { + for i := 0; i < len(all.Items); { + found, err := p.hasSecretRef(all.Items[i]) + if err != nil { + return err + } + if !found { + all.Items = append(all.Items[:i], all.Items[i+1:]...) + continue + } + i++ + } + + return nil +} + +func (p *ServiceInstanceProvider) hasSecretRef(item unstructured.Unstructured) (bool, error) { + _, found, err := unstructured.NestedString(item.Object, "spec", secretRefKey) + if err != nil { + p.logger.Error(fmt.Sprintf("while traversing \"%s\" unstructured object to find \"%s\" key", item.GetName(), secretRefKey), "error", err) + return false, err + } + + return found, nil +} diff --git a/internal/cluster-object/service_instance_provider_test.go b/internal/cluster-object/service_instance_provider_test.go new file mode 100644 index 000000000..feae4da5c --- /dev/null +++ b/internal/cluster-object/service_instance_provider_test.go @@ -0,0 +1,79 @@ +package clusterobject + +import ( + "context" + "log/slog" + "os" + "testing" + + "github.com/kyma-project/btp-manager/controllers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client/fake" +) + +func TestServiceInstanceProvider(t *testing.T) { + // given + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + + t.Run("should fetch all service instances", func(t *testing.T) { + // given + givenSiList := initServiceInstances(t) + k8sClient := fake.NewClientBuilder().WithLists(givenSiList).Build() + siProvider := NewServiceInstanceProvider(k8sClient, logger) + + // when + sis, err := siProvider.All(context.TODO()) + + // then + require.NoError(t, err) + assert.Len(t, sis.Items, 4) + }) + + t.Run("should fetch service instances with secret reference", func(t *testing.T) { + // given + givenSiList := initServiceInstances(t) + k8sClient := fake.NewClientBuilder().WithLists(givenSiList).Build() + siProvider := NewServiceInstanceProvider(k8sClient, logger) + + // when + sis, err := siProvider.AllWithSecretRef(context.TODO()) + + // then + require.NoError(t, err) + assert.Len(t, sis.Items, 2) + for _, si := range sis.Items { + secretRef, _, err := unstructured.NestedString(si.Object, "spec", secretRefKey) + require.NoError(t, err) + assert.NotEmpty(t, secretRef) + } + }) +} + +func initServiceInstances(t *testing.T) *unstructured.UnstructuredList { + siList := &unstructured.UnstructuredList{} + siList.SetGroupVersionKind(controllers.InstanceGvk) + siList.Items = []unstructured.Unstructured{ + initServiceInstance(t, "si1", "namespace1"), + initServiceInstance(t, "si2", "namespace2"), + initServiceInstance(t, "si3", "namespace3", "secret1"), + initServiceInstance(t, "si4", "namespace3", "secret2"), + } + + return siList +} + +func initServiceInstance(t *testing.T, name, namespace string, secretRef ...string) unstructured.Unstructured { + si := unstructured.Unstructured{} + si.SetGroupVersionKind(controllers.InstanceGvk) + si.SetName(name) + si.SetNamespace(namespace) + if len(secretRef) > 0 { + err := unstructured.SetNestedField(si.Object, secretRef[0], "spec", secretRefKey) + if err != nil { + t.Errorf("error while setting secret ref: %s", err) + } + } + return si +}