From 3d152c611aed3ac851d7cc9ec9c232c9aee22a8a Mon Sep 17 00:00:00 2001 From: santiago-ventura <160760719+santiago-ventura@users.noreply.github.com> Date: Tue, 25 Jun 2024 18:28:40 +0200 Subject: [PATCH] Adopting Orphan Instances via Kubernetes Annotation (#50) * Initial commit for adopting orphan instances in cf * Adoption for the existing service bindings * update for adopting instance annotation * Change GetInstance by name or by owner. Update CF instance with label and annotation. * new branch for orphan instances adoption * revert to original defer function * implement for orphan service binding * Update markdown documentation with the new annotation, adopt-instances. * Modify GetInstance and GetBinding functions based on PR comments * Add changes based or reviewer comments * changes for comments on listoptions * Update the documentation * Changes to getinstance call in clinet_test.go file * Update generated deepcopy go * Update adopt.md file following comment from reviewer --------- Co-authored-by: shilparamasamyreddy <164521358+shilparamasamyreddy@users.noreply.github.com> --- api/v1alpha1/constants.go | 3 + internal/cf/binding.go | 61 +++++++++++++-- internal/cf/client_test.go | 10 +-- internal/cf/instance.go | 58 ++++++++++++-- .../controllers/servicebinding_controller.go | 46 ++++++++++- .../controllers/serviceinstance_controller.go | 45 ++++++++++- internal/facade/client.go | 6 +- .../facade/facadefakes/fake_space_client.go | 42 +++++----- website/content/en/docs/tutorials/adopt.md | 77 +++++++++++-------- 9 files changed, 272 insertions(+), 76 deletions(-) diff --git a/api/v1alpha1/constants.go b/api/v1alpha1/constants.go index 5e4d199..97f8758 100644 --- a/api/v1alpha1/constants.go +++ b/api/v1alpha1/constants.go @@ -17,4 +17,7 @@ const ( AnnotationMaxRetries = "service-operator.cf.cs.sap.com/max-retries" // annotation to hold the reconciliation timeout value AnnotationReconcileTimeout = "service-operator.cf.cs.sap.com/timeout-on-reconcile" + // annotation to adopt orphan CF resources. If set to 'adopt', the operator will adopt orphan CF resource. + // Ex. "service-operator.cf.cs.sap.com/adopt-cf-resources"="adopt" + AnnotationAdoptCFResources = "service-operator.cf.cs.sap.com/adopt-cf-resources" ) diff --git a/internal/cf/binding.go b/internal/cf/binding.go index e46c043..f7f66c2 100644 --- a/internal/cf/binding.go +++ b/internal/cf/binding.go @@ -18,21 +18,64 @@ import ( "github.com/sap/cf-service-operator/internal/facade" ) -func (c *spaceClient) GetBinding(ctx context.Context, owner string) (*facade.Binding, error) { +type bindingFilter interface { + getListOptions() *cfclient.ServiceCredentialBindingListOptions +} + +type bindingFilterName struct { + name string +} +type bindingFilterOwner struct { + owner string +} + +func (bn *bindingFilterName) getListOptions() *cfclient.ServiceCredentialBindingListOptions { listOpts := cfclient.NewServiceCredentialBindingListOptions() - listOpts.LabelSelector.EqualTo(labelPrefix + "/" + labelKeyOwner + "=" + owner) + listOpts.Names.EqualTo(bn.name) + return listOpts +} + +func (bo *bindingFilterOwner) getListOptions() *cfclient.ServiceCredentialBindingListOptions { + listOpts := cfclient.NewServiceCredentialBindingListOptions() + listOpts.LabelSelector.EqualTo(fmt.Sprintf("%s/%s=%s", labelPrefix, labelKeyOwner, bo.owner)) + return listOpts +} + +// GetBinding returns the binding with the given bindingOpts["owner"] or bindingOpts["name"]. +// If bindingOpts["name"] is empty, the binding with the given bindingOpts["owner"] is returned. +// If bindingOpts["name"] is not empty, the binding with the given Name is returned for orphan bindings. +// If no binding is found, nil is returned. +// 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) { + var filterOpts bindingFilter + if bindingOpts["name"] != "" { + filterOpts = &bindingFilterName{name: bindingOpts["name"]} + } else { + filterOpts = &bindingFilterOwner{owner: bindingOpts["owner"]} + } + listOpts := filterOpts.getListOptions() serviceBindings, err := c.client.ServiceCredentialBindings.ListAll(ctx, listOpts) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list service credential bindings: %w", err) } if len(serviceBindings) == 0 { return nil, nil } else if len(serviceBindings) > 1 { - return nil, fmt.Errorf("found multiple service bindings with owner: %s", owner) + 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 + if bindingOpts["name"] != "" { + generationvalue := "0" + serviceBinding.Metadata.Annotations[annotationGeneration] = &generationvalue + parameterHashValue := "0" + serviceBinding.Metadata.Annotations[annotationParameterHash] = ¶meterHashValue + } + guid := serviceBinding.GUID name := serviceBinding.Name generation, err := strconv.ParseInt(*serviceBinding.Metadata.Annotations[annotationGeneration], 10, 64) @@ -71,7 +114,7 @@ func (c *spaceClient) GetBinding(ctx context.Context, owner string) (*facade.Bin return &facade.Binding{ Guid: guid, Name: name, - Owner: owner, + Owner: bindingOpts["owner"], Generation: generation, ParameterHash: parameterHash, State: state, @@ -101,12 +144,18 @@ 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) error { +func (c *spaceClient) UpdateBinding(ctx context.Context, guid string, generation int64, parameters map[string]interface{}) error { // TODO: why is there no cfresource.NewServiceCredentialBindingUpdate() method ? req := &cfresource.ServiceCredentialBindingUpdate{} req.Metadata = cfresource.NewMetadata(). WithAnnotation(annotationPrefix, annotationKeyGeneration, strconv.FormatInt(generation, 10)) + if parameters != nil { + req.Metadata.WithAnnotation(annotationPrefix, annotationKeyParameterHash, facade.ObjectHash(parameters)) + if parameters["owner"] != nil { + req.Metadata.WithLabel(labelPrefix, labelKeyOwner, parameters["owner"].(string)) + } + } _, err := c.client.ServiceCredentialBindings.Update(ctx, guid, req) return err } diff --git a/internal/cf/client_test.go b/internal/cf/client_test.go index b16d56f..a1fe747 100644 --- a/internal/cf/client_test.go +++ b/internal/cf/client_test.go @@ -220,7 +220,7 @@ var _ = Describe("CF Client tests", Ordered, func() { spaceClient, err := NewSpaceClient(OrgName, url, Username, Password) Expect(err).To(BeNil()) - spaceClient.GetInstance(ctx, Owner) + spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) // Discover UAA endpoint Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) @@ -239,10 +239,10 @@ var _ = Describe("CF Client tests", Ordered, func() { spaceClient, err := NewSpaceClient(OrgName, url, Username, Password) Expect(err).To(BeNil()) - spaceClient.GetInstance(ctx, Owner) + spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) spaceClient, err = NewSpaceClient(OrgName, url, Username, Password) Expect(err).To(BeNil()) - spaceClient.GetInstance(ctx, Owner) + spaceClient.GetInstance(ctx, map[string]string{"owner": Owner}) // Discover UAA endpoint Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) @@ -265,7 +265,7 @@ var _ = Describe("CF Client tests", Ordered, func() { // test space 1 spaceClient1, err1 := NewSpaceClient(SpaceName, url, Username, Password) Expect(err1).To(BeNil()) - spaceClient1.GetInstance(ctx, Owner) + spaceClient1.GetInstance(ctx, map[string]string{"owner": Owner}) // Discover UAA endpoint Expect(server.ReceivedRequests()[0].Method).To(Equal("GET")) Expect(server.ReceivedRequests()[0].URL.Path).To(Equal("/")) @@ -279,7 +279,7 @@ var _ = Describe("CF Client tests", Ordered, func() { // test space 2 spaceClient2, err2 := NewSpaceClient(SpaceName2, url, Username, Password) Expect(err2).To(BeNil()) - spaceClient2.GetInstance(ctx, Owner2) + spaceClient2.GetInstance(ctx, map[string]string{"owner": Owner2}) // no discovery of UAA endpoint or oAuth token here due to caching // Get instance Expect(server.ReceivedRequests()[3].Method).To(Equal("GET")) diff --git a/internal/cf/instance.go b/internal/cf/instance.go index 7dcfa03..5da95ff 100644 --- a/internal/cf/instance.go +++ b/internal/cf/instance.go @@ -18,21 +18,65 @@ import ( "github.com/sap/cf-service-operator/internal/facade" ) -func (c *spaceClient) GetInstance(ctx context.Context, owner string) (*facade.Instance, error) { +type instanceFilter interface { + getListOptions() *cfclient.ServiceInstanceListOptions +} + +type instanceFilterName struct { + name string +} +type instanceFilterOwner struct { + owner string +} + +func (in *instanceFilterName) getListOptions() *cfclient.ServiceInstanceListOptions { + listOpts := cfclient.NewServiceInstanceListOptions() + listOpts.Names.EqualTo(in.name) + return listOpts +} + +func (io *instanceFilterOwner) getListOptions() *cfclient.ServiceInstanceListOptions { listOpts := cfclient.NewServiceInstanceListOptions() - listOpts.LabelSelector.EqualTo(labelPrefix + "/" + labelKeyOwner + "=" + owner) + listOpts.LabelSelector.EqualTo(fmt.Sprintf("%s/%s=%s", labelPrefix, labelKeyOwner, io.owner)) + return listOpts +} + +// 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. +func (c *spaceClient) GetInstance(ctx context.Context, instanceOpts map[string]string) (*facade.Instance, error) { + + 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) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list service instances: %w", err) } if len(serviceInstances) == 0 { return nil, nil } else if len(serviceInstances) > 1 { - return nil, fmt.Errorf("found multiple service instances with owner: %s", owner) + 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 + if instanceOpts["name"] != "" { + generationvalue := "0" + serviceInstance.Metadata.Annotations[annotationGeneration] = &generationvalue + parameterHashValue := "0" + serviceInstance.Metadata.Annotations[annotationParameterHash] = ¶meterHashValue + } + guid := serviceInstance.GUID name := serviceInstance.Name servicePlanGuid := serviceInstance.Relationships.ServicePlan.Data.GUID @@ -70,7 +114,7 @@ func (c *spaceClient) GetInstance(ctx context.Context, owner string) (*facade.In Guid: guid, Name: name, ServicePlanGuid: servicePlanGuid, - Owner: owner, + Owner: instanceOpts["owner"], Generation: generation, ParameterHash: parameterHash, State: state, @@ -127,6 +171,10 @@ func (c *spaceClient) UpdateInstance(ctx context.Context, guid string, name stri WithAnnotation(annotationPrefix, annotationKeyGeneration, strconv.FormatInt(generation, 10)) if parameters != nil { req.Metadata.WithAnnotation(annotationPrefix, annotationKeyParameterHash, facade.ObjectHash(parameters)) + if parameters["owner"] != nil { + // Adding label to the metadata for orphan instance + req.Metadata.WithLabel(labelPrefix, labelKeyOwner, parameters["owner"].(string)) + } } _, _, err := c.client.ServiceInstances.UpdateManaged(ctx, guid, req) diff --git a/internal/controllers/servicebinding_controller.go b/internal/controllers/servicebinding_controller.go index b9099fd..0f3db34 100644 --- a/internal/controllers/servicebinding_controller.go +++ b/internal/controllers/servicebinding_controller.go @@ -174,13 +174,49 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque } } - // Retrieve cloud foundry instance + // Retrieve cloud foundry binding var cfbinding *facade.Binding + bindingOpts := map[string]string{"name": "", "owner": string(serviceBinding.UID)} if client != nil { - cfbinding, err = client.GetBinding(ctx, string(serviceBinding.UID)) + cfbinding, err = client.GetBinding(ctx, bindingOpts) if err != nil { return ctrl.Result{}, err } + orphan, exists := serviceBinding.Annotations[cfv1alpha1.AnnotationAdoptCFResources] + if exists && cfbinding == nil && orphan == "adopt" { + // find orphaned binding by name + bindingOpts["name"] = serviceBinding.Name + cfbinding, err = client.GetBinding(ctx, bindingOpts) + if err != nil { + return ctrl.Result{}, err + } + + //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("triggering update") + 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 + } } if serviceBinding.DeletionTimestamp.IsZero() { @@ -268,10 +304,12 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque } else if cfbinding.Generation < serviceBinding.Generation { // metadata updates (such as updating the generation here) are possible with service bindings log.V(1).Info("triggering update") + if err := client.UpdateBinding( ctx, cfbinding.Guid, serviceBinding.Generation, + nil, ); err != nil { return ctrl.Result{}, err } @@ -284,8 +322,8 @@ func (r *ServiceBindingReconciler) Reconcile(ctx context.Context, req ctrl.Reque status.ServiceInstanceDigest = serviceInstance.Status.ServiceInstanceDigest if cfbinding == nil { - // Re-retrieve cloud foundry binding; this happens exactly if the binding was created or updated above - cfbinding, err = client.GetBinding(ctx, string(serviceBinding.UID)) + // Re-retrieve cloud foundry binding by UID; this happens exactly if the binding was created or updated above + cfbinding, err = client.GetBinding(ctx, bindingOpts) if err != nil { return ctrl.Result{}, err } diff --git a/internal/controllers/serviceinstance_controller.go b/internal/controllers/serviceinstance_controller.go index 5b0efa0..aa9135a 100644 --- a/internal/controllers/serviceinstance_controller.go +++ b/internal/controllers/serviceinstance_controller.go @@ -192,11 +192,49 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ // Retrieve cloud foundry instance var cfinstance *facade.Instance + instanceOpts := map[string]string{"name": "", "owner": string(serviceInstance.UID)} if client != nil { - cfinstance, err = client.GetInstance(ctx, string(serviceInstance.UID)) + cfinstance, err = client.GetInstance(ctx, instanceOpts) if err != nil { return ctrl.Result{}, err } + orphan, exists := serviceInstance.Annotations[cfv1alpha1.AnnotationAdoptCFResources] + if exists && cfinstance == nil && orphan == "adopt" { + // find orphaned instance by name + instanceOpts["name"] = serviceInstance.Name + cfinstance, err = client.GetInstance(ctx, instanceOpts) + 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("triggering update") + if err := client.UpdateInstance( + ctx, + cfinstance.Guid, + spec.Name, + "", + 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 + } } if serviceInstance.DeletionTimestamp.IsZero() { @@ -243,6 +281,7 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ return ctrl.Result{}, fmt.Errorf("secret key not found, secret name: %s, key: %s", secretName, pf.SecretKeyRef.Key) } } + parameters, err := mergeObjects(parameterObjects...) if err != nil { return ctrl.Result{}, errors.Wrap(err, "failed to unmarshal/merge parameters") @@ -324,8 +363,8 @@ func (r *ServiceInstanceReconciler) Reconcile(ctx context.Context, req ctrl.Requ } if cfinstance == nil { - // Re-retrieve cloud foundry instance; this happens exactly if the instance was created or updated above - cfinstance, err = client.GetInstance(ctx, string(serviceInstance.UID)) + // Re-retrieve cloud foundry instance by UID; this happens exactly if the instance was created or updated above + cfinstance, err = client.GetInstance(ctx, instanceOpts) if err != nil { return ctrl.Result{}, err } diff --git a/internal/facade/client.go b/internal/facade/client.go index 0e89b9c..6990788 100644 --- a/internal/facade/client.go +++ b/internal/facade/client.go @@ -77,14 +77,14 @@ type OrganizationClientBuilder func(string, string, string, string) (Organizatio //counterfeiter:generate . SpaceClient type SpaceClient interface { - GetInstance(ctx context.Context, owner string) (*Instance, error) + 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 - GetBinding(ctx context.Context, owner string) (*Binding, 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) error + UpdateBinding(ctx context.Context, guid string, generation int64, parameters map[string]interface{}) error DeleteBinding(ctx context.Context, guid string) error FindServicePlan(ctx context.Context, serviceOfferingName string, servicePlanName string, spaceGuid string) (string, error) diff --git a/internal/facade/facadefakes/fake_space_client.go b/internal/facade/facadefakes/fake_space_client.go index 33dc43e..5f9b070 100644 --- a/internal/facade/facadefakes/fake_space_client.go +++ b/internal/facade/facadefakes/fake_space_client.go @@ -86,11 +86,11 @@ type FakeSpaceClient struct { result1 string result2 error } - GetBindingStub func(context.Context, string) (*facade.Binding, error) + GetBindingStub func(context.Context, map[string]string) (*facade.Binding, error) getBindingMutex sync.RWMutex getBindingArgsForCall []struct { arg1 context.Context - arg2 string + arg2 map[string]string } getBindingReturns struct { result1 *facade.Binding @@ -100,11 +100,11 @@ type FakeSpaceClient struct { result1 *facade.Binding result2 error } - GetInstanceStub func(context.Context, string) (*facade.Instance, error) + GetInstanceStub func(context.Context, map[string]string) (*facade.Instance, error) getInstanceMutex sync.RWMutex getInstanceArgsForCall []struct { arg1 context.Context - arg2 string + arg2 map[string]string } getInstanceReturns struct { result1 *facade.Instance @@ -114,12 +114,13 @@ type FakeSpaceClient struct { result1 *facade.Instance result2 error } - UpdateBindingStub func(context.Context, string, int64) error + UpdateBindingStub func(context.Context, string, int64, map[string]interface{}) error updateBindingMutex sync.RWMutex updateBindingArgsForCall []struct { arg1 context.Context arg2 string arg3 int64 + arg4 map[string]interface{} } updateBindingReturns struct { result1 error @@ -477,12 +478,12 @@ func (fake *FakeSpaceClient) FindServicePlanReturnsOnCall(i int, result1 string, }{result1, result2} } -func (fake *FakeSpaceClient) GetBinding(arg1 context.Context, arg2 string) (*facade.Binding, error) { +func (fake *FakeSpaceClient) GetBinding(arg1 context.Context, arg2 map[string]string) (*facade.Binding, error) { fake.getBindingMutex.Lock() ret, specificReturn := fake.getBindingReturnsOnCall[len(fake.getBindingArgsForCall)] fake.getBindingArgsForCall = append(fake.getBindingArgsForCall, struct { arg1 context.Context - arg2 string + arg2 map[string]string }{arg1, arg2}) stub := fake.GetBindingStub fakeReturns := fake.getBindingReturns @@ -503,13 +504,13 @@ func (fake *FakeSpaceClient) GetBindingCallCount() int { return len(fake.getBindingArgsForCall) } -func (fake *FakeSpaceClient) GetBindingCalls(stub func(context.Context, string) (*facade.Binding, error)) { +func (fake *FakeSpaceClient) GetBindingCalls(stub func(context.Context, map[string]string) (*facade.Binding, error)) { fake.getBindingMutex.Lock() defer fake.getBindingMutex.Unlock() fake.GetBindingStub = stub } -func (fake *FakeSpaceClient) GetBindingArgsForCall(i int) (context.Context, string) { +func (fake *FakeSpaceClient) GetBindingArgsForCall(i int) (context.Context, map[string]string) { fake.getBindingMutex.RLock() defer fake.getBindingMutex.RUnlock() argsForCall := fake.getBindingArgsForCall[i] @@ -542,12 +543,12 @@ func (fake *FakeSpaceClient) GetBindingReturnsOnCall(i int, result1 *facade.Bind }{result1, result2} } -func (fake *FakeSpaceClient) GetInstance(arg1 context.Context, arg2 string) (*facade.Instance, error) { +func (fake *FakeSpaceClient) GetInstance(arg1 context.Context, arg2 map[string]string) (*facade.Instance, error) { fake.getInstanceMutex.Lock() ret, specificReturn := fake.getInstanceReturnsOnCall[len(fake.getInstanceArgsForCall)] fake.getInstanceArgsForCall = append(fake.getInstanceArgsForCall, struct { arg1 context.Context - arg2 string + arg2 map[string]string }{arg1, arg2}) stub := fake.GetInstanceStub fakeReturns := fake.getInstanceReturns @@ -568,13 +569,13 @@ func (fake *FakeSpaceClient) GetInstanceCallCount() int { return len(fake.getInstanceArgsForCall) } -func (fake *FakeSpaceClient) GetInstanceCalls(stub func(context.Context, string) (*facade.Instance, error)) { +func (fake *FakeSpaceClient) GetInstanceCalls(stub func(context.Context, map[string]string) (*facade.Instance, error)) { fake.getInstanceMutex.Lock() defer fake.getInstanceMutex.Unlock() fake.GetInstanceStub = stub } -func (fake *FakeSpaceClient) GetInstanceArgsForCall(i int) (context.Context, string) { +func (fake *FakeSpaceClient) GetInstanceArgsForCall(i int) (context.Context, map[string]string) { fake.getInstanceMutex.RLock() defer fake.getInstanceMutex.RUnlock() argsForCall := fake.getInstanceArgsForCall[i] @@ -607,20 +608,21 @@ func (fake *FakeSpaceClient) GetInstanceReturnsOnCall(i int, result1 *facade.Ins }{result1, result2} } -func (fake *FakeSpaceClient) UpdateBinding(arg1 context.Context, arg2 string, arg3 int64) error { +func (fake *FakeSpaceClient) UpdateBinding(arg1 context.Context, arg2 string, arg3 int64, arg4 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 - }{arg1, arg2, arg3}) + arg4 map[string]interface{} + }{arg1, arg2, arg3, arg4}) stub := fake.UpdateBindingStub fakeReturns := fake.updateBindingReturns - fake.recordInvocation("UpdateBinding", []interface{}{arg1, arg2, arg3}) + fake.recordInvocation("UpdateBinding", []interface{}{arg1, arg2, arg3, arg4}) fake.updateBindingMutex.Unlock() if stub != nil { - return stub(arg1, arg2, arg3) + return stub(arg1, arg2, arg3, arg4) } if specificReturn { return ret.result1 @@ -634,17 +636,17 @@ func (fake *FakeSpaceClient) UpdateBindingCallCount() int { return len(fake.updateBindingArgsForCall) } -func (fake *FakeSpaceClient) UpdateBindingCalls(stub func(context.Context, string, int64) error) { +func (fake *FakeSpaceClient) UpdateBindingCalls(stub func(context.Context, 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) { +func (fake *FakeSpaceClient) UpdateBindingArgsForCall(i int) (context.Context, string, int64, map[string]interface{}) { fake.updateBindingMutex.RLock() defer fake.updateBindingMutex.RUnlock() argsForCall := fake.updateBindingArgsForCall[i] - return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3 + return argsForCall.arg1, argsForCall.arg2, argsForCall.arg3, argsForCall.arg4 } func (fake *FakeSpaceClient) UpdateBindingReturns(result1 error) { diff --git a/website/content/en/docs/tutorials/adopt.md b/website/content/en/docs/tutorials/adopt.md index 64142fb..c94a564 100644 --- a/website/content/en/docs/tutorials/adopt.md +++ b/website/content/en/docs/tutorials/adopt.md @@ -9,15 +9,15 @@ description: > ## Adopt service instances -In order to adopt an existing Cloud Foundry instance, create a Kubernetes ServiceInstance object which specifies `spec.name` -with the name of the existing Cloud Foundry instance, and provide offering, plan, parameters and tags matching the current state. +To adopt an existing Cloud Foundry instance, create a Kubernetes ServiceInstance object that specifies the `spec.name` with the name of the existing Cloud Foundry instance and provides the offering, plans, parameters, and tags matching the current state. + Such as: ```yaml apiVersion: cf.cs.sap.com/v1alpha1 kind: ServiceInstance metadata: - name: uaa + name: example-instance namespace: demo spec: # Name of a Space object in the same namespace; @@ -26,7 +26,7 @@ spec: # Name of service offering to be used serviceOfferingName: xsuaa # Name of service plan to be used - servicePlanName: application + servicePlanName: standard # Explicitly specify the name of the Cloud Foundry instance name: # Current paremeters (if any) @@ -37,32 +37,49 @@ spec: ... ``` -After deploying this object, it will enter an error state, complaining that an instance with the same name already exists in Cloud Foundry, -but is not managed by the controller. To solve this, update the Cloud Foundry metadata of the existing instance; more information about how this -controller leverages Cloud Foundry metadata can be found [here](../../concepts/cfmetadata). The update can e.g. be done with the cf command line client: - -```bash -cat > /tmp/patch <" - }, - "annotations": { - "service-operator.cf.cs.sap.com/generation": "0", - "service-operator.cf.cs.sap.com/parameter-hash": "0" - } - } -} -END -cf curl -X PATCH -H "Content-Type: application/json" /v3/service_instances/ -d @/tmp/patch -``` -More information about this Cloud Foundry API call can be found [here](https://v3-apidocs.cloudfoundry.org/version/3.113.0/index.html#update-a-service-instance). -After some time the controller will consider the instance as managed. +After deploying this object, it will enter an error state, complaining that an instance with the same name already exists in Cloud Foundry, but is not managed by the controller. + +Check the status of the Instance. The following error is expected: +`cfclient error (CF-UnprocessableEntity|10008): The service instance name is taken` + +To solve this, the Cloud Foundry metadata of the existing instance must be updated. + +>More information about how this controller leverages Cloud Foundry metadata can be found [here](../../concepts/cfmetadata). + +The CF Service Operator provides a way to adopt orphan instances via a Kubernetes Annotation. + +### Using the annotations adopt-cf-resources -## Adopt service bindings +An automated way of adopting Cloud Foundry instance is via the Kuberneste annotation `service-operator.cf.cs.sap.com/adopt-cf-resources`. -Works analogously. +During the reconciliation of an orphan ServiceInstance and ServiceBinding custom resource, the controller will check if this annotation is present. If the annotation is found then the controller will try to update the Cloud Foundry instance with label `service-operator.cf.cs.sap.com/owner`, and the annotations `service-operator.cf.cs.sap.com/generation` and `service-operator.cf.cs.sap.com/parameter-hash` + +Here's an example of how to use this annotation in a ServiceInstance and ServiceBinding: + +```yaml +apiVersion: cf.cs.sap.com/v1alpha1 +kind: ServiceInstance +metadata: + name: example-instance + namespace: demo + annotations: + service-operator.cf.cs.sap.com/adopt-cf-resources: "adopt" +spec: + spaceName: k8s + serviceOfferingName: xsuaa + servicePlanName: standard +``` + +```yaml +apiVersion: cf.cs.sap.com/v1alpha1 +kind: ServiceBinding +metadata: + name: example-binding-instance + namespace: demo + annotations: + service-operator.cf.cs.sap.com/adopt-cf-resources: "adopt" +spec: + serviceInstanceName: example-instance +``` -The according Cloud Foundry API endpoint is `/v3/service_credential_bindings`. -More information can be found [here](https://v3-apidocs.cloudfoundry.org/version/3.113.0/index.html#update-a-service-credential-binding). \ No newline at end of file +After some time the controller will consider the ServiceInstance and ServiceBinding as managed.