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  }