diff --git a/.vscode/launch.json b/.vscode/launch.json index 7210fae..32575cc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,6 +10,9 @@ "request": "launch", "mode": "auto", "program": "${workspaceFolder}", + "env": { + "RESOURCE_CACHE_ENABLED": "true" + }, "args": [ "--kubeconfig=${workspaceFolder}/.kubeconfig", "--webhook-bind-address=:2443", diff --git a/Dockerfile b/Dockerfile index 9566cbb..a7cc2f1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Build the manager binary -FROM --platform=$BUILDPLATFORM golang:1.23.0 as builder +FROM --platform=$BUILDPLATFORM golang:1.23.1 AS builder ARG TARGETOS ARG TARGETARCH diff --git a/Makefile b/Makefile index b7e5620..fee787d 100644 --- a/Makefile +++ b/Makefile @@ -157,7 +157,7 @@ KUSTOMIZE_VERSION ?= v3.8.7 CONTROLLER_TOOLS_VERSION ?= v0.14.0 CODE_GENERATOR_VERSION ?= v0.23.4 COUNTERFEITER_VERSION ?= v6.8.1 -GOLINT_VERSION ?= v1.57.1 +GOLINT_VERSION ?= v1.61.0 KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" .PHONY: kustomize diff --git a/go.mod b/go.mod index 7506b26..3770961 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/sap/cf-service-operator -go 1.22.5 +go 1.23.1 require ( github.com/cloudfoundry-community/go-cfclient/v3 v3.0.0-alpha.5 @@ -19,6 +19,7 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/caarlos0/env/v11 v11.2.2 github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/codegangsta/inject v0.0.0-20150114235600-33e0aa1cb7c0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect diff --git a/go.sum b/go.sum index 5e90dbd..137ea13 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/caarlos0/env/v11 v11.2.2 h1:95fApNrUyueipoZN/EhA8mMxiNxrBwDa+oAZrMWl3Kg= +github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= diff --git a/internal/cf/binding.go b/internal/cf/binding.go index f7f66c2..f578609 100644 --- a/internal/cf/binding.go +++ b/internal/cf/binding.go @@ -48,6 +48,18 @@ func (bo *bindingFilterOwner) getListOptions() *cfclient.ServiceCredentialBindin // If multiple bindings are found, an error is returned. // The function add the parameter values to the orphan cf binding, so that can be adopted. func (c *spaceClient) GetBinding(ctx context.Context, bindingOpts map[string]string) (*facade.Binding, error) { + if c.resourceCache.checkResourceCacheEnabled() { + // attempt to retrieve binding from cache + if c.resourceCache.isCacheExpired(bindingType) { + populateResourceCache(c, bindingType, "") + } + binding, inCache := c.resourceCache.getBindingFromCache(bindingOpts["owner"]) + if inCache { + return binding, nil + } + } + + // attempt to retrieve binding from Cloud Foundry var filterOpts bindingFilter if bindingOpts["name"] != "" { filterOpts = &bindingFilterName{name: bindingOpts["name"]} @@ -65,62 +77,17 @@ func (c *spaceClient) GetBinding(ctx context.Context, bindingOpts map[string]str } else if len(serviceBindings) > 1 { return nil, errors.New(fmt.Sprintf("found multiple service bindings with owner: %s", bindingOpts["owner"])) } - serviceBinding := serviceBindings[0] - // add parameter values to the cf orphan binding + // add parameter values to the orphaned binding in Cloud Foundry if bindingOpts["name"] != "" { generationvalue := "0" - serviceBinding.Metadata.Annotations[annotationGeneration] = &generationvalue parameterHashValue := "0" + serviceBinding.Metadata.Annotations[annotationGeneration] = &generationvalue serviceBinding.Metadata.Annotations[annotationParameterHash] = ¶meterHashValue } - guid := serviceBinding.GUID - name := serviceBinding.Name - generation, err := strconv.ParseInt(*serviceBinding.Metadata.Annotations[annotationGeneration], 10, 64) - if err != nil { - return nil, errors.Wrap(err, "error parsing service binding generation") - } - parameterHash := *serviceBinding.Metadata.Annotations[annotationParameterHash] - var state facade.BindingState - switch serviceBinding.LastOperation.Type + ":" + serviceBinding.LastOperation.State { - case "create:in progress": - state = facade.BindingStateCreating - case "create:succeeded": - state = facade.BindingStateReady - case "create:failed": - state = facade.BindingStateCreatedFailed - case "delete:in progress": - state = facade.BindingStateDeleting - case "delete:succeeded": - state = facade.BindingStateDeleted - case "delete:failed": - state = facade.BindingStateDeleteFailed - default: - state = facade.BindingStateUnknown - } - stateDescription := serviceBinding.LastOperation.Description - - var credentials map[string]interface{} - if state == facade.BindingStateReady { - details, err := c.client.ServiceCredentialBindings.GetDetails(ctx, guid) - if err != nil { - return nil, errors.Wrap(err, "error getting service binding details") - } - credentials = details.Credentials - } - - return &facade.Binding{ - Guid: guid, - Name: name, - Owner: bindingOpts["owner"], - Generation: generation, - ParameterHash: parameterHash, - State: state, - StateDescription: stateDescription, - Credentials: credentials, - }, nil + return c.InitBinding(ctx, serviceBinding, bindingOpts) } // Required parameters (may not be initial): name, serviceInstanceGuid, owner, generation @@ -144,7 +111,7 @@ func (c *spaceClient) CreateBinding(ctx context.Context, name string, serviceIns } // Required parameters (may not be initial): guid, generation -func (c *spaceClient) UpdateBinding(ctx context.Context, guid string, generation int64, parameters map[string]interface{}) error { +func (c *spaceClient) UpdateBinding(ctx context.Context, guid string, owner string, generation int64, parameters map[string]interface{}) error { // TODO: why is there no cfresource.NewServiceCredentialBindingUpdate() method ? req := &cfresource.ServiceCredentialBindingUpdate{} req.Metadata = cfresource.NewMetadata(). @@ -157,9 +124,85 @@ func (c *spaceClient) UpdateBinding(ctx context.Context, guid string, generation } } _, err := c.client.ServiceCredentialBindings.Update(ctx, guid, req) + + // update binding in cache + if err == nil && c.resourceCache.checkResourceCacheEnabled() { + isUpdated := c.resourceCache.updateBindingInCache(owner, parameters, generation) + if !isUpdated { + // add binding to cache if it is not found + // TODO: why getting binding here but not instance in CreateInstance() ? + binding, err := c.GetBinding(ctx, map[string]string{"owner": owner}) + if err != nil { + return err + } + c.resourceCache.addBindingInCache(owner, binding) + } + + } return err } -func (c *spaceClient) DeleteBinding(ctx context.Context, guid string) error { - return c.client.ServiceCredentialBindings.Delete(ctx, guid) +func (c *spaceClient) DeleteBinding(ctx context.Context, guid string, owner string) error { + err := c.client.ServiceCredentialBindings.Delete(ctx, guid) + + // delete binding from cache + if err == nil && c.resourceCache.checkResourceCacheEnabled() { + c.resourceCache.deleteBindingFromCache(owner) + } + + return err +} + +// InitBinding wraps cfclient.ServiceCredentialBinding as a facade.Binding. +func (c *spaceClient) InitBinding(ctx context.Context, serviceBinding *cfresource.ServiceCredentialBinding, bindingOpts map[string]string) (*facade.Binding, error) { + generation, err := strconv.ParseInt(*serviceBinding.Metadata.Annotations[annotationGeneration], 10, 64) + if err != nil { + return nil, errors.Wrap(err, "error parsing service binding generation") + } + + owner := bindingOpts["owner"] + if owner == "" { + owner = *serviceBinding.Metadata.Labels[labelOwner] + } + + var state facade.BindingState + switch serviceBinding.LastOperation.Type + ":" + serviceBinding.LastOperation.State { + case "create:in progress": + state = facade.BindingStateCreating + case "create:succeeded": + state = facade.BindingStateReady + case "create:failed": + state = facade.BindingStateCreatedFailed + case "delete:in progress": + state = facade.BindingStateDeleting + case "delete:succeeded": + state = facade.BindingStateDeleted + case "delete:failed": + state = facade.BindingStateDeleteFailed + default: + state = facade.BindingStateUnknown + } + + guid := serviceBinding.GUID + + return &facade.Binding{ + Guid: guid, + Name: serviceBinding.Name, + Owner: owner, + Generation: generation, + ParameterHash: *serviceBinding.Metadata.Annotations[annotationParameterHash], + State: state, + StateDescription: serviceBinding.LastOperation.Description, + Credentials: nil, // filled later + }, nil +} + +func (c *spaceClient) FillBindingDetails(ctx context.Context, binding *facade.Binding) error { + details, err := c.client.ServiceCredentialBindings.GetDetails(ctx, binding.Guid) + if err != nil { + return errors.Wrap(err, "error getting service binding details") + } + binding.Credentials = details.Credentials + + return nil } diff --git a/internal/cf/client.go b/internal/cf/client.go index 4285f36..77653e2 100644 --- a/internal/cf/client.go +++ b/internal/cf/client.go @@ -6,13 +6,17 @@ SPDX-License-Identifier: Apache-2.0 package cf import ( + "context" "fmt" "sync" cfclient "github.com/cloudfoundry-community/go-cfclient/v3/client" cfconfig "github.com/cloudfoundry-community/go-cfclient/v3/config" + cfresource "github.com/cloudfoundry-community/go-cfclient/v3/resource" + ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/metrics" + "github.com/sap/cf-service-operator/internal/config" "github.com/sap/cf-service-operator/internal/facade" cfmetrics "github.com/sap/cf-service-operator/pkg/metrics" ) @@ -31,11 +35,13 @@ const ( type organizationClient struct { organizationName string client cfclient.Client + resourceCache *resourceCache } type spaceClient struct { - spaceGuid string - client cfclient.Client + spaceGuid string + client cfclient.Client + resourceCache *resourceCache } type clientIdentifier struct { @@ -51,10 +57,29 @@ type clientCacheEntry struct { } var ( - cacheMutex = &sync.Mutex{} - clientCache = make(map[clientIdentifier]*clientCacheEntry) + clientCacheMutex = &sync.Mutex{} + clientCache = make(map[clientIdentifier]*clientCacheEntry) + refreshServiceInstanceResourceCacheMutex = sync.Mutex{} + refreshSpaceResourceCacheMutex = sync.Mutex{} + refreshServiceBindingResourceCacheMutex = sync.Mutex{} + refreshSpaceUserRoleCacheMutex = sync.Mutex{} ) +var ( + cfResourceCache *resourceCache + cacheInstanceOnce sync.Once +) + +func initAndConfigureResourceCache(config *config.Config) *resourceCache { + cacheInstanceOnce.Do(func() { + // TODO: make this initialize cache for different testing purposes + cfResourceCache = initResourceCache() + cfResourceCache.setResourceCacheEnabled(config.IsResourceCacheEnabled) + cfResourceCache.setCacheTimeOut(config.CacheTimeOut) + }) + return cfResourceCache +} + func newOrganizationClient(organizationName string, url string, username string, password string) (*organizationClient, error) { if organizationName == "" { return nil, fmt.Errorf("missing or empty organization name") @@ -83,6 +108,7 @@ func newOrganizationClient(organizationName string, url string, username string, if err != nil { return nil, err } + return &organizationClient{organizationName: organizationName, client: *c}, nil } @@ -114,12 +140,13 @@ func newSpaceClient(spaceGuid string, url string, username string, password stri if err != nil { return nil, err } + return &spaceClient{spaceGuid: spaceGuid, client: *c}, nil } -func NewOrganizationClient(organizationName string, url string, username string, password string) (facade.OrganizationClient, error) { - cacheMutex.Lock() - defer cacheMutex.Unlock() +func NewOrganizationClient(organizationName string, url string, username string, password string, config *config.Config) (facade.OrganizationClient, error) { + clientCacheMutex.Lock() + defer clientCacheMutex.Unlock() // look up CF client in cache identifier := clientIdentifier{url: url, username: username} @@ -129,7 +156,7 @@ func NewOrganizationClient(organizationName string, url string, username string, var client *organizationClient = nil if isInCache { // re-use CF client and wrap it as organizationClient - client = &organizationClient{organizationName: organizationName, client: cacheEntry.client} + client = &organizationClient{organizationName: organizationName, client: cacheEntry.client, resourceCache: cfResourceCache} if cacheEntry.password != password { // password was rotated => delete client from cache and create a new one below delete(clientCache, identifier) @@ -146,12 +173,18 @@ func NewOrganizationClient(organizationName string, url string, username string, } } + if config.IsResourceCacheEnabled && client.resourceCache == nil { + client.resourceCache = initAndConfigureResourceCache(config) + populateResourceCache(client, spaceType, "") + populateResourceCache(client, spaceUserRoleType, username) + } + return client, err } -func NewSpaceClient(spaceGuid string, url string, username string, password string) (facade.SpaceClient, error) { - cacheMutex.Lock() - defer cacheMutex.Unlock() +func NewSpaceClient(spaceGuid string, url string, username string, password string, config *config.Config) (facade.SpaceClient, error) { + clientCacheMutex.Lock() + defer clientCacheMutex.Unlock() // look up CF client in cache identifier := clientIdentifier{url: url, username: username} @@ -161,7 +194,7 @@ func NewSpaceClient(spaceGuid string, url string, username string, password stri var client *spaceClient = nil if isInCache { // re-use CF client from cache and wrap it as spaceClient - client = &spaceClient{spaceGuid: spaceGuid, client: cacheEntry.client} + client = &spaceClient{spaceGuid: spaceGuid, client: cacheEntry.client, resourceCache: cfResourceCache} if cacheEntry.password != password { // password was rotated => delete client from cache and create a new one below delete(clientCache, identifier) @@ -178,12 +211,19 @@ func NewSpaceClient(spaceGuid string, url string, username string, password stri } } + if config.IsResourceCacheEnabled && client.resourceCache == nil { + client.resourceCache = initAndConfigureResourceCache(config) + populateResourceCache(client, instanceType, "") + populateResourceCache(client, bindingType, "") + } + return client, err + } -func NewSpaceHealthChecker(spaceGuid string, url string, username string, password string) (facade.SpaceHealthChecker, error) { - cacheMutex.Lock() - defer cacheMutex.Unlock() +func NewSpaceHealthChecker(spaceGuid string, url string, username string, password string, config *config.Config) (facade.SpaceHealthChecker, error) { + clientCacheMutex.Lock() + defer clientCacheMutex.Unlock() // look up CF client in cache identifier := clientIdentifier{url: url, username: username} @@ -193,7 +233,7 @@ func NewSpaceHealthChecker(spaceGuid string, url string, username string, passwo var client *spaceClient = nil if isInCache { // re-use CF client from cache and wrap it as spaceClient - client = &spaceClient{spaceGuid: spaceGuid, client: cacheEntry.client} + client = &spaceClient{spaceGuid: spaceGuid, client: cacheEntry.client, resourceCache: cfResourceCache} if cacheEntry.password != password { // password was rotated => delete client from cache and create a new one below delete(clientCache, identifier) @@ -210,5 +250,292 @@ func NewSpaceHealthChecker(spaceGuid string, url string, username string, passwo } } + if config.IsResourceCacheEnabled && client.resourceCache == nil { + if cfResourceCache != nil { + // It is expected cfResourceCache be already populated + client.resourceCache = cfResourceCache + } + } + return client, err } + +type ResourceServicesClient[T any] interface { + populateServiceInstances(ctx context.Context) error + populateServiceBindings(ctx context.Context) error + manageResourceCache +} + +type ResourceSpaceClient[T any] interface { + populateSpaces(ctx context.Context) error + populateSpaceUserRoleCache(ctx context.Context, username string) error + manageResourceCache +} + +type manageResourceCache interface { + resetCache(resourceType cacheResourceType) +} + +func populateResourceCache[T manageResourceCache](c T, resourceType cacheResourceType, username string) { + ctx := context.Background() + + var err error + + switch resourceType { + case bindingType: + if client, ok := any(c).(ResourceServicesClient[T]); ok { + err = client.populateServiceBindings(ctx) + } + case instanceType: + if client, ok := any(c).(ResourceServicesClient[T]); ok { + err = client.populateServiceInstances(ctx) + } + case spaceType: + if client, ok := any(c).(ResourceSpaceClient[T]); ok { + err = client.populateSpaces(ctx) + } + case spaceUserRoleType: + if client, ok := any(c).(ResourceSpaceClient[T]); ok { + err = client.populateSpaceUserRoleCache(ctx, username) + } + } + + if err != nil { + // reset the cache to nil in case of error + logger := ctrl.LoggerFrom(ctx) + logger.Error(err, "Failed to populate cache", "type", resourceType) + c.resetCache(resourceType) + return + } +} + +func (c *spaceClient) populateServiceBindings(ctx context.Context) error { + refreshServiceBindingResourceCacheMutex.Lock() + defer refreshServiceBindingResourceCacheMutex.Unlock() + + if !c.resourceCache.isCacheExpired(bindingType) { + return nil + } + + logger := ctrl.LoggerFrom(ctx) + logger.V(1).Info("Populating cache for service bindings") + + // retrieve all service bindings with the specified owner + bindingOptions := cfclient.NewServiceCredentialBindingListOptions() + bindingOptions.ListOptions.LabelSelector.EqualTo(labelOwner) + bindingOptions.Page = 1 + bindingOptions.PerPage = 5000 + cfBindings, err := c.client.ServiceCredentialBindings.ListAll(ctx, bindingOptions) + if err != nil { + return err + } + + // wrap each service binding as a facade.Binding and add it to the cache (in parallel) + var waitGroup sync.WaitGroup + for _, cfBinding := range cfBindings { + waitGroup.Add(1) + go func(cfBinding *cfresource.ServiceCredentialBinding) { + defer waitGroup.Done() + if binding, err := c.InitBinding(ctx, cfBinding, nil); err == nil { + c.resourceCache.addBindingInCache(*cfBinding.Metadata.Labels[labelOwner], binding) + } else { + logger.Error(err, "Failed to wrap binding", "binding", cfBinding.GUID) + } + }(cfBinding) + } + waitGroup.Wait() + c.resourceCache.setLastCacheTime(bindingType) + + return nil +} + +func (c *spaceClient) populateServiceInstances(ctx context.Context) error { + refreshServiceInstanceResourceCacheMutex.Lock() + defer refreshServiceInstanceResourceCacheMutex.Unlock() + + if !c.resourceCache.isCacheExpired(instanceType) { + return nil + } + + logger := ctrl.LoggerFrom(ctx) + logger.V(1).Info("Populating cache for service instances") + + // retrieve all service instances with the specified owner + instanceOptions := cfclient.NewServiceInstanceListOptions() + instanceOptions.ListOptions.LabelSelector.EqualTo(labelOwner) + instanceOptions.Page = 1 + instanceOptions.PerPage = 5000 + cfInstances, err := c.client.ServiceInstances.ListAll(ctx, instanceOptions) + if err != nil { + return err + } + + // wrap each service instance as a facade.Instance and add it to the cache (in parallel) + var waitGroup sync.WaitGroup + for _, cfInstance := range cfInstances { + waitGroup.Add(1) + go func(cfInstance *cfresource.ServiceInstance) { + defer waitGroup.Done() + if instance, err := c.InitInstance(cfInstance, nil); err == nil { + c.resourceCache.addInstanceInCache(*cfInstance.Metadata.Labels[labelOwner], instance) + } else { + logger.Error(err, "Failed to wrap instance", "instance", cfInstance.GUID) + } + }(cfInstance) + } + waitGroup.Wait() + c.resourceCache.setLastCacheTime(instanceType) + + return nil +} + +func (c *organizationClient) populateSpaces(ctx context.Context) error { + refreshSpaceResourceCacheMutex.Lock() + defer refreshSpaceResourceCacheMutex.Unlock() + + if !c.resourceCache.isCacheExpired(spaceType) { + return nil + } + + logger := ctrl.LoggerFrom(ctx) + logger.V(1).Info("Populating cache for spaces") + + // retrieve all spaces with the specified owner + // TODO: check for existing spaces as label owner annotation wont be present + spaceOptions := cfclient.NewSpaceListOptions() + spaceOptions.ListOptions.LabelSelector.EqualTo(labelOwner) + spaceOptions.Page = 1 + spaceOptions.PerPage = 5000 + cfSpaces, err := c.client.Spaces.ListAll(ctx, spaceOptions) + if err != nil { + return err + } + + // wrap each space as a facade.Space and add it to the cache (in parallel) + var waitGroup sync.WaitGroup + for _, cfSpace := range cfSpaces { + waitGroup.Add(1) + go func(cfSpace *cfresource.Space) { + defer waitGroup.Done() + if space, err := c.InitSpace(cfSpace, ""); err == nil { + c.resourceCache.addSpaceInCache(*cfSpace.Metadata.Labels[labelOwner], space) + } else { + logger.Error(err, "Failed to wrap space", "space", cfSpace.GUID) + } + }(cfSpace) + } + waitGroup.Wait() + c.resourceCache.setLastCacheTime(spaceType) + + return nil +} + +func (c *organizationClient) populateSpaceUserRoleCache(ctx context.Context, username string) error { + refreshSpaceUserRoleCacheMutex.Lock() + defer refreshSpaceUserRoleCacheMutex.Unlock() + + if !c.resourceCache.isCacheExpired(spaceUserRoleType) { + return nil + } + + logger := ctrl.LoggerFrom(ctx) + logger.V(1).Info("Populating cache for roles") + + // retrieve all spaces with specified label 'owner' + spaceOptions := cfclient.NewSpaceListOptions() + spaceOptions.ListOptions.LabelSelector.EqualTo(labelOwner) + spaceOptions.Page = 1 + spaceOptions.PerPage = 5000 + cfSpaces, err := c.client.Spaces.ListAll(ctx, spaceOptions) + if err != nil { + return err + } + if len(cfSpaces) == 0 { + return fmt.Errorf("no spaces found") + } + + // collect GUIDs of spaces + var spaceGUIDs []string + for _, cfSpace := range cfSpaces { + spaceGUIDs = append(spaceGUIDs, cfSpace.GUID) + } + + // retrieve user with specified name (to get user GUID) + userOptions := cfclient.NewUserListOptions() + userOptions.UserNames.EqualTo(username) + userOptions.Page = 1 + userOptions.PerPage = 5000 + users, err := c.client.Users.ListAll(ctx, userOptions) + if err != nil { + return err + } + if len(users) == 0 { + return fmt.Errorf("no user found with name '%s'", username) + } else if len(users) > 1 { + return fmt.Errorf("multiple users found with name '%s'", username) + } + user := users[0] + + // prepare common filter options for retrieving SpaceDeveloper role for above user + roleListOpts := cfclient.NewRoleListOptions() + roleListOpts.Types.EqualTo(cfresource.SpaceRoleDeveloper.String()) + roleListOpts.UserGUIDs.EqualTo(user.GUID) + roleListOpts.Page = 1 + roleListOpts.PerPage = 5000 + + // collect SpaceDeveloper role for above user in chunks (each N spaces) + // otherwise, the request will fail with 414 Request-URI Too Long + const chunkSize = 30 // 30 space GUIDs each 36 characters plus one comma each => 1110 characters + + var spaceGUID string + var collectedCfRoles []*cfresource.Role + for len(spaceGUIDs) > 0 { + var spaceFilter = "" + for i := 0; i < chunkSize && len(spaceGUIDs) > 0; i++ { + if i > 0 { + spaceFilter += "," + } + // pop first item from list + spaceGUID, spaceGUIDs = spaceGUIDs[0], spaceGUIDs[1:] + spaceFilter += spaceGUID + } + roleListOpts.SpaceGUIDs.EqualTo(spaceFilter) + roles, err := c.client.Roles.ListAll(ctx, roleListOpts) + if err != nil { + return err + } + collectedCfRoles = append(collectedCfRoles, roles...) + } + + if len(collectedCfRoles) == 0 { + return fmt.Errorf("no SpaceDeveloper role found for user '%s'", username) + } + + // add each role to the cache (in parallel) + var waitGroup sync.WaitGroup + for _, cfRole := range collectedCfRoles { + waitGroup.Add(1) + go func(cfrole *cfresource.Role) { + defer waitGroup.Done() + c.resourceCache.addSpaceUserRoleInCache( + cfrole.Relationships.Space.Data.GUID, + cfrole.Relationships.User.Data.GUID, + username, + cfrole.Type) + }(cfRole) + } + waitGroup.Wait() + c.resourceCache.setLastCacheTime(spaceUserRoleType) + + return nil +} + +// Implementation for resetting the cache +func (c *spaceClient) resetCache(resourceType cacheResourceType) { + c.resourceCache.resetCache(resourceType) +} + +// Implementation for resetting the cache +func (c *organizationClient) resetCache(resourceType cacheResourceType) { + c.resourceCache.resetCache(resourceType) +} diff --git a/internal/cf/client_test.go b/internal/cf/client_test.go index eafc2aa..a13c998 100644 --- a/internal/cf/client_test.go +++ b/internal/cf/client_test.go @@ -6,6 +6,7 @@ package cf import ( "context" + "net/http" "testing" "time" @@ -14,6 +15,7 @@ import ( . "github.com/onsi/gomega" "github.com/onsi/gomega/ghttp" "github.com/prometheus/client_golang/prometheus" + "github.com/sap/cf-service-operator/internal/config" "sigs.k8s.io/controller-runtime/pkg/metrics" ) @@ -34,7 +36,12 @@ const ( spacesURI = "/v3/spaces" serviceInstancesURI = "/v3/service_instances" + spaceURI = "/v3/spaces" + serviceBindingURI = "/v3/service_credential_bindings" + userURI = "v3/users" + roleURI = "v3/roles" uaaURI = "/uaa/oauth/token" + labelSelector = "service-operator.cf.cs.sap.com" ) type Token struct { @@ -49,6 +56,125 @@ func TestCFClient(t *testing.T) { RunSpecs(t, "CF Client Test Suite") } +func String(s string) *string { + return &s +} + +// configuration for CF client +var clientConfig = &config.Config{ + IsResourceCacheEnabled: false, + CacheTimeOut: "5m", +} + +// fake response for service instances +var fakeServiceInstances = cfResource.ServiceInstanceList{ + Resources: []*cfResource.ServiceInstance{ + { + GUID: "test-instance-guid-1", + Name: "test-instance-name-1", + Tags: []string{}, + LastOperation: cfResource.LastOperation{ + Type: "create", + State: "succeeded", + Description: "", + }, + Relationships: cfResource.ServiceInstanceRelationships{ + ServicePlan: &cfResource.ToOneRelationship{ + Data: &cfResource.Relationship{ + GUID: "test-instance-service_plan-1", + }, + }, + }, + Metadata: &cfResource.Metadata{ + Labels: map[string]*string{ + "service-operator.cf.cs.sap.com/owner": String("testOwner"), + }, + Annotations: map[string]*string{ + "service-operator.cf.cs.sap.com/generation": String("1"), + "service-operator.cf.cs.sap.com/parameter-hash": String("74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b"), + }, + }, + }, + }, +} + +var fakeServiceBindings = cfResource.ServiceCredentialBindingList{ + Resources: []*cfResource.ServiceCredentialBinding{ + { + GUID: "test-binding-guid-1", + Name: "test-binding-name-1", + LastOperation: cfResource.LastOperation{ + Type: "create", + State: "succeeded", + Description: "", + }, + Metadata: &cfResource.Metadata{ + Labels: map[string]*string{ + "service-operator.cf.cs.sap.com/owner": String("testOwner"), + }, + Annotations: map[string]*string{ + "service-operator.cf.cs.sap.com/generation": String("1"), + "service-operator.cf.cs.sap.com/parameter-hash": String("74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b"), + }, + }, + }, + }, +} + +var fakeBingdingDetails = cfResource.ServiceCredentialBindingDetails{ + Credentials: map[string]interface{}{ + "key": "value", + }, +} + +var fakeSpaces = cfResource.SpaceList{ + Resources: []*cfResource.Space{ + { + GUID: "test-space-guid-1", + Name: "test-space-name-1", + Metadata: &cfResource.Metadata{ + Labels: map[string]*string{ + "service-operator.cf.cs.sap.com/owner": String("testOwner"), + }, + Annotations: map[string]*string{ + "service-operator.cf.cs.sap.com/generation": String("1"), + "service-operator.cf.cs.sap.com/parameter-hash": String("74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b"), + }, + }, + }, + }, +} + +var fakeUsers = cfResource.UserList{ + Resources: []*cfResource.User{ + { + GUID: "test-user-guid-1", + Username: "testUser", + }, + }, +} + +var fakeRoles = cfResource.RoleList{ + Resources: []*cfResource.Role{ + { + GUID: "test-role-guid-1", + Type: "test-role-type-1", + Relationships: cfResource.RoleSpaceUserOrganizationRelationships{ + User: cfResource.ToOneRelationship{ + Data: &cfResource.Relationship{ + GUID: "test-user-guid-1", + }, + }, + Space: cfResource.ToOneRelationship{ + Data: &cfResource.Relationship{ + GUID: "test-space-guid-1", + }, + }, + }, + }, + }, +} + // ----------------------------------------------------------------------------------------------- // Tests // ----------------------------------------------------------------------------------------------- @@ -97,20 +223,42 @@ var _ = Describe("CF Client tests", Ordered, func() { metrics.Registry = prometheus.NewRegistry() server.Reset() + // Create a new configuration for each test + clientConfig = &config.Config{ + IsResourceCacheEnabled: false, + CacheTimeOut: "5m", + } + // Register handlers + // - Fake response for discover UAA endpoint server.RouteToHandler("GET", "/", ghttp.CombineHandlers( ghttp.RespondWithJSONEncodedPtr(&statusCode, &rootResult), )) + // - Fake response for new oAuth token + server.RouteToHandler("POST", uaaURI, ghttp.CombineHandlers( + ghttp.RespondWithJSONEncodedPtr(&statusCode, &tokenResult), + )) + // - Fake response for get service instance server.RouteToHandler("GET", spacesURI, ghttp.CombineHandlers( ghttp.RespondWithJSONEncodedPtr(&statusCode, &rootResult), )) - server.RouteToHandler("POST", uaaURI, ghttp.CombineHandlers( - ghttp.RespondWithJSONEncodedPtr(&statusCode, &tokenResult), + // - Fake response for get service instance + server.RouteToHandler("GET", spaceURI, ghttp.CombineHandlers( + ghttp.RespondWithJSONEncodedPtr(&statusCode, &fakeSpaces), + )) + // - Fake response for get users + server.RouteToHandler("GET", "/v3/users", ghttp.CombineHandlers( + ghttp.RespondWithJSONEncodedPtr(&statusCode, &fakeUsers), )) + // - Fake response for get roles + server.RouteToHandler("GET", "/v3/roles", ghttp.CombineHandlers( + ghttp.RespondWithJSONEncodedPtr(&statusCode, &fakeRoles), + )) + }) It("should create OrgClient", func() { - NewOrganizationClient(OrgName, url, Username, Password) + NewOrganizationClient(OrgName, url, Username, Password, clientConfig) // Discover UAA endpoint Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) @@ -120,7 +268,7 @@ var _ = Describe("CF Client tests", Ordered, func() { }) It("should be able to query some org", func() { - orgClient, err := NewOrganizationClient(OrgName, url, Username, Password) + orgClient, err := NewOrganizationClient(OrgName, url, Username, Password, clientConfig) Expect(err).To(BeNil()) orgClient.GetSpace(ctx, Owner) @@ -152,11 +300,11 @@ var _ = Describe("CF Client tests", Ordered, func() { }) It("should be able to query some org twice", func() { - orgClient, err := NewOrganizationClient(OrgName, url, Username, Password) + orgClient, err := NewOrganizationClient(OrgName, url, Username, Password, clientConfig) Expect(err).To(BeNil()) orgClient.GetSpace(ctx, Owner) - orgClient, err = NewOrganizationClient(OrgName, url, Username, Password) + orgClient, err = NewOrganizationClient(OrgName, url, Username, Password, clientConfig) Expect(err).To(BeNil()) orgClient.GetSpace(ctx, Owner) @@ -179,7 +327,7 @@ var _ = Describe("CF Client tests", Ordered, func() { It("should be able to query two different orgs", func() { // test org 1 - orgClient1, err1 := NewOrganizationClient(OrgName, url, Username, Password) + orgClient1, err1 := NewOrganizationClient(OrgName, url, Username, Password, clientConfig) Expect(err1).To(BeNil()) orgClient1.GetSpace(ctx, Owner) // Discover UAA endpoint @@ -193,7 +341,7 @@ var _ = Describe("CF Client tests", Ordered, func() { Expect(server.ReceivedRequests()[2].RequestURI).To(ContainSubstring(Owner)) // test org 2 - orgClient2, err2 := NewOrganizationClient(OrgName2, url, Username, Password) + orgClient2, err2 := NewOrganizationClient(OrgName2, url, Username, Password, clientConfig) Expect(err2).To(BeNil()) orgClient2.GetSpace(ctx, Owner2) // no discovery of UAA endpoint or oAuth token here due to caching @@ -202,6 +350,94 @@ var _ = Describe("CF Client tests", Ordered, func() { Expect(server.ReceivedRequests()[3].RequestURI).To(ContainSubstring(Owner2)) }) + It("should not initialize resource cache if disabled in config", func() { + // Disable resource cache in config + clientConfig.IsResourceCacheEnabled = false + + // Create client + orgClient, err := NewOrganizationClient(OrgName, url, Username, Password, clientConfig) + Expect(err).To(BeNil()) + Expect(orgClient).ToNot(BeNil()) + + // Verify resource cache is NOT populated during client creation + // - Discover UAA endpoint + Expect(server.ReceivedRequests()).To(HaveLen(1)) + Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/")) + + // Make request and verify cache is NOT used + orgClient.GetSpace(ctx, Owner) + Expect(server.ReceivedRequests()).To(HaveLen(3)) // one more request to get instance + // - Get new oAuth token + Expect(server.ReceivedRequests()[1].Method).To(Equal("POST")) + Expect(server.ReceivedRequests()[1].URL.Path).To(Equal(uaaURI)) + // - Get space + Expect(server.ReceivedRequests()[2].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[2].RequestURI).To(ContainSubstring(Owner)) + }) + + It("should initialize/manage resource cache after start and on cache expiry", func() { + // Enable resource cache in config + clientConfig.IsResourceCacheEnabled = true + clientConfig.CacheTimeOut = "5s" // short duration for fast test + + // // Route to handler for DELETE request + server.RouteToHandler("DELETE", + spaceURI+"/test-space-guid-1", + ghttp.CombineHandlers(ghttp.RespondWith(http.StatusAccepted, nil))) + + // Create client + orgClient, err := NewOrganizationClient(OrgName, url, Username, Password, clientConfig) + Expect(err).To(BeNil()) + Expect(orgClient).ToNot(BeNil()) + + // Verify resource cache is populated during client creation + Expect(server.ReceivedRequests()).To(HaveLen(6)) + // - Discover UAA endpoint + Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/")) + // - Get new oAuth token + Expect(server.ReceivedRequests()[1].Method).To(Equal("POST")) + Expect(server.ReceivedRequests()[1].URL.Path).To(Equal(uaaURI)) + // - Populate cache with spaces + Expect(server.ReceivedRequests()[2].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[2].RequestURI).To(ContainSubstring(spaceURI)) + //- Populate cache with spaces,user and role + Expect(server.ReceivedRequests()[3].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[3].RequestURI).To(ContainSubstring(spaceURI)) + Expect(server.ReceivedRequests()[4].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[4].RequestURI).To(ContainSubstring(userURI)) + Expect(server.ReceivedRequests()[5].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[5].RequestURI).To(ContainSubstring(roleURI)) + + // Make a request and verify that cache is used and no additional requests expected + orgClient.GetSpace(ctx, Owner) + Expect(server.ReceivedRequests()).To(HaveLen(6)) // still same as above + + // Make another request after cache expired and verify that cache is repopulated + time.Sleep(10 * time.Second) + orgClient.GetSpace(ctx, Owner) + Expect(server.ReceivedRequests()).To(HaveLen(7)) // one more request to repopulate cache + Expect(server.ReceivedRequests()[6].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[6].RequestURI).To(ContainSubstring(spaceURI)) + Expect(server.ReceivedRequests()[6].RequestURI).NotTo(ContainSubstring(Owner)) + + // Delete space from cache + err = orgClient.DeleteSpace(ctx, "test-space-guid-1", Owner) + Expect(err).To(BeNil()) + Expect(server.ReceivedRequests()).To(HaveLen(8)) + // - Delete space from cache + Expect(server.ReceivedRequests()[7].Method).To(Equal("DELETE")) + Expect(server.ReceivedRequests()[7].RequestURI).To(ContainSubstring("test-space-guid-1")) + + // Get space from cache should return empty + orgClient.GetSpace(ctx, Owner) + Expect(server.ReceivedRequests()).To(HaveLen(9)) + // - Get call to cf to get the space + Expect(server.ReceivedRequests()[8].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[8].RequestURI).To(ContainSubstring(Owner)) + }) + }) Describe("NewSpaceClient", func() { @@ -211,20 +447,39 @@ var _ = Describe("CF Client tests", Ordered, func() { metrics.Registry = prometheus.NewRegistry() server.Reset() + // Create a new configuration for each test + clientConfig = &config.Config{ + IsResourceCacheEnabled: false, + CacheTimeOut: "5m", + } + // Register handlers + // - Fake response for discover UAA endpoint server.RouteToHandler("GET", "/", ghttp.CombineHandlers( ghttp.RespondWithJSONEncodedPtr(&statusCode, &rootResult), )) - server.RouteToHandler("GET", serviceInstancesURI, ghttp.CombineHandlers( - ghttp.RespondWithJSONEncodedPtr(&statusCode, &rootResult), - )) + // - Fake response for new oAuth token server.RouteToHandler("POST", uaaURI, ghttp.CombineHandlers( ghttp.RespondWithJSONEncodedPtr(&statusCode, &tokenResult), )) + // - Fake response for get service instance + server.RouteToHandler("GET", serviceInstancesURI, ghttp.CombineHandlers( + ghttp.RespondWithJSONEncodedPtr(&statusCode, &fakeServiceInstances), + )) + + // - Fake response for get service binding + server.RouteToHandler("GET", serviceBindingURI, ghttp.CombineHandlers( + ghttp.RespondWithJSONEncodedPtr(&statusCode, &fakeServiceBindings), + )) + + // - Fake response for get service binding + server.RouteToHandler("GET", serviceBindingURI+"/"+fakeServiceBindings.Resources[0].GUID+"/details", ghttp.CombineHandlers( + ghttp.RespondWithJSONEncodedPtr(&statusCode, &fakeBingdingDetails), + )) }) It("should create SpaceClient", func() { - NewSpaceClient(OrgName, url, Username, Password) + NewSpaceClient(OrgName, url, Username, Password, clientConfig) // Discover UAA endpoint Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) @@ -234,7 +489,7 @@ var _ = Describe("CF Client tests", Ordered, func() { }) It("should be able to query some space", func() { - spaceClient, err := NewSpaceClient(OrgName, url, Username, Password) + spaceClient, err := NewSpaceClient(OrgName, url, Username, Password, clientConfig) Expect(err).To(BeNil()) spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) @@ -266,11 +521,11 @@ var _ = Describe("CF Client tests", Ordered, func() { }) It("should be able to query some space twice", func() { - spaceClient, err := NewSpaceClient(OrgName, url, Username, Password) + spaceClient, err := NewSpaceClient(OrgName, url, Username, Password, clientConfig) Expect(err).To(BeNil()) spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) - spaceClient, err = NewSpaceClient(OrgName, url, Username, Password) + spaceClient, err = NewSpaceClient(OrgName, url, Username, Password, clientConfig) Expect(err).To(BeNil()) spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) @@ -293,7 +548,7 @@ var _ = Describe("CF Client tests", Ordered, func() { It("should be able to query two different spaces", func() { // test space 1 - spaceClient1, err1 := NewSpaceClient(SpaceName, url, Username, Password) + spaceClient1, err1 := NewSpaceClient(SpaceName, url, Username, Password, clientConfig) Expect(err1).To(BeNil()) spaceClient1.GetInstance(ctx, map[string]string{"owner": Owner}) // Discover UAA endpoint @@ -307,7 +562,7 @@ var _ = Describe("CF Client tests", Ordered, func() { Expect(server.ReceivedRequests()[2].RequestURI).To(ContainSubstring(Owner)) // test space 2 - spaceClient2, err2 := NewSpaceClient(SpaceName2, url, Username, Password) + spaceClient2, err2 := NewSpaceClient(SpaceName2, url, Username, Password, clientConfig) Expect(err2).To(BeNil()) spaceClient2.GetInstance(ctx, map[string]string{"owner": Owner2}) // no discovery of UAA endpoint or oAuth token here due to caching @@ -317,7 +572,7 @@ var _ = Describe("CF Client tests", Ordered, func() { }) It("should register prometheus metrics for OrgClient", func() { - orgClient, err := NewOrganizationClient(OrgName, url, Username, Password) + orgClient, err := NewOrganizationClient(OrgName, url, Username, Password, clientConfig) Expect(err).To(BeNil()) Expect(orgClient).ToNot(BeNil()) @@ -336,7 +591,7 @@ var _ = Describe("CF Client tests", Ordered, func() { }) It("should register prometheus metrics for SpaceClient", func() { - spaceClient, err := NewSpaceClient(SpaceName, url, Username, Password) + spaceClient, err := NewSpaceClient(SpaceName, url, Username, Password, clientConfig) Expect(err).To(BeNil()) Expect(spaceClient).ToNot(BeNil()) @@ -357,5 +612,129 @@ var _ = Describe("CF Client tests", Ordered, func() { // prometheus.WriteToTextfile("metrics.txt", metrics.Registry) }) + It("should not initialize resource cache if disabled in config", func() { + // Disable resource cache in config + clientConfig.IsResourceCacheEnabled = false + + // Create client + spaceClient, err := NewSpaceClient(SpaceName, url, Username, Password, clientConfig) + Expect(err).To(BeNil()) + Expect(spaceClient).ToNot(BeNil()) + + // Verify resource cache is NOT populated during client creation + // - Discover UAA endpoint + Expect(server.ReceivedRequests()).To(HaveLen(1)) + Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/")) + + // Make request and verify cache is NOT used + spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) + Expect(server.ReceivedRequests()).To(HaveLen(3)) // one more request to get instance + // - Get new oAuth token + Expect(server.ReceivedRequests()[1].Method).To(Equal("POST")) + Expect(server.ReceivedRequests()[1].URL.Path).To(Equal(uaaURI)) + // - Get instance + Expect(server.ReceivedRequests()[2].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[2].RequestURI).To(ContainSubstring(Owner)) + }) + + It("should initialize resource cache after start and on cache expiry", func() { + // Enable resource cache in config + clientConfig.IsResourceCacheEnabled = true + clientConfig.CacheTimeOut = "5s" // short duration for fast test + + // resource cache is initialized only once, so we need to wait till cache expiry from previous test + time.Sleep(10 * time.Second) + + // Route to handler for DELETE request + server.RouteToHandler("DELETE", + serviceInstancesURI+"/test-instance-guid-1", + ghttp.CombineHandlers(ghttp.RespondWith(http.StatusAccepted, nil))) + + // Route to handler for DELETE request + server.RouteToHandler("DELETE", + serviceBindingURI+"/test-binding-guid-1", + ghttp.CombineHandlers(ghttp.RespondWith(http.StatusAccepted, nil))) + + // Create client + spaceClient, err := NewSpaceClient(SpaceName, url, Username, Password, clientConfig) + Expect(err).To(BeNil()) + Expect(spaceClient).ToNot(BeNil()) + + // Verify resource cache is populated during client creation + numRequests := 4 + Expect(server.ReceivedRequests()).To(HaveLen(numRequests)) + // - Discover UAA endpoint + Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/")) + // - Get new oAuth token + Expect(server.ReceivedRequests()[1].Method).To(Equal("POST")) + Expect(server.ReceivedRequests()[1].URL.Path).To(Equal(uaaURI)) + // - Populate cache with instances + Expect(server.ReceivedRequests()[2].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[2].RequestURI).To(ContainSubstring(serviceInstancesURI)) + Expect(server.ReceivedRequests()[2].RequestURI).NotTo(ContainSubstring(Owner)) + // - Populate cache with bindings + Expect(server.ReceivedRequests()[3].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[3].RequestURI).To(ContainSubstring(serviceBindingURI)) + Expect(server.ReceivedRequests()[3].RequestURI).NotTo(ContainSubstring(Owner)) + + // Make a request and verify that cache is used and no additional requests expected + spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) + Expect(server.ReceivedRequests()).To(HaveLen(numRequests)) // still same as above + + // Make another request after cache expired and verify that cache is repopulated + numRequests += 2 // one more request for the repopulatation of the two caches + time.Sleep(10 * time.Second) + spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) + spaceClient.GetBinding(ctx, map[string]string{"owner": Owner}) + Expect(server.ReceivedRequests()).To(HaveLen(numRequests)) + // - Populate cache with instances + Expect(server.ReceivedRequests()[4].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[4].RequestURI).To(ContainSubstring(serviceInstancesURI)) + Expect(server.ReceivedRequests()[4].RequestURI).NotTo(ContainSubstring(Owner)) + // - Populate cache with bindings + Expect(server.ReceivedRequests()[5].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[5].RequestURI).To(ContainSubstring(serviceBindingURI)) + Expect(server.ReceivedRequests()[5].RequestURI).NotTo(ContainSubstring(Owner)) + + // Delete instance from cache + numRequests += 1 + err = spaceClient.DeleteInstance(ctx, "test-instance-guid-1", Owner) + Expect(err).To(BeNil()) + Expect(server.ReceivedRequests()).To(HaveLen(numRequests)) + // - Delete instance from cache + Expect(server.ReceivedRequests()[6].Method).To(Equal("DELETE")) + Expect(server.ReceivedRequests()[6].RequestURI).To(ContainSubstring("test-instance-guid-1")) + + // Get instance from cache should return empty + numRequests += 1 + spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) + Expect(server.ReceivedRequests()).To(HaveLen(numRequests)) + // - Get call to cf to get the instance + Expect(server.ReceivedRequests()[7].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[7].RequestURI).To(ContainSubstring(serviceInstancesURI)) + Expect(server.ReceivedRequests()[7].RequestURI).To(ContainSubstring(Owner)) + + // Delete binding from cache + numRequests += 1 + err = spaceClient.DeleteBinding(ctx, "test-binding-guid-1", Owner) + Expect(err).To(BeNil()) + Expect(server.ReceivedRequests()).To(HaveLen(numRequests)) + // - Delete binding from cache + Expect(server.ReceivedRequests()[8].Method).To(Equal("DELETE")) + Expect(server.ReceivedRequests()[8].RequestURI).To(ContainSubstring(serviceBindingURI)) + Expect(server.ReceivedRequests()[8].RequestURI).To(ContainSubstring("test-binding-guid-1")) + + // Get binding from cache should return empty + numRequests += 1 + spaceClient.GetBinding(ctx, map[string]string{"owner": Owner}) + Expect(server.ReceivedRequests()).To(HaveLen(numRequests)) + // - Get call to cf to get the binding + Expect(server.ReceivedRequests()[9].Method).To(Equal("GET")) + Expect(server.ReceivedRequests()[9].RequestURI).To(ContainSubstring(serviceBindingURI)) + Expect(server.ReceivedRequests()[9].RequestURI).To(ContainSubstring(Owner)) + }) + }) }) diff --git a/internal/cf/health.go b/internal/cf/health.go index a99f7dc..46789c8 100644 --- a/internal/cf/health.go +++ b/internal/cf/health.go @@ -7,7 +7,14 @@ package cf import "context" -func (c *spaceClient) Check(ctx context.Context) error { +// TODO: Ask why do we have the health check with a different client than the origanization unit? +func (c *spaceClient) Check(ctx context.Context, owner string) error { + if c.resourceCache.checkResourceCacheEnabled() { + _, inCache := c.resourceCache.getSpaceFromCache(owner) + if inCache { + return nil + } + } _, err := c.client.Spaces.Get(ctx, c.spaceGuid) if err != nil { return err diff --git a/internal/cf/instance.go b/internal/cf/instance.go index 5da95ff..f83496a 100644 --- a/internal/cf/instance.go +++ b/internal/cf/instance.go @@ -44,82 +44,47 @@ func (io *instanceFilterOwner) getListOptions() *cfclient.ServiceInstanceListOpt // GetInstance returns the instance with the given instanceOpts["owner"] or instanceOpts["name"]. // If instanceOpts["name"] is empty, the instance with the given instanceOpts["owner"] is returned. // If instanceOpts["name"] is not empty, the instance with the given Name is returned for orphan instances. -// If no instance is found, nil is returned. -// If multiple instances are found, an error is returned. -// The function add the parameter values to the orphan cf instance, so that can be adopted. +// If resource cache is enabled, the instance is first searched in the cache. +// If the instance is not found in the cache, it is searched in Cloud Foundry. func (c *spaceClient) GetInstance(ctx context.Context, instanceOpts map[string]string) (*facade.Instance, error) { + if c.resourceCache.checkResourceCacheEnabled() { + // attempt to retrieve instance from cache + if c.resourceCache.isCacheExpired(instanceType) { + populateResourceCache(c, instanceType, "") + } + instance, inCache := c.resourceCache.getInstanceFromCache(instanceOpts["owner"]) + if inCache { + return instance, nil + } + } + // attempt to retrieve instance from Cloud Foundry var filterOpts instanceFilter if instanceOpts["name"] != "" { filterOpts = &instanceFilterName{name: instanceOpts["name"]} } else { filterOpts = &instanceFilterOwner{owner: instanceOpts["owner"]} } - listOpts := filterOpts.getListOptions() - serviceInstances, err := c.client.ServiceInstances.ListAll(ctx, listOpts) + serviceInstances, err := c.client.ServiceInstances.ListAll(ctx, filterOpts.getListOptions()) if err != nil { return nil, fmt.Errorf("failed to list service instances: %w", err) } - if len(serviceInstances) == 0 { - return nil, nil + return nil, nil // instance not found } else if len(serviceInstances) > 1 { return nil, errors.New(fmt.Sprintf("found multiple service instances with owner: %s", instanceOpts["owner"])) } - serviceInstance := serviceInstances[0] - // add parameter values to the orphan cf instance + // add parameter values to the orphaned instance in Cloud Foundry if instanceOpts["name"] != "" { generationvalue := "0" - serviceInstance.Metadata.Annotations[annotationGeneration] = &generationvalue parameterHashValue := "0" + serviceInstance.Metadata.Annotations[annotationGeneration] = &generationvalue serviceInstance.Metadata.Annotations[annotationParameterHash] = ¶meterHashValue } - guid := serviceInstance.GUID - name := serviceInstance.Name - servicePlanGuid := serviceInstance.Relationships.ServicePlan.Data.GUID - generation, err := strconv.ParseInt(*serviceInstance.Metadata.Annotations[annotationGeneration], 10, 64) - if err != nil { - return nil, errors.Wrap(err, "error parsing service instance generation") - } - parameterHash := *serviceInstance.Metadata.Annotations[annotationParameterHash] - var state facade.InstanceState - switch serviceInstance.LastOperation.Type + ":" + serviceInstance.LastOperation.State { - case "create:in progress": - state = facade.InstanceStateCreating - case "create:succeeded": - state = facade.InstanceStateReady - case "create:failed": - state = facade.InstanceStateCreatedFailed - case "update:in progress": - state = facade.InstanceStateUpdating - case "update:succeeded": - state = facade.InstanceStateReady - case "update:failed": - state = facade.InstanceStateUpdateFailed - case "delete:in progress": - state = facade.InstanceStateDeleting - case "delete:succeeded": - state = facade.InstanceStateDeleted - case "delete:failed": - state = facade.InstanceStateDeleteFailed - default: - state = facade.InstanceStateUnknown - } - stateDescription := serviceInstance.LastOperation.Description - - return &facade.Instance{ - Guid: guid, - Name: name, - ServicePlanGuid: servicePlanGuid, - Owner: instanceOpts["owner"], - Generation: generation, - ParameterHash: parameterHash, - State: state, - StateDescription: stateDescription, - }, nil + return c.InitInstance(serviceInstance, instanceOpts) } // Required parameters (may not be initial): name, servicePlanGuid, owner, generation @@ -149,7 +114,7 @@ func (c *spaceClient) CreateInstance(ctx context.Context, name string, servicePl // Required parameters (may not be initial): guid, generation // Optional parameters (may be initial): name, servicePlanGuid, parameters, tags -func (c *spaceClient) UpdateInstance(ctx context.Context, guid string, name string, servicePlanGuid string, parameters map[string]interface{}, tags []string, generation int64) error { +func (c *spaceClient) UpdateInstance(ctx context.Context, guid string, name string, owner string, servicePlanGuid string, parameters map[string]interface{}, tags []string, generation int64) error { req := cfresource.NewServiceInstanceManagedUpdate() if name != "" { req.WithName(name) @@ -178,11 +143,85 @@ func (c *spaceClient) UpdateInstance(ctx context.Context, guid string, name stri } _, _, err := c.client.ServiceInstances.UpdateManaged(ctx, guid, req) + + // update instance in cache + if err == nil && c.resourceCache.checkResourceCacheEnabled() { + isUpdated := c.resourceCache.updateInstanceInCache(name, owner, servicePlanGuid, parameters, generation) + if !isUpdated { + // add instance to cache in case of orphan instance + instance := &facade.Instance{ + Guid: guid, + Name: name, + ServicePlanGuid: servicePlanGuid, + Owner: owner, + Generation: generation, + ParameterHash: facade.ObjectHash(parameters), + State: facade.InstanceStateReady, + StateDescription: "", + } + c.resourceCache.addInstanceInCache(owner, instance) + } + } + return err } -func (c *spaceClient) DeleteInstance(ctx context.Context, guid string) error { +func (c *spaceClient) DeleteInstance(ctx context.Context, guid string, owner string) error { // TODO: return jobGUID to enable querying the job deletion status _, err := c.client.ServiceInstances.Delete(ctx, guid) + + // delete instance from cache + if err == nil && c.resourceCache.checkResourceCacheEnabled() { + c.resourceCache.deleteInstanceFromCache(owner) + } + return err } + +// InitInstance wraps cfclient.ServiceInstance as a facade.Instance. +func (c *spaceClient) InitInstance(serviceInstance *cfresource.ServiceInstance, instanceOpts map[string]string) (*facade.Instance, error) { + generation, err := strconv.ParseInt(*serviceInstance.Metadata.Annotations[annotationGeneration], 10, 64) + if err != nil { + return nil, errors.Wrap(err, "error parsing service instance generation") + } + + owner := instanceOpts["owner"] + if owner == "" { + owner = *serviceInstance.Metadata.Labels[labelOwner] + } + + var state facade.InstanceState + switch serviceInstance.LastOperation.Type + ":" + serviceInstance.LastOperation.State { + case "create:in progress": + state = facade.InstanceStateCreating + case "create:succeeded": + state = facade.InstanceStateReady + case "create:failed": + state = facade.InstanceStateCreatedFailed + case "update:in progress": + state = facade.InstanceStateUpdating + case "update:succeeded": + state = facade.InstanceStateReady + case "update:failed": + state = facade.InstanceStateUpdateFailed + case "delete:in progress": + state = facade.InstanceStateDeleting + case "delete:succeeded": + state = facade.InstanceStateDeleted + case "delete:failed": + state = facade.InstanceStateDeleteFailed + default: + state = facade.InstanceStateUnknown + } + + return &facade.Instance{ + Guid: serviceInstance.GUID, + Name: serviceInstance.Name, + ServicePlanGuid: serviceInstance.Relationships.ServicePlan.Data.GUID, + Owner: owner, + Generation: generation, + ParameterHash: *serviceInstance.Metadata.Annotations[annotationParameterHash], + State: state, + StateDescription: serviceInstance.LastOperation.Description, + }, nil +} diff --git a/internal/cf/resourcecache.go b/internal/cf/resourcecache.go new file mode 100644 index 0000000..b77ffff --- /dev/null +++ b/internal/cf/resourcecache.go @@ -0,0 +1,335 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cf-service-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cf + +import ( + "log" + "sync" + "time" + + "github.com/sap/cf-service-operator/internal/facade" +) + +// The resource cache is a simple in-memory cache to store CF resources like spaces, instances and +// bindings using a map protected by a mutex. +// The resource cache is used to avoid making multiple calls to the CF API and avoid rate limits. +type resourceCache struct { + instanceMutex sync.RWMutex + bindingMutex sync.RWMutex + spaceMutex sync.RWMutex + spaceUserRoleMutex sync.RWMutex + + // cache for each resource type + // (owner of the corresponding custom resource (i.e. Kubernetes UID) is used as key) + bindings map[string]*facade.Binding + instances map[string]*facade.Instance + spaces map[string]*facade.Space + spaceUserRoles map[string]*spaceUserRole + + // last cache time for each resource type + bindingLastCacheTime time.Time + instanceLastCacheTime time.Time + spaceLastCacheTime time.Time + spaceUserRoleLastCacheTime time.Time + + // configuration + cacheTimeOut time.Duration + isResourceCacheEnabled bool +} + +type spaceUserRole struct { + user string + spaceGuid string + userGUID string + roleType string +} + +type cacheResourceType string + +const ( + bindingType cacheResourceType = "bindingType" + instanceType cacheResourceType = "instanceType" + spaceType cacheResourceType = "spaceType" + spaceUserRoleType cacheResourceType = "spaceUserRoleType" +) + +// InitResourcesCache initializes a new cache +func initResourceCache() *resourceCache { + cache := &resourceCache{ + bindings: make(map[string]*facade.Binding), + instances: make(map[string]*facade.Instance), + spaces: make(map[string]*facade.Space), + spaceUserRoles: make(map[string]*spaceUserRole), + } + + return cache +} + +// setResourceCacheEnabled enables or disables the resource cahce +func (c *resourceCache) setResourceCacheEnabled(enabled bool) { + c.isResourceCacheEnabled = enabled +} + +// checkResourceCacheEnabled checks if the resource cache is enabled (object might be nil) +func (c *resourceCache) checkResourceCacheEnabled() bool { + if c == nil { + log.Println("Resource cache is nil") + return false + } + return c.isResourceCacheEnabled +} + +// setCacheTimeOut sets the timeout used for expiration of the cache +func (c *resourceCache) setCacheTimeOut(timeOut string) { + cacheTimeOut, err := time.ParseDuration(timeOut) + if err != nil { + log.Printf("Error parsing duration: %v\n", err) + c.cacheTimeOut = 1 * time.Minute + return + } + c.cacheTimeOut = cacheTimeOut +} + +// setLastCacheTime sets the last cache time for a specific resource type +func (c *resourceCache) setLastCacheTime(resourceType cacheResourceType) { + now := time.Now() + switch resourceType { + case bindingType: + c.bindingLastCacheTime = now + case instanceType: + c.instanceLastCacheTime = now + case spaceType: + c.spaceLastCacheTime = now + case spaceUserRoleType: + c.spaceUserRoleLastCacheTime = now + } +} + +// isCacheExpired checks if the cache is expired for a specific resource type +func (c *resourceCache) isCacheExpired(resourceType cacheResourceType) bool { + var lastCacheTime time.Time + switch resourceType { + case bindingType: + lastCacheTime = c.bindingLastCacheTime + case instanceType: + lastCacheTime = c.instanceLastCacheTime + case spaceType: + lastCacheTime = c.spaceLastCacheTime + case spaceUserRoleType: + lastCacheTime = c.spaceUserRoleLastCacheTime + } + + // Ensure lastCacheTime is properly initialized + if lastCacheTime.IsZero() { + return true + } + + expirationTime := lastCacheTime.Add(c.cacheTimeOut) + isExpired := time.Now().After(expirationTime) + + return isExpired +} + +// reset cache of a specific resource type and last cache time +func (c *resourceCache) resetCache(resourceType cacheResourceType) { + switch resourceType { + case bindingType: + c.bindings = make(map[string]*facade.Binding) + c.bindingLastCacheTime = time.Now() + case instanceType: + c.instances = make(map[string]*facade.Instance) + c.instanceLastCacheTime = time.Now() + case spaceType: + c.spaces = make(map[string]*facade.Space) + c.spaceLastCacheTime = time.Now() + case spaceUserRoleType: + c.spaceUserRoles = make(map[string]*spaceUserRole) + c.spaceUserRoleLastCacheTime = time.Now() + } +} + +// ----------------------------------------------------------------------------------------------- +// Bindings +// ----------------------------------------------------------------------------------------------- + +// addBindingInCache stores a binding to the cache +func (c *resourceCache) addBindingInCache(key string, binding *facade.Binding) { + c.bindingMutex.Lock() + defer c.bindingMutex.Unlock() + c.bindings[key] = binding +} + +// deleteBindingFromCache deletes a specific binding from the cache +func (c *resourceCache) deleteBindingFromCache(key string) { + c.bindingMutex.Lock() + defer c.bindingMutex.Unlock() + delete(c.bindings, key) +} + +// getBindingFromCache retrieves a specific binding from the cache +func (c *resourceCache) getBindingFromCache(key string) (*facade.Binding, bool) { + c.bindingMutex.RLock() + defer c.bindingMutex.RUnlock() + binding, found := c.bindings[key] + return binding, found +} + +// updateBindingInCache updates a specific binding in the cache +func (c *resourceCache) updateBindingInCache(owner string, parameters map[string]interface{}, generation int64) (status bool) { + c.bindingMutex.Lock() + defer c.bindingMutex.Unlock() + //update if the instance is found in the cache + //update all the struct variables if they are not nil or empty + binding, found := c.bindings[owner] + if found { + if parameters != nil { + binding.ParameterHash = facade.ObjectHash(parameters) + } + if generation != 0 { + binding.Generation = generation + } + c.bindings[owner] = binding + return true + + } + return false +} + +// ----------------------------------------------------------------------------------------------- +// Instances +// ----------------------------------------------------------------------------------------------- + +// addInstanceInCache stores an instance to the cache +func (c *resourceCache) addInstanceInCache(key string, instance *facade.Instance) { + c.instanceMutex.Lock() + defer c.instanceMutex.Unlock() + c.instances[key] = instance +} + +// deleteInstanceFromCache deletes a specific instance from the cache +func (c *resourceCache) deleteInstanceFromCache(key string) { + c.instanceMutex.Lock() + defer c.instanceMutex.Unlock() + delete(c.instances, key) +} + +// getInstanceFromCache retrieves a specific instance from the cache +func (c *resourceCache) getInstanceFromCache(key string) (*facade.Instance, bool) { + c.instanceMutex.RLock() + defer c.instanceMutex.RUnlock() + instance, found := c.instances[key] + return instance, found +} + +// updateInstanceInCache updates a specific instance in the cache +func (c *resourceCache) updateInstanceInCache(owner string, name string, servicePlanGuid string, parameters map[string]interface{}, generation int64) (status bool) { + c.instanceMutex.Lock() + defer c.instanceMutex.Unlock() + // update if the instance is found in the cache + // update all the struct variables if they are not nil or empty + instance, found := c.instances[owner] + if found { + if name != "" { + instance.Name = name + } + if servicePlanGuid != "" { + instance.ServicePlanGuid = servicePlanGuid + } + if parameters != nil { + instance.ParameterHash = facade.ObjectHash(parameters) + } + if generation != 0 { + instance.Generation = generation + } + c.instances[owner] = instance + return true + + } + return false +} + +// ----------------------------------------------------------------------------------------------- +// Spaces +// ----------------------------------------------------------------------------------------------- + +// addSpaceInCache stores a space to the cache +func (c *resourceCache) addSpaceInCache(key string, space *facade.Space) { + c.spaceMutex.Lock() + defer c.spaceMutex.Unlock() + c.spaces[key] = space +} + +// deleteSpaceFromCache deletes a specific space from the cache +func (c *resourceCache) deleteSpaceFromCache(key string) { + c.spaceMutex.Lock() + defer c.spaceMutex.Unlock() + delete(c.spaces, key) +} + +// getSpaceFromCache retrieves a specific space from the cache +func (c *resourceCache) getSpaceFromCache(key string) (*facade.Space, bool) { + c.spaceMutex.RLock() + defer c.spaceMutex.RUnlock() + space, found := c.spaces[key] + return space, found +} + +// updateSpaceInCache updates a specific space in the cache +func (c *resourceCache) updateSpaceInCache(owner string, name string, generation int64) (status bool) { + c.spaceMutex.Lock() + defer c.spaceMutex.Unlock() + // update if the space is found in the cache + // update all the struct variables if they are not nil or empty + space, found := c.spaces[owner] + if found { + if name != "" { + space.Name = name + } + if generation != 0 { + space.Generation = generation + } + c.spaces[owner] = space + return true + } + return false +} + +// ----------------------------------------------------------------------------------------------- +// Space User Roles +// ----------------------------------------------------------------------------------------------- + +// addSpaceUserRoleInCache adds a specific spaceuserrole to the cache +func (c *resourceCache) addSpaceUserRoleInCache(spaceGuid string, userGuid string, username string, roleType string) { + c.spaceUserRoleMutex.Lock() + defer c.spaceUserRoleMutex.Unlock() + role := &spaceUserRole{ + user: username, + spaceGuid: spaceGuid, + userGUID: userGuid, + roleType: roleType, + } + c.spaceUserRoles[spaceGuid] = role +} + +// deleteSpaceUserRoleFromCache deletes a specifc spaceuserrole from the cache +func (c *resourceCache) deleteSpaceUserRoleFromCache(spaceGuid string) { + c.spaceUserRoleMutex.Lock() + defer c.spaceUserRoleMutex.Unlock() + delete(c.spaceUserRoles, spaceGuid) +} + +// getCachedSpaceUserRoles lists all spaceuserroles from the cache +func (c *resourceCache) getCachedSpaceUserRoles() map[string]*spaceUserRole { + return c.spaceUserRoles +} + +// getSpaceUserRoleFromCache gets a specific spaceuserrole from the cache +func (c *resourceCache) getSpaceUserRoleFromCache(key string) (*spaceUserRole, bool) { + c.spaceUserRoleMutex.RLock() + defer c.spaceUserRoleMutex.RUnlock() + spaceUserRole, found := c.spaceUserRoles[key] + return spaceUserRole, found +} diff --git a/internal/cf/resourcecache_test.go b/internal/cf/resourcecache_test.go new file mode 100644 index 0000000..be47454 --- /dev/null +++ b/internal/cf/resourcecache_test.go @@ -0,0 +1,584 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cf-service-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package cf + +import ( + "strconv" + "sync" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/sap/cf-service-operator/internal/facade" +) + +func TestFacade(t *testing.T) { + RegisterFailHandler(Fail) + //RunSpecs(t, "ResourceCache Suite") +} + +var _ = Describe("ResourceCache", func() { + var cache *resourceCache + var instance *facade.Instance + var binding *facade.Binding + var space *facade.Space + var testspaceUserRole *spaceUserRole + var testBindingOwnerkey string + var testInstanceOwnerkey string + var testSpaceOwnerKey string + var testSpaceGuidKey string + var wg sync.WaitGroup + concurrencyLevel := 20 + + BeforeEach(func() { + cache = initResourceCache() + + instance = &facade.Instance{ + Guid: "testGuid", + Name: "testName", + Owner: "testInstanceOwner", + ServicePlanGuid: "testPlan", + ParameterHash: "testHash", + Generation: 1, + } + + binding = &facade.Binding{ + Guid: "testGuid", + Name: "testName", + Owner: "testBindingOwner", + ParameterHash: "testHash", + Generation: 1, + State: facade.BindingStateReady, + StateDescription: "", + } + + space = &facade.Space{ + Guid: "testGuid", + Name: "testName", + Owner: "testSpaceOwner", + Generation: 1, + } + + testspaceUserRole = &spaceUserRole{ + user: "testUsername", + spaceGuid: "testSpaceGuid", + userGUID: "testUserGuid", + roleType: "developer", + } + testInstanceOwnerkey = "testInstanceOwner" + testBindingOwnerkey = "testBindingOwner" + testSpaceOwnerKey = "testSpaceOwner" + testSpaceGuidKey = "testSpaceGuid" + + }) + + Context("service instance basic CRUD operation test cases", func() { + It("should add, get, update, and delete an instance in the cache", func() { + // Add instance + cache.addInstanceInCache(testInstanceOwnerkey, instance) + + // Get instance + retrievedInstance, found := cache.getInstanceFromCache(testInstanceOwnerkey) + Expect(found).To(BeTrue()) + Expect(retrievedInstance).To(Equal(instance)) + + // Update instance + updatedInstance := &facade.Instance{ + Guid: "testGuid", + Name: "updatedInstanceName", + Owner: testInstanceOwnerkey, + ServicePlanGuid: "updatedPlan", + ParameterHash: "testHash", + Generation: 2, + } + cache.updateInstanceInCache(testInstanceOwnerkey, "updatedInstanceName", "updatedPlan", nil, 2) + retrievedInstance, found = cache.getInstanceFromCache(testInstanceOwnerkey) + Expect(found).To(BeTrue()) + Expect(retrievedInstance).To(Equal(updatedInstance)) + + // Delete instance + cache.deleteInstanceFromCache(testInstanceOwnerkey) + _, found = cache.getInstanceFromCache(testInstanceOwnerkey) + Expect(found).To(BeFalse()) + }) + + It("should handle adding an instance with an existing key", func() { + instance2 := &facade.Instance{ + Guid: "testguid2", + Name: "testname2", + Owner: testInstanceOwnerkey, + ServicePlanGuid: "testplan2", + ParameterHash: "testhash2", + Generation: 1, + } + cache.addInstanceInCache(testInstanceOwnerkey, instance) + cache.addInstanceInCache(testInstanceOwnerkey, instance2) + retrievedInstance, found := cache.getInstanceFromCache(testInstanceOwnerkey) + Expect(found).To(BeTrue()) + Expect(retrievedInstance).To(Equal(instance2)) + }) + + It("should handle updating a non-existent instance", func() { + cache.updateInstanceInCache("nonExistentInstanceOwner", "name", "plan", nil, 1) + _, found := cache.getInstanceFromCache("owner") + Expect(found).To(BeFalse()) + }) + + It("should handle deleting a non-existent instance", func() { + cache.deleteInstanceFromCache("nonExistentInstanceOwner") + _, found := cache.getInstanceFromCache("nonExistentInstanceOwner") + Expect(found).To(BeFalse()) + }) + + It("concurrent instance CRUD operations, data integrity and load test", func() { + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + instance := &facade.Instance{ + Guid: "testGuid" + strconv.Itoa(i), + Name: "testName" + strconv.Itoa(i), + Owner: testInstanceOwnerkey + strconv.Itoa(i), + ServicePlanGuid: "testPlan" + strconv.Itoa(i), + ParameterHash: "hash", + Generation: 1, + } + cache.addInstanceInCache(testInstanceOwnerkey+strconv.Itoa(i), instance) + }(i) + } + wg.Wait() + + // Verify that all instances have been added to the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := testInstanceOwnerkey + strconv.Itoa(i) + instance := &facade.Instance{ + Guid: "testGuid" + strconv.Itoa(i), + Name: "testName" + strconv.Itoa(i), + Owner: testInstanceOwnerkey + strconv.Itoa(i), + ServicePlanGuid: "testPlan" + strconv.Itoa(i), + ParameterHash: "hash", + Generation: 1, + } + retrievedInstance, found := cache.getInstanceFromCache(key) + Expect(found).To(BeTrue(), "Instance should be found in cache for key: %s", key) + Expect(retrievedInstance).To(Equal(instance), "Retrieved instance should match the added instance for key: %s", key) + }(i) + } + wg.Wait() + + // Concurrently update instances in the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.updateInstanceInCache(testInstanceOwnerkey+strconv.Itoa(i), "updatedName"+strconv.Itoa(i), "updatedPlan"+strconv.Itoa(i), nil, 1) + }(i) + } + wg.Wait() + + // Verify that all instances have been updated in the cache + for i := 0; i < concurrencyLevel; i++ { + key := testInstanceOwnerkey + strconv.Itoa(i) + expectedInstance := &facade.Instance{ + Guid: "testGuid" + strconv.Itoa(i), + Name: "updatedName" + strconv.Itoa(i), + Owner: key, + ServicePlanGuid: "updatedPlan" + strconv.Itoa(i), + ParameterHash: "hash", + Generation: 1, + } + retrievedInstance, found := cache.getInstanceFromCache(key) + + Expect(found).To(BeTrue(), "Instance should be found in cache for key: %s", key) + Expect(retrievedInstance).To(Equal(expectedInstance), "Retrieved instance should match the updated instance for key: %s", key) + } + + // Concurrently delete instances from the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.deleteInstanceFromCache(testInstanceOwnerkey + strconv.Itoa(i)) + }(i) + } + wg.Wait() + + // Verify final state + for i := 0; i < concurrencyLevel; i++ { + _, found := cache.getInstanceFromCache(testInstanceOwnerkey + strconv.Itoa(i)) + Expect(found).To(BeFalse(), "Expected instance to be deleted from cache") + } + }) + }) + + Context("service binding basic CRUD operation test ", func() { + It("should add, get, update, and delete a binding in the cache", func() { + // Add binding + cache.addBindingInCache(testBindingOwnerkey, binding) + + // Get binding + retrievedBinding, found := cache.getBindingFromCache(testBindingOwnerkey) + Expect(found).To(BeTrue()) + Expect(retrievedBinding).To(Equal(binding)) + + // Update binding + updatedBinding := &facade.Binding{ + Guid: "testGuid", + Name: "testName", + Owner: testBindingOwnerkey, + ParameterHash: "testHash", + Generation: 2, + State: facade.BindingStateReady, + StateDescription: "", + } + cache.updateBindingInCache(testBindingOwnerkey, nil, 2) + retrievedBinding, found = cache.getBindingFromCache(testBindingOwnerkey) + Expect(found).To(BeTrue()) + Expect(retrievedBinding).To(Equal(updatedBinding)) + + // Delete binding + cache.deleteBindingFromCache(testBindingOwnerkey) + _, found = cache.getBindingFromCache(testBindingOwnerkey) + Expect(found).To(BeFalse()) + }) + + It("should handle adding a binding with an existing key", func() { + binding2 := &facade.Binding{ + Guid: "testGuid2", + Name: "testName2", + Owner: testBindingOwnerkey, + ParameterHash: "testHash", + Generation: 2, + State: facade.BindingStateReady, + StateDescription: "", + } + cache.addBindingInCache(testBindingOwnerkey, binding) + cache.addBindingInCache(testBindingOwnerkey, binding2) + retrievedBinding, found := cache.getBindingFromCache(testBindingOwnerkey) + Expect(found).To(BeTrue()) + Expect(retrievedBinding).To(Equal(binding2)) + }) + + It("should handle updating a non-existent binding", func() { + cache.updateBindingInCache("nonExistBindingOwner", nil, 1) + _, found := cache.getBindingFromCache("nonExistBindingOwner") + Expect(found).To(BeFalse()) + }) + + It("should handle deleting a non-existent binding", func() { + cache.deleteBindingFromCache("nonExistBindingOwner") + _, found := cache.getBindingFromCache("nonExistBindingOwner") + Expect(found).To(BeFalse()) + }) + + It("service binding concurrent CRUD operations, data integrity and load test ", func() { + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + binding := &facade.Binding{ + Guid: "testGuid" + strconv.Itoa(i), + Name: "testName" + strconv.Itoa(i), + Owner: testBindingOwnerkey + strconv.Itoa(i), + ParameterHash: "testhash", + Generation: 1, + State: facade.BindingStateReady, + StateDescription: "", + } + cache.addBindingInCache(testBindingOwnerkey+strconv.Itoa(i), binding) + }(i) + } + wg.Wait() + + // Verify that all bindings have been added to the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := testBindingOwnerkey + strconv.Itoa(i) + binding := &facade.Binding{ + Guid: "testGuid" + strconv.Itoa(i), + Name: "testName" + strconv.Itoa(i), + Owner: testBindingOwnerkey + strconv.Itoa(i), + ParameterHash: "testhash", + Generation: 1, + State: facade.BindingStateReady, + StateDescription: "", + } + retrievedBinding, found := cache.getBindingFromCache(key) + Expect(found).To(BeTrue(), "Binding should be found in cache for key: %s", key) + Expect(retrievedBinding).To(Equal(binding), "Retrieved binding should match the added binding for key: %s", key) + }(i) + } + wg.Wait() + + // Concurrently update bindings in the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.updateBindingInCache(testBindingOwnerkey+strconv.Itoa(i), nil, 2) + }(i) + } + wg.Wait() + + // Verify that all bindings have been updated in the cache + for i := 0; i < concurrencyLevel; i++ { + key := testBindingOwnerkey + strconv.Itoa(i) + expectedBinding := &facade.Binding{ + Guid: "testGuid" + strconv.Itoa(i), + Name: "testName" + strconv.Itoa(i), + Owner: key, + ParameterHash: "testhash", + Generation: 2, + State: facade.BindingStateReady, + StateDescription: "", + } + retrievedBinding, found := cache.getBindingFromCache(key) + + Expect(found).To(BeTrue(), "Binding should be found in cache for key: %s", key) + Expect(retrievedBinding).To(Equal(expectedBinding), "Retrieved binding should match the updated binding for key: %s", key) + } + + // Concurrently delete bindings from the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.deleteBindingFromCache(testBindingOwnerkey + strconv.Itoa(i)) + }(i) + } + wg.Wait() + + // Verify final state + for i := 0; i < concurrencyLevel; i++ { + _, found := cache.getBindingFromCache(testBindingOwnerkey + strconv.Itoa(i)) + Expect(found).To(BeFalse(), "Expected binding to be deleted from cache") + } + }) + }) + + // tests for space cache + Context("space basic CRUD operation test cases", func() { + It("should add, get, update, and delete a space in the cache", func() { + // Add space + cache.addSpaceInCache(testSpaceOwnerKey, space) + + // Get space + retrievedSpace, found := cache.getSpaceFromCache(testSpaceOwnerKey) + Expect(found).To(BeTrue()) + Expect(retrievedSpace).To(Equal(space)) + + // Update space + updatedSpace := &facade.Space{ + Guid: "testGuid", + Name: "updatedName", + Owner: testSpaceOwnerKey, + Generation: 2, + } + cache.updateSpaceInCache(testSpaceOwnerKey, "updatedName", 2) + retrievedSpace, found = cache.getSpaceFromCache(testSpaceOwnerKey) + Expect(found).To(BeTrue()) + Expect(retrievedSpace).To(Equal(updatedSpace)) + + // Delete space + cache.deleteSpaceFromCache(testSpaceOwnerKey) + _, found = cache.getSpaceFromCache(testSpaceOwnerKey) + Expect(found).To(BeFalse()) + }) + + It("should handle adding a space with an existing key", func() { + space2 := &facade.Space{ + Guid: "testGuid", + Name: "testName", + Owner: testSpaceOwnerKey, + Generation: 2, + } + cache.addSpaceInCache(testSpaceOwnerKey, space) + cache.addSpaceInCache(testSpaceOwnerKey, space2) + retrievedSpace, found := cache.getSpaceFromCache(testSpaceOwnerKey) + Expect(found).To(BeTrue()) + Expect(retrievedSpace).To(Equal(space2)) + }) + + It("should handle updating a non-existent space", func() { + cache.updateSpaceInCache("nonExistSpaceOwner", "testname", 1) + _, found := cache.getSpaceFromCache("nonExistSpaceOwner") + Expect(found).To(BeFalse()) + }) + + It("should handle deleting a non-existent space", func() { + cache.deleteSpaceFromCache("nonExistSpaceOwner") + _, found := cache.getSpaceFromCache("nonExistSpaceOwner") + Expect(found).To(BeFalse()) + }) + + It("concurrent CRUD operations, data integrity and load test", func() { + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + space := &facade.Space{ + Guid: "guid" + strconv.Itoa(i), + Name: "name" + strconv.Itoa(i), + Owner: testSpaceOwnerKey + strconv.Itoa(i), + Generation: 1, + } + cache.addSpaceInCache(testSpaceOwnerKey+strconv.Itoa(i), space) + }(i) + } + wg.Wait() + + // Verify that all spaces have been added to the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := testSpaceOwnerKey + strconv.Itoa(i) + space := &facade.Space{ + Guid: "guid" + strconv.Itoa(i), + Name: "name" + strconv.Itoa(i), + Owner: testSpaceOwnerKey + strconv.Itoa(i), + Generation: 1, + } + retrievedSpace, found := cache.getSpaceFromCache(key) + Expect(found).To(BeTrue(), "Space should be found in cache for key: %s", key) + Expect(retrievedSpace).To(Equal(space), "Retrieved space should match the added space for key: %s", key) + }(i) + } + wg.Wait() + + // Concurrently update spaces in the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.updateSpaceInCache(testSpaceOwnerKey+strconv.Itoa(i), "updatedname"+strconv.Itoa(i), 2) + }(i) + } + wg.Wait() + + // Verify that all spaces have been updated in the cache + for i := 0; i < concurrencyLevel; i++ { + key := testSpaceOwnerKey + strconv.Itoa(i) + expectedSpace := &facade.Space{ + Guid: "guid" + strconv.Itoa(i), + Name: "updatedname" + strconv.Itoa(i), + Owner: key, + Generation: 2, + } + retrievedSpace, found := cache.getSpaceFromCache(key) + + Expect(found).To(BeTrue(), "Space should be found in cache for key: %s", key) + Expect(retrievedSpace).To(Equal(expectedSpace), "Retrieved space should match the updated space for key: %s", key) + } + + // Concurrently delete spaces from the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.deleteSpaceFromCache(testSpaceOwnerKey + strconv.Itoa(i)) + }(i) + } + wg.Wait() + + // Verify final state + for i := 0; i < concurrencyLevel; i++ { + _, found := cache.getSpaceFromCache(testSpaceOwnerKey + strconv.Itoa(i)) + Expect(found).To(BeFalse(), "Expected space to be deleted from cache") + } + }) + }) + + Context("space user role relationship basic CRUD operation test cases", func() { + It("should add, get, update, and delete a space user role in the cache", func() { + // Add space user role + + cache.addSpaceUserRoleInCache(testSpaceGuidKey, "testUserGuid", "testUsername", "developer") + + // Get space user role + retrievedSpaceUserRole, found := cache.getSpaceUserRoleFromCache(testSpaceGuidKey) + Expect(found).To(BeTrue()) + Expect(retrievedSpaceUserRole).To(Equal(testspaceUserRole)) + + // Delete space user role + cache.deleteSpaceUserRoleFromCache(testSpaceGuidKey) + _, found = cache.getSpaceUserRoleFromCache(testSpaceGuidKey) + Expect(found).To(BeFalse()) + }) + + It("should handle adding a space user role with an existing key", func() { + spaceUserRole2 := &spaceUserRole{ + user: "testUsername2", + spaceGuid: testSpaceGuidKey, + userGUID: "testUserGuid2", + roleType: "developer", + } + cache.addSpaceUserRoleInCache(testSpaceGuidKey, "testUserGuid", "testUsername", "developer") + cache.addSpaceUserRoleInCache(testSpaceGuidKey, "testUserGuid2", "testUsername2", "developer") + retrievedSpaceUserRole, found := cache.getSpaceUserRoleFromCache(testSpaceGuidKey) + Expect(found).To(BeTrue()) + Expect(retrievedSpaceUserRole).To(Equal(spaceUserRole2)) + + }) + + It("should handle deleting a non-existent space user role", func() { + cache.deleteSpaceUserRoleFromCache("nonExistSpaceUserRole") + _, found := cache.getSpaceUserRoleFromCache("nonExistSpaceUserRole") + Expect(found).To(BeFalse()) + }) + + It("concurrent CRUD operations, data integrity and load test", func() { + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.addSpaceUserRoleInCache(testSpaceGuidKey+strconv.Itoa(i), "testUserGuid"+strconv.Itoa(i), "testUsername"+strconv.Itoa(i), "developer") + }(i) + } + wg.Wait() + + // Verify that all space user roles have been added to the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + key := testSpaceGuidKey + strconv.Itoa(i) + expetcSpaceUserRole := &spaceUserRole{ + user: "testUsername" + strconv.Itoa(i), + spaceGuid: testSpaceGuidKey + strconv.Itoa(i), + userGUID: "testUserGuid" + strconv.Itoa(i), + roleType: "developer", + } + retrievedSpaceUserRole, found := cache.getSpaceUserRoleFromCache(key) + Expect(found).To(BeTrue(), "Space user role should be found in cache for key: %s", key) + Expect(retrievedSpaceUserRole).To(Equal(expetcSpaceUserRole), "Retrieved space user role should match the added space user role for key: %s", key) + }(i) + } + wg.Wait() + + // Concurrently delete space user roles from the cache + for i := 0; i < concurrencyLevel; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.deleteSpaceUserRoleFromCache(testSpaceGuidKey + strconv.Itoa(i)) + }(i) + } + wg.Wait() + + // Verify final state + for i := 0; i < concurrencyLevel; i++ { + _, found := cache.getSpaceUserRoleFromCache(testSpaceGuidKey + strconv.Itoa(i)) + Expect(found).To(BeFalse(), "Expected space user role to be deleted from cache") + } + }) + }) +}) diff --git a/internal/cf/space.go b/internal/cf/space.go index 909e818..f8147a3 100644 --- a/internal/cf/space.go +++ b/internal/cf/space.go @@ -12,12 +12,23 @@ import ( cfclient "github.com/cloudfoundry-community/go-cfclient/v3/client" cfresource "github.com/cloudfoundry-community/go-cfclient/v3/resource" - "github.com/pkg/errors" "github.com/sap/cf-service-operator/internal/facade" ) func (c *organizationClient) GetSpace(ctx context.Context, owner string) (*facade.Space, error) { + if c.resourceCache.checkResourceCacheEnabled() { + // attempt to retrieve space from cache + if c.resourceCache.isCacheExpired(spaceType) { + populateResourceCache(c, spaceType, "") + } + space, inCache := c.resourceCache.getSpaceFromCache(owner) + if inCache { + return space, nil + } + } + + // attempt to retrieve space from Cloud Foundry listOpts := cfclient.NewSpaceListOptions() listOpts.LabelSelector.EqualTo(labelPrefix + "/" + labelKeyOwner + "=" + owner) spaces, err := c.client.Spaces.ListAll(ctx, listOpts) @@ -32,19 +43,8 @@ func (c *organizationClient) GetSpace(ctx context.Context, owner string) (*facad } space := spaces[0] - guid := space.GUID - name := space.Name - generation, err := strconv.ParseInt(*space.Metadata.Annotations[annotationGeneration], 10, 64) - if err != nil { - return nil, errors.Wrap(err, "error parsing space generation") - } - - return &facade.Space{ - Guid: guid, - Name: name, - Owner: owner, - Generation: generation, - }, nil + // TODO: add directly to cache + return c.InitSpace(space, owner) } // Required parameters (may not be initial): name, owner, generation @@ -68,12 +68,14 @@ func (c *organizationClient) CreateSpace(ctx context.Context, name string, owner WithAnnotation(annotationPrefix, annotationKeyGeneration, strconv.FormatInt(generation, 10)) _, err = c.client.Spaces.Create(ctx, req) + // do not add space to the cache here because we wait until the space GUID is known + return err } // Required parameters (may not be initial): guid, generation // Optional parameters (may be initial): name -func (c *organizationClient) UpdateSpace(ctx context.Context, guid string, name string, generation int64) error { +func (c *organizationClient) UpdateSpace(ctx context.Context, guid string, name string, owner string, generation int64) error { // TODO: why is there no cfresource.NewSpaceUpdate() method ? req := &cfresource.SpaceUpdate{} if name != "" { @@ -83,11 +85,34 @@ func (c *organizationClient) UpdateSpace(ctx context.Context, guid string, name WithAnnotation(annotationPrefix, annotationKeyGeneration, strconv.FormatInt(generation, 10)) _, err := c.client.Spaces.Update(ctx, guid, req) + + // update space in cache + if err == nil && c.resourceCache.checkResourceCacheEnabled() { + isUpdated := c.resourceCache.updateSpaceInCache(owner, name, generation) + if !isUpdated { + // add space to cache + space := &facade.Space{ + Guid: guid, + Name: name, + Owner: owner, + Generation: generation, + } + c.resourceCache.addSpaceInCache(owner, space) + } + } + return err } -func (c *organizationClient) DeleteSpace(ctx context.Context, guid string) error { +func (c *organizationClient) DeleteSpace(ctx context.Context, guid string, owner string) error { _, err := c.client.Spaces.Delete(ctx, guid) + + // delete space from cache + if err == nil && c.resourceCache.checkResourceCacheEnabled() { + c.resourceCache.deleteSpaceFromCache(owner) + c.resourceCache.deleteSpaceUserRoleFromCache(guid) + } + return err } @@ -96,6 +121,20 @@ func (c *organizationClient) AddAuditor(ctx context.Context, guid string, userna } func (c *organizationClient) AddDeveloper(ctx context.Context, guid string, username string) error { + if c.resourceCache.checkResourceCacheEnabled() { + // attempt to retrieve space user role from cache + if c.resourceCache.isCacheExpired(spaceUserRoleType) { + populateResourceCache(c, spaceUserRoleType, username) + } + if len(c.resourceCache.getCachedSpaceUserRoles()) != 0 { + _, inCache := c.resourceCache.getSpaceUserRoleFromCache(guid) + if inCache { + return nil + } + } + } + + // attempt to retrieve space user role from Cloud Foundry userListOpts := cfclient.NewUserListOptions() userListOpts.UserNames.EqualTo(username) users, err := c.client.Users.ListAll(ctx, userListOpts) @@ -127,3 +166,24 @@ func (c *organizationClient) AddDeveloper(ctx context.Context, guid string, user func (c *organizationClient) AddManager(ctx context.Context, guid string, username string) error { return nil } + +// InitSpace wraps cfclient.Space as a facade.Space +func (c *organizationClient) InitSpace(space *cfresource.Space, owner string) (*facade.Space, error) { + guid := space.GUID + name := space.Name + generation, err := strconv.ParseInt(*space.Metadata.Annotations[annotationGeneration], 10, 64) + if err != nil { + return nil, err + } + + if owner == "" { + owner = *space.Metadata.Labels[labelOwner] + } + + return &facade.Space{ + Guid: guid, + Name: name, + Owner: owner, + Generation: generation, + }, nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..cb7c2de --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,32 @@ +/* +SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and cf-service-operator contributors +SPDX-License-Identifier: Apache-2.0 +*/ + +package config + +import ( + "log" + + "github.com/caarlos0/env/v11" +) + +// Config defines the configuration keys +type Config struct { + // Resource cache is enabled or disabled + IsResourceCacheEnabled bool `env:"RESOURCE_CACHE_ENABLED" envDefault:"false"` + + // Timeout for resource cache in seconds, minutes or hours + CacheTimeOut string `env:"CACHE_TIMEOUT" envDefault:"1m"` +} + +// Load the configuration +func Load() (*Config, error) { + cfg := &Config{} + if err := env.Parse(cfg); err != nil { + log.Printf("Error parsing environment variables: %v\n", err) + return nil, err + } + + return cfg, nil +} diff --git a/internal/controllers/servicebinding_controller.go b/internal/controllers/servicebinding_controller.go index 12ff006..7295961 100644 --- a/internal/controllers/servicebinding_controller.go +++ b/internal/controllers/servicebinding_controller.go @@ -24,6 +24,7 @@ import ( cfv1alpha1 "github.com/sap/cf-service-operator/api/v1alpha1" "github.com/sap/cf-service-operator/internal/binding" + "github.com/sap/cf-service-operator/internal/config" "github.com/sap/cf-service-operator/internal/facade" ) @@ -47,6 +48,7 @@ type ServiceBindingReconciler struct { ClusterResourceNamespace string EnableBindingMetadata bool ClientBuilder facade.SpaceClientBuilder + Config *config.Config } // +kubebuilder:rbac:groups=cf.cs.sap.com,resources=servicebindings,verbs=get;list;watch;update @@ -168,7 +170,7 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque // Build cloud foundry client var client facade.SpaceClient if spaceGuid != "" { - client, err = r.ClientBuilder(spaceGuid, string(spaceSecret.Data["url"]), string(spaceSecret.Data["username"]), string(spaceSecret.Data["password"])) + client, err = r.ClientBuilder(spaceGuid, string(spaceSecret.Data["url"]), string(spaceSecret.Data["username"]), string(spaceSecret.Data["password"]), r.Config) if err != nil { return ctrl.Result{}, errors.Wrapf(err, "failed to build the client from secret %s", spaceSecretName) } @@ -192,32 +194,37 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque if err != nil { return ctrl.Result{}, err } + if cfbinding != nil && cfbinding.State == facade.BindingStateReady { + //Add parameters to adopt the orphaned binding + var parameterObjects []map[string]interface{} + paramMap := make(map[string]interface{}) + paramMap["parameter-hash"] = cfbinding.ParameterHash + paramMap["owner"] = cfbinding.Owner + parameterObjects = append(parameterObjects, paramMap) + parameters, err := mergeObjects(parameterObjects...) + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "failed to unmarshal/merge parameters") + } + // update the orphaned cloud foundry service binding + log.V(1).Info("Updating binding") + if err := client.UpdateBinding( + ctx, + cfbinding.Guid, + cfbinding.Owner, + serviceBinding.Generation, + parameters, + ); err != nil { + return ctrl.Result{}, err + } + status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] - //Add parameters to adopt the orphaned binding - var parameterObjects []map[string]interface{} - paramMap := make(map[string]interface{}) - paramMap["parameter-hash"] = cfbinding.ParameterHash - paramMap["owner"] = cfbinding.Owner - parameterObjects = append(parameterObjects, paramMap) - parameters, err := mergeObjects(parameterObjects...) - if err != nil { - return ctrl.Result{}, errors.Wrap(err, "failed to unmarshal/merge parameters") + // return the reconcile function to requeue inmediatly after the update + serviceBinding.SetReadyCondition(cfv1alpha1.ConditionUnknown, string(cfbinding.State), cfbinding.StateDescription) + return ctrl.Result{Requeue: true}, nil + } else if cfbinding != nil && cfbinding.State != facade.BindingStateReady { + // return the reconcile function to not reconcile and error message + return ctrl.Result{}, fmt.Errorf("orphaned binding is not ready to be adopted") } - // update the orphaned cloud foundry service binding - log.V(1).Info("Updating binding") - if err := client.UpdateBinding( - ctx, - cfbinding.Guid, - serviceBinding.Generation, - parameters, - ); err != nil { - return ctrl.Result{}, err - } - status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] - - // return the reconcile function to requeue inmediatly after the update - serviceBinding.SetReadyCondition(cfv1alpha1.ConditionUnknown, string(cfbinding.State), cfbinding.StateDescription) - return ctrl.Result{Requeue: true}, nil } } @@ -296,7 +303,7 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque cfbinding.State == facade.BindingStateCreatedFailed || cfbinding.State == facade.BindingStateDeleteFailed { // Re-create binding (unfortunately, cloud foundry does not support binding updates, other than metadata) log.V(1).Info("Deleting binding for later re-creation") - if err := client.DeleteBinding(ctx, cfbinding.Guid); err != nil { + if err := client.DeleteBinding(ctx, cfbinding.Guid, cfbinding.Owner); err != nil { return ctrl.Result{}, err } status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] @@ -309,6 +316,7 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque if err := client.UpdateBinding( ctx, cfbinding.Guid, + cfbinding.Owner, serviceBinding.Generation, nil, ); err != nil { @@ -351,6 +359,13 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque } else if serviceBinding.Annotations["service-operator.cf.cs.sap.com/with-sap-binding-metadata"] == "false" { withMetadata = false } + + err = client.FillBindingDetails(ctx, cfbinding) + if err != nil { + // TODO: implement error handling + return ctrl.Result{RequeueAfter: 10 * time.Minute}, nil + } + err = r.storeBindingSecret(ctx, serviceInstance, serviceBinding, cfbinding.Credentials, spec.SecretName, spec.SecretKey, withMetadata) if err != nil { // TODO: implement error handling @@ -402,7 +417,7 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque } else { if cfbinding.State != facade.BindingStateDeleting { log.V(1).Info("Deleting binding") - if err := client.DeleteBinding(ctx, cfbinding.Guid); err != nil { + if err := client.DeleteBinding(ctx, cfbinding.Guid, cfbinding.Owner); err != nil { return ctrl.Result{}, err } status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] diff --git a/internal/controllers/serviceinstance_controller.go b/internal/controllers/serviceinstance_controller.go index a4e51d2..3aad34e 100644 --- a/internal/controllers/serviceinstance_controller.go +++ b/internal/controllers/serviceinstance_controller.go @@ -25,6 +25,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" cfv1alpha1 "github.com/sap/cf-service-operator/api/v1alpha1" + "github.com/sap/cf-service-operator/internal/config" "github.com/sap/cf-service-operator/internal/facade" ) @@ -54,6 +55,7 @@ type ServiceInstanceReconciler struct { Scheme *runtime.Scheme ClusterResourceNamespace string ClientBuilder facade.SpaceClientBuilder + Config *config.Config } // RetryError is a special error to indicate that the operation should be retried. @@ -182,7 +184,7 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ // Build cloud foundry client var client facade.SpaceClient if spaceGuid != "" { - client, err = r.ClientBuilder(spaceGuid, string(spaceSecret.Data["url"]), string(spaceSecret.Data["username"]), string(spaceSecret.Data["password"])) + client, err = r.ClientBuilder(spaceGuid, string(spaceSecret.Data["url"]), string(spaceSecret.Data["username"]), string(spaceSecret.Data["password"]), r.Config) if err != nil { return ctrl.Result{}, errors.Wrapf(err, "failed to build the client from secret %s", spaceSecretName) } @@ -206,34 +208,48 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ if err != nil { return ctrl.Result{}, err } - - //Add parameters to adopt the orphaned instance - var parameterObjects []map[string]interface{} - paramMap := make(map[string]interface{}) - paramMap["parameter-hash"] = cfinstance.ParameterHash - paramMap["owner"] = cfinstance.Owner - parameterObjects = append(parameterObjects, paramMap) - parameters, err := mergeObjects(parameterObjects...) - if err != nil { - return ctrl.Result{}, errors.Wrap(err, "failed to unmarshal/merge parameters") - } - // update the orphaned cloud foundry instance - log.V(1).Info("Updating instance") - if err := client.UpdateInstance( - ctx, - cfinstance.Guid, - spec.Name, - "", - parameters, - nil, - serviceInstance.Generation, - ); err != nil { - return ctrl.Result{}, err + if cfinstance != nil && cfinstance.State == facade.InstanceStateReady { + //Add parameters to adopt the orphaned instance + var parameterObjects []map[string]interface{} + paramMap := make(map[string]interface{}) + paramMap["parameter-hash"] = cfinstance.ParameterHash + paramMap["owner"] = cfinstance.Owner + parameterObjects = append(parameterObjects, paramMap) + parameters, err := mergeObjects(parameterObjects...) + if err != nil { + return ctrl.Result{}, errors.Wrap(err, "failed to unmarshal/merge parameters") + } + servicePlanGuid := cfinstance.ServicePlanGuid + if servicePlanGuid == "" { + log.V(1).Info("Searching service plan") + servicePlanGuid, err = client.FindServicePlan(ctx, spec.ServiceOfferingName, spec.ServicePlanName, spaceGuid) + if err != nil { + return ctrl.Result{}, err + } + } + // update the orphaned cloud foundry instance + log.V(1).Info("Updating instance") + if err := client.UpdateInstance( + ctx, + cfinstance.Guid, + spec.Name, + string(serviceInstance.UID), + servicePlanGuid, + parameters, + nil, + serviceInstance.Generation, + ); err != nil { + return ctrl.Result{}, err + } + status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] + // return the reconcile function to requeue inmediatly after the update + serviceInstance.SetReadyCondition(cfv1alpha1.ConditionUnknown, string(cfinstance.State), cfinstance.StateDescription) + return ctrl.Result{Requeue: true}, nil + } else if cfinstance != nil && cfinstance.State != facade.InstanceStateReady { + //return the reconcile function to not reconcile and error message + return ctrl.Result{}, fmt.Errorf("orphaned instance is not ready to be adopted") } - status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] - // return the reconcile function to requeue inmediatly after the update - serviceInstance.SetReadyCondition(cfv1alpha1.ConditionUnknown, string(cfinstance.State), cfinstance.StateDescription) - return ctrl.Result{Requeue: true}, nil + } } @@ -313,7 +329,7 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ } else if recreateOnCreationFailure && (cfinstance.State == facade.InstanceStateCreatedFailed || cfinstance.State == facade.InstanceStateDeleteFailed) { // Re-create instance log.V(1).Info("Deleting instance for later re-creation") - if err := client.DeleteInstance(ctx, cfinstance.Guid); err != nil { + if err := client.DeleteInstance(ctx, cfinstance.Guid, cfinstance.Owner); err != nil { return ctrl.Result{}, RetryError } status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] @@ -350,6 +366,7 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ ctx, cfinstance.Guid, updateName, + string(serviceInstance.UID), updateServicePlanGuid, updateParameters, updateTags, @@ -423,7 +440,7 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ } else { if cfinstance.State != facade.InstanceStateDeleting { log.V(1).Info("Deleting instance") - if err := client.DeleteInstance(ctx, cfinstance.Guid); err != nil { + if err := client.DeleteInstance(ctx, cfinstance.Guid, cfinstance.Owner); err != nil { return ctrl.Result{}, err } status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] diff --git a/internal/controllers/space_controller.go b/internal/controllers/space_controller.go index 161036b..243f09e 100644 --- a/internal/controllers/space_controller.go +++ b/internal/controllers/space_controller.go @@ -23,6 +23,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/predicate" cfv1alpha1 "github.com/sap/cf-service-operator/api/v1alpha1" + "github.com/sap/cf-service-operator/internal/config" "github.com/sap/cf-service-operator/internal/facade" ) @@ -46,6 +47,7 @@ type SpaceReconciler struct { ClusterResourceNamespace string ClientBuilder facade.OrganizationClientBuilder HealthCheckerBuilder facade.SpaceHealthCheckerBuilder + Config *config.Config } // +kubebuilder:rbac:groups=cf.cs.sap.com,resources=clusterspaces,verbs=get;list;watch;update @@ -147,7 +149,7 @@ func (r *SpaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu password = string(secret.Data["password"]) } - client, err = r.ClientBuilder(spec.OrganizationName, url, username, password) + client, err = r.ClientBuilder(spec.OrganizationName, url, username, password, r.Config) if err != nil { return ctrl.Result{}, errors.Wrapf(err, "failed to build the client from secret %s", secretName) } @@ -197,6 +199,7 @@ func (r *SpaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu ctx, cfspace.Guid, updateName, + cfspace.Owner, space.GetGeneration(), ); err != nil { return ctrl.Result{}, err @@ -232,13 +235,13 @@ func (r *SpaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu url := string(secret.Data["url"]) username := string(secret.Data["username"]) password := string(secret.Data["password"]) - checker, err := r.HealthCheckerBuilder(status.SpaceGuid, url, username, password) + checker, err := r.HealthCheckerBuilder(status.SpaceGuid, url, username, password, r.Config) if err != nil { return ctrl.Result{}, errors.Wrapf(err, "failed to build the healthchecker from secret %s", secretName) } log.V(1).Info("Checking space") - if err := checker.Check(ctx); err != nil { + if err := checker.Check(ctx, cfspace.Owner); err != nil { return ctrl.Result{}, errors.Wrap(err, "healthcheck failed") } @@ -274,7 +277,7 @@ func (r *SpaceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (resu return ctrl.Result{}, nil } else { log.V(1).Info("Deleting space") - if err := client.DeleteSpace(ctx, cfspace.Guid); err != nil { + if err := client.DeleteSpace(ctx, cfspace.Guid, cfspace.Owner); err != nil { return ctrl.Result{}, err } status.LastModifiedAt = &[]metav1.Time{metav1.Now()}[0] diff --git a/internal/controllers/suite_test.go b/internal/controllers/suite_test.go index f6895ec..055a23e 100644 --- a/internal/controllers/suite_test.go +++ b/internal/controllers/suite_test.go @@ -16,6 +16,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/sap/cf-service-operator/api/v1alpha1" + "github.com/sap/cf-service-operator/internal/config" "github.com/sap/cf-service-operator/internal/facade" "github.com/sap/cf-service-operator/internal/facade/facadefakes" @@ -65,6 +66,9 @@ const ( // (overridden by environment variable TEST_TIMEOUT) var timeout = 5 * time.Minute +// is resource cache enabled and cache timeout +var cfg = &config.Config{} + // interval used for polling custom resource var interval = 500 * time.Millisecond @@ -204,12 +208,13 @@ func addControllers(k8sManager ctrl.Manager) { Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), ClusterResourceNamespace: testK8sNamespace, - ClientBuilder: func(organizationName string, url string, username string, password string) (facade.OrganizationClient, error) { + ClientBuilder: func(organizationName string, url string, username string, password string, config *config.Config) (facade.OrganizationClient, error) { return fakeOrgClient, nil }, - HealthCheckerBuilder: func(spaceGuid string, url string, username string, password string) (facade.SpaceHealthChecker, error) { + HealthCheckerBuilder: func(spaceGuid string, url string, username string, password string, config *config.Config) (facade.SpaceHealthChecker, error) { return fakeSpaceHealthChecker, nil }, + Config: cfg, } Expect(spaceReconciler.SetupWithManager(k8sManager)).To(Succeed()) @@ -218,9 +223,10 @@ func addControllers(k8sManager ctrl.Manager) { Client: k8sManager.GetClient(), Scheme: k8sManager.GetScheme(), ClusterResourceNamespace: testK8sNamespace, - ClientBuilder: func(organizationName string, url string, username string, password string) (facade.SpaceClient, error) { + ClientBuilder: func(organizationName string, url string, username string, password string, config *config.Config) (facade.SpaceClient, error) { return fakeSpaceClient, nil }, + Config: cfg, } Expect(instanceReconciler.SetupWithManager(k8sManager)).To(Succeed()) diff --git a/internal/facade/client.go b/internal/facade/client.go index 6990788..ff4183b 100644 --- a/internal/facade/client.go +++ b/internal/facade/client.go @@ -5,7 +5,11 @@ SPDX-License-Identifier: Apache-2.0 package facade -import "context" +import ( + "context" + + "github.com/sap/cf-service-operator/internal/config" +) type Space struct { Guid string @@ -66,28 +70,29 @@ const ( type OrganizationClient interface { GetSpace(ctx context.Context, owner string) (*Space, error) CreateSpace(ctx context.Context, name string, owner string, generation int64) error - UpdateSpace(ctx context.Context, guid string, name string, generation int64) error - DeleteSpace(ctx context.Context, guid string) error + UpdateSpace(ctx context.Context, guid string, name string, owner string, generation int64) error + DeleteSpace(ctx context.Context, guid string, owner string) error AddAuditor(ctx context.Context, guid string, username string) error AddDeveloper(ctx context.Context, guid string, username string) error AddManager(ctx context.Context, guid string, username string) error } -type OrganizationClientBuilder func(string, string, string, string) (OrganizationClient, error) +type OrganizationClientBuilder func(string, string, string, string, *config.Config) (OrganizationClient, error) //counterfeiter:generate . SpaceClient type SpaceClient interface { GetInstance(ctx context.Context, instanceOpts map[string]string) (*Instance, error) CreateInstance(ctx context.Context, name string, servicePlanGuid string, parameters map[string]interface{}, tags []string, owner string, generation int64) error - UpdateInstance(ctx context.Context, guid string, name string, servicePlanGuid string, parameters map[string]interface{}, tags []string, generation int64) error - DeleteInstance(ctx context.Context, guid string) error + UpdateInstance(ctx context.Context, guid string, name string, owner string, servicePlanGuid string, parameters map[string]interface{}, tags []string, generation int64) error + DeleteInstance(ctx context.Context, guid string, owner string) error GetBinding(ctx context.Context, bindingOpts map[string]string) (*Binding, error) CreateBinding(ctx context.Context, name string, serviceInstanceGuid string, parameters map[string]interface{}, owner string, generation int64) error - UpdateBinding(ctx context.Context, guid string, generation int64, parameters map[string]interface{}) error - DeleteBinding(ctx context.Context, guid string) error + UpdateBinding(ctx context.Context, guid string, owner string, generation int64, parameters map[string]interface{}) error + DeleteBinding(ctx context.Context, guid string, owner string) error + FillBindingDetails(ctx context.Context, binding *Binding) error FindServicePlan(ctx context.Context, serviceOfferingName string, servicePlanName string, spaceGuid string) (string, error) } -type SpaceClientBuilder func(string, string, string, string) (SpaceClient, error) +type SpaceClientBuilder func(string, string, string, string, *config.Config) (SpaceClient, error) diff --git a/internal/facade/facadefakes/fake_organization_client.go b/internal/facade/facadefakes/fake_organization_client.go index c5df333..b8193e1 100644 --- a/internal/facade/facadefakes/fake_organization_client.go +++ b/internal/facade/facadefakes/fake_organization_client.go @@ -66,11 +66,12 @@ type FakeOrganizationClient struct { createSpaceReturnsOnCall map[int]struct { result1 error } - DeleteSpaceStub func(context.Context, string) error + DeleteSpaceStub func(context.Context, string, string) error deleteSpaceMutex sync.RWMutex deleteSpaceArgsForCall []struct { arg1 context.Context arg2 string + arg3 string } deleteSpaceReturns struct { result1 error @@ -92,13 +93,14 @@ type FakeOrganizationClient struct { result1 *facade.Space result2 error } - UpdateSpaceStub func(context.Context, string, string, int64) error + UpdateSpaceStub func(context.Context, string, string, string, int64) error updateSpaceMutex sync.RWMutex updateSpaceArgsForCall []struct { arg1 context.Context arg2 string arg3 string - arg4 int64 + arg4 string + arg5 int64 } updateSpaceReturns struct { result1 error @@ -363,19 +365,20 @@ func (fake *FakeOrganizationClient) CreateSpaceReturnsOnCall(i int, result1 erro }{result1} } -func (fake *FakeOrganizationClient) DeleteSpace(arg1 context.Context, arg2 string) error { +func (fake *FakeOrganizationClient) DeleteSpace(arg1 context.Context, arg2 string, arg3 string) error { fake.deleteSpaceMutex.Lock() ret, specificReturn := fake.deleteSpaceReturnsOnCall[len(fake.deleteSpaceArgsForCall)] fake.deleteSpaceArgsForCall = append(fake.deleteSpaceArgsForCall, struct { arg1 context.Context arg2 string - }{arg1, arg2}) + arg3 string + }{arg1, arg2, arg3}) stub := fake.DeleteSpaceStub fakeReturns := fake.deleteSpaceReturns - fake.recordInvocation("DeleteSpace", []interface{}{arg1, arg2}) + fake.recordInvocation("DeleteSpace", []interface{}{arg1, arg2, arg3}) fake.deleteSpaceMutex.Unlock() if stub != nil { - return stub(arg1, arg2) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1 @@ -389,17 +392,17 @@ func (fake *FakeOrganizationClient) DeleteSpaceCallCount() int { return len(fake.deleteSpaceArgsForCall) } -func (fake *FakeOrganizationClient) DeleteSpaceCalls(stub func(context.Context, string) error) { +func (fake *FakeOrganizationClient) DeleteSpaceCalls(stub func(context.Context, string, string) error) { fake.deleteSpaceMutex.Lock() defer fake.deleteSpaceMutex.Unlock() fake.DeleteSpaceStub = stub } -func (fake *FakeOrganizationClient) DeleteSpaceArgsForCall(i int) (context.Context, string) { +func (fake *FakeOrganizationClient) DeleteSpaceArgsForCall(i int) (context.Context, string, string) { fake.deleteSpaceMutex.RLock() defer fake.deleteSpaceMutex.RUnlock() argsForCall := fake.deleteSpaceArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } func (fake *FakeOrganizationClient) DeleteSpaceReturns(result1 error) { @@ -490,21 +493,22 @@ func (fake *FakeOrganizationClient) GetSpaceReturnsOnCall(i int, result1 *facade }{result1, result2} } -func (fake *FakeOrganizationClient) UpdateSpace(arg1 context.Context, arg2 string, arg3 string, arg4 int64) error { +func (fake *FakeOrganizationClient) UpdateSpace(arg1 context.Context, arg2 string, arg3 string, arg4 string, arg5 int64) error { fake.updateSpaceMutex.Lock() ret, specificReturn := fake.updateSpaceReturnsOnCall[len(fake.updateSpaceArgsForCall)] fake.updateSpaceArgsForCall = append(fake.updateSpaceArgsForCall, struct { arg1 context.Context arg2 string arg3 string - arg4 int64 - }{arg1, arg2, arg3, arg4}) + arg4 string + arg5 int64 + }{arg1, arg2, arg3, arg4, arg5}) stub := fake.UpdateSpaceStub fakeReturns := fake.updateSpaceReturns - fake.recordInvocation("UpdateSpace", []interface{}{arg1, arg2, arg3, arg4}) + fake.recordInvocation("UpdateSpace", []interface{}{arg1, arg2, arg3, arg4, arg5}) fake.updateSpaceMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4) + return stub(arg1, arg2, arg3, arg4, arg5) } if specificReturn { return ret.result1 @@ -518,17 +522,17 @@ func (fake *FakeOrganizationClient) UpdateSpaceCallCount() int { return len(fake.updateSpaceArgsForCall) } -func (fake *FakeOrganizationClient) UpdateSpaceCalls(stub func(context.Context, string, string, int64) error) { +func (fake *FakeOrganizationClient) UpdateSpaceCalls(stub func(context.Context, string, string, string, int64) error) { fake.updateSpaceMutex.Lock() defer fake.updateSpaceMutex.Unlock() fake.UpdateSpaceStub = stub } -func (fake *FakeOrganizationClient) UpdateSpaceArgsForCall(i int) (context.Context, string, string, int64) { +func (fake *FakeOrganizationClient) UpdateSpaceArgsForCall(i int) (context.Context, string, string, string, int64) { fake.updateSpaceMutex.RLock() defer fake.updateSpaceMutex.RUnlock() argsForCall := fake.updateSpaceArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 } func (fake *FakeOrganizationClient) UpdateSpaceReturns(result1 error) { diff --git a/internal/facade/facadefakes/fake_space_client.go b/internal/facade/facadefakes/fake_space_client.go index 5f9b070..98686cc 100644 --- a/internal/facade/facadefakes/fake_space_client.go +++ b/internal/facade/facadefakes/fake_space_client.go @@ -46,11 +46,12 @@ type FakeSpaceClient struct { createInstanceReturnsOnCall map[int]struct { result1 error } - DeleteBindingStub func(context.Context, string) error + DeleteBindingStub func(context.Context, string, string) error deleteBindingMutex sync.RWMutex deleteBindingArgsForCall []struct { arg1 context.Context arg2 string + arg3 string } deleteBindingReturns struct { result1 error @@ -58,11 +59,12 @@ type FakeSpaceClient struct { deleteBindingReturnsOnCall map[int]struct { result1 error } - DeleteInstanceStub func(context.Context, string) error + DeleteInstanceStub func(context.Context, string, string) error deleteInstanceMutex sync.RWMutex deleteInstanceArgsForCall []struct { arg1 context.Context arg2 string + arg3 string } deleteInstanceReturns struct { result1 error @@ -70,6 +72,18 @@ type FakeSpaceClient struct { deleteInstanceReturnsOnCall map[int]struct { result1 error } + FillBindingDetailsStub func(context.Context, *facade.Binding) error + fillBindingDetailsMutex sync.RWMutex + fillBindingDetailsArgsForCall []struct { + arg1 context.Context + arg2 *facade.Binding + } + fillBindingDetailsReturns struct { + result1 error + } + fillBindingDetailsReturnsOnCall map[int]struct { + result1 error + } FindServicePlanStub func(context.Context, string, string, string) (string, error) findServicePlanMutex sync.RWMutex findServicePlanArgsForCall []struct { @@ -114,13 +128,14 @@ type FakeSpaceClient struct { result1 *facade.Instance result2 error } - UpdateBindingStub func(context.Context, string, int64, map[string]interface{}) error + UpdateBindingStub func(context.Context, string, string, int64, map[string]interface{}) error updateBindingMutex sync.RWMutex updateBindingArgsForCall []struct { arg1 context.Context arg2 string - arg3 int64 - arg4 map[string]interface{} + arg3 string + arg4 int64 + arg5 map[string]interface{} } updateBindingReturns struct { result1 error @@ -128,16 +143,17 @@ type FakeSpaceClient struct { updateBindingReturnsOnCall map[int]struct { result1 error } - UpdateInstanceStub func(context.Context, string, string, string, map[string]interface{}, []string, int64) error + UpdateInstanceStub func(context.Context, string, string, string, string, map[string]interface{}, []string, int64) error updateInstanceMutex sync.RWMutex updateInstanceArgsForCall []struct { arg1 context.Context arg2 string arg3 string arg4 string - arg5 map[string]interface{} - arg6 []string - arg7 int64 + arg5 string + arg6 map[string]interface{} + arg7 []string + arg8 int64 } updateInstanceReturns struct { result1 error @@ -287,19 +303,20 @@ func (fake *FakeSpaceClient) CreateInstanceReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeSpaceClient) DeleteBinding(arg1 context.Context, arg2 string) error { +func (fake *FakeSpaceClient) DeleteBinding(arg1 context.Context, arg2 string, arg3 string) error { fake.deleteBindingMutex.Lock() ret, specificReturn := fake.deleteBindingReturnsOnCall[len(fake.deleteBindingArgsForCall)] fake.deleteBindingArgsForCall = append(fake.deleteBindingArgsForCall, struct { arg1 context.Context arg2 string - }{arg1, arg2}) + arg3 string + }{arg1, arg2, arg3}) stub := fake.DeleteBindingStub fakeReturns := fake.deleteBindingReturns - fake.recordInvocation("DeleteBinding", []interface{}{arg1, arg2}) + fake.recordInvocation("DeleteBinding", []interface{}{arg1, arg2, arg3}) fake.deleteBindingMutex.Unlock() if stub != nil { - return stub(arg1, arg2) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1 @@ -313,17 +330,17 @@ func (fake *FakeSpaceClient) DeleteBindingCallCount() int { return len(fake.deleteBindingArgsForCall) } -func (fake *FakeSpaceClient) DeleteBindingCalls(stub func(context.Context, string) error) { +func (fake *FakeSpaceClient) DeleteBindingCalls(stub func(context.Context, string, string) error) { fake.deleteBindingMutex.Lock() defer fake.deleteBindingMutex.Unlock() fake.DeleteBindingStub = stub } -func (fake *FakeSpaceClient) DeleteBindingArgsForCall(i int) (context.Context, string) { +func (fake *FakeSpaceClient) DeleteBindingArgsForCall(i int) (context.Context, string, string) { fake.deleteBindingMutex.RLock() defer fake.deleteBindingMutex.RUnlock() argsForCall := fake.deleteBindingArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } func (fake *FakeSpaceClient) DeleteBindingReturns(result1 error) { @@ -349,19 +366,20 @@ func (fake *FakeSpaceClient) DeleteBindingReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeSpaceClient) DeleteInstance(arg1 context.Context, arg2 string) error { +func (fake *FakeSpaceClient) DeleteInstance(arg1 context.Context, arg2 string, arg3 string) error { fake.deleteInstanceMutex.Lock() ret, specificReturn := fake.deleteInstanceReturnsOnCall[len(fake.deleteInstanceArgsForCall)] fake.deleteInstanceArgsForCall = append(fake.deleteInstanceArgsForCall, struct { arg1 context.Context arg2 string - }{arg1, arg2}) + arg3 string + }{arg1, arg2, arg3}) stub := fake.DeleteInstanceStub fakeReturns := fake.deleteInstanceReturns - fake.recordInvocation("DeleteInstance", []interface{}{arg1, arg2}) + fake.recordInvocation("DeleteInstance", []interface{}{arg1, arg2, arg3}) fake.deleteInstanceMutex.Unlock() if stub != nil { - return stub(arg1, arg2) + return stub(arg1, arg2, arg3) } if specificReturn { return ret.result1 @@ -375,17 +393,17 @@ func (fake *FakeSpaceClient) DeleteInstanceCallCount() int { return len(fake.deleteInstanceArgsForCall) } -func (fake *FakeSpaceClient) DeleteInstanceCalls(stub func(context.Context, string) error) { +func (fake *FakeSpaceClient) DeleteInstanceCalls(stub func(context.Context, string, string) error) { fake.deleteInstanceMutex.Lock() defer fake.deleteInstanceMutex.Unlock() fake.DeleteInstanceStub = stub } -func (fake *FakeSpaceClient) DeleteInstanceArgsForCall(i int) (context.Context, string) { +func (fake *FakeSpaceClient) DeleteInstanceArgsForCall(i int) (context.Context, string, string) { fake.deleteInstanceMutex.RLock() defer fake.deleteInstanceMutex.RUnlock() argsForCall := fake.deleteInstanceArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 } func (fake *FakeSpaceClient) DeleteInstanceReturns(result1 error) { @@ -411,6 +429,68 @@ func (fake *FakeSpaceClient) DeleteInstanceReturnsOnCall(i int, result1 error) { }{result1} } +func (fake *FakeSpaceClient) FillBindingDetails(arg1 context.Context, arg2 *facade.Binding) error { + fake.fillBindingDetailsMutex.Lock() + ret, specificReturn := fake.fillBindingDetailsReturnsOnCall[len(fake.fillBindingDetailsArgsForCall)] + fake.fillBindingDetailsArgsForCall = append(fake.fillBindingDetailsArgsForCall, struct { + arg1 context.Context + arg2 *facade.Binding + }{arg1, arg2}) + stub := fake.FillBindingDetailsStub + fakeReturns := fake.fillBindingDetailsReturns + fake.recordInvocation("FillBindingDetails", []interface{}{arg1, arg2}) + fake.fillBindingDetailsMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1 + } + return fakeReturns.result1 +} + +func (fake *FakeSpaceClient) FillBindingDetailsCallCount() int { + fake.fillBindingDetailsMutex.RLock() + defer fake.fillBindingDetailsMutex.RUnlock() + return len(fake.fillBindingDetailsArgsForCall) +} + +func (fake *FakeSpaceClient) FillBindingDetailsCalls(stub func(context.Context, *facade.Binding) error) { + fake.fillBindingDetailsMutex.Lock() + defer fake.fillBindingDetailsMutex.Unlock() + fake.FillBindingDetailsStub = stub +} + +func (fake *FakeSpaceClient) FillBindingDetailsArgsForCall(i int) (context.Context, *facade.Binding) { + fake.fillBindingDetailsMutex.RLock() + defer fake.fillBindingDetailsMutex.RUnlock() + argsForCall := fake.fillBindingDetailsArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *FakeSpaceClient) FillBindingDetailsReturns(result1 error) { + fake.fillBindingDetailsMutex.Lock() + defer fake.fillBindingDetailsMutex.Unlock() + fake.FillBindingDetailsStub = nil + fake.fillBindingDetailsReturns = struct { + result1 error + }{result1} +} + +func (fake *FakeSpaceClient) FillBindingDetailsReturnsOnCall(i int, result1 error) { + fake.fillBindingDetailsMutex.Lock() + defer fake.fillBindingDetailsMutex.Unlock() + fake.FillBindingDetailsStub = nil + if fake.fillBindingDetailsReturnsOnCall == nil { + fake.fillBindingDetailsReturnsOnCall = make(map[int]struct { + result1 error + }) + } + fake.fillBindingDetailsReturnsOnCall[i] = struct { + result1 error + }{result1} +} + func (fake *FakeSpaceClient) FindServicePlan(arg1 context.Context, arg2 string, arg3 string, arg4 string) (string, error) { fake.findServicePlanMutex.Lock() ret, specificReturn := fake.findServicePlanReturnsOnCall[len(fake.findServicePlanArgsForCall)] @@ -608,21 +688,22 @@ func (fake *FakeSpaceClient) GetInstanceReturnsOnCall(i int, result1 *facade.Ins }{result1, result2} } -func (fake *FakeSpaceClient) UpdateBinding(arg1 context.Context, arg2 string, arg3 int64, arg4 map[string]interface{}) error { +func (fake *FakeSpaceClient) UpdateBinding(arg1 context.Context, arg2 string, arg3 string, arg4 int64, arg5 map[string]interface{}) error { fake.updateBindingMutex.Lock() ret, specificReturn := fake.updateBindingReturnsOnCall[len(fake.updateBindingArgsForCall)] fake.updateBindingArgsForCall = append(fake.updateBindingArgsForCall, struct { arg1 context.Context arg2 string - arg3 int64 - arg4 map[string]interface{} - }{arg1, arg2, arg3, arg4}) + arg3 string + arg4 int64 + arg5 map[string]interface{} + }{arg1, arg2, arg3, arg4, arg5}) stub := fake.UpdateBindingStub fakeReturns := fake.updateBindingReturns - fake.recordInvocation("UpdateBinding", []interface{}{arg1, arg2, arg3, arg4}) + fake.recordInvocation("UpdateBinding", []interface{}{arg1, arg2, arg3, arg4, arg5}) fake.updateBindingMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4) + return stub(arg1, arg2, arg3, arg4, arg5) } if specificReturn { return ret.result1 @@ -636,17 +717,17 @@ func (fake *FakeSpaceClient) UpdateBindingCallCount() int { return len(fake.updateBindingArgsForCall) } -func (fake *FakeSpaceClient) UpdateBindingCalls(stub func(context.Context, string, int64, map[string]interface{}) error) { +func (fake *FakeSpaceClient) UpdateBindingCalls(stub func(context.Context, string, string, int64, map[string]interface{}) error) { fake.updateBindingMutex.Lock() defer fake.updateBindingMutex.Unlock() fake.UpdateBindingStub = stub } -func (fake *FakeSpaceClient) UpdateBindingArgsForCall(i int) (context.Context, string, int64, map[string]interface{}) { +func (fake *FakeSpaceClient) UpdateBindingArgsForCall(i int) (context.Context, string, string, int64, map[string]interface{}) { fake.updateBindingMutex.RLock() defer fake.updateBindingMutex.RUnlock() argsForCall := fake.updateBindingArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5 } func (fake *FakeSpaceClient) UpdateBindingReturns(result1 error) { @@ -672,11 +753,11 @@ func (fake *FakeSpaceClient) UpdateBindingReturnsOnCall(i int, result1 error) { }{result1} } -func (fake *FakeSpaceClient) UpdateInstance(arg1 context.Context, arg2 string, arg3 string, arg4 string, arg5 map[string]interface{}, arg6 []string, arg7 int64) error { - var arg6Copy []string - if arg6 != nil { - arg6Copy = make([]string, len(arg6)) - copy(arg6Copy, arg6) +func (fake *FakeSpaceClient) UpdateInstance(arg1 context.Context, arg2 string, arg3 string, arg4 string, arg5 string, arg6 map[string]interface{}, arg7 []string, arg8 int64) error { + var arg7Copy []string + if arg7 != nil { + arg7Copy = make([]string, len(arg7)) + copy(arg7Copy, arg7) } fake.updateInstanceMutex.Lock() ret, specificReturn := fake.updateInstanceReturnsOnCall[len(fake.updateInstanceArgsForCall)] @@ -685,16 +766,17 @@ func (fake *FakeSpaceClient) UpdateInstance(arg1 context.Context, arg2 string, a arg2 string arg3 string arg4 string - arg5 map[string]interface{} - arg6 []string - arg7 int64 - }{arg1, arg2, arg3, arg4, arg5, arg6Copy, arg7}) + arg5 string + arg6 map[string]interface{} + arg7 []string + arg8 int64 + }{arg1, arg2, arg3, arg4, arg5, arg6, arg7Copy, arg8}) stub := fake.UpdateInstanceStub fakeReturns := fake.updateInstanceReturns - fake.recordInvocation("UpdateInstance", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6Copy, arg7}) + fake.recordInvocation("UpdateInstance", []interface{}{arg1, arg2, arg3, arg4, arg5, arg6, arg7Copy, arg8}) fake.updateInstanceMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3, arg4, arg5, arg6, arg7) + return stub(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8) } if specificReturn { return ret.result1 @@ -708,17 +790,17 @@ func (fake *FakeSpaceClient) UpdateInstanceCallCount() int { return len(fake.updateInstanceArgsForCall) } -func (fake *FakeSpaceClient) UpdateInstanceCalls(stub func(context.Context, string, string, string, map[string]interface{}, []string, int64) error) { +func (fake *FakeSpaceClient) UpdateInstanceCalls(stub func(context.Context, string, string, string, string, map[string]interface{}, []string, int64) error) { fake.updateInstanceMutex.Lock() defer fake.updateInstanceMutex.Unlock() fake.UpdateInstanceStub = stub } -func (fake *FakeSpaceClient) UpdateInstanceArgsForCall(i int) (context.Context, string, string, string, map[string]interface{}, []string, int64) { +func (fake *FakeSpaceClient) UpdateInstanceArgsForCall(i int) (context.Context, string, string, string, string, map[string]interface{}, []string, int64) { fake.updateInstanceMutex.RLock() defer fake.updateInstanceMutex.RUnlock() argsForCall := fake.updateInstanceArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6, argsForCall.arg7 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4, argsForCall.arg5, argsForCall.arg6, argsForCall.arg7, argsForCall.arg8 } func (fake *FakeSpaceClient) UpdateInstanceReturns(result1 error) { @@ -755,6 +837,8 @@ func (fake *FakeSpaceClient) Invocations() map[string][][]interface{} { defer fake.deleteBindingMutex.RUnlock() fake.deleteInstanceMutex.RLock() defer fake.deleteInstanceMutex.RUnlock() + fake.fillBindingDetailsMutex.RLock() + defer fake.fillBindingDetailsMutex.RUnlock() fake.findServicePlanMutex.RLock() defer fake.findServicePlanMutex.RUnlock() fake.getBindingMutex.RLock() diff --git a/internal/facade/facadefakes/fake_space_health_checker.go b/internal/facade/facadefakes/fake_space_health_checker.go index 0ac62e2..81f5bad 100644 --- a/internal/facade/facadefakes/fake_space_health_checker.go +++ b/internal/facade/facadefakes/fake_space_health_checker.go @@ -13,10 +13,11 @@ import ( ) type FakeSpaceHealthChecker struct { - CheckStub func(context.Context) error + CheckStub func(context.Context, string) error checkMutex sync.RWMutex checkArgsForCall []struct { arg1 context.Context + arg2 string } checkReturns struct { result1 error @@ -28,18 +29,19 @@ type FakeSpaceHealthChecker struct { invocationsMutex sync.RWMutex } -func (fake *FakeSpaceHealthChecker) Check(arg1 context.Context) error { +func (fake *FakeSpaceHealthChecker) Check(arg1 context.Context, arg2 string) error { fake.checkMutex.Lock() ret, specificReturn := fake.checkReturnsOnCall[len(fake.checkArgsForCall)] fake.checkArgsForCall = append(fake.checkArgsForCall, struct { arg1 context.Context - }{arg1}) + arg2 string + }{arg1, arg2}) stub := fake.CheckStub fakeReturns := fake.checkReturns - fake.recordInvocation("Check", []interface{}{arg1}) + fake.recordInvocation("Check", []interface{}{arg1, arg2}) fake.checkMutex.Unlock() if stub != nil { - return stub(arg1) + return stub(arg1, arg2) } if specificReturn { return ret.result1 @@ -53,17 +55,17 @@ func (fake *FakeSpaceHealthChecker) CheckCallCount() int { return len(fake.checkArgsForCall) } -func (fake *FakeSpaceHealthChecker) CheckCalls(stub func(context.Context) error) { +func (fake *FakeSpaceHealthChecker) CheckCalls(stub func(context.Context, string) error) { fake.checkMutex.Lock() defer fake.checkMutex.Unlock() fake.CheckStub = stub } -func (fake *FakeSpaceHealthChecker) CheckArgsForCall(i int) context.Context { +func (fake *FakeSpaceHealthChecker) CheckArgsForCall(i int) (context.Context, string) { fake.checkMutex.RLock() defer fake.checkMutex.RUnlock() argsForCall := fake.checkArgsForCall[i] - return argsForCall.arg1 + return argsForCall.arg1, argsForCall.arg2 } func (fake *FakeSpaceHealthChecker) CheckReturns(result1 error) { diff --git a/internal/facade/health.go b/internal/facade/health.go index 394ba6d..b525838 100644 --- a/internal/facade/health.go +++ b/internal/facade/health.go @@ -5,11 +5,15 @@ SPDX-License-Identifier: Apache-2.0 package facade -import "context" +import ( + "context" + + "github.com/sap/cf-service-operator/internal/config" +) //counterfeiter:generate . SpaceHealthChecker type SpaceHealthChecker interface { - Check(ctx context.Context) error + Check(ctx context.Context, owner string) error } -type SpaceHealthCheckerBuilder func(string, string, string, string) (SpaceHealthChecker, error) +type SpaceHealthCheckerBuilder func(string, string, string, string, *config.Config) (SpaceHealthChecker, error) diff --git a/main.go b/main.go index 825f142..ea7038f 100644 --- a/main.go +++ b/main.go @@ -28,6 +28,7 @@ import ( cfv1alpha1 "github.com/sap/cf-service-operator/api/v1alpha1" "github.com/sap/cf-service-operator/internal/cf" + "github.com/sap/cf-service-operator/internal/config" "github.com/sap/cf-service-operator/internal/controllers" // +kubebuilder:scaffold:imports ) @@ -98,7 +99,11 @@ func main() { setupLog.Error(err, "unable to parse webhook bind address", "controller", "Space") os.Exit(1) } - + cfg, err := config.Load() + if err != nil { + setupLog.Error(err, "failed to load config") + os.Exit(1) + } options := ctrl.Options{ Scheme: scheme, // TODO: disable cache for further resources (e.g. secrets) ? @@ -140,6 +145,7 @@ func main() { ClusterResourceNamespace: clusterResourceNamespace, ClientBuilder: cf.NewOrganizationClient, HealthCheckerBuilder: cf.NewSpaceHealthChecker, + Config: cfg, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Space") os.Exit(1) @@ -151,6 +157,7 @@ func main() { ClusterResourceNamespace: clusterResourceNamespace, ClientBuilder: cf.NewOrganizationClient, HealthCheckerBuilder: cf.NewSpaceHealthChecker, + Config: cfg, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ClusterSpace") os.Exit(1) @@ -160,6 +167,7 @@ func main() { Scheme: mgr.GetScheme(), ClusterResourceNamespace: clusterResourceNamespace, ClientBuilder: cf.NewSpaceClient, + Config: cfg, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ServiceInstance") os.Exit(1) @@ -170,6 +178,7 @@ func main() { ClusterResourceNamespace: clusterResourceNamespace, EnableBindingMetadata: enableBindingMetadata, ClientBuilder: cf.NewSpaceClient, + Config: cfg, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "ServiceBinding") os.Exit(1) diff --git a/website/content/en/docs/configuration/operator.md b/website/content/en/docs/configuration/operator.md index f6f44f0..376942a 100644 --- a/website/content/en/docs/configuration/operator.md +++ b/website/content/en/docs/configuration/operator.md @@ -9,9 +9,9 @@ description: > ## Command line parameters -cf-service-operator accepts the following command line flags: +The `cf-service-operator` accepts the following command line flags: -``` +```bash Usage of manager: -cluster-resource-namespace string The namespace for secrets in which cluster-scoped resources are found. @@ -47,7 +47,9 @@ Usage of manager: ``` Notes: -- When running in-cluster, then `-cluster-resource-namespace` defaults to the operator's namespace; otherwise this flag is mandatory. + +- When running in-cluster, then `-cluster-resource-namespace` defaults to the operator's namespace; + otherwise this flag is mandatory. - The logic for looking up the kubeconfig file is - path provided as `-kubeconfig` (if present) - value of environment variable `$KUBECONFIG`(if present) @@ -62,11 +64,67 @@ Notes: ## Environment variables -cf-service-operator honors the following environment variables: +The `cf-service-operator` honors the following environment variables: + +### Kubernetes Configuration + +- `$KUBECONFIG` the path to the kubeconfig used by the operator executable. \ + Note, that this has lower precedence than the command line flag `-kubeconfig`. + +### Cache Configuration + +To optimize the usage of CF resources and reduce the number of API calls, the CF service operator +supports an optional caching mechanism. This feature allows resources to be stored in memory and +refreshed based on a configurable timeout. \ +By storing the CF resources in memory, we aim to reduce the number of requests to the CF API and +avoid reaching the rate limit. + +Below are the environment variables that control the caching behavior: -- `$KUBECONFIG` the path to the kubeconfig used by the operator executable; note that this has lower precedence than the command line flag `-kubeconfig`. +- **`RESOURCE_CACHE_ENABLED`** + - Description: Determines whether the caching mechanism is enabled or disabled. + - Type: Boolean + - Default: `false` + - Values: + - `true`: Enables caching of CF resources. + - `false`: Disables the cache, and the operator will fetch CF resources directly from the + CF API on each request. + +- **`CACHE_TIMEOUT`** + - Description: This defines the duration after which the cache is refreshed. + The cache is refreshed based on the last time it was refreshed. + - Type: String + - Default: `1m` (1 minute) + - Values: + - The timeout can be specified in seconds (`s`), minutes (`m`), or hours (`h`). \ + For example: + - `30s` for 30 seconds + - `10m` for 10 minutes + - `1h` for 1 hour. + +These environment variables can be configured in your `deployment.yaml` file as follows: + +```yaml + env: + - name: CACHE_TIMEOUT + value: "{{ .Values.cache.timeout }}" + - name: RESOURCE_CACHE_ENABLED + value: "{{ .Values.cache.enabled }}" +``` + +Additionally, the corresponding values can be set in the `values.yaml` file of the [helm chart](https://github.com/SAP/cf-service-operator-helm/blob/main/chart/values.yaml), allowing the operator to be easily configured: + +```yaml +# -- Enable Resources Cache +cache: + # -- Whether to enable the cache + enabled: false # default: false + # -- Cache expiration time + timeout: 1m # default: 1m +``` ## Logging -cf-service-operator uses [logr](https://github.com/go-logr) with [zap](https://github.com/uber-go/zap) for logging. +The `cf-service-operator` uses [logr](https://github.com/go-logr) with +[zap](https://github.com/uber-go/zap) for logging. Please check the according documentation for details about how to configure logging.