github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/pkg/operation/directive.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  	"fmt"
    23  	"math"
    24  	"net/http"
    25  
    26  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    27  
    28  	"github.com/kyma-incubator/compass/components/director/pkg/webhook"
    29  
    30  	"github.com/kyma-incubator/compass/components/director/pkg/header"
    31  	"github.com/kyma-incubator/compass/components/director/pkg/str"
    32  	"github.com/pkg/errors"
    33  
    34  	"github.com/kyma-incubator/compass/components/director/internal/model"
    35  
    36  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    37  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    38  
    39  	gqlgen "github.com/99designs/gqlgen/graphql"
    40  	"github.com/kyma-incubator/compass/components/director/pkg/graphql"
    41  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    42  )
    43  
    44  // ModeParam missing godoc
    45  const ModeParam = "mode"
    46  
    47  // WebhookFetcherFunc defines a function which fetches the webhooks for a specific resource ID
    48  type WebhookFetcherFunc func(ctx context.Context, resourceID string) ([]*model.Webhook, error)
    49  
    50  type directive struct {
    51  	transact            persistence.Transactioner
    52  	webhookFetcherFunc  WebhookFetcherFunc
    53  	resourceFetcherFunc ResourceFetcherFunc
    54  	resourceUpdaterFunc ResourceUpdaterFunc
    55  	tenantLoaderFunc    TenantLoaderFunc
    56  	scheduler           Scheduler
    57  }
    58  
    59  // NewDirective creates a new handler struct responsible for the Async directive business logic
    60  func NewDirective(transact persistence.Transactioner, webhookFetcherFunc WebhookFetcherFunc, resourceFetcherFunc ResourceFetcherFunc, resourceUpdaterFunc ResourceUpdaterFunc, tenantLoaderFunc TenantLoaderFunc, scheduler Scheduler) *directive {
    61  	return &directive{
    62  		transact:            transact,
    63  		webhookFetcherFunc:  webhookFetcherFunc,
    64  		resourceFetcherFunc: resourceFetcherFunc,
    65  		resourceUpdaterFunc: resourceUpdaterFunc,
    66  		tenantLoaderFunc:    tenantLoaderFunc,
    67  		scheduler:           scheduler,
    68  	}
    69  }
    70  
    71  // HandleOperation enriches the request with an Operation information when the requesting mutation is annotated with the Async directive
    72  func (d *directive) HandleOperation(ctx context.Context, _ interface{}, next gqlgen.Resolver, operationType graphql.OperationType, webhookType *graphql.WebhookType, idField *string) (res interface{}, err error) {
    73  	resCtx := gqlgen.GetFieldContext(ctx)
    74  	mode, err := getOperationMode(resCtx)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  
    79  	ctx = SaveModeToContext(ctx, *mode)
    80  
    81  	tx, err := d.transact.Begin()
    82  	if err != nil {
    83  		log.C(ctx).WithError(err).Errorf("An error occurred while opening database transaction: %s", err.Error())
    84  		return nil, apperrors.NewInternalError("Unable to initialize database operation")
    85  	}
    86  	defer d.transact.RollbackUnlessCommitted(ctx, tx)
    87  
    88  	ctx = persistence.SaveToContext(ctx, tx)
    89  
    90  	if err := d.concurrencyCheck(ctx, operationType, resCtx, idField); err != nil {
    91  		return nil, err
    92  	}
    93  
    94  	if *mode == graphql.OperationModeSync {
    95  		return executeSyncOperation(ctx, next, tx)
    96  	}
    97  
    98  	operation := &Operation{
    99  		OperationType:     OperationType(str.Title(operationType.String())),
   100  		OperationCategory: resCtx.Field.Name,
   101  		CorrelationID:     log.C(ctx).Data[log.FieldRequestID].(string),
   102  	}
   103  
   104  	ctx = SaveToContext(ctx, &[]*Operation{operation})
   105  	operationsArr, _ := FromCtx(ctx)
   106  
   107  	committed := false
   108  	defer func() {
   109  		if !committed {
   110  			lastIndex := int(math.Max(0, float64(len(*operationsArr)-1)))
   111  			*operationsArr = (*operationsArr)[:lastIndex]
   112  		}
   113  	}()
   114  
   115  	resp, err := next(ctx)
   116  	if err != nil {
   117  		log.C(ctx).WithError(err).Errorf("An error occurred while processing operation: %s", err.Error())
   118  		return nil, err
   119  	}
   120  
   121  	entity, ok := resp.(graphql.Entity)
   122  	if !ok {
   123  		log.C(ctx).WithError(err).Errorf("An error occurred while casting the response entity: %v", err)
   124  		return nil, apperrors.NewInternalError("Failed to process operation")
   125  	}
   126  
   127  	appConditionStatus, err := determineApplicationInProgressStatus(operation)
   128  	if err != nil {
   129  		log.C(ctx).WithError(err).Errorf("While determining the application status condition: %v", err)
   130  		return nil, err
   131  	}
   132  
   133  	if err := d.resourceUpdaterFunc(ctx, entity.GetID(), false, nil, *appConditionStatus); err != nil {
   134  		log.C(ctx).WithError(err).Errorf("While updating resource %s with id %s and status condition %v: %v", entity.GetType(), entity.GetID(), appConditionStatus, err)
   135  		return nil, apperrors.NewInternalError("Unable to update resource %s with id %s", entity.GetType(), entity.GetID())
   136  	}
   137  
   138  	operation.ResourceID = entity.GetID()
   139  	operation.ResourceType = entity.GetType()
   140  
   141  	if webhookType != nil {
   142  		webhookIDs, err := d.prepareWebhookIDs(ctx, err, operation, *webhookType)
   143  		if err != nil {
   144  			log.C(ctx).WithError(err).Errorf("An error occurred while retrieving webhooks: %s", err.Error())
   145  			return nil, apperrors.NewInternalError("Unable to retrieve webhooks")
   146  		}
   147  
   148  		operation.WebhookIDs = webhookIDs
   149  	}
   150  
   151  	requestObject, err := d.prepareRequestObject(ctx, err, resp)
   152  	if err != nil {
   153  		log.C(ctx).WithError(err).Errorf("An error occurred while preparing request data: %s", err.Error())
   154  		return nil, apperrors.NewInternalError("Unable to prepare webhook request data")
   155  	}
   156  
   157  	operation.RequestObject = requestObject
   158  
   159  	operationID, err := d.scheduler.Schedule(ctx, operation)
   160  	if err != nil {
   161  		log.C(ctx).WithError(err).Errorf("An error occurred while scheduling operation: %s", err.Error())
   162  		return nil, apperrors.NewInternalError("Unable to schedule operation")
   163  	}
   164  
   165  	operation.OperationID = operationID
   166  
   167  	err = tx.Commit()
   168  	if err != nil {
   169  		log.C(ctx).WithError(err).Errorf("An error occurred while closing database transaction: %s", err.Error())
   170  		return nil, apperrors.NewInternalError("Unable to finalize database operation")
   171  	}
   172  	committed = true
   173  
   174  	return resp, nil
   175  }
   176  
   177  func (d *directive) concurrencyCheck(ctx context.Context, op graphql.OperationType, resCtx *gqlgen.FieldContext, idField *string) error {
   178  	if op == graphql.OperationTypeCreate {
   179  		return nil
   180  	}
   181  
   182  	if idField == nil {
   183  		return apperrors.NewInternalError("idField from context should not be empty")
   184  	}
   185  
   186  	resourceID, ok := resCtx.Args[*idField].(string)
   187  	if !ok {
   188  		return apperrors.NewInternalError(fmt.Sprintf("could not get idField: %q from request context", *idField))
   189  	}
   190  
   191  	tenant, err := d.tenantLoaderFunc(ctx)
   192  	if err != nil {
   193  		return apperrors.NewTenantRequiredError()
   194  	}
   195  
   196  	app, err := d.resourceFetcherFunc(ctx, tenant, resourceID)
   197  	if err != nil {
   198  		if apperrors.IsNotFoundError(err) {
   199  			return err
   200  		}
   201  
   202  		return apperrors.NewInternalError("failed to fetch resource with id %s", resourceID)
   203  	}
   204  
   205  	if app.GetDeletedAt().IsZero() && app.GetUpdatedAt().IsZero() && !app.GetReady() && hasNoErrors(app) { // CREATING
   206  		return apperrors.NewConcurrentOperationInProgressError("create operation is in progress")
   207  	}
   208  	if !app.GetDeletedAt().IsZero() && hasNoErrors(app) { // DELETING
   209  		return apperrors.NewConcurrentOperationInProgressError("delete operation is in progress")
   210  	}
   211  
   212  	if app.GetDeletedAt().IsZero() && app.GetUpdatedAt().After(app.GetCreatedAt()) && !app.GetReady() && hasNoErrors(app) { // UPDATING or UNPAIRING
   213  		return apperrors.NewConcurrentOperationInProgressError("another operation is in progress")
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  func (d *directive) prepareRequestObject(ctx context.Context, err error, res interface{}) (string, error) {
   220  	if err != nil {
   221  		return "", err
   222  	}
   223  
   224  	tenantID, err := d.tenantLoaderFunc(ctx)
   225  	if err != nil {
   226  		return "", errors.Wrap(err, "failed to retrieve tenant from request")
   227  	}
   228  
   229  	resource, ok := res.(webhook.Resource)
   230  	if !ok {
   231  		return "", errors.New("entity is not a webhook provider")
   232  	}
   233  
   234  	reqHeaders, ok := ctx.Value(header.ContextKey).(http.Header)
   235  	if !ok {
   236  		return "", errors.New("failed to retrieve request headers")
   237  	}
   238  
   239  	headers := make(map[string]string)
   240  	for key, value := range reqHeaders {
   241  		headers[key] = value[0]
   242  	}
   243  
   244  	requestObject := &webhook.ApplicationLifecycleWebhookRequestObject{
   245  		Application: resource,
   246  		TenantID:    tenantID,
   247  		Headers:     headers,
   248  	}
   249  
   250  	data, err := json.Marshal(requestObject)
   251  	if err != nil {
   252  		return "", err
   253  	}
   254  
   255  	return string(data), nil
   256  }
   257  
   258  func (d *directive) prepareWebhookIDs(ctx context.Context, err error, operation *Operation, webhookType graphql.WebhookType) ([]string, error) {
   259  	if err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	webhooks, err := d.webhookFetcherFunc(ctx, operation.ResourceID)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  
   268  	webhookIDs := make([]string, 0)
   269  	for _, currWebhook := range webhooks {
   270  		if graphql.WebhookType(currWebhook.Type) == webhookType {
   271  			webhookIDs = append(webhookIDs, currWebhook.ID)
   272  		}
   273  	}
   274  
   275  	if len(webhookIDs) > 1 {
   276  		return nil, errors.New("multiple webhooks per operation are not supported")
   277  	}
   278  
   279  	return webhookIDs, nil
   280  }
   281  
   282  func getOperationMode(resCtx *gqlgen.FieldContext) (*graphql.OperationMode, error) {
   283  	var mode graphql.OperationMode
   284  	if _, found := resCtx.Args[ModeParam]; !found {
   285  		mode = graphql.OperationModeSync
   286  	} else {
   287  		modePointer, ok := resCtx.Args[ModeParam].(*graphql.OperationMode)
   288  		if !ok {
   289  			return nil, apperrors.NewInternalError(fmt.Sprintf("could not get %s parameter", ModeParam))
   290  		}
   291  		mode = *modePointer
   292  	}
   293  
   294  	return &mode, nil
   295  }
   296  
   297  func executeSyncOperation(ctx context.Context, next gqlgen.Resolver, tx persistence.PersistenceTx) (interface{}, error) {
   298  	resp, err := next(ctx)
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  
   303  	err = tx.Commit()
   304  	if err != nil {
   305  		log.C(ctx).WithError(err).Errorf("An error occurred while closing database transaction: %s", err.Error())
   306  		return nil, apperrors.NewInternalError("Unable to finalize database operation")
   307  	}
   308  
   309  	return resp, nil
   310  }
   311  
   312  func hasNoErrors(app model.Entity) bool {
   313  	return (app.GetError() == nil || *app.GetError() == "")
   314  }
   315  
   316  func determineApplicationInProgressStatus(op *Operation) (*model.ApplicationStatusCondition, error) {
   317  	var appStatusCondition model.ApplicationStatusCondition
   318  	switch op.OperationType {
   319  	case OperationTypeCreate:
   320  		appStatusCondition = model.ApplicationStatusConditionCreating
   321  	case OperationTypeUpdate:
   322  		if op.OperationCategory == OperationCategoryUnpairApplication {
   323  			appStatusCondition = model.ApplicationStatusConditionUnpairing
   324  		} else {
   325  			appStatusCondition = model.ApplicationStatusConditionUpdating
   326  		}
   327  	case OperationTypeDelete:
   328  		appStatusCondition = model.ApplicationStatusConditionDeleting
   329  	default:
   330  		return nil, apperrors.NewInvalidStatusCondition(resource.Application)
   331  	}
   332  
   333  	return &appStatusCondition, nil
   334  }