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 }