From 272ca43de2ab0f4cd3aba589f5a2d5ebdfe63ee4 Mon Sep 17 00:00:00 2001 From: Georgi Sabev Date: Wed, 15 Feb 2023 15:40:08 +0000 Subject: [PATCH] Split the different env builders into separate files Co-authored-by: Kieron Browne --- api/repositories/app_repository.go | 2 +- api/repositories/app_repository_test.go | 6 +- .../controllers/workloads/cfapp_controller.go | 99 ++---- .../controllers/workloads/env/builder.go | 170 +--------- .../controllers/workloads/env/builder_test.go | 312 +----------------- .../workloads/env/env_suite_test.go | 61 ++++ .../workloads/env/vcap_app_builder.go | 78 +++++ .../workloads/env/vcap_app_builder_test.go | 58 ++++ .../workloads/env/vcap_services_builder.go | 125 +++++++ .../env/vcap_services_builder_test.go | 253 ++++++++++++++ .../controllers/workloads/suite_test.go | 9 +- controllers/main.go | 9 +- 12 files changed, 631 insertions(+), 551 deletions(-) create mode 100644 controllers/controllers/workloads/env/vcap_app_builder.go create mode 100644 controllers/controllers/workloads/env/vcap_app_builder_test.go create mode 100644 controllers/controllers/workloads/env/vcap_services_builder.go create mode 100644 controllers/controllers/workloads/env/vcap_services_builder_test.go diff --git a/api/repositories/app_repository.go b/api/repositories/app_repository.go index facb98791..a635cbdbf 100644 --- a/api/repositories/app_repository.go +++ b/api/repositories/app_repository.go @@ -609,7 +609,7 @@ func getSystemEnv(ctx context.Context, userClient client.Client, app AppRecord) } if vcapServicesData, ok := vcapServiceSecret.Data["VCAP_SERVICES"]; ok { - vcapServicesPresenter := new(env.VcapServicesPresenter) + vcapServicesPresenter := new(env.VCAPServices) if err = json.Unmarshal(vcapServicesData, &vcapServicesPresenter); err != nil { return map[string]any{}, fmt.Errorf("error unmarshalling VCAP Service Secret %q for App %q: %w", app.vcapServiceSecretName, diff --git a/api/repositories/app_repository_test.go b/api/repositories/app_repository_test.go index e35dfff67..7cc4e2fa9 100644 --- a/api/repositories/app_repository_test.go +++ b/api/repositories/app_repository_test.go @@ -1359,7 +1359,7 @@ var _ = Describe("AppRepository", func() { var ( vcapServiceSecretDataByte map[string][]byte vcapServiceSecretData map[string]string - vcapServiceDataPresenter *env.VcapServicesPresenter + vcapServiceDataPresenter *env.VCAPServices err error ) @@ -1368,7 +1368,7 @@ var _ = Describe("AppRepository", func() { vcapServiceSecretDataByte, err = generateVcapServiceSecretDataByte() Expect(err).NotTo(HaveOccurred()) vcapServiceSecretData = asMapOfStrings(vcapServiceSecretDataByte) - vcapServiceDataPresenter = new(env.VcapServicesPresenter) + vcapServiceDataPresenter = new(env.VCAPServices) err = json.Unmarshal(vcapServiceSecretDataByte["VCAP_SERVICES"], vcapServiceDataPresenter) Expect(err).NotTo(HaveOccurred()) @@ -1592,7 +1592,7 @@ func generateVcapServiceSecretDataByte() (map[string][]byte, error) { VolumeMounts: nil, } - vcapServicesData, err := json.Marshal(env.VcapServicesPresenter{ + vcapServicesData, err := json.Marshal(env.VCAPServices{ UserProvided: []env.ServiceDetails{ serviceDetails, }, diff --git a/controllers/controllers/workloads/cfapp_controller.go b/controllers/controllers/workloads/cfapp_controller.go index ad4a0c50a..b6a594fd4 100644 --- a/controllers/controllers/workloads/cfapp_controller.go +++ b/controllers/controllers/workloads/cfapp_controller.go @@ -30,25 +30,26 @@ const ( cfAppFinalizerName = "cfApp.korifi.cloudfoundry.org" ) -type VCAPEnvBuilder interface { - BuildVCAPServicesEnvValue(context.Context, *korifiv1alpha1.CFApp) (string, error) - BuildVCAPApplicationEnvValue(context.Context, *korifiv1alpha1.CFApp) (string, error) +type EnvValueBuilder interface { + BuildEnvValue(context.Context, *korifiv1alpha1.CFApp) (map[string]string, error) } // CFAppReconciler reconciles a CFApp object type CFAppReconciler struct { - log logr.Logger - k8sClient client.Client - scheme *runtime.Scheme - vcapEnvBuilder VCAPEnvBuilder + log logr.Logger + k8sClient client.Client + scheme *runtime.Scheme + vcapServicesEnvBuilder EnvValueBuilder + vcapApplicationEnvBuilder EnvValueBuilder } -func NewCFAppReconciler(k8sClient client.Client, scheme *runtime.Scheme, log logr.Logger, vcapServicesBuilder VCAPEnvBuilder) *k8s.PatchingReconciler[korifiv1alpha1.CFApp, *korifiv1alpha1.CFApp] { +func NewCFAppReconciler(k8sClient client.Client, scheme *runtime.Scheme, log logr.Logger, vcapServicesBuilder, vcapApplicationBuilder EnvValueBuilder) *k8s.PatchingReconciler[korifiv1alpha1.CFApp, *korifiv1alpha1.CFApp] { appReconciler := CFAppReconciler{ - log: log, - k8sClient: k8sClient, - scheme: scheme, - vcapEnvBuilder: vcapServicesBuilder, + log: log, + k8sClient: k8sClient, + scheme: scheme, + vcapServicesEnvBuilder: vcapServicesBuilder, + vcapApplicationEnvBuilder: vcapApplicationBuilder, } return k8s.NewPatchingReconciler[korifiv1alpha1.CFApp, *korifiv1alpha1.CFApp](log, k8sClient, &appReconciler) } @@ -72,17 +73,21 @@ func (r *CFAppReconciler) ReconcileResource(ctx context.Context, cfApp *korifiv1 return ctrl.Result{}, err } - err = r.reconcileVCAPApplicationSecret(ctx, log, cfApp) + secretName := cfApp.Name + "-vcap-application" + err = r.reconcileVCAPSecret(ctx, log, cfApp, secretName, r.vcapApplicationEnvBuilder) if err != nil { log.Error(err, "unable to create CFApp VCAP Application secret") return ctrl.Result{}, err } + cfApp.Status.VCAPApplicationSecretName = secretName - err = r.reconcileVCAPServicesSecret(ctx, log, cfApp) + secretName = cfApp.Name + "-vcap-services" + err = r.reconcileVCAPSecret(ctx, log, cfApp, secretName, r.vcapServicesEnvBuilder) if err != nil { log.Error(err, "unable to create CFApp VCAP Services secret") return ctrl.Result{}, err } + cfApp.Status.VCAPServicesSecretName = secretName if cfApp.Status.Conditions == nil { cfApp.Status.Conditions = make([]metav1.Condition, 0) @@ -382,71 +387,37 @@ func serviceBindingToApp(o client.Object) []reconcile.Request { return result } -func (r *CFAppReconciler) reconcileVCAPApplicationSecret(ctx context.Context, log logr.Logger, cfApp *korifiv1alpha1.CFApp) error { - vcapApplicationSecretName := cfApp.Name + "-vcap-application" +func (r *CFAppReconciler) reconcileVCAPSecret( + ctx context.Context, + log logr.Logger, + cfApp *korifiv1alpha1.CFApp, + secretName string, + envBuilder EnvValueBuilder, +) error { + log = log.WithName("reconcileVCAPSecret").WithValues("secretName", secretName) - log = log.WithName("reconcileVCAPApplicationSecret").WithValues("vcapApplicationSecretName", vcapApplicationSecretName) - - vcapApplicationSecret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: vcapApplicationSecretName, - Namespace: cfApp.Namespace, - }, - } - - vcapApplicationValue, err := r.vcapEnvBuilder.BuildVCAPApplicationEnvValue(ctx, cfApp) - if err != nil { - log.Error(err, "failed to build 'VCAP_APPLICATION' value") - return err - } - _, err = controllerutil.CreateOrPatch(ctx, r.k8sClient, vcapApplicationSecret, func() error { - vcapApplicationSecret.StringData = map[string]string{ - "VCAP_APPLICATION": vcapApplicationValue, - } - - return controllerutil.SetOwnerReference(cfApp, vcapApplicationSecret, r.scheme) - }) - if err != nil { - log.Error(err, "unable to create or patch 'VCAP_APPLICATION' Secret") - return err - } - - cfApp.Status.VCAPApplicationSecretName = vcapApplicationSecretName - - return nil -} - -func (r *CFAppReconciler) reconcileVCAPServicesSecret(ctx context.Context, log logr.Logger, cfApp *korifiv1alpha1.CFApp) error { - vcapServicesSecretName := cfApp.Name + "-vcap-services" - - log = log.WithName("reconcileVCAPServicesSecret").WithValues("vcapServicesSecretName", vcapServicesSecretName) - - vcapServicesSecret := &corev1.Secret{ + secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: vcapServicesSecretName, + Name: secretName, Namespace: cfApp.Namespace, }, } - vcapServicesValue, err := r.vcapEnvBuilder.BuildVCAPServicesEnvValue(ctx, cfApp) + envValue, err := envBuilder.BuildEnvValue(ctx, cfApp) if err != nil { - log.Error(err, "failed to build 'VCAP_SERVICES' value") + log.Error(err, "failed to build env value") return err } - _, err = controllerutil.CreateOrPatch(ctx, r.k8sClient, vcapServicesSecret, func() error { - vcapServicesSecret.StringData = map[string]string{ - "VCAP_SERVICES": vcapServicesValue, - } + _, err = controllerutil.CreateOrPatch(ctx, r.k8sClient, secret, func() error { + secret.StringData = envValue - return controllerutil.SetOwnerReference(cfApp, vcapServicesSecret, r.scheme) + return controllerutil.SetOwnerReference(cfApp, secret, r.scheme) }) if err != nil { - log.Error(err, "unable to create or patch 'VCAP_SERVICES' Secret") + log.Error(err, "unable to create or patch Secret") return err } - cfApp.Status.VCAPServicesSecretName = vcapServicesSecretName - return nil } diff --git a/controllers/controllers/workloads/env/builder.go b/controllers/controllers/workloads/env/builder.go index 5cdb2a0dd..ca9ad1600 100644 --- a/controllers/controllers/workloads/env/builder.go +++ b/controllers/controllers/workloads/env/builder.go @@ -2,18 +2,16 @@ package env import ( "context" - "encoding/json" "fmt" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" - "code.cloudfoundry.org/korifi/controllers/controllers/shared" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ) -type VcapServicesPresenter struct { +type VCAPServices struct { UserProvided []ServiceDetails `json:"user-provided,omitempty"` } @@ -30,15 +28,15 @@ type ServiceDetails struct { VolumeMounts []string `json:"volume_mounts"` } -type Builder struct { +type WorkloadEnvBuilder struct { k8sClient client.Client } -func NewBuilder(k8sClient client.Client) *Builder { - return &Builder{k8sClient: k8sClient} +func NewWorkloadEnvBuilder(k8sClient client.Client) *WorkloadEnvBuilder { + return &WorkloadEnvBuilder{k8sClient: k8sClient} } -func (b *Builder) BuildEnv(ctx context.Context, cfApp *korifiv1alpha1.CFApp) ([]corev1.EnvVar, error) { +func (b *WorkloadEnvBuilder) BuildEnv(ctx context.Context, cfApp *korifiv1alpha1.CFApp) ([]corev1.EnvVar, error) { var appEnvSecret, vcapServicesSecret, vcapApplicationSecret corev1.Secret if cfApp.Spec.EnvSecretName != "" { @@ -66,109 +64,6 @@ func (b *Builder) BuildEnv(ctx context.Context, cfApp *korifiv1alpha1.CFApp) ([] return envVarsFromSecrets(appEnvSecret, vcapServicesSecret, vcapApplicationSecret), nil } -func (b *Builder) getSpaceFromNamespace(ctx context.Context, ns string) (korifiv1alpha1.CFSpace, error) { - spaces := korifiv1alpha1.CFSpaceList{} - if err := b.k8sClient.List(ctx, &spaces, client.MatchingFields{ - shared.IndexSpaceNamespaceName: ns, - }); err != nil { - return korifiv1alpha1.CFSpace{}, fmt.Errorf("error listing cfSpaces: %w", err) - } - - if len(spaces.Items) != 1 { - return korifiv1alpha1.CFSpace{}, fmt.Errorf("expected a unique CFSpace for namespace %q, got %d", ns, len(spaces.Items)) - } - - return spaces.Items[0], nil -} - -func (b *Builder) getOrgFromNamespace(ctx context.Context, ns string) (korifiv1alpha1.CFOrg, error) { - orgs := korifiv1alpha1.CFOrgList{} - if err := b.k8sClient.List(ctx, &orgs, client.MatchingFields{ - shared.IndexOrgNamespaceName: ns, - }); err != nil { - return korifiv1alpha1.CFOrg{}, fmt.Errorf("error listing cfOrgs: %w", err) - } - - if len(orgs.Items) != 1 { - return korifiv1alpha1.CFOrg{}, fmt.Errorf("expected a unique CFOrg for namespace %q, got %d", ns, len(orgs.Items)) - } - - return orgs.Items[0], nil -} - -func (b *Builder) BuildVCAPApplicationEnvValue(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (string, error) { - space, err := b.getSpaceFromNamespace(ctx, cfApp.Namespace) - if err != nil { - return "", fmt.Errorf("failed retrieving space for CFApp: %w", err) - } - org, err := b.getOrgFromNamespace(ctx, space.Namespace) - if err != nil { - return "", fmt.Errorf("failed retrieving org for CFSpace: %w", err) - } - - vars := map[string]string{ - "application_id": cfApp.Name, - "application_name": cfApp.Spec.DisplayName, - "cf_api": "", - "name": cfApp.Spec.DisplayName, - "organization_id": org.Name, - "organization_name": org.Spec.DisplayName, - "space_id": space.Name, - "space_name": space.Spec.DisplayName, - } - - out, _ := json.Marshal(vars) - return string(out), nil -} - -func (b *Builder) BuildVCAPServicesEnvValue(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (string, error) { - serviceBindings := &korifiv1alpha1.CFServiceBindingList{} - err := b.k8sClient.List(ctx, serviceBindings, - client.InNamespace(cfApp.Namespace), - client.MatchingFields{shared.IndexServiceBindingAppGUID: cfApp.Name}, - ) - if err != nil { - return "", fmt.Errorf("error listing CFServiceBindings: %w", err) - } - - if len(serviceBindings.Items) == 0 { - return "{}", nil - } - - serviceEnvs := []ServiceDetails{} - for _, currentServiceBinding := range serviceBindings.Items { - // If finalizing do not append - if !currentServiceBinding.DeletionTimestamp.IsZero() { - continue - } - - var serviceEnv ServiceDetails - serviceEnv, err = buildSingleServiceEnv(ctx, b.k8sClient, currentServiceBinding) - if err != nil { - return "", err - } - - serviceEnvs = append(serviceEnvs, serviceEnv) - } - - toReturn, err := json.Marshal(VcapServicesPresenter{ - UserProvided: serviceEnvs, - }) - if err != nil { - return "", err - } - - return string(toReturn), nil -} - -func mapFromSecret(secret corev1.Secret) map[string]string { - convertedMap := make(map[string]string) - for k, v := range secret.Data { - convertedMap[k] = string(v) - } - return convertedMap -} - func envVarsFromSecrets(secrets ...corev1.Secret) []corev1.EnvVar { var envVars []corev1.EnvVar for _, secret := range secrets { @@ -186,58 +81,3 @@ func envVarsFromSecrets(secrets ...corev1.Secret) []corev1.EnvVar { } return envVars } - -func fromServiceBinding( - serviceBinding korifiv1alpha1.CFServiceBinding, - serviceInstance korifiv1alpha1.CFServiceInstance, - serviceBindingSecret corev1.Secret, -) ServiceDetails { - var serviceName string - var bindingName *string - - if serviceBinding.Spec.DisplayName != nil { - serviceName = *serviceBinding.Spec.DisplayName - bindingName = serviceBinding.Spec.DisplayName - } else { - serviceName = serviceInstance.Spec.DisplayName - bindingName = nil - } - - tags := serviceInstance.Spec.Tags - if tags == nil { - tags = []string{} - } - - return ServiceDetails{ - Label: "user-provided", - Name: serviceName, - Tags: tags, - InstanceGUID: serviceInstance.Name, - InstanceName: serviceInstance.Spec.DisplayName, - BindingGUID: serviceBinding.Name, - BindingName: bindingName, - Credentials: mapFromSecret(serviceBindingSecret), - SyslogDrainURL: nil, - VolumeMounts: []string{}, - } -} - -func buildSingleServiceEnv(ctx context.Context, k8sClient client.Client, serviceBinding korifiv1alpha1.CFServiceBinding) (ServiceDetails, error) { - if serviceBinding.Status.Binding.Name == "" { - return ServiceDetails{}, fmt.Errorf("service binding secret name is empty") - } - - serviceInstance := korifiv1alpha1.CFServiceInstance{} - err := k8sClient.Get(ctx, types.NamespacedName{Namespace: serviceBinding.Namespace, Name: serviceBinding.Spec.Service.Name}, &serviceInstance) - if err != nil { - return ServiceDetails{}, fmt.Errorf("error fetching CFServiceInstance: %w", err) - } - - secret := corev1.Secret{} - err = k8sClient.Get(ctx, types.NamespacedName{Namespace: serviceBinding.Namespace, Name: serviceBinding.Status.Binding.Name}, &secret) - if err != nil { - return ServiceDetails{}, fmt.Errorf("error fetching CFServiceBinding Secret: %w", err) - } - - return fromServiceBinding(serviceBinding, serviceInstance, secret), nil -} diff --git a/controllers/controllers/workloads/env/builder_test.go b/controllers/controllers/workloads/env/builder_test.go index fadb34a6c..659e781e6 100644 --- a/controllers/controllers/workloads/env/builder_test.go +++ b/controllers/controllers/workloads/env/builder_test.go @@ -2,142 +2,31 @@ package env_test import ( "context" - "encoding/json" - "time" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" - "code.cloudfoundry.org/korifi/tools/k8s" - "k8s.io/apimachinery/pkg/api/equality" - k8serrors "k8s.io/apimachinery/pkg/api/errors" - "sigs.k8s.io/controller-runtime/pkg/client" - . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" "github.com/onsi/gomega/types" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) var _ = Describe("Builder", func() { var ( - serviceBinding *korifiv1alpha1.CFServiceBinding - serviceInstance *korifiv1alpha1.CFServiceInstance - serviceBindingSecret *corev1.Secret vcapServicesSecret *corev1.Secret vcapApplicationSecret *corev1.Secret appSecret *corev1.Secret - cfApp *korifiv1alpha1.CFApp - builder *env.Builder + builder *env.WorkloadEnvBuilder envVars []corev1.EnvVar buildEnvErr error ) BeforeEach(func() { - builder = env.NewBuilder(k8sClient) - ctx = context.Background() - - serviceInstance = &korifiv1alpha1.CFServiceInstance{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: "my-service-instance-guid", - }, - Spec: korifiv1alpha1.CFServiceInstanceSpec{ - DisplayName: "my-service-instance", - Tags: []string{"t1", "t2"}, - Type: "user-provided", - }, - } - ensureCreate(serviceInstance) - - serviceBindingSecret = &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: "service-binding-secret", - }, - Data: map[string][]byte{ - "foo": []byte("bar"), - }, - } - ensureCreate(serviceBindingSecret) - - serviceBindingName := "my-service-binding" - serviceBinding = &korifiv1alpha1.CFServiceBinding{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: "my-service-binding-guid", - }, - Spec: korifiv1alpha1.CFServiceBindingSpec{ - DisplayName: &serviceBindingName, - Service: corev1.ObjectReference{ - Name: "my-service-instance-guid", - }, - AppRef: corev1.LocalObjectReference{ - Name: "app-guid", - }, - }, - } - ensureCreate(serviceBinding) - ensurePatch(serviceBinding, func(sb *korifiv1alpha1.CFServiceBinding) { - sb.Status = korifiv1alpha1.CFServiceBindingStatus{ - Conditions: []metav1.Condition{}, - Binding: corev1.LocalObjectReference{ - Name: "service-binding-secret", - }, - } - }) - - ensureCreate(&korifiv1alpha1.CFServiceInstance{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: "my-service-instance-guid-2", - }, - Spec: korifiv1alpha1.CFServiceInstanceSpec{ - DisplayName: "my-service-instance-2", - Tags: []string{"t1", "t2"}, - Type: "user-provided", - }, - }) - - ensureCreate(&corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: "service-binding-secret-2", - }, - Data: map[string][]byte{ - "bar": []byte("foo"), - }, - }) - - serviceBindingName2 := "my-service-binding-2" - serviceBinding2 := &korifiv1alpha1.CFServiceBinding{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: "my-service-binding-guid-2", - }, - Spec: korifiv1alpha1.CFServiceBindingSpec{ - DisplayName: &serviceBindingName2, - Service: corev1.ObjectReference{ - Name: "my-service-instance-guid-2", - }, - AppRef: corev1.LocalObjectReference{ - Name: "app-guid", - }, - }, - } - ensureCreate(serviceBinding2) - ensurePatch(serviceBinding2, func(sb *korifiv1alpha1.CFServiceBinding) { - sb.Status = korifiv1alpha1.CFServiceBindingStatus{ - Conditions: []metav1.Condition{}, - Binding: corev1.LocalObjectReference{ - Name: "service-binding-secret-2", - }, - } - }) + builder = env.NewWorkloadEnvBuilder(k8sClient) appSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -150,35 +39,6 @@ var _ = Describe("Builder", func() { } ensureCreate(appSecret) - cfApp = &korifiv1alpha1.CFApp{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: cfSpace.Status.GUID, - Name: "app-guid", - }, - Spec: korifiv1alpha1.CFAppSpec{ - EnvSecretName: "app-env-secret", - DisplayName: "app-display-name", - DesiredState: korifiv1alpha1.StoppedState, - Lifecycle: korifiv1alpha1.Lifecycle{ - Type: "buildpack", - }, - }, - } - ensureCreate(cfApp) - ensurePatch(cfApp, func(app *korifiv1alpha1.CFApp) { - app.Status = korifiv1alpha1.CFAppStatus{ - Conditions: []metav1.Condition{}, - VCAPServicesSecretName: "app-guid-vcap-services", - VCAPApplicationSecretName: "app-guid-vcap-application", - } - meta.SetStatusCondition(&app.Status.Conditions, metav1.Condition{ - Type: "Ready", - Status: metav1.ConditionTrue, - Reason: "testing", - LastTransitionTime: metav1.Date(2023, 2, 15, 12, 0, 0, 0, time.FixedZone("", 0)), - }) - }) - vcapServicesSecret = &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "app-guid-vcap-services", @@ -386,172 +246,4 @@ var _ = Describe("Builder", func() { }) }) }) - - Describe("BuildVCAPServicesEnvValue", func() { - var ( - vcapServicesString string - buildVCAPServicesEnvValueErr error - ) - - JustBeforeEach(func() { - vcapServicesString, buildVCAPServicesEnvValueErr = builder.BuildVCAPServicesEnvValue(context.Background(), cfApp) - }) - - It("returns the service info", func() { - Expect(extractServiceInfo(vcapServicesString)).To(ContainElements( - SatisfyAll( - HaveLen(10), - HaveKeyWithValue("label", "user-provided"), - HaveKeyWithValue("name", "my-service-binding"), - HaveKeyWithValue("tags", ConsistOf("t1", "t2")), - HaveKeyWithValue("instance_guid", "my-service-instance-guid"), - HaveKeyWithValue("instance_name", "my-service-instance"), - HaveKeyWithValue("binding_guid", "my-service-binding-guid"), - HaveKeyWithValue("binding_name", Equal("my-service-binding")), - HaveKeyWithValue("credentials", SatisfyAll(HaveKeyWithValue("foo", "bar"), HaveLen(1))), - HaveKeyWithValue("syslog_drain_url", BeNil()), - HaveKeyWithValue("volume_mounts", BeEmpty()), - ), - SatisfyAll( - HaveLen(10), - HaveKeyWithValue("label", "user-provided"), - HaveKeyWithValue("name", "my-service-binding-2"), - HaveKeyWithValue("tags", ConsistOf("t1", "t2")), - HaveKeyWithValue("instance_guid", "my-service-instance-guid-2"), - HaveKeyWithValue("instance_name", "my-service-instance-2"), - HaveKeyWithValue("binding_guid", "my-service-binding-guid-2"), - HaveKeyWithValue("binding_name", Equal("my-service-binding-2")), - HaveKeyWithValue("credentials", SatisfyAll(HaveKeyWithValue("bar", "foo"), HaveLen(1))), - HaveKeyWithValue("syslog_drain_url", BeNil()), - HaveKeyWithValue("volume_mounts", BeEmpty()), - ), - )) - }) - - When("the service binding has no name", func() { - BeforeEach(func() { - ensurePatch(serviceBinding, func(sb *korifiv1alpha1.CFServiceBinding) { - sb.Spec.DisplayName = nil - }) - }) - - It("uses the service instance name as name", func() { - Expect(extractServiceInfo(vcapServicesString)).To(ContainElement(HaveKeyWithValue("name", serviceInstance.Spec.DisplayName))) - }) - - It("sets the binding name to nil", func() { - Expect(extractServiceInfo(vcapServicesString)).To(ContainElement(HaveKeyWithValue("binding_name", BeNil()))) - }) - }) - - When("service instance tags are nil", func() { - BeforeEach(func() { - ensurePatch(serviceInstance, func(si *korifiv1alpha1.CFServiceInstance) { - si.Spec.Tags = nil - }) - }) - - It("sets an empty array to tags", func() { - Expect(extractServiceInfo(vcapServicesString)).To(ContainElement(HaveKeyWithValue("tags", BeEmpty()))) - }) - }) - - When("there are no service bindings for the app", func() { - BeforeEach(func() { - Expect(k8sClient.DeleteAllOf(ctx, &korifiv1alpha1.CFServiceBinding{}, client.InNamespace(cfSpace.Status.GUID))).To(Succeed()) - Eventually(func(g Gomega) { - sbList := &korifiv1alpha1.CFServiceBindingList{} - g.Expect(k8sClient.List(ctx, sbList, client.InNamespace(cfSpace.Status.GUID))).To(Succeed()) - g.Expect(sbList.Items).To(BeEmpty()) - }).Should(Succeed()) - }) - - It("returns an empty JSON string", func() { - Expect(vcapServicesString).To(MatchJSON(`{}`)) - }) - }) - - When("getting the service binding secret fails", func() { - BeforeEach(func() { - ensureDelete(serviceBindingSecret) - }) - - It("returns an error", func() { - Expect(buildVCAPServicesEnvValueErr).To(MatchError(ContainSubstring("error fetching CFServiceBinding Secret"))) - }) - }) - }) - - Describe("BuildVCAPApplicationEnvValue", func() { - var ( - vcapApplicationString string - buildVCAPApplicationEnvValueErr error - ) - - JustBeforeEach(func() { - vcapApplicationString, buildVCAPApplicationEnvValueErr = builder.BuildVCAPApplicationEnvValue(context.Background(), cfApp) - }) - - It("sets the basic fields", func() { - Expect(buildVCAPApplicationEnvValueErr).ToNot(HaveOccurred()) - appMap := map[string]string{} - Expect(json.Unmarshal([]byte(vcapApplicationString), &appMap)).To(Succeed()) - Expect(appMap).To(HaveKeyWithValue("application_id", cfApp.Name)) - Expect(appMap).To(HaveKeyWithValue("application_name", cfApp.Spec.DisplayName)) - Expect(appMap).To(HaveKeyWithValue("name", cfApp.Spec.DisplayName)) - Expect(appMap).To(HaveKeyWithValue("cf_api", BeEmpty())) - Expect(appMap).To(HaveKeyWithValue("space_id", cfSpace.Name)) - Expect(appMap).To(HaveKeyWithValue("space_name", cfSpace.Spec.DisplayName)) - Expect(appMap).To(HaveKeyWithValue("organization_id", cfOrg.Name)) - Expect(appMap).To(HaveKeyWithValue("organization_name", cfOrg.Spec.DisplayName)) - }) - }) }) - -func ensureCreate(obj client.Object) { - Expect(k8sClient.Create(ctx, obj)).To(Succeed()) - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) - }).Should(Succeed()) -} - -func ensurePatch[T any, PT k8s.ObjectWithDeepCopy[T]](obj PT, modifyFunc func(PT)) { - Expect(k8s.Patch(ctx, k8sClient, obj, func() { - modifyFunc(obj) - })).To(Succeed()) - Eventually(func(g Gomega) { - g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) - objCopy := obj.DeepCopy() - modifyFunc(objCopy) - g.Expect(equality.Semantic.DeepEqual(objCopy, obj)).To(BeTrue()) - }).Should(Succeed()) -} - -func ensureDelete(obj client.Object) { - Expect(k8sClient.Delete(ctx, obj)).To(Succeed()) - Eventually(func(g Gomega) { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) - g.Expect(k8serrors.IsNotFound(err)).To(BeTrue()) - }).Should(Succeed()) -} - -func extractServiceInfo(vcapServicesData string) []map[string]interface{} { - var vcapServices map[string]interface{} - Expect(json.Unmarshal([]byte(vcapServicesData), &vcapServices)).To(Succeed()) - - Expect(vcapServices).To(HaveLen(1)) - Expect(vcapServices).To(HaveKey("user-provided")) - - serviceInfos, ok := vcapServices["user-provided"].([]interface{}) - Expect(ok).To(BeTrue()) - Expect(serviceInfos).To(HaveLen(2)) - - infos := make([]map[string]interface{}, 0, 2) - for i := range serviceInfos { - info, ok := serviceInfos[i].(map[string]interface{}) - Expect(ok).To(BeTrue()) - infos = append(infos, info) - } - - return infos -} diff --git a/controllers/controllers/workloads/env/env_suite_test.go b/controllers/controllers/workloads/env/env_suite_test.go index c2ef33c35..a7605698f 100644 --- a/controllers/controllers/workloads/env/env_suite_test.go +++ b/controllers/controllers/workloads/env/env_suite_test.go @@ -7,6 +7,8 @@ import ( "time" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" "code.cloudfoundry.org/korifi/controllers/controllers/workloads/testutils" "code.cloudfoundry.org/korifi/tools/k8s" @@ -16,6 +18,8 @@ import ( servicebindingv1beta1 "github.com/servicebinding/service-binding-controller/apis/v1beta1" "go.uber.org/zap/zapcore" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/equality" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" @@ -33,6 +37,7 @@ var ( cfOrg *korifiv1alpha1.CFOrg cfSpace *korifiv1alpha1.CFSpace ctx context.Context + cfApp *korifiv1alpha1.CFApp ) func TestEnvBuilders(t *testing.T) { @@ -123,6 +128,35 @@ var _ = BeforeEach(func() { cfSpace.Status.GUID = testutils.PrefixedGUID("space") }) createNamespace(cfSpace.Status.GUID) + + cfApp = &korifiv1alpha1.CFApp{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfSpace.Status.GUID, + Name: "app-guid", + }, + Spec: korifiv1alpha1.CFAppSpec{ + EnvSecretName: "app-env-secret", + DisplayName: "app-display-name", + DesiredState: korifiv1alpha1.StoppedState, + Lifecycle: korifiv1alpha1.Lifecycle{ + Type: "buildpack", + }, + }, + } + ensureCreate(cfApp) + ensurePatch(cfApp, func(app *korifiv1alpha1.CFApp) { + app.Status = korifiv1alpha1.CFAppStatus{ + Conditions: []metav1.Condition{}, + VCAPServicesSecretName: "app-guid-vcap-services", + VCAPApplicationSecretName: "app-guid-vcap-application", + } + meta.SetStatusCondition(&app.Status.Conditions, metav1.Condition{ + Type: "Ready", + Status: metav1.ConditionTrue, + Reason: "testing", + LastTransitionTime: metav1.Date(2023, 2, 15, 12, 0, 0, 0, time.FixedZone("", 0)), + }) + }) }) func createNamespace(name string) *corev1.Namespace { @@ -169,3 +203,30 @@ func patchCFAppAndWait(cfApp *korifiv1alpha1.CFApp, setFn func(a *korifiv1alpha1 g.Expect(checkFn(cfApp)).To(BeTrue()) }).Should(Succeed()) } + +func ensureCreate(obj client.Object) { + Expect(k8sClient.Create(ctx, obj)).To(Succeed()) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + }).Should(Succeed()) +} + +func ensurePatch[T any, PT k8s.ObjectWithDeepCopy[T]](obj PT, modifyFunc func(PT)) { + Expect(k8s.Patch(ctx, k8sClient, obj, func() { + modifyFunc(obj) + })).To(Succeed()) + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj)).To(Succeed()) + objCopy := obj.DeepCopy() + modifyFunc(objCopy) + g.Expect(equality.Semantic.DeepEqual(objCopy, obj)).To(BeTrue()) + }).Should(Succeed()) +} + +func ensureDelete(obj client.Object) { + Expect(k8sClient.Delete(ctx, obj)).To(Succeed()) + Eventually(func(g Gomega) { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(obj), obj) + g.Expect(k8serrors.IsNotFound(err)).To(BeTrue()) + }).Should(Succeed()) +} diff --git a/controllers/controllers/workloads/env/vcap_app_builder.go b/controllers/controllers/workloads/env/vcap_app_builder.go new file mode 100644 index 000000000..7d3da7a35 --- /dev/null +++ b/controllers/controllers/workloads/env/vcap_app_builder.go @@ -0,0 +1,78 @@ +package env + +import ( + "context" + "encoding/json" + "fmt" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type VCAPApplicationEnvValueBuilder struct { + k8sClient client.Client +} + +func NewVCAPApplicationEnvValueBuilder(k8sClient client.Client) *VCAPApplicationEnvValueBuilder { + return &VCAPApplicationEnvValueBuilder{k8sClient: k8sClient} +} + +func (b *VCAPApplicationEnvValueBuilder) BuildEnvValue(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (map[string]string, error) { + space, err := b.getSpaceFromNamespace(ctx, cfApp.Namespace) + if err != nil { + return nil, fmt.Errorf("failed retrieving space for CFApp: %w", err) + } + org, err := b.getOrgFromNamespace(ctx, space.Namespace) + if err != nil { + return nil, fmt.Errorf("failed retrieving org for CFSpace: %w", err) + } + + vars := map[string]string{ + "application_id": cfApp.Name, + "application_name": cfApp.Spec.DisplayName, + "cf_api": "", + "name": cfApp.Spec.DisplayName, + "organization_id": org.Name, + "organization_name": org.Spec.DisplayName, + "space_id": space.Name, + "space_name": space.Spec.DisplayName, + } + + marshalledVars, _ := json.Marshal(vars) + + return map[string]string{ + "VCAP_APPLICATION": string(marshalledVars), + }, nil +} + +func (b *VCAPApplicationEnvValueBuilder) getSpaceFromNamespace(ctx context.Context, ns string) (korifiv1alpha1.CFSpace, error) { + spaces := korifiv1alpha1.CFSpaceList{} + if err := b.k8sClient.List(ctx, &spaces, client.MatchingFields{ + shared.IndexSpaceNamespaceName: ns, + }); err != nil { + return korifiv1alpha1.CFSpace{}, fmt.Errorf("error listing cfSpaces: %w", err) + } + + if len(spaces.Items) != 1 { + return korifiv1alpha1.CFSpace{}, fmt.Errorf("expected a unique CFSpace for namespace %q, got %d", ns, len(spaces.Items)) + } + + return spaces.Items[0], nil +} + +func (b *VCAPApplicationEnvValueBuilder) getOrgFromNamespace(ctx context.Context, ns string) (korifiv1alpha1.CFOrg, error) { + orgs := korifiv1alpha1.CFOrgList{} + if err := b.k8sClient.List(ctx, &orgs, client.MatchingFields{ + shared.IndexOrgNamespaceName: ns, + }); err != nil { + return korifiv1alpha1.CFOrg{}, fmt.Errorf("error listing cfOrgs: %w", err) + } + + if len(orgs.Items) != 1 { + return korifiv1alpha1.CFOrg{}, fmt.Errorf("expected a unique CFOrg for namespace %q, got %d", ns, len(orgs.Items)) + } + + return orgs.Items[0], nil +} diff --git a/controllers/controllers/workloads/env/vcap_app_builder_test.go b/controllers/controllers/workloads/env/vcap_app_builder_test.go new file mode 100644 index 000000000..82fe8a32f --- /dev/null +++ b/controllers/controllers/workloads/env/vcap_app_builder_test.go @@ -0,0 +1,58 @@ +package env_test + +import ( + "encoding/json" + + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("VCAP_APPLICATION env value builder", func() { + var ( + vcapApplicationSecret *corev1.Secret + builder *env.VCAPApplicationEnvValueBuilder + ) + + BeforeEach(func() { + builder = env.NewVCAPApplicationEnvValueBuilder(k8sClient) + + vcapApplicationSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-guid-vcap-application", + Namespace: cfSpace.Status.GUID, + }, + Data: map[string][]byte{"VCAP_APPLICATION": []byte(`{"foo":"bar"}`)}, + } + Expect(k8sClient.Create(ctx, vcapApplicationSecret)).To(Succeed()) + }) + + Describe("BuildEnvValue", func() { + var ( + vcapApplication map[string]string + buildVCAPApplicationEnvValueErr error + ) + + JustBeforeEach(func() { + vcapApplication, buildVCAPApplicationEnvValueErr = builder.BuildEnvValue(ctx, cfApp) + }) + + It("sets the basic fields", func() { + Expect(buildVCAPApplicationEnvValueErr).ToNot(HaveOccurred()) + Expect(vcapApplication).To(HaveKey("VCAP_APPLICATION")) + vcapAppValue := map[string]string{} + Expect(json.Unmarshal([]byte(vcapApplication["VCAP_APPLICATION"]), &vcapAppValue)).To(Succeed()) + Expect(vcapAppValue).To(HaveKeyWithValue("application_id", cfApp.Name)) + Expect(vcapAppValue).To(HaveKeyWithValue("application_name", cfApp.Spec.DisplayName)) + Expect(vcapAppValue).To(HaveKeyWithValue("name", cfApp.Spec.DisplayName)) + Expect(vcapAppValue).To(HaveKeyWithValue("cf_api", BeEmpty())) + Expect(vcapAppValue).To(HaveKeyWithValue("space_id", cfSpace.Name)) + Expect(vcapAppValue).To(HaveKeyWithValue("space_name", cfSpace.Spec.DisplayName)) + Expect(vcapAppValue).To(HaveKeyWithValue("organization_id", cfOrg.Name)) + Expect(vcapAppValue).To(HaveKeyWithValue("organization_name", cfOrg.Spec.DisplayName)) + }) + }) +}) diff --git a/controllers/controllers/workloads/env/vcap_services_builder.go b/controllers/controllers/workloads/env/vcap_services_builder.go new file mode 100644 index 000000000..94bb0ecbb --- /dev/null +++ b/controllers/controllers/workloads/env/vcap_services_builder.go @@ -0,0 +1,125 @@ +package env + +import ( + "context" + "encoding/json" + "fmt" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/shared" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type VCAPServicesEnvValueBuilder struct { + k8sClient client.Client +} + +func NewVCAPServicesEnvValueBuilder(k8sClient client.Client) *VCAPServicesEnvValueBuilder { + return &VCAPServicesEnvValueBuilder{k8sClient: k8sClient} +} + +func (b *VCAPServicesEnvValueBuilder) BuildEnvValue(ctx context.Context, cfApp *korifiv1alpha1.CFApp) (map[string]string, error) { + serviceBindings := &korifiv1alpha1.CFServiceBindingList{} + err := b.k8sClient.List(ctx, serviceBindings, + client.InNamespace(cfApp.Namespace), + client.MatchingFields{shared.IndexServiceBindingAppGUID: cfApp.Name}, + ) + if err != nil { + return nil, fmt.Errorf("error listing CFServiceBindings: %w", err) + } + + if len(serviceBindings.Items) == 0 { + return map[string]string{"VCAP_SERVICES": "{}"}, nil + } + + serviceEnvs := []ServiceDetails{} + for _, currentServiceBinding := range serviceBindings.Items { + // If finalizing do not append + if !currentServiceBinding.DeletionTimestamp.IsZero() { + continue + } + + var serviceEnv ServiceDetails + serviceEnv, err = buildSingleServiceEnv(ctx, b.k8sClient, currentServiceBinding) + if err != nil { + return nil, err + } + + serviceEnvs = append(serviceEnvs, serviceEnv) + } + + jsonVal, err := json.Marshal(VCAPServices{UserProvided: serviceEnvs}) + if err != nil { + return nil, err + } + + return map[string]string{ + "VCAP_SERVICES": string(jsonVal), + }, nil +} + +func buildSingleServiceEnv(ctx context.Context, k8sClient client.Client, serviceBinding korifiv1alpha1.CFServiceBinding) (ServiceDetails, error) { + if serviceBinding.Status.Binding.Name == "" { + return ServiceDetails{}, fmt.Errorf("service binding secret name is empty") + } + + serviceInstance := korifiv1alpha1.CFServiceInstance{} + err := k8sClient.Get(ctx, types.NamespacedName{Namespace: serviceBinding.Namespace, Name: serviceBinding.Spec.Service.Name}, &serviceInstance) + if err != nil { + return ServiceDetails{}, fmt.Errorf("error fetching CFServiceInstance: %w", err) + } + + secret := corev1.Secret{} + err = k8sClient.Get(ctx, types.NamespacedName{Namespace: serviceBinding.Namespace, Name: serviceBinding.Status.Binding.Name}, &secret) + if err != nil { + return ServiceDetails{}, fmt.Errorf("error fetching CFServiceBinding Secret: %w", err) + } + + return fromServiceBinding(serviceBinding, serviceInstance, secret), nil +} + +func fromServiceBinding( + serviceBinding korifiv1alpha1.CFServiceBinding, + serviceInstance korifiv1alpha1.CFServiceInstance, + serviceBindingSecret corev1.Secret, +) ServiceDetails { + var serviceName string + var bindingName *string + + if serviceBinding.Spec.DisplayName != nil { + serviceName = *serviceBinding.Spec.DisplayName + bindingName = serviceBinding.Spec.DisplayName + } else { + serviceName = serviceInstance.Spec.DisplayName + bindingName = nil + } + + tags := serviceInstance.Spec.Tags + if tags == nil { + tags = []string{} + } + + return ServiceDetails{ + Label: "user-provided", + Name: serviceName, + Tags: tags, + InstanceGUID: serviceInstance.Name, + InstanceName: serviceInstance.Spec.DisplayName, + BindingGUID: serviceBinding.Name, + BindingName: bindingName, + Credentials: mapFromSecret(serviceBindingSecret), + SyslogDrainURL: nil, + VolumeMounts: []string{}, + } +} + +func mapFromSecret(secret corev1.Secret) map[string]string { + convertedMap := make(map[string]string) + for k, v := range secret.Data { + convertedMap[k] = string(v) + } + return convertedMap +} diff --git a/controllers/controllers/workloads/env/vcap_services_builder_test.go b/controllers/controllers/workloads/env/vcap_services_builder_test.go new file mode 100644 index 000000000..b4391a1da --- /dev/null +++ b/controllers/controllers/workloads/env/vcap_services_builder_test.go @@ -0,0 +1,253 @@ +package env_test + +import ( + "encoding/json" + + korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/controllers/controllers/workloads/env" + "code.cloudfoundry.org/korifi/tools/k8s" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("Builder", func() { + var ( + serviceBinding *korifiv1alpha1.CFServiceBinding + serviceInstance *korifiv1alpha1.CFServiceInstance + serviceBindingSecret *corev1.Secret + vcapServicesSecret *corev1.Secret + builder *env.VCAPServicesEnvValueBuilder + ) + + BeforeEach(func() { + builder = env.NewVCAPServicesEnvValueBuilder(k8sClient) + + serviceInstance = &korifiv1alpha1.CFServiceInstance{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfSpace.Status.GUID, + Name: "my-service-instance-guid", + }, + Spec: korifiv1alpha1.CFServiceInstanceSpec{ + DisplayName: "my-service-instance", + Tags: []string{"t1", "t2"}, + Type: "user-provided", + }, + } + ensureCreate(serviceInstance) + + serviceBindingSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfSpace.Status.GUID, + Name: "service-binding-secret", + }, + Data: map[string][]byte{ + "foo": []byte("bar"), + }, + } + ensureCreate(serviceBindingSecret) + + serviceBindingName := "my-service-binding" + serviceBinding = &korifiv1alpha1.CFServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfSpace.Status.GUID, + Name: "my-service-binding-guid", + }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + DisplayName: &serviceBindingName, + Service: corev1.ObjectReference{ + Name: "my-service-instance-guid", + }, + AppRef: corev1.LocalObjectReference{ + Name: "app-guid", + }, + }, + } + ensureCreate(serviceBinding) + ensurePatch(serviceBinding, func(sb *korifiv1alpha1.CFServiceBinding) { + sb.Status = korifiv1alpha1.CFServiceBindingStatus{ + Conditions: []metav1.Condition{}, + Binding: corev1.LocalObjectReference{ + Name: "service-binding-secret", + }, + } + }) + + ensureCreate(&korifiv1alpha1.CFServiceInstance{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfSpace.Status.GUID, + Name: "my-service-instance-guid-2", + }, + Spec: korifiv1alpha1.CFServiceInstanceSpec{ + DisplayName: "my-service-instance-2", + Tags: []string{"t1", "t2"}, + Type: "user-provided", + }, + }) + + ensureCreate(&corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfSpace.Status.GUID, + Name: "service-binding-secret-2", + }, + Data: map[string][]byte{ + "bar": []byte("foo"), + }, + }) + + serviceBindingName2 := "my-service-binding-2" + serviceBinding2 := &korifiv1alpha1.CFServiceBinding{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: cfSpace.Status.GUID, + Name: "my-service-binding-guid-2", + }, + Spec: korifiv1alpha1.CFServiceBindingSpec{ + DisplayName: &serviceBindingName2, + Service: corev1.ObjectReference{ + Name: "my-service-instance-guid-2", + }, + AppRef: corev1.LocalObjectReference{ + Name: "app-guid", + }, + }, + } + ensureCreate(serviceBinding2) + ensurePatch(serviceBinding2, func(sb *korifiv1alpha1.CFServiceBinding) { + sb.Status = korifiv1alpha1.CFServiceBindingStatus{ + Conditions: []metav1.Condition{}, + Binding: corev1.LocalObjectReference{ + Name: "service-binding-secret-2", + }, + } + }) + + vcapServicesSecret = &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-guid-vcap-services", + Namespace: cfSpace.Status.GUID, + }, + Data: map[string][]byte{"VCAP_SERVICES": []byte("{}")}, + } + ensureCreate(vcapServicesSecret) + }) + + Describe("BuildVCAPServicesEnvValue", func() { + var ( + vcapServices map[string]string + buildVCAPServicesEnvValueErr error + ) + + JustBeforeEach(func() { + vcapServices, buildVCAPServicesEnvValueErr = builder.BuildEnvValue(ctx, cfApp) + }) + + It("returns the service info", func() { + Expect(extractServiceInfo(vcapServices)).To(ContainElements( + SatisfyAll( + HaveLen(10), + HaveKeyWithValue("label", "user-provided"), + HaveKeyWithValue("name", "my-service-binding"), + HaveKeyWithValue("tags", ConsistOf("t1", "t2")), + HaveKeyWithValue("instance_guid", "my-service-instance-guid"), + HaveKeyWithValue("instance_name", "my-service-instance"), + HaveKeyWithValue("binding_guid", "my-service-binding-guid"), + HaveKeyWithValue("binding_name", Equal("my-service-binding")), + HaveKeyWithValue("credentials", SatisfyAll(HaveKeyWithValue("foo", "bar"), HaveLen(1))), + HaveKeyWithValue("syslog_drain_url", BeNil()), + HaveKeyWithValue("volume_mounts", BeEmpty()), + ), + SatisfyAll( + HaveLen(10), + HaveKeyWithValue("label", "user-provided"), + HaveKeyWithValue("name", "my-service-binding-2"), + HaveKeyWithValue("tags", ConsistOf("t1", "t2")), + HaveKeyWithValue("instance_guid", "my-service-instance-guid-2"), + HaveKeyWithValue("instance_name", "my-service-instance-2"), + HaveKeyWithValue("binding_guid", "my-service-binding-guid-2"), + HaveKeyWithValue("binding_name", Equal("my-service-binding-2")), + HaveKeyWithValue("credentials", SatisfyAll(HaveKeyWithValue("bar", "foo"), HaveLen(1))), + HaveKeyWithValue("syslog_drain_url", BeNil()), + HaveKeyWithValue("volume_mounts", BeEmpty()), + ), + )) + }) + + When("the service binding has no name", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, k8sClient, serviceBinding, func() { + serviceBinding.Spec.DisplayName = nil + })).To(Succeed()) + }) + + It("uses the service instance name as name", func() { + Expect(extractServiceInfo(vcapServices)).To(ContainElement(HaveKeyWithValue("name", serviceInstance.Spec.DisplayName))) + }) + + It("sets the binding name to nil", func() { + Expect(extractServiceInfo(vcapServices)).To(ContainElement(HaveKeyWithValue("binding_name", BeNil()))) + }) + }) + + When("service instance tags are nil", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, k8sClient, serviceInstance, func() { + serviceInstance.Spec.Tags = nil + })).To(Succeed()) + }) + + It("sets an empty array to tags", func() { + Expect(extractServiceInfo(vcapServices)).To(ContainElement(HaveKeyWithValue("tags", BeEmpty()))) + }) + }) + + When("there are no service bindings for the app", func() { + BeforeEach(func() { + Expect(k8sClient.DeleteAllOf(ctx, &korifiv1alpha1.CFServiceBinding{}, client.InNamespace(cfSpace.Status.GUID))).To(Succeed()) + Eventually(func(g Gomega) { + sbList := &korifiv1alpha1.CFServiceBindingList{} + g.Expect(k8sClient.List(ctx, sbList, client.InNamespace(cfSpace.Status.GUID))).To(Succeed()) + g.Expect(sbList.Items).To(BeEmpty()) + }).Should(Succeed()) + }) + + It("returns an empty JSON string", func() { + Expect(vcapServices).To(HaveKeyWithValue("VCAP_SERVICES", "{}")) + }) + }) + + When("getting the service binding secret fails", func() { + BeforeEach(func() { + Expect(k8sClient.Delete(ctx, serviceBindingSecret)).To(Succeed()) + }) + + It("returns an error", func() { + Expect(buildVCAPServicesEnvValueErr).To(MatchError(ContainSubstring("error fetching CFServiceBinding Secret"))) + }) + }) + }) +}) + +func extractServiceInfo(vcapServicesData map[string]string) []map[string]interface{} { + Expect(vcapServicesData).To(HaveKey("VCAP_SERVICES")) + var vcapServices map[string]interface{} + Expect(json.Unmarshal([]byte(vcapServicesData["VCAP_SERVICES"]), &vcapServices)).To(Succeed()) + + Expect(vcapServices).To(HaveLen(1)) + Expect(vcapServices).To(HaveKey("user-provided")) + + serviceInfos, ok := vcapServices["user-provided"].([]interface{}) + Expect(ok).To(BeTrue()) + Expect(serviceInfos).To(HaveLen(2)) + + infos := make([]map[string]interface{}, 0, 2) + for i := range serviceInfos { + info, ok := serviceInfos[i].(map[string]interface{}) + Expect(ok).To(BeTrue()) + infos = append(infos, info) + } + + return infos +} diff --git a/controllers/controllers/workloads/suite_test.go b/controllers/controllers/workloads/suite_test.go index e764637e4..53b340b09 100644 --- a/controllers/controllers/workloads/suite_test.go +++ b/controllers/controllers/workloads/suite_test.go @@ -110,7 +110,8 @@ var _ = BeforeSuite(func() { k8sManager.GetClient(), k8sManager.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFApp"), - env.NewBuilder(k8sManager.GetClient()), + env.NewVCAPServicesEnvValueBuilder(k8sManager.GetClient()), + env.NewVCAPApplicationEnvValueBuilder(k8sManager.GetClient()), )).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) @@ -122,7 +123,7 @@ var _ = BeforeSuite(func() { k8sManager.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFBuild"), controllerConfig, - env.NewBuilder(k8sManager.GetClient()), + env.NewWorkloadEnvBuilder(k8sManager.GetClient()), ) err = (cfBuildReconciler).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) @@ -132,7 +133,7 @@ var _ = BeforeSuite(func() { k8sManager.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFProcess"), controllerConfig, - env.NewBuilder(k8sManager.GetClient()), + env.NewWorkloadEnvBuilder(k8sManager.GetClient()), )).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) @@ -162,7 +163,7 @@ var _ = BeforeSuite(func() { k8sManager.GetScheme(), k8sManager.GetEventRecorderFor("cftask-controller"), ctrl.Log.WithName("controllers").WithName("CFTask"), - env.NewBuilder(k8sManager.GetClient()), + env.NewWorkloadEnvBuilder(k8sManager.GetClient()), 2*time.Second, ).SetupWithManager(k8sManager) Expect(err).NotTo(HaveOccurred()) diff --git a/controllers/main.go b/controllers/main.go index c13a5df39..0117d5033 100644 --- a/controllers/main.go +++ b/controllers/main.go @@ -141,7 +141,8 @@ func main() { mgr.GetClient(), mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFApp"), - env.NewBuilder(mgr.GetClient()), + env.NewVCAPServicesEnvValueBuilder(mgr.GetClient()), + env.NewVCAPApplicationEnvValueBuilder(mgr.GetClient()), )).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFApp") os.Exit(1) @@ -152,7 +153,7 @@ func main() { mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFBuild"), controllerConfig, - env.NewBuilder(mgr.GetClient()), + env.NewWorkloadEnvBuilder(mgr.GetClient()), )).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFBuild") os.Exit(1) @@ -172,7 +173,7 @@ func main() { mgr.GetScheme(), ctrl.Log.WithName("controllers").WithName("CFProcess"), controllerConfig, - env.NewBuilder(mgr.GetClient()), + env.NewWorkloadEnvBuilder(mgr.GetClient()), )).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFProcess") os.Exit(1) @@ -238,7 +239,7 @@ func main() { mgr.GetScheme(), mgr.GetEventRecorderFor("cftask-controller"), ctrl.Log.WithName("controllers").WithName("CFTask"), - env.NewBuilder(mgr.GetClient()), + env.NewWorkloadEnvBuilder(mgr.GetClient()), taskTTL, ).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "CFTask")