github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/formationmapping/handler.go (about) 1 package formationmapping 2 3 import ( 4 "context" 5 "encoding/json" 6 7 "github.com/kyma-incubator/compass/components/director/pkg/graphql" 8 9 "github.com/kyma-incubator/compass/components/director/pkg/correlation" 10 11 "github.com/kyma-incubator/compass/components/director/internal/domain/formationassignment" 12 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 13 "github.com/kyma-incubator/compass/components/director/internal/model" 14 "github.com/kyma-incubator/compass/components/director/pkg/persistence" 15 "github.com/pkg/errors" 16 17 "fmt" 18 "io" 19 "net/http" 20 "strings" 21 22 "github.com/go-openapi/runtime/middleware/header" 23 validation "github.com/go-ozzo/ozzo-validation/v4" 24 "github.com/gorilla/mux" 25 "github.com/kyma-incubator/compass/components/director/pkg/httputils" 26 "github.com/kyma-incubator/compass/components/director/pkg/log" 27 ) 28 29 type malformedRequest struct { 30 status int 31 msg string 32 } 33 34 // FormationAssignmentRequestBody contains the request input of the formation assignment async status request 35 type FormationAssignmentRequestBody struct { 36 State model.FormationAssignmentState `json:"state,omitempty"` 37 Configuration json.RawMessage `json:"configuration,omitempty"` 38 Error string `json:"error,omitempty"` 39 } 40 41 // FormationRequestBody contains the request input of the formation async status request 42 type FormationRequestBody struct { 43 State model.FormationState `json:"state"` 44 Error string `json:"error,omitempty"` 45 } 46 47 // Handler is the base struct definition of the FormationMappingHandler 48 type Handler struct { 49 transact persistence.Transactioner 50 faService FormationAssignmentService 51 faStatusService formationAssignmentStatusService 52 faNotificationService FormationAssignmentNotificationService 53 formationService formationService 54 formationStatusService formationStatusService 55 } 56 57 // NewFormationMappingHandler creates a formation mapping Handler 58 func NewFormationMappingHandler(transact persistence.Transactioner, faService FormationAssignmentService, faStatusService formationAssignmentStatusService, faNotificationService FormationAssignmentNotificationService, formationService formationService, formationStatusService formationStatusService) *Handler { 59 return &Handler{ 60 transact: transact, 61 faService: faService, 62 faStatusService: faStatusService, 63 faNotificationService: faNotificationService, 64 formationService: formationService, 65 formationStatusService: formationStatusService, 66 } 67 } 68 69 // UpdateFormationAssignmentStatus handles formation assignment status updates 70 func (h *Handler) UpdateFormationAssignmentStatus(w http.ResponseWriter, r *http.Request) { 71 ctx := r.Context() 72 correlationID := correlation.CorrelationIDFromContext(ctx) 73 errResp := errors.Errorf("An unexpected error occurred while processing the request. X-Request-Id: %s", correlationID) 74 75 var reqBody FormationAssignmentRequestBody 76 err := decodeJSONBody(w, r, &reqBody) 77 if err != nil { 78 var mr *malformedRequest 79 if errors.As(err, &mr) { 80 log.C(ctx).Error(mr.msg) 81 respondWithError(ctx, w, mr.status, mr) 82 } else { 83 log.C(ctx).Error(err.Error()) 84 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 85 } 86 return 87 } 88 89 log.C(ctx).Info("Validating formation assignment request body...") 90 if err = reqBody.Validate(); err != nil { 91 log.C(ctx).WithError(err).Error("An error occurred while validating the request body") 92 respondWithError(ctx, w, http.StatusBadRequest, errors.Errorf("Request Body contains invalid input: %q. X-Request-Id: %s", err.Error(), correlationID)) 93 return 94 } 95 96 routeVars := mux.Vars(r) 97 formationID := routeVars[FormationIDParam] 98 formationAssignmentID := routeVars[FormationAssignmentIDParam] 99 100 if formationID == "" || formationAssignmentID == "" { 101 log.C(ctx).Errorf("Missing required parameters: %q or/and %q", FormationIDParam, FormationAssignmentIDParam) 102 respondWithError(ctx, w, http.StatusBadRequest, errors.Errorf("Not all of the required parameters are provided. X-Request-Id: %s", correlationID)) 103 return 104 } 105 106 tx, err := h.transact.Begin() 107 if err != nil { 108 log.C(ctx).WithError(err).Error("unable to establish connection with database") 109 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 110 return 111 } 112 defer h.transact.RollbackUnlessCommitted(ctx, tx) 113 ctx = persistence.SaveToContext(ctx, tx) 114 115 fa, err := h.faService.GetGlobalByIDAndFormationID(ctx, formationAssignmentID, formationID) 116 if err != nil { 117 log.C(ctx).Error(err) 118 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 119 return 120 } 121 122 ctx = tenant.SaveToContext(ctx, fa.TenantID, "") 123 124 formation, err := h.formationService.Get(ctx, formationID) 125 if err != nil { 126 log.C(ctx).WithError(err).Errorf("An error occurred while getting formation from formation assignment with ID: %q", fa.FormationID) 127 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 128 return 129 } 130 131 if len(reqBody.State) > 0 && formation.State != model.ReadyFormationState { 132 log.C(ctx).WithError(err).Errorf("Cannot update formation assignment for formation with ID %q as formation is not in %q state. X-Request-Id: %s", fa.FormationID, model.ReadyFormationState, correlationID) 133 respondWithError(ctx, w, http.StatusBadRequest, errResp) 134 return 135 } 136 137 if fa.Source == fa.Target && fa.SourceType == fa.TargetType { 138 errResp := errors.Errorf("Cannot update formation assignment with source %q and target %q. X-Request-Id: %s", fa.Source, fa.Target, correlationID) 139 respondWithError(ctx, w, http.StatusBadRequest, errResp) 140 return 141 } 142 143 if fa.State == string(model.DeletingAssignmentState) { 144 isFADeleted, err := h.processFormationAssignmentUnassignStatusUpdate(ctx, fa, reqBody) 145 if err != nil { 146 log.C(ctx).WithError(err).Errorf("An error occurred while processing formation assignment staus update for %q operation", model.UnassignFormation) 147 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 148 return 149 } 150 151 if err = tx.Commit(); err != nil { 152 log.C(ctx).WithError(err).Error("An error occurred while closing database transaction") 153 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 154 return 155 } 156 157 if isFADeleted { 158 if err = h.processFormationUnassign(ctx, formation, fa); err != nil { 159 log.C(ctx).WithError(err).Error("An error occurred while unassigning from formation") 160 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 161 return 162 } 163 } 164 165 log.C(ctx).Infof("The formation assignment with ID: %q was successfully processed for %q operation", formationAssignmentID, model.UnassignFormation) 166 httputils.Respond(w, http.StatusOK) 167 return 168 } 169 170 if len(reqBody.State) > 0 && reqBody.State != model.CreateErrorAssignmentState && reqBody.State != model.ReadyAssignmentState && reqBody.State != model.ConfigPendingAssignmentState { 171 log.C(ctx).Errorf("An invalid state: %q is provided for %q operation", reqBody.State, model.AssignFormation) 172 respondWithError(ctx, w, http.StatusBadRequest, errors.Errorf("An invalid state: %s is provided for %s operation. X-Request-Id: %s", reqBody.State, model.AssignFormation, correlationID)) 173 return 174 } 175 176 if reqBody.State == model.CreateErrorAssignmentState { 177 if err = h.faStatusService.SetAssignmentToErrorStateWithConstraints(ctx, fa, reqBody.Error, formationassignment.ClientError, reqBody.State, model.CreateFormation); err != nil { 178 log.C(ctx).WithError(err).Errorf("while updating error state to: %s for formation assignment with ID: %q", reqBody.State, formationAssignmentID) 179 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 180 return 181 } 182 } 183 184 if len(reqBody.State) > 0 { 185 fa.State = string(reqBody.State) 186 } else { 187 log.C(ctx).Infof("State is not provided, proceeding with the current state of the FA %q", fa.State) 188 } 189 190 if len(reqBody.Configuration) > 0 { 191 fa.Value = reqBody.Configuration 192 } 193 194 log.C(ctx).Infof("Updating formation assignment with ID: %q and formation ID: %q with state: %q", formationAssignmentID, formationID, fa.State) 195 if err = h.faStatusService.UpdateWithConstraints(ctx, fa, model.AssignFormation); err != nil { 196 log.C(ctx).WithError(err).Errorf("An error occurred while updating formation assignment with ID: %q and formation ID: %q with state: %q", formationAssignmentID, formationID, fa.State) 197 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 198 return 199 } 200 201 if err = tx.Commit(); err != nil { 202 log.C(ctx).WithError(err).Error("An error occurred while closing database transaction") 203 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 204 } 205 log.C(ctx).Infof("The formation assignment with ID: %q and formation ID: %q was successfully updated with state: %q", formationAssignmentID, formationID, fa.State) 206 207 if len(reqBody.Configuration) == 0 { // do not generate formation assignment notifications when configuration is not provided 208 log.C(ctx).Info("No configuration is provided in the request body. Formation assignment notification won't be generated") 209 httputils.Respond(w, http.StatusOK) 210 return 211 } 212 213 // The formation assignment notifications processing is independent of the status update request handling. 214 // That's why we're executing it in a go routine and in parallel to this returning a response to the client 215 go func() { 216 ctx, cancel := context.WithCancel(context.TODO()) 217 defer cancel() 218 219 correlationIDKey := correlation.RequestIDHeaderKey 220 correlation.SaveCorrelationIDHeaderToContext(ctx, &correlationIDKey, &correlationID) 221 ctx = tenant.SaveToContext(ctx, fa.TenantID, "") 222 223 log.C(ctx).Info("Configuration is provided in the request body. Starting formation assignment asynchronous notifications processing...") 224 225 tx, err = h.transact.Begin() 226 if err != nil { 227 log.C(ctx).WithError(err).Error("unable to establish connection with database") 228 return 229 } 230 defer h.transact.RollbackUnlessCommitted(ctx, tx) 231 ctx = persistence.SaveToContext(ctx, tx) 232 233 log.C(ctx).Infof("Generating formation assignment notifications for ID: %q and formation ID: %q", fa.ID, fa.FormationID) 234 notificationReq, err := h.faNotificationService.GenerateFormationAssignmentNotification(ctx, fa, model.AssignFormation) 235 if err != nil { 236 log.C(ctx).WithError(err).Errorf("An error occurred while generating formation assignment notifications for ID: %q and formation ID: %q", formationAssignmentID, formationID) 237 return 238 } 239 if notificationReq == nil { 240 log.C(ctx).Info("No formation assignment notification is generated. Returning...") 241 return 242 } 243 244 reverseFA, err := h.faService.GetReverseBySourceAndTarget(ctx, fa.FormationID, fa.Source, fa.Target) 245 if err != nil { 246 log.C(ctx).WithError(err).Errorf("An error occurred while getting reverse formation assignment by source: %q and target: %q", fa.Source, fa.Target) 247 return 248 } 249 250 log.C(ctx).Infof("Generating reverse formation assignment notifications for ID: %q and formation ID: %q", reverseFA.ID, reverseFA.FormationID) 251 reverseNotificationReq, err := h.faNotificationService.GenerateFormationAssignmentNotification(ctx, reverseFA, model.AssignFormation) 252 if err != nil { 253 log.C(ctx).WithError(err).Errorf("An error occurred while generating reverse formation assignment notifications for ID: %q and formation ID: %q", formationAssignmentID, formationID) 254 return 255 } 256 257 faReqMapping := formationassignment.FormationAssignmentRequestMapping{ 258 Request: notificationReq, 259 FormationAssignment: fa, 260 } 261 262 reverseFAReqMapping := formationassignment.FormationAssignmentRequestMapping{ 263 Request: reverseNotificationReq, 264 FormationAssignment: reverseFA, 265 } 266 267 assignmentPair := formationassignment.AssignmentMappingPairWithOperation{ 268 AssignmentMappingPair: &formationassignment.AssignmentMappingPair{ 269 Assignment: &reverseFAReqMapping, // the status update call is a response to the original notification that's why here we switch the assignment and reverse assignment 270 ReverseAssignment: &faReqMapping, 271 }, 272 Operation: model.AssignFormation, 273 } 274 275 log.C(ctx).Infof("Processing formation assignment pairs and their notifications") 276 _, err = h.faService.ProcessFormationAssignmentPair(ctx, &assignmentPair) 277 if err != nil { 278 log.C(ctx).WithError(err).Error("An error occurred while processing formation assignment pairs and their notifications") 279 return 280 } 281 282 if err = tx.Commit(); err != nil { 283 log.C(ctx).WithError(err).Error("An error occurred while closing database transaction") 284 return 285 } 286 287 log.C(ctx).Info("Finished formation assignment asynchronous notifications processing") 288 }() 289 290 httputils.Respond(w, http.StatusOK) 291 } 292 293 // UpdateFormationStatus handles asynchronous formation status updates 294 func (h *Handler) UpdateFormationStatus(w http.ResponseWriter, r *http.Request) { 295 ctx := r.Context() 296 correlationID := correlation.CorrelationIDFromContext(ctx) 297 errResp := errors.Errorf("An unexpected error occurred while processing the request. X-Request-Id: %s", correlationID) 298 299 var reqBody FormationRequestBody 300 err := decodeJSONBody(w, r, &reqBody) 301 if err != nil { 302 var mr *malformedRequest 303 if errors.As(err, &mr) { 304 log.C(ctx).Error(mr.msg) 305 respondWithError(ctx, w, mr.status, mr) 306 } else { 307 log.C(ctx).Error(err.Error()) 308 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 309 } 310 return 311 } 312 313 log.C(ctx).Info("Validating formation request body...") 314 if err = reqBody.Validate(); err != nil { 315 log.C(ctx).WithError(err).Error("An error occurred while validating the request body") 316 respondWithError(ctx, w, http.StatusBadRequest, errors.Errorf("Request Body contains invalid input: %q. X-Request-Id: %s", err.Error(), correlationID)) 317 return 318 } 319 320 routeVars := mux.Vars(r) 321 formationID := routeVars[FormationIDParam] 322 if formationID == "" { 323 log.C(ctx).Errorf("Missing required parameters: %q", FormationIDParam) 324 respondWithError(ctx, w, http.StatusBadRequest, errors.Errorf("Not all of the required parameters are provided. X-Request-Id: %s", correlationID)) 325 return 326 } 327 328 tx, err := h.transact.Begin() 329 if err != nil { 330 log.C(ctx).WithError(err).Error("unable to establish connection with database") 331 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 332 return 333 } 334 defer h.transact.RollbackUnlessCommitted(ctx, tx) 335 ctx = persistence.SaveToContext(ctx, tx) 336 337 f, err := h.formationService.GetGlobalByID(ctx, formationID) 338 if err != nil { 339 log.C(ctx).Error(err) 340 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 341 return 342 } 343 344 ctx = tenant.SaveToContext(ctx, f.TenantID, "") 345 346 if f.State == model.DeletingFormationState { 347 log.C(ctx).Infof("Processing asynchronous formation status update for %q operation...", model.DeleteFormation) 348 err = h.processAsynchronousFormationDelete(ctx, f, reqBody) 349 if err != nil { 350 log.C(ctx).WithError(err).Errorf("An error occurred while processing asynchronous formation status update for %q operation. X-Request-Id: %s", model.DeleteFormation, correlationID) 351 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 352 return 353 } 354 355 if err = tx.Commit(); err != nil { 356 log.C(ctx).WithError(err).Error("An error occurred while closing database transaction") 357 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 358 return 359 } 360 361 log.C(ctx).Infof("The status update for formation with ID: %q was successfully processed asynchronously for %q operation", formationID, model.DeleteFormation) 362 httputils.Respond(w, http.StatusOK) 363 return 364 } 365 366 log.C(ctx).Infof("Processing asynchronous formation status update for %q operation...", model.CreateFormation) 367 err = h.processAsynchronousFormationCreate(ctx, f, reqBody) 368 if err != nil { 369 log.C(ctx).WithError(err).Errorf("An error occurred while processing asynchronous formation status update for %q operation. X-Request-Id: %s", model.CreateFormation, correlationID) 370 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 371 return 372 } 373 374 if err = tx.Commit(); err != nil { 375 log.C(ctx).WithError(err).Error("An error occurred while closing database transaction") 376 respondWithError(ctx, w, http.StatusInternalServerError, errResp) 377 return 378 } 379 380 log.C(ctx).Infof("The status update for formation with ID: %q was successfully processed asynchronously for %q operation", formationID, model.CreateFormation) 381 httputils.Respond(w, http.StatusOK) 382 } 383 384 func (h *Handler) processFormationUnassign(ctx context.Context, formation *model.Formation, fa *model.FormationAssignment) error { 385 unassignTx, err := h.transact.Begin() 386 if err != nil { 387 return errors.Wrapf(err, "while beginning transaction") 388 } 389 defer h.transact.RollbackUnlessCommitted(ctx, unassignTx) 390 391 unassignCtx := persistence.SaveToContext(ctx, unassignTx) 392 393 if err = h.unassignObjectFromFormationWhenThereAreNoFormationAssignments(unassignCtx, fa, formation, fa.Source, fa.SourceType); err != nil { 394 return errors.Wrapf(err, "while unassigning object with type: %q and ID: %q", fa.SourceType, fa.Source) 395 } 396 397 if err = h.unassignObjectFromFormationWhenThereAreNoFormationAssignments(unassignCtx, fa, formation, fa.Target, fa.TargetType); err != nil { 398 return errors.Wrapf(err, "while unassigning object with type: %q and ID: %q", fa.TargetType, fa.Target) 399 } 400 401 if err = unassignTx.Commit(); err != nil { 402 return errors.Wrapf(err, "while committing transaction") 403 } 404 405 return nil 406 } 407 408 func (h *Handler) unassignObjectFromFormationWhenThereAreNoFormationAssignments(ctx context.Context, fa *model.FormationAssignment, formation *model.Formation, objectID string, objectType model.FormationAssignmentType) error { 409 formationAssignmentsForObject, err := h.faService.ListFormationAssignmentsForObjectID(ctx, fa.FormationID, objectID) 410 if err != nil { 411 return errors.Wrapf(err, "while listing formation assignments for object with type: %q and ID: %q", objectType, objectID) 412 } 413 414 // if there are no formation assignments left after the deletion, execute unassign formation for the object 415 if len(formationAssignmentsForObject) == 0 { 416 log.C(ctx).Infof("Unassining formation with name: %q for object with ID: %q and type: %q", formation.Name, objectID, objectType) 417 f, err := h.formationService.UnassignFormation(ctx, fa.TenantID, objectID, graphql.FormationObjectType(objectType), *formation) 418 if err != nil { 419 return errors.Wrapf(err, "while unassigning formation with name: %q for object ID: %q and type: %q", formation.Name, objectID, objectType) 420 } 421 log.C(ctx).Infof("Object with type: %q and ID: %q was successfully unassigned from formation with name: %q", objectType, objectID, f.Name) 422 } 423 return nil 424 } 425 426 // Validate validates the formation assignment's request body input 427 func (b FormationAssignmentRequestBody) Validate() error { 428 var fieldRules []*validation.FieldRules 429 fieldRules = append(fieldRules, validation.Field(&b.State, validation.In(model.ReadyAssignmentState, model.CreateErrorAssignmentState, model.DeleteErrorAssignmentState, model.ConfigPendingAssignmentState))) 430 431 if b.Error != "" { 432 fieldRules = make([]*validation.FieldRules, 0) 433 fieldRules = append(fieldRules, validation.Field(&b.State, validation.In(model.CreateErrorAssignmentState, model.DeleteErrorAssignmentState))) 434 fieldRules = append(fieldRules, validation.Field(&b.Configuration, validation.Empty)) 435 return validation.ValidateStruct(&b, fieldRules...) 436 } else if len(b.Configuration) > 0 { 437 fieldRules = make([]*validation.FieldRules, 0) 438 fieldRules = append(fieldRules, validation.Field(&b.State, validation.In(model.ReadyAssignmentState, model.ConfigPendingAssignmentState))) 439 fieldRules = append(fieldRules, validation.Field(&b.Error, validation.Empty)) 440 return validation.ValidateStruct(&b, fieldRules...) 441 } 442 443 return validation.ValidateStruct(&b, fieldRules...) 444 } 445 446 // Validate validates the formation's request body input 447 func (b FormationRequestBody) Validate() error { 448 return validation.ValidateStruct(&b, 449 validation.Field(&b.State, 450 validation.When(len(b.Error) == 0, validation.In(model.ReadyFormationState, model.CreateErrorFormationState, model.DeleteErrorFormationState)). 451 Else(validation.In(model.CreateErrorFormationState, model.DeleteErrorFormationState)))) 452 } 453 454 // processFormationAssignmentUnassignStatusUpdate handles the async unassign formation assignment status update 455 func (h *Handler) processFormationAssignmentUnassignStatusUpdate(ctx context.Context, fa *model.FormationAssignment, reqBody FormationAssignmentRequestBody) (bool, error) { 456 if reqBody.State != model.DeleteErrorAssignmentState && reqBody.State != model.ReadyAssignmentState { 457 return false, errors.Errorf("An invalid state: %q is provided for %q operation", reqBody.State, model.UnassignFormation) 458 } 459 460 if reqBody.State == model.DeleteErrorAssignmentState { 461 if err := h.faStatusService.SetAssignmentToErrorStateWithConstraints(ctx, fa, reqBody.Error, formationassignment.ClientError, reqBody.State, model.UnassignFormation); err != nil { 462 return false, errors.Wrapf(err, "while updating error state to: %s for formation assignment with ID: %q", reqBody.State, fa.ID) 463 } 464 return false, nil 465 } 466 467 if err := h.faStatusService.DeleteWithConstraints(ctx, fa.ID); err != nil { 468 return false, errors.Wrapf(err, "while deleting formation assignment with ID: %q", fa.ID) 469 } 470 471 return true, nil 472 } 473 474 // processAsynchronousFormationDelete handles the async delete formation status update 475 func (h *Handler) processAsynchronousFormationDelete(ctx context.Context, formation *model.Formation, reqBody FormationRequestBody) error { 476 if reqBody.State != model.ReadyFormationState && reqBody.State != model.DeleteErrorFormationState { 477 return errors.Errorf("An invalid state: %q is provided for %q operation", reqBody.State, model.DeleteFormation) 478 } 479 480 if reqBody.State == model.DeleteErrorFormationState { 481 if err := h.formationStatusService.SetFormationToErrorStateWithConstraints(ctx, formation, reqBody.Error, formationassignment.ClientError, reqBody.State, model.DeleteFormation); err != nil { 482 return errors.Wrapf(err, "while updating error state to: %s for formation with ID: %q", reqBody.State, formation.ID) 483 } 484 return nil 485 } 486 487 log.C(ctx).Infof("Deleting formation with ID: %q and name: %q", formation.ID, formation.Name) 488 if err := h.formationStatusService.DeleteFormationEntityAndScenariosWithConstraints(ctx, formation.TenantID, formation); err != nil { 489 return errors.Wrapf(err, "while deleting formation with ID: %q and name: %q", formation.ID, formation.Name) 490 } 491 492 return nil 493 } 494 495 // processAsynchronousFormationCreate handles the async create formation status update 496 func (h *Handler) processAsynchronousFormationCreate(ctx context.Context, formation *model.Formation, reqBody FormationRequestBody) error { 497 if reqBody.State != model.ReadyFormationState && reqBody.State != model.CreateErrorFormationState { 498 return errors.Errorf("An invalid state: %q is provided for %q operation", reqBody.State, model.CreateFormation) 499 } 500 501 if reqBody.State == model.CreateErrorFormationState { 502 if err := h.formationStatusService.SetFormationToErrorStateWithConstraints(ctx, formation, reqBody.Error, formationassignment.ClientError, reqBody.State, model.CreateFormation); err != nil { 503 return errors.Wrapf(err, "while updating error state to: %s for formation with ID: %q", reqBody.State, formation.ID) 504 } 505 return nil 506 } 507 508 log.C(ctx).Infof("Updating formation with ID: %q and name: %q to: %q state", formation.ID, formation.Name, reqBody.State) 509 formation.State = model.ReadyFormationState 510 if err := h.formationStatusService.UpdateWithConstraints(ctx, formation, model.CreateFormation); err != nil { 511 return errors.Wrapf(err, "while updating formation with ID: %q to: %q state", formation.ID, model.ReadyFormationState) 512 } 513 514 log.C(ctx).Infof("Resynchronizing formation with ID: %q and name: %q", formation.ID, formation.Name) 515 if _, err := h.formationService.ResynchronizeFormationNotifications(ctx, formation.ID); err != nil { 516 return errors.Wrapf(err, "while resynchronize formation notifications for formation with ID: %q", formation.ID) 517 } 518 519 return nil 520 } 521 522 func decodeJSONBody(w http.ResponseWriter, r *http.Request, dst interface{}) error { 523 if r.Header.Get(httputils.HeaderContentTypeKey) != "" { 524 value, _ := header.ParseValueAndParams(r.Header, httputils.HeaderContentTypeKey) 525 if value != httputils.ContentTypeApplicationJSON { 526 return &malformedRequest{status: http.StatusUnsupportedMediaType, msg: "Content-Type header is not application/json"} 527 } 528 } 529 530 // Use http.MaxBytesReader to enforce a maximum read of 1MB from the 531 // response body. A request body larger than that will now result in 532 // Decode() returning a "http: request body too large" error. 533 r.Body = http.MaxBytesReader(w, r.Body, 1048576) 534 535 dec := json.NewDecoder(r.Body) 536 dec.DisallowUnknownFields() 537 538 err := dec.Decode(&dst) 539 if err != nil { 540 var syntaxError *json.SyntaxError 541 var unmarshalTypeError *json.UnmarshalTypeError 542 543 switch { 544 case errors.As(err, &syntaxError): 545 msg := fmt.Sprintf("Request body contains badly-formed JSON (at position %d)", syntaxError.Offset) 546 return &malformedRequest{status: http.StatusBadRequest, msg: msg} 547 548 case errors.Is(err, io.ErrUnexpectedEOF): 549 return &malformedRequest{status: http.StatusBadRequest, msg: "Request body contains badly-formed JSON"} 550 551 case errors.As(err, &unmarshalTypeError): 552 msg := fmt.Sprintf("Request body contains an invalid value for the %q field (at position %d)", unmarshalTypeError.Field, unmarshalTypeError.Offset) 553 return &malformedRequest{status: http.StatusBadRequest, msg: msg} 554 555 case strings.HasPrefix(err.Error(), "json: unknown field "): 556 fieldName := strings.TrimPrefix(err.Error(), "json: unknown field ") 557 msg := fmt.Sprintf("Request body contains unknown field %s", fieldName) 558 return &malformedRequest{status: http.StatusBadRequest, msg: msg} 559 560 case errors.Is(err, io.EOF): 561 return &malformedRequest{status: http.StatusBadRequest, msg: "Request body must not be empty"} 562 563 case err.Error() == "http: request body too large": 564 return &malformedRequest{status: http.StatusRequestEntityTooLarge, msg: "Request body must not be larger than 1MB"} 565 566 default: 567 return err 568 } 569 } 570 571 err = dec.Decode(&struct{}{}) 572 if err != io.EOF { 573 return &malformedRequest{status: http.StatusBadRequest, msg: "Request body must only contain a single JSON object"} 574 } 575 576 return nil 577 } 578 579 func (mr *malformedRequest) Error() string { 580 return mr.msg 581 }