Skip to content

Commit 571b2e1

Browse files
author
Matthew Barnes
committed
frontend: Add CheckForConflict method
Returns a "409 Conflict" error response if the provisioning state of the resource or any parent resources (in the same namespace) should block a request from proceeding. Furthermore, if a DELETE request is accepted, mark any active operation on the resource as canceled. (Cluster Service will handle the actual cancellation of operations.)
1 parent 35fe99a commit 571b2e1

File tree

4 files changed

+143
-5
lines changed

4 files changed

+143
-5
lines changed

frontend/pkg/frontend/frontend.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"strconv"
1717
"strings"
1818
"sync/atomic"
19+
"time"
1920

2021
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
2122
"golang.org/x/sync/errgroup"
@@ -408,6 +409,14 @@ func (f *Frontend) ArmResourceCreateOrUpdate(writer http.ResponseWriter, request
408409
doc = database.NewResourceDocument(resourceID)
409410
}
410411

412+
// CheckForProvisioningStateConflict does not log conflict errors
413+
// but does log unexpected errors like database failures.
414+
cloudError := f.CheckForProvisioningStateConflict(ctx, operationRequest, doc)
415+
if cloudError != nil {
416+
arm.WriteCloudError(writer, cloudError)
417+
return
418+
}
419+
411420
body, err := BodyFromContext(ctx)
412421
if err != nil {
413422
f.logger.Error(err.Error())
@@ -420,7 +429,7 @@ func (f *Frontend) ArmResourceCreateOrUpdate(writer http.ResponseWriter, request
420429
return
421430
}
422431

423-
cloudError := versionedRequestCluster.ValidateStatic(versionedCurrentCluster, updating, request.Method)
432+
cloudError = versionedRequestCluster.ValidateStatic(versionedCurrentCluster, updating, request.Method)
424433
if cloudError != nil {
425434
f.logger.Error(cloudError.Error())
426435
arm.WriteCloudError(writer, cloudError)
@@ -563,14 +572,44 @@ func (f *Frontend) ArmResourceDelete(writer http.ResponseWriter, request *http.R
563572
return
564573
}
565574

575+
operationRequest := database.OperationRequestDelete
576+
577+
// CheckForProvisioningStateConflict does not log conflict errors
578+
// but does log unexpected errors like database failures.
579+
cloudError := f.CheckForProvisioningStateConflict(ctx, operationRequest, doc)
580+
if cloudError != nil {
581+
arm.WriteCloudError(writer, cloudError)
582+
return
583+
}
584+
566585
err = f.clusterServiceClient.DeleteCSCluster(ctx, doc.InternalID)
567586
if err != nil {
568587
f.logger.Error(fmt.Sprintf("failed to delete cluster %s: %v", resourceID, err))
569588
arm.WriteInternalServerError(writer)
570589
return
571590
}
572591

573-
operationDoc, err := f.StartOperation(writer, request, doc, database.OperationRequestDelete)
592+
// Deletion is underway; mark any active operation as canceled.
593+
if doc.ActiveOperationID != "" {
594+
updated, err := f.dbClient.UpdateOperationDoc(ctx, doc.ActiveOperationID, func(updateDoc *database.OperationDocument) bool {
595+
if updateDoc.Status != arm.ProvisioningStateCanceled {
596+
updateDoc.LastTransitionTime = time.Now()
597+
updateDoc.Status = arm.ProvisioningStateCanceled
598+
return true
599+
}
600+
return false
601+
})
602+
if err != nil {
603+
f.logger.Error(err.Error())
604+
arm.WriteInternalServerError(writer)
605+
return
606+
}
607+
if updated {
608+
f.logger.Info(fmt.Sprintf("canceled operation '%s'", doc.ActiveOperationID))
609+
}
610+
}
611+
612+
operationDoc, err := f.StartOperation(writer, request, doc, operationRequest)
574613
if err != nil {
575614
f.logger.Error(fmt.Sprintf("failed to write operation document: %v", err))
576615
arm.WriteInternalServerError(writer)

frontend/pkg/frontend/helpers.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"errors"
99
"fmt"
1010
"net/http"
11+
"strings"
1112

1213
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
1314
ocmerrors "github.com/openshift-online/ocm-sdk-go/errors"
@@ -17,6 +18,57 @@ import (
1718
"github.com/Azure/ARO-HCP/internal/database"
1819
)
1920

21+
// CheckForProvisioningStateConflict returns a "409 Conflict" error response if the
22+
// provisioning state of the resource is non-terminal, or any of its parent resources
23+
// within the same provider namespace are in a "Deleting" state.
24+
func (f *Frontend) CheckForProvisioningStateConflict(ctx context.Context, operationRequest database.OperationRequest, doc *database.ResourceDocument) *arm.CloudError {
25+
switch operationRequest {
26+
case database.OperationRequestCreate:
27+
// Resource must already exist for there to be a conflict.
28+
case database.OperationRequestDelete:
29+
if doc.ProvisioningState == arm.ProvisioningStateDeleting {
30+
return arm.NewCloudError(
31+
http.StatusConflict,
32+
arm.CloudErrorCodeConflict,
33+
doc.Key.String(),
34+
"Resource is already deleting")
35+
}
36+
case database.OperationRequestUpdate:
37+
if !doc.ProvisioningState.IsTerminal() {
38+
return arm.NewCloudError(
39+
http.StatusConflict,
40+
arm.CloudErrorCodeConflict,
41+
doc.Key.String(),
42+
"Cannot update resource while resource is %s",
43+
strings.ToLower(string(doc.ProvisioningState)))
44+
}
45+
}
46+
47+
parent := doc.Key.GetParent()
48+
49+
// ResourceType casing is preserved for parents in the same namespace.
50+
for parent.ResourceType.Namespace == doc.Key.ResourceType.Namespace {
51+
parentDoc, err := f.dbClient.GetResourceDoc(ctx, parent)
52+
if err != nil {
53+
f.logger.Error(err.Error())
54+
return arm.NewInternalServerError()
55+
}
56+
57+
if parentDoc.ProvisioningState == arm.ProvisioningStateDeleting {
58+
return arm.NewCloudError(
59+
http.StatusConflict,
60+
arm.CloudErrorCodeConflict,
61+
doc.Key.String(),
62+
"Cannot %s resource while parent resource is deleting",
63+
strings.ToLower(string(operationRequest)))
64+
}
65+
66+
parent = parent.GetParent()
67+
}
68+
69+
return nil
70+
}
71+
2072
func (f *Frontend) MarshalResource(ctx context.Context, resourceID *arm.ResourceID, versionedInterface api.Version) ([]byte, *arm.CloudError) {
2173
var responseBody []byte
2274

frontend/pkg/frontend/node_pool.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"fmt"
1010
"maps"
1111
"net/http"
12+
"time"
1213

1314
cmv1 "github.com/openshift-online/ocm-sdk-go/clustersmgmt/v1"
1415

@@ -123,6 +124,14 @@ func (f *Frontend) CreateOrUpdateNodePool(writer http.ResponseWriter, request *h
123124
doc = database.NewResourceDocument(resourceID)
124125
}
125126

127+
// CheckForProvisioningStateConflict does not log conflict errors
128+
// but does log unexpected errors like database failures.
129+
cloudError := f.CheckForProvisioningStateConflict(ctx, operationRequest, doc)
130+
if cloudError != nil {
131+
arm.WriteCloudError(writer, cloudError)
132+
return
133+
}
134+
126135
body, err := BodyFromContext(ctx)
127136
if err != nil {
128137
f.logger.Error(err.Error())
@@ -135,7 +144,7 @@ func (f *Frontend) CreateOrUpdateNodePool(writer http.ResponseWriter, request *h
135144
return
136145
}
137146

138-
cloudError := versionedRequestNodePool.ValidateStatic(versionedCurrentNodePool, updating, request.Method)
147+
cloudError = versionedRequestNodePool.ValidateStatic(versionedCurrentNodePool, updating, request.Method)
139148
if cloudError != nil {
140149
f.logger.Error(cloudError.Error())
141150
arm.WriteCloudError(writer, cloudError)
@@ -281,14 +290,44 @@ func (f *Frontend) DeleteNodePool(writer http.ResponseWriter, request *http.Requ
281290
return
282291
}
283292

293+
operationRequest := database.OperationRequestDelete
294+
295+
// CheckForProvisioningStateConflict does not log conflict errors
296+
// but does log unexpected errors like database failures.
297+
cloudError := f.CheckForProvisioningStateConflict(ctx, operationRequest, doc)
298+
if cloudError != nil {
299+
arm.WriteCloudError(writer, cloudError)
300+
return
301+
}
302+
284303
err = f.clusterServiceClient.DeleteCSNodePool(ctx, doc.InternalID)
285304
if err != nil {
286305
f.logger.Error(fmt.Sprintf("failed to delete node pool %s: %v", resourceID, err))
287306
arm.WriteInternalServerError(writer)
288307
return
289308
}
290309

291-
operationDoc, err := f.StartOperation(writer, request, doc, database.OperationRequestDelete)
310+
// Deletion is underway; mark any active operation as canceled.
311+
if doc.ActiveOperationID != "" {
312+
updated, err := f.dbClient.UpdateOperationDoc(ctx, doc.ActiveOperationID, func(updateDoc *database.OperationDocument) bool {
313+
if updateDoc.Status != arm.ProvisioningStateCanceled {
314+
updateDoc.LastTransitionTime = time.Now()
315+
updateDoc.Status = arm.ProvisioningStateCanceled
316+
return true
317+
}
318+
return false
319+
})
320+
if err != nil {
321+
f.logger.Error(err.Error())
322+
arm.WriteInternalServerError(writer)
323+
return
324+
}
325+
if updated {
326+
f.logger.Info(fmt.Sprintf("canceled operation '%s'", doc.ActiveOperationID))
327+
}
328+
}
329+
330+
operationDoc, err := f.StartOperation(writer, request, doc, operationRequest)
292331
if err != nil {
293332
f.logger.Error(fmt.Sprintf("failed to write operation document: %v", err))
294333
arm.WriteInternalServerError(writer)

internal/database/document.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,14 +97,22 @@ type OperationDocument struct {
9797
func NewOperationDocument(request OperationRequest) *OperationDocument {
9898
now := time.Now().UTC()
9999

100-
return &OperationDocument{
100+
doc := &OperationDocument{
101101
BaseDocument: newBaseDocument(),
102102
PartitionKey: operationsPartitionKey,
103103
Request: request,
104104
StartTime: now,
105105
LastTransitionTime: now,
106106
Status: arm.ProvisioningStateAccepted,
107107
}
108+
109+
// When deleting, set Status directly to ProvisioningStateDeleting
110+
// so any further deletion requests are rejected with 409 Conflict.
111+
if request == OperationRequestDelete {
112+
doc.Status = arm.ProvisioningStateDeleting
113+
}
114+
115+
return doc
108116
}
109117

110118
// ToStatus converts an OperationDocument to the ARM operation status format.

0 commit comments

Comments
 (0)