Skip to content

Conversation

@ybykov-a9s
Copy link
Contributor

Is there a related GitHub Issue?

The PR is aimed to solve this Issue 4163

What is this change about?

Implementing PATCH /v3/service_instances for managed services

Does this PR introduce a breaking change?

It shouldn't be a breaking change

Acceptance Steps

The PR is still WIP

Tag your pair, your PM, and/or team

danail-branekov

Copy link
Member

@danail-branekov danail-branekov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks promising. The most important bits of my comments are on the managed instance controller

type PatchManagedSIMessage struct {
GUID string
SpaceGUID string
PlanGUID string
Copy link
Member

@danail-branekov danail-branekov Sep 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether PlanGUID should not be a pointer? A patch may want to update the metadata, but not the plan

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

log.Error(err, "failed to provision service instance")
return ctrl.Result{}, fmt.Errorf("failed to provision service instance: %w", err)
}
if serviceInstance.Status.ObservedGeneration == 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I get the intention of this check correctly, the idea here is to perform provision when the CFServiceInstance has been just created, otherwise proceed to update.

The generation might drift because of whatever updates (maybe a new label appeared) that may occur while provisioning is already running.

Instead of relying on the observed generation, I would suggest to add PlanGUID to CFServiceInstance.Status. Then the plan guid on the spec would have the semantics of "desired plan", while the plan guid on the status would mean "actual plan". Then if both differ, the controller should perform a plan update. Once the update succeed, the controller should set the new plan guid on the status, thus "writing down" that this is already the actual plan.

Also, it is worth mentioning that once the instance is ready (isReady function returns true), the controller would stop listening for updates. Therefore you should change the function to also check whether the "desired plan" matches the "actual plan"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the logic here as you proposed to use Status. Please check it.


if lastOpResponse.State == "failed" {
meta.SetStatusCondition(&serviceInstance.Status.Conditions, metav1.Condition{
Type: korifiv1alpha1.ProvisioningFailedCondition,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should set UpdateFailedCondition here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the catch.

if err != nil {
return UpdateResponse{}, fmt.Errorf("update request failed: %w", err)
}
if statusCode == http.StatusBadRequest || statusCode == http.StatusConflict || statusCode == http.StatusUnprocessableEntity {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the osbapi spec, an update operation cannot result into StatusConflict

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

@ybykov-a9s ybykov-a9s force-pushed the extend-update-service-instance-endpoint branch from bdb5833 to 6651e36 Compare January 23, 2026 12:53
@ybykov-a9s
Copy link
Contributor Author

As an updated summary - I applied all suggestions. Maybe used not ideal naming 😁
Updated PR also contains e2e tests with some adjustments.
@danail-branekov Please check if it looks better now.
I would like to mention that the version i showed initially was really far from being ready because I wanted to ask about the general approach. And it worked,

Copy link
Member

@danail-branekov danail-branekov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comments below, significant bits are on the controller. I think you are going in the right direction.

A general style comment - we usually put a blank line between ginkgo blocks - Describe, When, It, etc to visually separate them. If you could keep that convention in the PR, that would be great.


It("returns an error", func() {
expectUnprocessableEntityError("nope")
When("getting the service instance fails with not found", func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Style: By introducing a separate context for managed services, the describe has now the floowing structure:

	Describe("PATCH /v3/service_instances/:guid", func() {
		When("updating a user provided service instance", func() { // all existing tests go here
		When("updating a managed service instance", func() {

What I see as an issue here is that error cases (such as service instance repository returning an error) are checked in the upsi context, though being irrelevant to whether the service is upsi or not. As a matter of fact, the only difference between uspi and managed is the repository method being invoked.

Therefore, how about the following structure:

	Describe("PATCH /v3/service_instances/:guid", func() {
        // existing (upsi) tests still here, i.e. this is the "default" case
        When("the service instance is managed", func() {
			It("patches the managed service instance", func() {
			It("returns HTTP 202 Accepted response", func() {
			When("patching the managed service instances fails", func() {
				It("returns the error", func() {
         })

This should also reduce the diff as indentation white spaces would not be there


func (p ServiceInstancePatch) ToManagedSIPatchMessage(spaceGUID, appGUID string) repositories.PatchManagedSIMessage {
var planGUID *string
if p.Relationships.ServicePlan.Data.GUID != "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I get the intention correctly, you want to check whether the plan is being updated. In that case I think you should be checking on p.Relationships.ServicePlan != nil rather than p.Relationships.ServicePlan.Data.GUID != "". If p.Relationships.ServicePlan is set, then the validation should ensure that p.Relationships.ServicePlan.Data.GUID is not empty.

return cfServiceInstanceToRecord(*cfServiceInstance), nil
}

func (r *ServiceInstanceRepo) PatchManagedServiceInstance(ctx context.Context, authInfo authorization.Info, message PatchManagedSIMessage) (ServiceInstanceRecord, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are no tests for this new method. They would be pretty much the same as the ones for UPSI but I cannot think a proper way to avoid the duplication

//+kubebuilder:validation:Optional
MaintenanceInfo MaintenanceInfo `json:"maintenanceInfo"`

// The service instance actual plan
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would add a note that this only makes sense for managed services (see MaintenanceInfo)


if isFailed(serviceInstance) {
return ctrl.Result{}, k8s.NewNotReadyError().WithReason("ProvisioningFailed").WithNoRequeue()
return ctrl.Result{}, k8s.NewNotReadyError().WithReason("ReconciliationFailed").WithNoRequeue()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not that reconciliation failed per se, it is just that the service instance is in a failed state. Maybe ServiceInstanceFailed would be a proper reason here?

))
})
})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file should not be affected by the PR at all. While I agree that removing those new lines makes sense, the generated diff is a high price to pay for such a small improvement.

return serviceInstance.GUID
}

func createManagedServiceInstance(brokerGUID, spaceGUID string, name string) string {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any particular reason for the name parameter? All references supply an inline-generated guid, therefore they do not seem to care about it. Let's not overload the method signature if that is the case.

Patch("/v3/service_instances/" + serviceInstanceGUID)
})

When("updating a user-provided service instance", func() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test structure: The describe has two sibling contexts and there is no default case. I would recommend having just introducing the serviceInstanceGUID variable at the root of the context and default it to the upsiGUID in the BeforeEach.

Then, have a context When("the service instance is managed, where you create the managed instance and set serviceInstanceGUID to its guid. Such an approach would also minimise the diff.

catalogResp, err := adminClient.R().SetResult(&plansResp).Get("/v3/service_plans?service_broker_guids=" + brokerGUID)
Expect(err).NotTo(HaveOccurred())
Expect(catalogResp).To(HaveRestyStatusCode(http.StatusOK))
Expect(plansResp.Resources).NotTo(BeEmpty())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe assert that Resource has length at least 2 (plansResp.Resources[1] is used below)

Expect(httpErrorService).NotTo(HaveOccurred())
Expect(httpRespService).To(HaveRestyStatusCode(http.StatusOK))
Expect(
result.resource.Relationships["service_plan"].Data.GUID,
Copy link
Member

@danail-branekov danail-branekov Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about checking the service_plan relationship in It("updates a managed service"? Every new It slows down the suite execution time so we have to strike a balance

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants