github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/operation/update_operation_handler.go (about) 1 /* 2 * Copyright 2020 The Compass Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package operation 18 19 import ( 20 "context" 21 "encoding/json" 22 "io" 23 "net/http" 24 25 "github.com/kyma-incubator/compass/components/director/pkg/str" 26 27 "github.com/kyma-incubator/compass/components/director/internal/model" 28 29 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 30 "github.com/kyma-incubator/compass/components/director/pkg/persistence" 31 32 "github.com/go-ozzo/ozzo-validation/v4/is" 33 34 validation "github.com/go-ozzo/ozzo-validation/v4" 35 36 "github.com/kyma-incubator/compass/components/director/pkg/resource" 37 38 "github.com/kyma-incubator/compass/components/director/pkg/log" 39 ) 40 41 // OperationRequest is the expected request body when updating certain operation status 42 type OperationRequest struct { 43 OperationType OperationType `json:"operation_type,omitempty"` 44 ResourceType resource.Type `json:"resource_type"` 45 ResourceID string `json:"resource_id"` 46 Error string `json:"error"` 47 OperationCategory string `json:"operation_category,omitempty"` 48 } 49 50 // ResourceUpdaterFunc defines a function which updates a particular resource ready and error status 51 type ResourceUpdaterFunc func(ctx context.Context, id string, ready bool, errorMsg *string, appStatusCondition model.ApplicationStatusCondition) error 52 53 // ResourceDeleterFunc defines a function which deletes a particular resource by ID 54 type ResourceDeleterFunc func(ctx context.Context, id string) error 55 56 type updateOperationHandler struct { 57 transact persistence.Transactioner 58 resourceUpdaterFuncs map[resource.Type]ResourceUpdaterFunc 59 resourceDeleterFuncs map[resource.Type]ResourceDeleterFunc 60 } 61 62 type errResponse struct { 63 err error 64 statusCode int 65 } 66 67 type operationError struct { 68 Error string `json:"error"` 69 } 70 71 // OperationCategoryUnpairApplication Operation category for unpair application mutation. It's used determine the operation status 72 const OperationCategoryUnpairApplication = "unpairApplication" 73 74 // NewUpdateOperationHandler creates a new handler struct to update resource by operation 75 func NewUpdateOperationHandler(transact persistence.Transactioner, resourceUpdaterFuncs map[resource.Type]ResourceUpdaterFunc, resourceDeleterFuncs map[resource.Type]ResourceDeleterFunc) *updateOperationHandler { 76 return &updateOperationHandler{ 77 transact: transact, 78 resourceUpdaterFuncs: resourceUpdaterFuncs, 79 resourceDeleterFuncs: resourceDeleterFuncs, 80 } 81 } 82 83 // ServeHTTP handles the Operations API requests 84 func (h *updateOperationHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) { 85 ctx := request.Context() 86 87 if request.Method != http.MethodPut { 88 apperrors.WriteAppError(ctx, writer, apperrors.NewInternalError("Method not allowed"), http.StatusMethodNotAllowed) 89 return 90 } 91 92 operation, errResp := operationRequestFromBody(ctx, request) 93 if errResp != nil { 94 apperrors.WriteAppError(ctx, writer, errResp.err, errResp.statusCode) 95 return 96 } 97 98 if err := validation.ValidateStruct(operation, 99 validation.Field(&operation.ResourceID, is.UUID), 100 validation.Field(&operation.OperationType, validation.Required, validation.In(OperationTypeCreate, OperationTypeUpdate, OperationTypeDelete)), 101 validation.Field(&operation.ResourceType, validation.Required, validation.In(resource.Application))); err != nil { 102 apperrors.WriteAppError(ctx, writer, apperrors.NewInvalidDataError("Invalid operation properties: %s", err), http.StatusBadRequest) 103 return 104 } 105 106 tx, err := h.transact.Begin() 107 if err != nil { 108 log.C(ctx).WithError(err).Errorf("An error occurred while opening db transaction: %s", err.Error()) 109 apperrors.WriteAppError(ctx, writer, apperrors.NewInternalError("Unable to establish connection with database"), http.StatusInternalServerError) 110 return 111 } 112 defer h.transact.RollbackUnlessCommitted(ctx, tx) 113 114 ctx = persistence.SaveToContext(ctx, tx) 115 116 resourceUpdaterFunc := h.resourceUpdaterFuncs[operation.ResourceType] 117 opError, err := stringifiedJSONError(operation.Error) 118 if err != nil { 119 log.C(ctx).WithError(err).Errorf("An error occurred while marshalling operation error: %s", err.Error()) 120 apperrors.WriteAppError(ctx, writer, apperrors.NewInternalError("Unable to marshal error"), http.StatusInternalServerError) 121 return 122 } 123 124 appConditionStatus := determineApplicationFinalStatus(operation, opError) 125 switch operation.OperationType { 126 case OperationTypeCreate: 127 fallthrough 128 case OperationTypeUpdate: 129 if err := resourceUpdaterFunc(ctx, operation.ResourceID, true, opError, appConditionStatus); err != nil { 130 log.C(ctx).WithError(err).Errorf("While updating resource %s with id %s: %v", operation.ResourceType, operation.ResourceID, err) 131 apperrors.WriteAppError(ctx, writer, apperrors.NewInternalError("Unable to update resource %s with id %s", operation.ResourceType, operation.ResourceID), http.StatusInternalServerError) 132 return 133 } 134 case OperationTypeDelete: 135 resourceDeleterFunc := h.resourceDeleterFuncs[operation.ResourceType] 136 if operation.Error != "" { 137 if err := resourceUpdaterFunc(ctx, operation.ResourceID, true, opError, appConditionStatus); err != nil { 138 log.C(ctx).WithError(err).Errorf("While updating resource %s with id %s: %v", operation.ResourceType, operation.ResourceID, err) 139 apperrors.WriteAppError(ctx, writer, apperrors.NewInternalError("Unable to update resource %s with id %s", operation.ResourceType, operation.ResourceID), http.StatusInternalServerError) 140 return 141 } 142 } else { 143 if err := resourceDeleterFunc(ctx, operation.ResourceID); err != nil { 144 log.C(ctx).WithError(err).Errorf("While deleting resource %s with id %s: %v", operation.ResourceType, operation.ResourceID, err) 145 apperrors.WriteAppError(ctx, writer, apperrors.NewInternalError("Unable to delete resource %s with id %s", operation.ResourceType, operation.ResourceID), http.StatusInternalServerError) 146 return 147 } 148 } 149 } 150 151 if err := tx.Commit(); err != nil { 152 log.C(ctx).WithError(err).Errorf("An error occurred while closing database transaction: %s", err.Error()) 153 apperrors.WriteAppError(ctx, writer, apperrors.NewInternalError("Unable to finalize database operation"), http.StatusInternalServerError) 154 return 155 } 156 157 writer.WriteHeader(http.StatusOK) 158 } 159 160 func operationRequestFromBody(ctx context.Context, request *http.Request) (*OperationRequest, *errResponse) { 161 bytes, err := io.ReadAll(request.Body) 162 if err != nil { 163 return nil, &errResponse{apperrors.NewInternalError("Unable to read request body"), http.StatusInternalServerError} 164 } 165 166 defer func() { 167 err := request.Body.Close() 168 if err != nil { 169 log.C(ctx).WithError(err).Errorf("Failed to close request body: %v", err) 170 } 171 }() 172 173 var operation OperationRequest 174 if err := json.Unmarshal(bytes, &operation); err != nil { 175 return nil, &errResponse{apperrors.NewInternalError("Unable to decode body to JSON"), http.StatusBadRequest} 176 } 177 178 return &operation, nil 179 } 180 181 func stringifiedJSONError(errorMsg string) (*string, error) { 182 if len(errorMsg) == 0 { 183 return nil, nil 184 } 185 186 opErr := operationError{Error: errorMsg} 187 bytesErr, err := json.Marshal(opErr) 188 if err != nil { 189 return nil, err 190 } 191 192 stringifiedErr := string(bytesErr) 193 return &stringifiedErr, nil 194 } 195 196 func determineApplicationFinalStatus(op *OperationRequest, opError *string) model.ApplicationStatusCondition { 197 appConditionStatus := model.ApplicationStatusConditionInitial 198 switch op.OperationType { 199 case OperationTypeCreate: 200 appConditionStatus = model.ApplicationStatusConditionCreateSucceeded 201 if opError != nil || str.PtrStrToStr(opError) != "" { 202 appConditionStatus = model.ApplicationStatusConditionCreateFailed 203 } 204 case OperationTypeUpdate: 205 if op.OperationCategory == OperationCategoryUnpairApplication { 206 appConditionStatus = model.ApplicationStatusConditionInitial 207 if opError != nil || str.PtrStrToStr(opError) != "" { 208 appConditionStatus = model.ApplicationStatusConditionUnpairFailed 209 } 210 } else { 211 appConditionStatus = model.ApplicationStatusConditionUpdateSucceeded 212 if opError != nil || str.PtrStrToStr(opError) != "" { 213 appConditionStatus = model.ApplicationStatusConditionUpdateFailed 214 } 215 } 216 case OperationTypeDelete: 217 appConditionStatus = model.ApplicationStatusConditionDeleteSucceeded 218 if opError != nil || str.PtrStrToStr(opError) != "" { 219 appConditionStatus = model.ApplicationStatusConditionDeleteFailed 220 } 221 } 222 223 return appConditionStatus 224 }