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  }