From a596cfd99731e5a65663b64eb43610978c0b3a8d Mon Sep 17 00:00:00 2001 From: Georgi Sabev Date: Tue, 12 Nov 2024 13:44:55 +0000 Subject: [PATCH] Implement instances last operation Previously we were setting the ProvisioningFailedCondition to true if the broker returned error on provision, no matter the error. The problem with doing this is that the error can be recoverable, such as the network flaking, but the instance is sent to a state from which it cannot recover (When the ProvisioningFailedCondition is set the reconciler gives up early in the reconciliation loop). This change fixes the problem described above by defining a new UnrecoverableError type in the osbapi client. The controller would then set the ProvisioningFailedCondition only if the error it received is an UnrecoverableError. Also LastOperation field is added in CFServiceInstance and there is a logic implemented in the controllers about the different states and this last operation information is propagated to the presenter logic According to the OSBAPI spec unrecoverable perovision error codes are 400, 409 and 422. Co-authored-by: Danail Branekov WIP Co-authored-by: Danail Branekov WIP Co-authored-by: Danail Branekov --- api/presenter/service_instance.go | 11 +-- api/presenter/service_instance_test.go | 16 ++-- .../service_instance_repository.go | 55 +++++++------ .../service_instance_repository_test.go | 45 +++++++++++ .../api/v1alpha1/cfserviceinstance_types.go | 4 + .../api/v1alpha1/zz_generated.deepcopy.go | 1 + .../services/instances/managed/controller.go | 13 ++- .../instances/managed/controller_test.go | 56 +++++++++++++ .../services/instances/upsi/controller.go | 26 +++++- .../instances/upsi/controller_test.go | 81 ++++++++++++++----- ...i.cloudfoundry.org_cfserviceinstances.yaml | 21 +++++ helm/korifi/controllers/role.yaml | 10 +-- model/services/instances.go | 11 +++ 13 files changed, 274 insertions(+), 76 deletions(-) create mode 100644 model/services/instances.go diff --git a/api/presenter/service_instance.go b/api/presenter/service_instance.go index a9d653e02..629fccdf7 100644 --- a/api/presenter/service_instance.go +++ b/api/presenter/service_instance.go @@ -45,11 +45,6 @@ type ServiceInstanceLinks struct { } func ForServiceInstance(serviceInstanceRecord repositories.ServiceInstanceRecord, baseURL url.URL, includes ...model.IncludedResource) ServiceInstanceResponse { - lastOperationType := "update" - if serviceInstanceRecord.UpdatedAt == nil || serviceInstanceRecord.CreatedAt.Equal(*serviceInstanceRecord.UpdatedAt) { - lastOperationType = "create" - } - return ServiceInstanceResponse{ Name: serviceInstanceRecord.Name, GUID: serviceInstanceRecord.GUID, @@ -58,9 +53,9 @@ func ForServiceInstance(serviceInstanceRecord repositories.ServiceInstanceRecord LastOperation: lastOperation{ CreatedAt: formatTimestamp(&serviceInstanceRecord.CreatedAt), UpdatedAt: formatTimestamp(serviceInstanceRecord.UpdatedAt), - Description: "Operation succeeded", - State: "succeeded", - Type: lastOperationType, + Description: serviceInstanceRecord.LastOperation.Description, + State: serviceInstanceRecord.LastOperation.State, + Type: serviceInstanceRecord.LastOperation.Type, }, CreatedAt: formatTimestamp(&serviceInstanceRecord.CreatedAt), UpdatedAt: formatTimestamp(serviceInstanceRecord.UpdatedAt), diff --git a/api/presenter/service_instance_test.go b/api/presenter/service_instance_test.go index 31880c6ba..8898e5606 100644 --- a/api/presenter/service_instance_test.go +++ b/api/presenter/service_instance_test.go @@ -7,6 +7,7 @@ import ( "code.cloudfoundry.org/korifi/api/presenter" "code.cloudfoundry.org/korifi/api/repositories" + "code.cloudfoundry.org/korifi/model/services" . "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" . "github.com/onsi/ginkgo/v2" @@ -36,6 +37,11 @@ var _ = Describe("Service Instance", func() { Labels: map[string]string{ "foo": "bar", }, + LastOperation: services.LastOperation{ + Type: "update", + State: "succeeded", + Description: "Operation succeeded", + }, Annotations: map[string]string{ "one": "two", }, @@ -104,16 +110,6 @@ var _ = Describe("Service Instance", func() { }`)) }) - When("create and update times are the same", func() { - BeforeEach(func() { - record.UpdatedAt = &record.CreatedAt - }) - - It("sets last operation type to create", func() { - Expect(output).To(MatchJSONPath("$.last_operation.type", "create")) - }) - }) - When("labels is nil", func() { BeforeEach(func() { record.Labels = nil diff --git a/api/repositories/service_instance_repository.go b/api/repositories/service_instance_repository.go index dce09dc6f..12b08c405 100644 --- a/api/repositories/service_instance_repository.go +++ b/api/repositories/service_instance_repository.go @@ -14,6 +14,7 @@ import ( "code.cloudfoundry.org/korifi/api/repositories/compare" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/model" + "code.cloudfoundry.org/korifi/model/services" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" @@ -169,19 +170,20 @@ type DeleteServiceInstanceMessage struct { } type ServiceInstanceRecord struct { - Name string - GUID string - SpaceGUID string - PlanGUID string - SecretName string - Tags []string - Type string - Labels map[string]string - Annotations map[string]string - CreatedAt time.Time - UpdatedAt *time.Time - DeletedAt *time.Time - Ready bool + Name string + GUID string + SpaceGUID string + PlanGUID string + SecretName string + Tags []string + Type string + Labels map[string]string + Annotations map[string]string + CreatedAt time.Time + UpdatedAt *time.Time + DeletedAt *time.Time + LastOperation services.LastOperation + Ready bool } func (r ServiceInstanceRecord) Relationships() map[string]string { @@ -545,19 +547,20 @@ func (r *ServiceInstanceRepo) removeBindingsFinalizer(ctx context.Context, userC func cfServiceInstanceToRecord(cfServiceInstance korifiv1alpha1.CFServiceInstance) ServiceInstanceRecord { return ServiceInstanceRecord{ - Name: cfServiceInstance.Spec.DisplayName, - GUID: cfServiceInstance.Name, - SpaceGUID: cfServiceInstance.Namespace, - PlanGUID: cfServiceInstance.Spec.PlanGUID, - SecretName: cfServiceInstance.Spec.SecretName, - Tags: cfServiceInstance.Spec.Tags, - Type: string(cfServiceInstance.Spec.Type), - Labels: cfServiceInstance.Labels, - Annotations: cfServiceInstance.Annotations, - CreatedAt: cfServiceInstance.CreationTimestamp.Time, - UpdatedAt: getLastUpdatedTime(&cfServiceInstance), - DeletedAt: golangTime(cfServiceInstance.DeletionTimestamp), - Ready: isInstanceReady(cfServiceInstance), + Name: cfServiceInstance.Spec.DisplayName, + GUID: cfServiceInstance.Name, + SpaceGUID: cfServiceInstance.Namespace, + PlanGUID: cfServiceInstance.Spec.PlanGUID, + SecretName: cfServiceInstance.Spec.SecretName, + Tags: cfServiceInstance.Spec.Tags, + Type: string(cfServiceInstance.Spec.Type), + Labels: cfServiceInstance.Labels, + Annotations: cfServiceInstance.Annotations, + CreatedAt: cfServiceInstance.CreationTimestamp.Time, + UpdatedAt: getLastUpdatedTime(&cfServiceInstance), + DeletedAt: golangTime(cfServiceInstance.DeletionTimestamp), + LastOperation: cfServiceInstance.Status.LastOperation, + Ready: isInstanceReady(cfServiceInstance), } } diff --git a/api/repositories/service_instance_repository_test.go b/api/repositories/service_instance_repository_test.go index 610741818..3ae36c2c4 100644 --- a/api/repositories/service_instance_repository_test.go +++ b/api/repositories/service_instance_repository_test.go @@ -13,6 +13,7 @@ import ( "code.cloudfoundry.org/korifi/api/repositories/fakeawaiter" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/model" + "code.cloudfoundry.org/korifi/model/services" "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" @@ -306,6 +307,50 @@ var _ = Describe("ServiceInstanceRepository", func() { }) }) + Describe("instance record last operation", func() { + var ( + cfServiceInstance *korifiv1alpha1.CFServiceInstance + serviceInstanceRecord repositories.ServiceInstanceRecord + ) + + BeforeEach(func() { + createRoleBinding(ctx, userName, spaceDeveloperRole.Name, space.Name) + + cfServiceInstance = &korifiv1alpha1.CFServiceInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: space.Name, + }, + Spec: korifiv1alpha1.CFServiceInstanceSpec{ + Type: korifiv1alpha1.ManagedType, + }, + } + Expect(k8sClient.Create(ctx, cfServiceInstance)).To(Succeed()) + + Expect(k8s.Patch(ctx, k8sClient, cfServiceInstance, func() { + cfServiceInstance.Status.LastOperation = services.LastOperation{ + Type: "create", + State: "failed", + Description: "failed due to error", + } + })).To(Succeed()) + }) + + JustBeforeEach(func() { + var err error + serviceInstanceRecord, err = serviceInstanceRepo.GetServiceInstance(ctx, authInfo, cfServiceInstance.Name) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns last operation", func() { + Expect(serviceInstanceRecord.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "failed", + Description: "failed due to error", + })) + }) + }) + Describe("GetDeletedAt", func() { var ( cfServiceInstance *korifiv1alpha1.CFServiceInstance diff --git a/controllers/api/v1alpha1/cfserviceinstance_types.go b/controllers/api/v1alpha1/cfserviceinstance_types.go index 61d9197fe..56269457c 100644 --- a/controllers/api/v1alpha1/cfserviceinstance_types.go +++ b/controllers/api/v1alpha1/cfserviceinstance_types.go @@ -19,6 +19,7 @@ package v1alpha1 import ( "fmt" + "code.cloudfoundry.org/korifi/model/services" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" @@ -79,6 +80,9 @@ type CFServiceInstanceStatus struct { // This will ensure that interested contollers are notified on instance credentials change //+kubebuilder:validation:Optional CredentialsObservedVersion string `json:"credentialsObservedVersion,omitempty"` + + //+kubebuilder:validation:Optional + LastOperation services.LastOperation `json:"last_operation"` } //+kubebuilder:object:root=true diff --git a/controllers/api/v1alpha1/zz_generated.deepcopy.go b/controllers/api/v1alpha1/zz_generated.deepcopy.go index 59e84ec5e..fda821d7b 100644 --- a/controllers/api/v1alpha1/zz_generated.deepcopy.go +++ b/controllers/api/v1alpha1/zz_generated.deepcopy.go @@ -1479,6 +1479,7 @@ func (in *CFServiceInstanceStatus) DeepCopyInto(out *CFServiceInstanceStatus) { } } out.Credentials = in.Credentials + out.LastOperation = in.LastOperation } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CFServiceInstanceStatus. diff --git a/controllers/controllers/services/instances/managed/controller.go b/controllers/controllers/services/instances/managed/controller.go index 8bc48ca9b..a83194c83 100644 --- a/controllers/controllers/services/instances/managed/controller.go +++ b/controllers/controllers/services/instances/managed/controller.go @@ -26,6 +26,7 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/services/osbapi" "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/model/services" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" @@ -116,7 +117,7 @@ func (r *Reconciler) isManaged(object client.Object) bool { } //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceinstances,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceinstances/status,verbs=get;update;atch +//+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceinstances/status,verbs=get;update;patch //+kubebuilder:rbac:groups=korifi.cloudfoundry.org,resources=cfserviceinstances/finalizers,verbs=update func (r *Reconciler) ReconcileResource(ctx context.Context, serviceInstance *korifiv1alpha1.CFServiceInstance) (ctrl.Result, error) { @@ -174,6 +175,7 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, serviceInstance *kor return r.pollProvisionOperation(ctx, serviceInstance, serviceInstanceAssets, osbapiClient, provisionResponse.Operation) } + serviceInstance.Status.LastOperation.State = "succeeded" return ctrl.Result{}, nil } @@ -224,6 +226,11 @@ func (r *Reconciler) provisionServiceInstance( return osbapi.ServiceInstanceOperationResponse{}, err } + serviceInstance.Status.LastOperation = services.LastOperation{ + Type: "create", + State: "initial", + } + var provisionResponse osbapi.ServiceInstanceOperationResponse provisionResponse, err = osbapiClient.Provision(ctx, osbapi.InstanceProvisionPayload{ InstanceID: serviceInstance.Name, @@ -239,6 +246,7 @@ func (r *Reconciler) provisionServiceInstance( log.Error(err, "failed to provision service") if osbapi.IsUnrecoveralbeError(err) { + serviceInstance.Status.LastOperation.State = "failed" meta.SetStatusCondition(&serviceInstance.Status.Conditions, metav1.Condition{ Type: korifiv1alpha1.ProvisioningFailedCondition, Status: metav1.ConditionTrue, @@ -279,6 +287,9 @@ func (r *Reconciler) pollProvisionOperation( return ctrl.Result{}, k8s.NewNotReadyError().WithCause(err).WithReason("GetLastOperationFailed") } + serviceInstance.Status.LastOperation.State = lastOpResponse.State + serviceInstance.Status.LastOperation.Description = lastOpResponse.Description + if lastOpResponse.State == "in progress" { return ctrl.Result{}, k8s.NewNotReadyError().WithReason("ProvisionInProgress").WithRequeue() } diff --git a/controllers/controllers/services/instances/managed/controller_test.go b/controllers/controllers/services/instances/managed/controller_test.go index f1caba64c..a7d4d6be6 100644 --- a/controllers/controllers/services/instances/managed/controller_test.go +++ b/controllers/controllers/services/instances/managed/controller_test.go @@ -138,6 +138,7 @@ var _ = Describe("CFServiceInstance", func() { }, }, } + Expect(adminClient.Create(ctx, instance)).To(Succeed()) }) @@ -211,6 +212,17 @@ var _ = Describe("CFServiceInstance", func() { }).Should(Succeed()) }) + It("sets succeeded state in instance last operation", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "succeeded", + })) + }).Should(Succeed()) + }) + When("the service instance parameters are not set", func() { BeforeEach(func() { Expect(k8s.PatchResource(ctx, adminClient, instance, func() { @@ -330,6 +342,17 @@ var _ = Describe("CFServiceInstance", func() { })) }).Should(Succeed()) }) + + It("sets initial state in instance last operation", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "initial", + })) + }).Should(Succeed()) + }) }) When("service provisioning fails with unrecoverable error", func() { @@ -355,6 +378,17 @@ var _ = Describe("CFServiceInstance", func() { )) }).Should(Succeed()) }) + + It("sets failed state in instance last operation", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "failed", + })) + }).Should(Succeed()) + }) }) When("getting service last operation fails", func() { @@ -392,6 +426,17 @@ var _ = Describe("CFServiceInstance", func() { }).Should(Succeed()) }) + It("sets in progress state in instance last operation", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "in progress", + })) + }).Should(Succeed()) + }) + It("keeps checking last operation", func() { Eventually(func(g Gomega) { g.Expect(brokerClient.GetServiceInstanceLastOperationCallCount()).To(BeNumerically(">", 1)) @@ -436,6 +481,17 @@ var _ = Describe("CFServiceInstance", func() { ))) }).Should(Succeed()) }) + + It("sets failed state in instance last operation", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(brokerClient.ProvisionCallCount()).To(BeNumerically(">=", 1)) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "failed", + })) + }).Should(Succeed()) + }) }) When("the instance has become ready", func() { diff --git a/controllers/controllers/services/instances/upsi/controller.go b/controllers/controllers/services/instances/upsi/controller.go index 04068d89c..d016c00d9 100644 --- a/controllers/controllers/services/instances/upsi/controller.go +++ b/controllers/controllers/services/instances/upsi/controller.go @@ -23,6 +23,7 @@ import ( korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" "code.cloudfoundry.org/korifi/controllers/controllers/shared" + "code.cloudfoundry.org/korifi/model/services" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" @@ -125,11 +126,18 @@ func (r *Reconciler) ReconcileResource(ctx context.Context, cfServiceInstance *k } if err = r.validateCredentials(credentialsSecret); err != nil { - return ctrl.Result{}, k8s.NewNotReadyError().WithCause(err).WithReason("SecretInvalid") + cfServiceInstance.Status.LastOperation = services.LastOperation{ + Type: "create", + State: "failed", + } + return ctrl.Result{}, k8s.NewNotReadyError().WithCause(err).WithReason("SecretInvalid").WithNoRequeue() } log.V(1).Info("credentials secret", "name", credentialsSecret.Name, "version", credentialsSecret.ResourceVersion) cfServiceInstance.Status.Credentials = corev1.LocalObjectReference{Name: credentialsSecret.Name} + + cfServiceInstance.Status.LastOperation = reconcileLastOperation(cfServiceInstance, credentialsSecret) + cfServiceInstance.Status.CredentialsObservedVersion = credentialsSecret.ResourceVersion return ctrl.Result{}, nil @@ -142,3 +150,19 @@ func (r *Reconciler) validateCredentials(credentialsSecret *corev1.Secret) error credentialsSecret.Name, ) } + +func reconcileLastOperation(cfServiceInstance *korifiv1alpha1.CFServiceInstance, credentialsSecret *corev1.Secret) services.LastOperation { + if cfServiceInstance.Status.CredentialsObservedVersion == "" { + return services.LastOperation{ + Type: "create", + State: "succeeded", + } + } + if cfServiceInstance.Status.CredentialsObservedVersion != credentialsSecret.ResourceVersion && cfServiceInstance.Status.CredentialsObservedVersion != "" { + return services.LastOperation{ + Type: "update", + State: "succeeded", + } + } + return services.LastOperation{} +} diff --git a/controllers/controllers/services/instances/upsi/controller_test.go b/controllers/controllers/services/instances/upsi/controller_test.go index 3ca2247bd..36a830107 100644 --- a/controllers/controllers/services/instances/upsi/controller_test.go +++ b/controllers/controllers/services/instances/upsi/controller_test.go @@ -5,6 +5,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" korifiv1alpha1 "code.cloudfoundry.org/korifi/controllers/api/v1alpha1" + "code.cloudfoundry.org/korifi/model/services" . "code.cloudfoundry.org/korifi/tests/matchers" "code.cloudfoundry.org/korifi/tools" "code.cloudfoundry.org/korifi/tools/k8s" @@ -118,6 +119,26 @@ var _ = Describe("CFServiceInstance", func() { ))) }).Should(Succeed()) }) + + It("sets the instance last operation failed state", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "failed", + })) + }).Should(Succeed()) + }) + }) + + It("sets the instance last operation succeed state", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "create", + State: "succeeded", + })) + }).Should(Succeed()) }) When("the credentials secret changes", func() { @@ -170,30 +191,48 @@ var _ = Describe("CFServiceInstance", func() { }).Should(Succeed()) }) }) - }) - }) - When("the service instance is managed", func() { - BeforeEach(func() { - instance = &korifiv1alpha1.CFServiceInstance{ - ObjectMeta: metav1.ObjectMeta{ - Name: uuid.NewString(), - Namespace: testNamespace, - }, - Spec: korifiv1alpha1.CFServiceInstanceSpec{ - DisplayName: "service-instance-name", - Type: korifiv1alpha1.ManagedType, - Tags: []string{}, - }, - } - Expect(adminClient.Create(ctx, instance)).To(Succeed()) + When("credentials observed version is not equal to the secret version", func() { + BeforeEach(func() { + Expect(k8s.Patch(ctx, adminClient, instance, func() { + instance.Status.CredentialsObservedVersion = "invalid-version" + })).To(Succeed()) + }) + + It("sets the instance last operation update type", func() { + Eventually(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(instance.Status.LastOperation).To(Equal(services.LastOperation{ + Type: "update", + State: "succeeded", + })) + }).Should(Succeed()) + }) + }) }) - It("does not reconcile it", func() { - Consistently(func(g Gomega) { - g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) - g.Expect(instance.Status).To(BeZero()) - }).Should(Succeed()) + When("the service instance is managed", func() { + BeforeEach(func() { + instance = &korifiv1alpha1.CFServiceInstance{ + ObjectMeta: metav1.ObjectMeta{ + Name: uuid.NewString(), + Namespace: testNamespace, + }, + Spec: korifiv1alpha1.CFServiceInstanceSpec{ + DisplayName: "service-instance-name", + Type: korifiv1alpha1.ManagedType, + Tags: []string{}, + }, + } + Expect(adminClient.Create(ctx, instance)).To(Succeed()) + }) + + It("does not reconcile it", func() { + Consistently(func(g Gomega) { + g.Expect(adminClient.Get(ctx, client.ObjectKeyFromObject(instance), instance)).To(Succeed()) + g.Expect(instance.Status).To(BeZero()) + }).Should(Succeed()) + }) }) }) }) diff --git a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceinstances.yaml b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceinstances.yaml index 47a37e0c6..02097acf0 100644 --- a/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceinstances.yaml +++ b/helm/korifi/controllers/crds/korifi.cloudfoundry.org_cfserviceinstances.yaml @@ -163,6 +163,27 @@ spec: ObservedGeneration captures the latest version of the spec.secretName that has been reconciled This will ensure that interested contollers are notified on instance credentials change type: string + last_operation: + properties: + description: + type: string + state: + enum: + - initial + - in progress + - succeeded + - failed + type: string + type: + enum: + - create + - update + - delete + type: string + required: + - state + - type + type: object observedGeneration: description: ObservedGeneration captures the latest generation of the CFServiceInstance that has been reconciled diff --git a/helm/korifi/controllers/role.yaml b/helm/korifi/controllers/role.yaml index aa003ebb7..2f53e64e2 100644 --- a/helm/korifi/controllers/role.yaml +++ b/helm/korifi/controllers/role.yaml @@ -158,6 +158,7 @@ rules: - cfroutes/status - cfservicebindings/status - cfservicebrokers/status + - cfserviceinstances/status - cfspaces/status - cftasks/status verbs: @@ -195,15 +196,6 @@ rules: - list - patch - watch -- apiGroups: - - korifi.cloudfoundry.org - resources: - - cfserviceinstances/status - verbs: - - atch - - get - - patch - - update - apiGroups: - korifi.cloudfoundry.org resources: diff --git a/model/services/instances.go b/model/services/instances.go new file mode 100644 index 000000000..ec431c46f --- /dev/null +++ b/model/services/instances.go @@ -0,0 +1,11 @@ +package services + +type LastOperation struct { + // +kubebuilder:validation:Enum=create;update;delete + Type string `json:"type"` + // +kubebuilder:validation:Enum=initial;in progress;succeeded;failed + State string `json:"state"` + + //+kubebuilder:validation:Optional + Description string `json:"description"` +}