github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/formationassignment/service.go (about) 1 package formationassignment 2 3 import ( 4 "context" 5 "encoding/json" 6 7 "github.com/kyma-incubator/compass/components/director/pkg/resource" 8 9 "github.com/hashicorp/go-multierror" 10 "github.com/kyma-incubator/compass/components/director/pkg/formationconstraint" 11 12 "github.com/kyma-incubator/compass/components/director/pkg/graphql" 13 webhookdir "github.com/kyma-incubator/compass/components/director/pkg/webhook" 14 webhookclient "github.com/kyma-incubator/compass/components/director/pkg/webhook_client" 15 16 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 17 "github.com/kyma-incubator/compass/components/director/internal/model" 18 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 19 "github.com/kyma-incubator/compass/components/director/pkg/log" 20 "github.com/pkg/errors" 21 ) 22 23 // FormationAssignmentRepository represents the Formation Assignment repository layer 24 // 25 //go:generate mockery --name=FormationAssignmentRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 26 type FormationAssignmentRepository interface { 27 Create(ctx context.Context, item *model.FormationAssignment) error 28 GetByTargetAndSource(ctx context.Context, target, source, tenantID, formationID string) (*model.FormationAssignment, error) 29 Get(ctx context.Context, id, tenantID string) (*model.FormationAssignment, error) 30 GetGlobalByID(ctx context.Context, id string) (*model.FormationAssignment, error) 31 GetGlobalByIDAndFormationID(ctx context.Context, id, formationID string) (*model.FormationAssignment, error) 32 GetForFormation(ctx context.Context, tenantID, id, formationID string) (*model.FormationAssignment, error) 33 GetAssignmentsForFormationWithStates(ctx context.Context, tenantID, formationID string, states []string) ([]*model.FormationAssignment, error) 34 GetReverseBySourceAndTarget(ctx context.Context, tenantID, formationID, sourceID, targetID string) (*model.FormationAssignment, error) 35 List(ctx context.Context, pageSize int, cursor, tenantID string) (*model.FormationAssignmentPage, error) 36 ListByFormationIDs(ctx context.Context, tenantID string, formationIDs []string, pageSize int, cursor string) ([]*model.FormationAssignmentPage, error) 37 ListByFormationIDsNoPaging(ctx context.Context, tenantID string, formationIDs []string) ([][]*model.FormationAssignment, error) 38 ListAllForObject(ctx context.Context, tenant, formationID, objectID string) ([]*model.FormationAssignment, error) 39 ListAllForObjectIDs(ctx context.Context, tenant, formationID string, objectIDs []string) ([]*model.FormationAssignment, error) 40 ListForIDs(ctx context.Context, tenant string, ids []string) ([]*model.FormationAssignment, error) 41 Update(ctx context.Context, model *model.FormationAssignment) error 42 Delete(ctx context.Context, id, tenantID string) error 43 DeleteAssignmentsForObjectID(ctx context.Context, tnt, formationID, objectID string) error 44 Exists(ctx context.Context, id, tenantID string) (bool, error) 45 } 46 47 //go:generate mockery --exported --name=applicationRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 48 type applicationRepository interface { 49 ListByScenariosNoPaging(ctx context.Context, tenant string, scenarios []string) ([]*model.Application, error) 50 } 51 52 //go:generate mockery --exported --name=runtimeContextRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 53 type runtimeContextRepository interface { 54 ListByScenarios(ctx context.Context, tenant string, scenarios []string) ([]*model.RuntimeContext, error) 55 GetByID(ctx context.Context, tenant, id string) (*model.RuntimeContext, error) 56 } 57 58 //go:generate mockery --exported --name=runtimeRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 59 type runtimeRepository interface { 60 ListByScenarios(ctx context.Context, tenant string, scenarios []string) ([]*model.Runtime, error) 61 } 62 63 //go:generate mockery --exported --name=webhookRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 64 type webhookRepository interface { 65 GetByIDAndWebhookType(ctx context.Context, tenant, objectID string, objectType model.WebhookReferenceObjectType, webhookType model.WebhookType) (*model.Webhook, error) 66 } 67 68 //go:generate mockery --exported --name=webhookConverter --output=automock --outpkg=automock --case=underscore --disable-version-string 69 type webhookConverter interface { 70 ToGraphQL(in *model.Webhook) (*graphql.Webhook, error) 71 } 72 73 //go:generate mockery --exported --name=tenantRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 74 type tenantRepository interface { 75 Get(ctx context.Context, id string) (*model.BusinessTenantMapping, error) 76 GetCustomerIDParentRecursively(ctx context.Context, tenant string) (string, error) 77 } 78 79 // Used for testing 80 //nolint 81 // 82 //go:generate mockery --exported --name=templateInput --output=automock --outpkg=automock --case=underscore --disable-version-string 83 type templateInput interface { 84 webhookdir.TemplateInput 85 GetParticipantsIDs() []string 86 GetAssignment() *model.FormationAssignment 87 GetReverseAssignment() *model.FormationAssignment 88 SetAssignment(*model.FormationAssignment) 89 SetReverseAssignment(*model.FormationAssignment) 90 Clone() webhookdir.FormationAssignmentTemplateInput 91 } 92 93 // UIDService generates UUIDs for new entities 94 // 95 //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore --disable-version-string 96 type UIDService interface { 97 Generate() string 98 } 99 100 //go:generate mockery --exported --name=labelService --output=automock --outpkg=automock --case=underscore --disable-version-string 101 type labelService interface { 102 GetLabel(ctx context.Context, tenant string, labelInput *model.LabelInput) (*model.Label, error) 103 } 104 105 //go:generate mockery --exported --name=constraintEngine --output=automock --outpkg=automock --case=underscore --disable-version-string 106 type constraintEngine interface { 107 EnforceConstraints(ctx context.Context, location formationconstraint.JoinPointLocation, details formationconstraint.JoinPointDetails, formationTemplateID string) error 108 } 109 110 //go:generate mockery --exported --name=statusService --output=automock --outpkg=automock --case=underscore --disable-version-string 111 type statusService interface { 112 UpdateWithConstraints(ctx context.Context, fa *model.FormationAssignment, operation model.FormationOperation) error 113 SetAssignmentToErrorStateWithConstraints(ctx context.Context, assignment *model.FormationAssignment, errorMessage string, errorCode AssignmentErrorCode, state model.FormationAssignmentState, operation model.FormationOperation) error 114 DeleteWithConstraints(ctx context.Context, id string) error 115 } 116 117 //go:generate mockery --exported --name=faNotificationService --output=automock --outpkg=automock --case=underscore --disable-version-string 118 type faNotificationService interface { 119 GenerateFormationAssignmentNotificationExt(ctx context.Context, faRequestMapping, reverseFaRequestMapping *FormationAssignmentRequestMapping, operation model.FormationOperation) (*webhookclient.FormationAssignmentNotificationRequestExt, error) 120 PrepareDetailsForNotificationStatusReturned(ctx context.Context, tenantID string, fa *model.FormationAssignment, operation model.FormationOperation) (*formationconstraint.NotificationStatusReturnedOperationDetails, error) 121 } 122 123 type service struct { 124 repo FormationAssignmentRepository 125 uidSvc UIDService 126 applicationRepository applicationRepository 127 runtimeRepo runtimeRepository 128 runtimeContextRepo runtimeContextRepository 129 notificationService notificationService 130 faNotificationService faNotificationService 131 labelService labelService 132 formationRepository formationRepository 133 statusService statusService 134 runtimeTypeLabelKey string 135 applicationTypeLabelKey string 136 } 137 138 // NewService creates a FormationTemplate service 139 func NewService(repo FormationAssignmentRepository, uidSvc UIDService, applicationRepository applicationRepository, runtimeRepository runtimeRepository, runtimeContextRepo runtimeContextRepository, notificationService notificationService, faNotificationService faNotificationService, labelService labelService, formationRepository formationRepository, statusService statusService, runtimeTypeLabelKey, applicationTypeLabelKey string) *service { 140 return &service{ 141 repo: repo, 142 uidSvc: uidSvc, 143 applicationRepository: applicationRepository, 144 runtimeRepo: runtimeRepository, 145 runtimeContextRepo: runtimeContextRepo, 146 notificationService: notificationService, 147 faNotificationService: faNotificationService, 148 labelService: labelService, 149 formationRepository: formationRepository, 150 statusService: statusService, 151 runtimeTypeLabelKey: runtimeTypeLabelKey, 152 applicationTypeLabelKey: applicationTypeLabelKey, 153 } 154 } 155 156 // Create creates a Formation Assignment using `in` 157 func (s *service) Create(ctx context.Context, in *model.FormationAssignmentInput) (string, error) { 158 formationAssignmentID := s.uidSvc.Generate() 159 tenantID, err := tenant.LoadFromContext(ctx) 160 if err != nil { 161 return "", errors.Wrapf(err, "while loading tenant from context") 162 } 163 log.C(ctx).Debugf("ID: %q generated for formation assignment for tenant with ID: %q", formationAssignmentID, tenantID) 164 165 log.C(ctx).Infof("Creating formation assignment with source: %q and source type: %q, and target: %q with target type: %q", in.Source, in.SourceType, in.Target, in.TargetType) 166 if err = s.repo.Create(ctx, in.ToModel(formationAssignmentID, tenantID)); err != nil { 167 return "", errors.Wrapf(err, "while creating formation assignment for formation with ID: %q", in.FormationID) 168 } 169 170 return formationAssignmentID, nil 171 } 172 173 // CreateIfNotExists creates a Formation Assignment using `in` 174 func (s *service) CreateIfNotExists(ctx context.Context, in *model.FormationAssignmentInput) (string, error) { 175 tenantID, err := tenant.LoadFromContext(ctx) 176 if err != nil { 177 return "", errors.Wrapf(err, "while loading tenant from context") 178 } 179 180 existingEntity, err := s.repo.GetByTargetAndSource(ctx, in.Target, in.Source, tenantID, in.FormationID) 181 if err != nil && !apperrors.IsNotFoundError(err) { 182 return "", errors.Wrapf(err, "while getting formation assignment by target %q and source %q", in.Target, in.Source) 183 } 184 if err != nil && apperrors.IsNotFoundError(err) { 185 return s.Create(ctx, in) 186 } 187 return existingEntity.ID, nil 188 } 189 190 // Get queries Formation Assignment matching ID `id` 191 func (s *service) Get(ctx context.Context, id string) (*model.FormationAssignment, error) { 192 log.C(ctx).Infof("Getting formation assignment with ID: %q", id) 193 194 tenantID, err := tenant.LoadFromContext(ctx) 195 if err != nil { 196 return nil, errors.Wrapf(err, "while loading tenant from context") 197 } 198 199 fa, err := s.repo.Get(ctx, id, tenantID) 200 if err != nil { 201 return nil, errors.Wrapf(err, "while getting formation assignment with ID: %q and tenant: %q", id, tenantID) 202 } 203 204 return fa, nil 205 } 206 207 // GetAssignmentsForFormationWithStates retrieves formation assignments matching formation ID `formationID` and with state among `states` for tenant with ID `tenantID` 208 func (s *service) GetAssignmentsForFormationWithStates(ctx context.Context, tenantID, formationID string, states []string) ([]*model.FormationAssignment, error) { 209 formationAssignments, err := s.repo.GetAssignmentsForFormationWithStates(ctx, tenantID, formationID, states) 210 if err != nil { 211 return nil, errors.Wrapf(err, "while getting formation assignments with states for formation with ID: %q and tenant: %q", formationID, tenantID) 212 } 213 214 return formationAssignments, nil 215 } 216 217 // GetGlobalByID retrieves the formation assignment matching ID `id` globally without tenant parameter 218 func (s *service) GetGlobalByID(ctx context.Context, id string) (*model.FormationAssignment, error) { 219 log.C(ctx).Infof("Getting formation assignment with ID: %q globally", id) 220 221 fa, err := s.repo.GetGlobalByID(ctx, id) 222 if err != nil { 223 return nil, errors.Wrapf(err, "while getting formation assignment with ID: %q globally", id) 224 } 225 226 return fa, nil 227 } 228 229 // GetGlobalByIDAndFormationID retrieves the formation assignment matching ID `id` and formation ID `formationID` globally, without tenant parameter 230 func (s *service) GetGlobalByIDAndFormationID(ctx context.Context, id, formationID string) (*model.FormationAssignment, error) { 231 log.C(ctx).Infof("Getting formation assignment with ID: %q and formation ID: %q globally", id, formationID) 232 233 fa, err := s.repo.GetGlobalByIDAndFormationID(ctx, id, formationID) 234 if err != nil { 235 return nil, errors.Wrapf(err, "while getting formation assignment with ID: %q and formation ID: %q globally", id, formationID) 236 } 237 238 return fa, nil 239 } 240 241 // GetForFormation retrieves the Formation Assignment with the provided `id` associated with Formation with id `formationID` 242 func (s *service) GetForFormation(ctx context.Context, id, formationID string) (*model.FormationAssignment, error) { 243 log.C(ctx).Infof("Getting formation assignment for ID: %q and formationID: %q", id, formationID) 244 245 tenantID, err := tenant.LoadFromContext(ctx) 246 if err != nil { 247 return nil, errors.Wrapf(err, "while loading tenant from context") 248 } 249 250 fa, err := s.repo.GetForFormation(ctx, tenantID, id, formationID) 251 if err != nil { 252 return nil, errors.Wrapf(err, "while getting formation assignment with ID: %q for formation with ID: %q", id, formationID) 253 } 254 255 return fa, nil 256 } 257 258 // GetReverseBySourceAndTarget retrieves the Formation Assignment with the provided `id` associated with Formation with id `formationID` 259 func (s *service) GetReverseBySourceAndTarget(ctx context.Context, formationID, sourceID, targetID string) (*model.FormationAssignment, error) { 260 log.C(ctx).Infof("Getting reverse formation assignment for formation ID: %q and source: %q and target: %q", formationID, sourceID, targetID) 261 262 tenantID, err := tenant.LoadFromContext(ctx) 263 if err != nil { 264 return nil, errors.Wrapf(err, "while loading tenant from context") 265 } 266 267 reverseFA, err := s.repo.GetReverseBySourceAndTarget(ctx, tenantID, formationID, sourceID, targetID) 268 if err != nil { 269 return nil, errors.Wrapf(err, "while getting reverse formation assignment for formation ID: %q and source: %q and target: %q", formationID, sourceID, targetID) 270 } 271 272 return reverseFA, nil 273 } 274 275 // List pagination lists Formation Assignment based on `pageSize` and `cursor` 276 func (s *service) List(ctx context.Context, pageSize int, cursor string) (*model.FormationAssignmentPage, error) { 277 log.C(ctx).Info("Listing formation assignments") 278 279 if pageSize < 1 || pageSize > 200 { 280 return nil, apperrors.NewInvalidDataError("page size must be between 1 and 200") 281 } 282 283 tenantID, err := tenant.LoadFromContext(ctx) 284 if err != nil { 285 return nil, errors.Wrapf(err, "while loading tenant from context") 286 } 287 288 return s.repo.List(ctx, pageSize, cursor, tenantID) 289 } 290 291 // ListByFormationIDs retrieves a pages of Formation Assignment objects for each of the provided formation IDs 292 func (s *service) ListByFormationIDs(ctx context.Context, formationIDs []string, pageSize int, cursor string) ([]*model.FormationAssignmentPage, error) { 293 log.C(ctx).Infof("Listing formation assignment for formation with IDs: %q", formationIDs) 294 295 tnt, err := tenant.LoadFromContext(ctx) 296 if err != nil { 297 return nil, errors.Wrapf(err, "while loading tenant from context") 298 } 299 300 if pageSize < 1 || pageSize > 200 { 301 return nil, apperrors.NewInvalidDataError("page size must be between 1 and 200") 302 } 303 304 return s.repo.ListByFormationIDs(ctx, tnt, formationIDs, pageSize, cursor) 305 } 306 307 func (s *service) ListByFormationIDsNoPaging(ctx context.Context, formationIDs []string) ([][]*model.FormationAssignment, error) { 308 log.C(ctx).Infof("Listing all formation assignment for formation with IDs: %q", formationIDs) 309 310 tnt, err := tenant.LoadFromContext(ctx) 311 if err != nil { 312 return nil, errors.Wrapf(err, "while loading tenant from context") 313 } 314 315 return s.repo.ListByFormationIDsNoPaging(ctx, tnt, formationIDs) 316 } 317 318 // ListFormationAssignmentsForObjectID retrieves all Formation Assignment objects for formation with ID `formationID` that have `objectID` as source or target 319 func (s *service) ListFormationAssignmentsForObjectID(ctx context.Context, formationID, objectID string) ([]*model.FormationAssignment, error) { 320 log.C(ctx).Infof("Listing formation assignments for object ID: %q and formation ID: %q", objectID, formationID) 321 tnt, err := tenant.LoadFromContext(ctx) 322 if err != nil { 323 return nil, errors.Wrapf(err, "while loading tenant from context") 324 } 325 326 return s.repo.ListAllForObject(ctx, tnt, formationID, objectID) 327 } 328 329 // DeleteAssignmentsForObjectID deletes formation assignments for formation for given objectID 330 func (s *service) DeleteAssignmentsForObjectID(ctx context.Context, formationID, objectID string) error { 331 tnt, err := tenant.LoadFromContext(ctx) 332 if err != nil { 333 return errors.Wrapf(err, "while loading tenant from context") 334 } 335 336 return s.repo.DeleteAssignmentsForObjectID(ctx, tnt, formationID, objectID) 337 } 338 339 // ListFormationAssignmentsForObjectIDs retrieves all Formation Assignment objects for formation with ID `formationID` that have any of the `objectIDs` as source or target 340 func (s *service) ListFormationAssignmentsForObjectIDs(ctx context.Context, formationID string, objectIDs []string) ([]*model.FormationAssignment, error) { 341 tnt, err := tenant.LoadFromContext(ctx) 342 if err != nil { 343 return nil, errors.Wrapf(err, "while loading tenant from context") 344 } 345 346 return s.repo.ListAllForObjectIDs(ctx, tnt, formationID, objectIDs) 347 } 348 349 // Update updates a Formation Assignment matching ID `id` using `in` 350 func (s *service) Update(ctx context.Context, id string, fa *model.FormationAssignment) error { 351 log.C(ctx).Infof("Updating formation assignment with ID: %q", id) 352 353 tenantID, err := tenant.LoadFromContext(ctx) 354 if err != nil { 355 return errors.Wrapf(err, "while loading tenant from context") 356 } 357 358 if exists, err := s.repo.Exists(ctx, id, tenantID); err != nil { 359 return errors.Wrapf(err, "while ensuring formation assignment with ID: %q exists", id) 360 } else if !exists { 361 return apperrors.NewNotFoundError(resource.FormationAssignment, id) 362 } 363 364 if err = s.repo.Update(ctx, fa); err != nil { 365 return errors.Wrapf(err, "while updating formation assignment with ID: %q", id) 366 } 367 return nil 368 } 369 370 // Delete deletes a Formation Assignment matching ID `id` 371 func (s *service) Delete(ctx context.Context, id string) error { 372 log.C(ctx).Infof("Deleting formation assignment with ID: %q", id) 373 374 tenantID, err := tenant.LoadFromContext(ctx) 375 if err != nil { 376 return errors.Wrapf(err, "while loading tenant from context") 377 } 378 379 if err := s.repo.Delete(ctx, id, tenantID); err != nil { 380 return errors.Wrapf(err, "while deleting formation assignment with ID: %q", id) 381 } 382 return nil 383 } 384 385 // Exists check if a Formation Assignment with given ID exists 386 func (s *service) Exists(ctx context.Context, id string) (bool, error) { 387 log.C(ctx).Infof("Checking formation assignment existence for ID: %q", id) 388 389 tenantID, err := tenant.LoadFromContext(ctx) 390 if err != nil { 391 return false, errors.Wrapf(err, "while loading tenant from context") 392 } 393 394 exists, err := s.repo.Exists(ctx, id, tenantID) 395 if err != nil { 396 return false, errors.Wrapf(err, "while checking formation assignment existence for ID: %q and tenant: %q", id, tenantID) 397 } 398 return exists, nil 399 } 400 401 // GenerateAssignments creates and persists two formation assignments per participant in the formation `formation`. 402 // For the first formation assignment the source is the objectID and the target is participant's ID. 403 // For the second assignment the source and target are swapped. 404 // 405 // In case of objectType==RUNTIME_CONTEXT formationAssignments for the object and it's parent runtime are not generated. 406 func (s *service) GenerateAssignments(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation *model.Formation) ([]*model.FormationAssignment, error) { 407 applications, err := s.applicationRepository.ListByScenariosNoPaging(ctx, tnt, []string{formation.Name}) 408 if err != nil { 409 return nil, err 410 } 411 412 runtimes, err := s.runtimeRepo.ListByScenarios(ctx, tnt, []string{formation.Name}) 413 if err != nil { 414 return nil, err 415 } 416 417 runtimeContexts, err := s.runtimeContextRepo.ListByScenarios(ctx, tnt, []string{formation.Name}) 418 if err != nil { 419 return nil, err 420 } 421 422 allIDs := make([]string, 0, len(applications)+len(runtimes)+len(runtimeContexts)) 423 appIDs := make(map[string]bool, len(applications)) 424 rtIDs := make(map[string]bool, len(runtimes)) 425 rtCtxIDs := make(map[string]bool, len(runtimeContexts)) 426 for _, app := range applications { 427 allIDs = append(allIDs, app.ID) 428 appIDs[app.ID] = false 429 } 430 for _, rt := range runtimes { 431 allIDs = append(allIDs, rt.ID) 432 rtIDs[rt.ID] = false 433 } 434 for _, rtCtx := range runtimeContexts { 435 allIDs = append(allIDs, rtCtx.ID) 436 rtCtxIDs[rtCtx.ID] = false 437 } 438 439 allAssignments, err := s.ListFormationAssignmentsForObjectIDs(ctx, formation.ID, allIDs) 440 if err != nil { 441 return nil, err 442 } 443 444 // We should not generate notifications for formation participants that are being unassigned asynchronously 445 for _, assignment := range allAssignments { 446 if assignment.Source == assignment.Target && assignment.SourceType == assignment.TargetType { 447 switch assignment.SourceType { 448 case model.FormationAssignmentTypeApplication: 449 appIDs[assignment.Source] = true 450 case model.FormationAssignmentTypeRuntime: 451 rtIDs[assignment.Source] = true 452 case model.FormationAssignmentTypeRuntimeContext: 453 rtCtxIDs[assignment.Source] = true 454 } 455 } 456 } 457 458 // When assigning an object to a formation we need to create two formation assignments per participant. 459 // In the first formation assignment the object we're assigning will be the source and in the second it will be the target 460 assignments := make([]*model.FormationAssignmentInput, 0, (len(applications)+len(runtimes)+len(runtimeContexts))*2+1) 461 for appID, isAssigned := range appIDs { 462 if !isAssigned || appID == objectID { 463 continue 464 } 465 assignments = append(assignments, s.GenerateAssignmentsForParticipant(objectID, objectType, formation, model.FormationAssignmentTypeApplication, appID)...) 466 } 467 468 // When runtime context is assigned to formation its parent runtime is unassigned from the formation. 469 // There is no need to create formation assignments between the runtime context and the runtime. If such 470 // formation assignments were to be created the runtime unassignment from the formation would fail. 471 // The reason for this is that the formation assignments are created in one transaction and the runtime 472 // unassignment is done in a separate transaction which does not "see" them but will try to delete them. 473 parentID := "" 474 if objectType == graphql.FormationObjectTypeRuntimeContext { 475 rtmCtx, err := s.runtimeContextRepo.GetByID(ctx, tnt, objectID) 476 if err != nil { 477 return nil, err 478 } 479 parentID = rtmCtx.RuntimeID 480 } 481 for runtimeID, isAssigned := range rtIDs { 482 if !isAssigned || runtimeID == objectID || runtimeID == parentID { 483 continue 484 } 485 assignments = append(assignments, s.GenerateAssignmentsForParticipant(objectID, objectType, formation, model.FormationAssignmentTypeRuntime, runtimeID)...) 486 } 487 488 for runtimeCtxID, isAssigned := range rtCtxIDs { 489 if !isAssigned || runtimeCtxID == objectID { 490 continue 491 } 492 assignments = append(assignments, s.GenerateAssignmentsForParticipant(objectID, objectType, formation, model.FormationAssignmentTypeRuntimeContext, runtimeCtxID)...) 493 } 494 495 assignments = append(assignments, &model.FormationAssignmentInput{ 496 FormationID: formation.ID, 497 Source: objectID, 498 SourceType: model.FormationAssignmentType(objectType), 499 Target: objectID, 500 TargetType: model.FormationAssignmentType(objectType), 501 State: string(model.ReadyAssignmentState), 502 Value: nil, 503 }) 504 505 ids := make([]string, 0, len(assignments)) 506 for _, assignment := range assignments { 507 id, err := s.CreateIfNotExists(ctx, assignment) 508 if err != nil { 509 return nil, errors.Wrapf(err, "while creating formationAssignment for formation %q with source %q of type %q and target %q of type %q", assignment.FormationID, assignment.Source, assignment.SourceType, assignment.Target, assignment.TargetType) 510 } 511 ids = append(ids, id) 512 } 513 514 formationAssignments, err := s.repo.ListForIDs(ctx, tnt, ids) 515 if err != nil { 516 return nil, errors.Wrap(err, "while listing formationAssignments") 517 } 518 519 return formationAssignments, nil 520 } 521 522 // GenerateAssignmentsForParticipant creates in-memory the assignments for two participants in the initial state 523 func (s *service) GenerateAssignmentsForParticipant(objectID string, objectType graphql.FormationObjectType, formation *model.Formation, participantType model.FormationAssignmentType, participantID string) []*model.FormationAssignmentInput { 524 assignments := make([]*model.FormationAssignmentInput, 0, 2) 525 assignments = append(assignments, &model.FormationAssignmentInput{ 526 FormationID: formation.ID, 527 Source: objectID, 528 SourceType: model.FormationAssignmentType(objectType), 529 Target: participantID, 530 TargetType: participantType, 531 State: string(model.InitialAssignmentState), 532 Value: nil, 533 }) 534 assignments = append(assignments, &model.FormationAssignmentInput{ 535 FormationID: formation.ID, 536 Source: participantID, 537 SourceType: participantType, 538 Target: objectID, 539 TargetType: model.FormationAssignmentType(objectType), 540 State: string(model.InitialAssignmentState), 541 Value: nil, 542 }) 543 return assignments 544 } 545 546 // ProcessFormationAssignments matches the formation assignments with the corresponding notification requests and packs them in FormationAssignmentRequestMapping. 547 // Each FormationAssignmentRequestMapping is then packed with its reverse in AssignmentMappingPair. Then the provided `formationAssignmentFunc` is executed against the AssignmentMappingPairs. 548 // 549 // Assignment and reverseAssignment example 550 // assignment{source=X, target=Y} - reverseAssignment{source=Y, target=X} 551 // 552 // Mapping and reverseMapping example 553 // mapping{notificationRequest=request, formationAssignment=assignment} - reverseMapping{notificationRequest=reverseRequest, formationAssignment=reverseAssignment} 554 func (s *service) ProcessFormationAssignments(ctx context.Context, formationAssignmentsForObject []*model.FormationAssignment, runtimeContextIDToRuntimeIDMapping map[string]string, applicationIDToApplicationTemplateIDMapping map[string]string, requests []*webhookclient.FormationAssignmentNotificationRequest, formationAssignmentFunc func(context.Context, *AssignmentMappingPairWithOperation) (bool, error), formationOperation model.FormationOperation) error { 555 var errs *multierror.Error 556 assignmentRequestMappings := s.matchFormationAssignmentsWithRequests(ctx, formationAssignmentsForObject, runtimeContextIDToRuntimeIDMapping, applicationIDToApplicationTemplateIDMapping, requests) 557 alreadyProcessedFAs := make(map[string]bool, 0) 558 for _, mapping := range assignmentRequestMappings { 559 if alreadyProcessedFAs[mapping.Assignment.FormationAssignment.ID] { 560 continue 561 } 562 mappingWithOperation := &AssignmentMappingPairWithOperation{ 563 AssignmentMappingPair: mapping, 564 Operation: formationOperation, 565 } 566 isReverseProcessed, err := formationAssignmentFunc(ctx, mappingWithOperation) 567 if err != nil { 568 errs = multierror.Append(errs, errors.Wrapf(err, "while processing formation assignment with id %q", mapping.Assignment.FormationAssignment.ID)) 569 } 570 if isReverseProcessed { 571 alreadyProcessedFAs[mapping.ReverseAssignment.FormationAssignment.ID] = true 572 } 573 } 574 log.C(ctx).Infof("Finished processing %d formation assignments", len(formationAssignmentsForObject)) 575 576 return errs.ErrorOrNil() 577 } 578 579 // ProcessFormationAssignmentPair prepares and update the `State` and `Config` of the formation assignment based on the response and process the notifications 580 func (s *service) ProcessFormationAssignmentPair(ctx context.Context, mappingPair *AssignmentMappingPairWithOperation) (bool, error) { 581 var isReverseProcessed bool 582 err := s.processFormationAssignmentsWithReverseNotification(ctx, mappingPair, 0, &isReverseProcessed) 583 return isReverseProcessed, err 584 } 585 586 func (s *service) processFormationAssignmentsWithReverseNotification(ctx context.Context, mappingPair *AssignmentMappingPairWithOperation, depth int, isReverseProcessed *bool) error { 587 fa := mappingPair.Assignment.FormationAssignment 588 log.C(ctx).Infof("Processing formation assignment with ID: %q for formation with ID: %q with Source: %q of Type: %q and Target: %q of Type: %q and State %q", fa.ID, fa.FormationID, fa.Source, fa.SourceType, fa.Target, fa.TargetType, fa.State) 589 assignmentClone := mappingPair.Assignment.Clone() 590 var reverseClone *FormationAssignmentRequestMapping 591 if mappingPair.ReverseAssignment != nil { 592 reverseClone = mappingPair.ReverseAssignment.Clone() 593 } 594 assignment := assignmentClone.FormationAssignment 595 596 if assignment.State == string(model.ReadyAssignmentState) { 597 log.C(ctx).Infof("The formation assignment with ID: %q is in %q state. No notifications will be sent for it.", assignment.ID, assignment.State) 598 return nil 599 } 600 601 if assignmentClone.Request == nil { 602 log.C(ctx).Infof("In the formation assignment mapping pair, assignment with ID: %q hasn't attached webhook request. Updating the formation assignment to %q state without sending notification", assignment.ID, assignment.State) 603 assignment.State = string(model.ReadyAssignmentState) 604 if err := s.Update(ctx, assignment.ID, assignment); err != nil { 605 return errors.Wrapf(err, "while updating formation assignment for formation with ID: %q with source: %q and target: %q", assignment.FormationID, assignment.Source, assignment.Target) 606 } 607 return nil 608 } 609 if assignment.Source == assignment.Target { 610 assignment.State = string(model.ReadyAssignmentState) 611 log.C(ctx).Infof("In the formation assignment mapping pair, assignment with ID: %q is self-referenced. Updating the formation assignment to %q state without sending notification", assignment.ID, assignment.State) 612 if err := s.Update(ctx, assignment.ID, assignment); err != nil { 613 return errors.Wrapf(err, "while updating self-referenced formation assignment for formation with ID: %q with source and target: %q", assignment.FormationID, assignment.Source) 614 } 615 return nil 616 } 617 618 extendedRequest, err := s.faNotificationService.GenerateFormationAssignmentNotificationExt(ctx, assignmentClone, reverseClone, mappingPair.Operation) 619 if err != nil { 620 return errors.Wrap(err, "while creating extended formation assignment request") 621 } 622 623 response, err := s.notificationService.SendNotification(ctx, extendedRequest) 624 if err != nil { 625 updateError := s.SetAssignmentToErrorState(ctx, assignment, err.Error(), TechnicalError, model.CreateErrorAssignmentState) 626 if updateError != nil { 627 return errors.Wrapf( 628 updateError, 629 "while updating error state: %s", 630 errors.Wrapf(err, "while sending notification for formation assignment with ID %q", assignment.ID).Error()) 631 } 632 log.C(ctx).Error(errors.Wrapf(err, "while sending notification for formation assignment with ID %q", assignment.ID).Error()) 633 return nil 634 } 635 636 if response.Error != nil && *response.Error != "" { 637 err = s.statusService.SetAssignmentToErrorStateWithConstraints(ctx, assignment, *response.Error, ClientError, model.CreateErrorAssignmentState, mappingPair.Operation) 638 if err != nil { 639 return errors.Wrapf(err, "while updating error state for formation with ID %q", assignment.ID) 640 } 641 642 log.C(ctx).Error(errors.Errorf("Received error from response: %v", *response.Error).Error()) 643 return nil 644 } 645 646 requestWebhookMode := assignmentClone.Request.Webhook.Mode 647 if requestWebhookMode != nil && *requestWebhookMode == graphql.WebhookModeAsyncCallback { 648 log.C(ctx).Infof("The webhook with ID: %q in the notification is in %q mode. Updating the assignment state to: %q and waiting for the receiver to report the status on the status API...", assignmentClone.Request.Webhook.ID, graphql.WebhookModeAsyncCallback, string(model.InitialFormationState)) 649 assignment.State = string(model.InitialFormationState) 650 assignment.Value = nil 651 if err := s.Update(ctx, assignment.ID, assignment); err != nil { 652 return errors.Wrapf(err, "While updating formation assignment with id %q", assignment.ID) 653 } 654 655 return nil 656 } 657 658 if response.State != nil { // if there is a state in the response 659 log.C(ctx).Info("There is a state in the response. Validating it...") 660 if isValid := validateResponseState(*response.State, assignment.State); !isValid { 661 return errors.Errorf("The provided state in the response %q is not valid.", *response.State) 662 } 663 if *response.State == string(model.ReadyAssignmentState) { 664 assignment.Value = nil 665 } 666 assignment.State = *response.State 667 } else { 668 if *response.ActualStatusCode == *response.SuccessStatusCode { 669 assignment.State = string(model.ReadyAssignmentState) 670 assignment.Value = nil 671 } 672 673 if response.IncompleteStatusCode != nil && *response.ActualStatusCode == *response.IncompleteStatusCode { 674 assignment.State = string(model.ConfigPendingAssignmentState) 675 } 676 } 677 678 var shouldSendReverseNotification bool 679 if response.Config != nil && *response.Config != "" { 680 assignment.Value = []byte(*response.Config) 681 shouldSendReverseNotification = true 682 } 683 684 storedAssignment, err := s.Get(ctx, assignment.ID) 685 if err != nil { 686 return errors.Wrapf(err, "while fetching formation assignment with ID: %q", assignment.ID) 687 } 688 689 if storedAssignment.State != string(model.ReadyAssignmentState) { 690 if err = s.statusService.UpdateWithConstraints(ctx, assignment, mappingPair.Operation); err != nil { 691 return errors.Wrapf(err, "while creating formation assignment for formation %q with source %q and target %q", assignment.FormationID, assignment.Source, assignment.Target) 692 } 693 log.C(ctx).Infof("Assignment with ID: %q was updated with %q state", assignment.ID, assignment.State) 694 } 695 696 if shouldSendReverseNotification { 697 if reverseClone == nil { 698 return nil 699 } 700 701 *isReverseProcessed = true 702 703 if depth >= model.NotificationRecursionDepthLimit { 704 log.C(ctx).Errorf("Depth limit exceeded for assignments: %q and %q", assignmentClone.FormationAssignment.ID, reverseClone.FormationAssignment.ID) 705 return nil 706 } 707 708 newAssignment := reverseClone.Clone() 709 newReverseAssignment := assignmentClone.Clone() 710 711 if newAssignment.Request != nil { 712 newAssignment.Request.Object.SetAssignment(newAssignment.FormationAssignment) 713 newAssignment.Request.Object.SetReverseAssignment(newReverseAssignment.FormationAssignment) 714 } 715 if newReverseAssignment.Request != nil { 716 newReverseAssignment.Request.Object.SetAssignment(newReverseAssignment.FormationAssignment) 717 newReverseAssignment.Request.Object.SetReverseAssignment(newAssignment.FormationAssignment) 718 } 719 720 newAssignmentMappingPair := &AssignmentMappingPairWithOperation{ 721 AssignmentMappingPair: &AssignmentMappingPair{ 722 Assignment: newAssignment, 723 ReverseAssignment: newReverseAssignment, 724 }, 725 Operation: mappingPair.Operation, 726 } 727 728 if err = s.processFormationAssignmentsWithReverseNotification(ctx, newAssignmentMappingPair, depth+1, isReverseProcessed); err != nil { 729 return errors.Wrap(err, "while sending reverse notification") 730 } 731 } 732 733 return nil 734 } 735 736 // CleanupFormationAssignment If the provided mappingPair does not contain notification request the assignment is deleted. 737 // If the provided pair contains notification request - sends it and adapts the `State` and `Config` of the formation assignment 738 // based on the response. 739 // In the case the response is successful it deletes the formation assignment 740 // In all other cases the `State` and `Config` are updated accordingly 741 func (s *service) CleanupFormationAssignment(ctx context.Context, mappingPair *AssignmentMappingPairWithOperation) (bool, error) { 742 assignment := mappingPair.Assignment.FormationAssignment 743 if mappingPair.Assignment.Request == nil { 744 if err := s.Delete(ctx, assignment.ID); err != nil { 745 if apperrors.IsNotFoundError(err) { 746 log.C(ctx).Infof("Assignment with ID %q has already been deleted", assignment.ID) 747 return false, nil 748 } 749 750 // It is possible that the deletion fails due to some kind of DB constraint, so we will try to update the state 751 if updateError := s.SetAssignmentToErrorState(ctx, assignment, err.Error(), TechnicalError, model.DeleteErrorAssignmentState); updateError != nil { 752 return false, errors.Wrapf( 753 updateError, 754 "while updating error state: %s", 755 errors.Wrapf(err, "while deleting formation assignment with id %q", assignment.ID).Error()) 756 } 757 return false, errors.Wrapf(err, "while deleting formation assignment with id %q", assignment.ID) 758 } 759 log.C(ctx).Infof("Assignment with ID %s was deleted", assignment.ID) 760 761 return false, nil 762 } 763 764 extendedRequest, err := s.faNotificationService.GenerateFormationAssignmentNotificationExt(ctx, mappingPair.Assignment, mappingPair.ReverseAssignment, mappingPair.Operation) 765 if err != nil { 766 return false, errors.Wrap(err, "while creating extended formation assignment request") 767 } 768 769 response, err := s.notificationService.SendNotification(ctx, extendedRequest) 770 if err != nil { 771 if updateError := s.SetAssignmentToErrorState(ctx, assignment, err.Error(), TechnicalError, model.DeleteErrorAssignmentState); updateError != nil { 772 return false, errors.Wrapf( 773 updateError, 774 "while updating error state: %s", 775 errors.Wrapf(err, "while sending notification for formation assignment with ID %q", assignment.ID).Error()) 776 } 777 return false, errors.Wrapf(err, "while sending notification for formation assignment with ID %q", assignment.ID) 778 } 779 780 if response.Error != nil && *response.Error != "" { 781 if err = s.statusService.SetAssignmentToErrorStateWithConstraints(ctx, assignment, *response.Error, ClientError, model.DeleteErrorAssignmentState, mappingPair.Operation); err != nil { 782 return false, errors.Wrapf(err, "while updating error state for formation with ID %q", assignment.ID) 783 } 784 return false, errors.Errorf("Received error from response: %v", *response.Error) 785 } 786 787 requestWebhookMode := mappingPair.Assignment.Request.Webhook.Mode 788 if requestWebhookMode != nil && *requestWebhookMode == graphql.WebhookModeAsyncCallback { 789 log.C(ctx).Infof("The webhook with ID: %q in the notification is in %q mode. Updating the assignment state to: %q and waiting for the receiver to report the status on the status API...", mappingPair.Assignment.Request.Webhook.ID, graphql.WebhookModeAsyncCallback, string(model.DeletingAssignmentState)) 790 assignment.State = string(model.DeletingAssignmentState) 791 assignment.Value = nil 792 if err = s.Update(ctx, assignment.ID, assignment); err != nil { 793 if apperrors.IsNotFoundError(err) { 794 log.C(ctx).Infof("Assignment with ID %q has already been deleted", assignment.ID) 795 return false, nil 796 } 797 return false, errors.Wrapf(err, "While updating formation assignment with id %q", assignment.ID) 798 } 799 return false, nil 800 } 801 802 if response.State != nil { // if there is a state in the response 803 log.C(ctx).Info("There is a state in the response. Validating it...") 804 if isValid := validateResponseState(*response.State, assignment.State); !isValid { 805 return false, errors.Errorf("The provided state in the response %q is not valid.", *response.State) 806 } 807 } 808 809 // if there is a state in the body - check if it is READY 810 // if there is no state in the body - check if the status code is 'success' 811 if (response.State != nil && *response.State == string(model.ReadyAssignmentState)) || 812 (response.State == nil && *response.ActualStatusCode == *response.SuccessStatusCode) { 813 if err = s.statusService.DeleteWithConstraints(ctx, assignment.ID); err != nil { 814 if apperrors.IsNotFoundError(err) { 815 log.C(ctx).Infof("Assignment with ID %q has already been deleted", assignment.ID) 816 return false, nil 817 } 818 // It is possible that the deletion fails due to some kind of DB constraint, so we will try to update the state 819 if updateError := s.SetAssignmentToErrorState(ctx, assignment, "error while deleting assignment", TechnicalError, model.DeleteErrorAssignmentState); updateError != nil { 820 if apperrors.IsNotFoundError(updateError) { 821 log.C(ctx).Infof("Assignment with ID %q has already been deleted", assignment.ID) 822 return false, nil 823 } 824 return false, errors.Wrapf( 825 updateError, 826 "while updating error state: %s", 827 errors.Wrapf(err, "while deleting formation assignment with id %q", assignment.ID).Error()) 828 } 829 return false, errors.Wrapf(err, "while deleting formation assignment with id %q", assignment.ID) 830 } 831 log.C(ctx).Infof("Assignment with ID %s was deleted", assignment.ID) 832 833 return false, nil 834 } 835 836 if response.State != nil && *response.State == string(model.DeleteErrorAssignmentState) { 837 if err = s.statusService.SetAssignmentToErrorStateWithConstraints(ctx, assignment, "", ClientError, model.DeleteErrorAssignmentState, mappingPair.Operation); err != nil { 838 if apperrors.IsNotFoundError(err) { 839 log.C(ctx).Infof("Assignment with ID %q has already been deleted", assignment.ID) 840 return false, nil 841 } 842 return false, errors.Wrapf(err, "while updating error state for formation with ID %q", assignment.ID) 843 } 844 } 845 846 if response.IncompleteStatusCode != nil && *response.ActualStatusCode == *response.IncompleteStatusCode { 847 err = errors.New("Error while deleting assignment: config propagation is not supported on unassign notifications") 848 if updateErr := s.SetAssignmentToErrorState(ctx, assignment, err.Error(), ClientError, model.DeleteErrorAssignmentState); updateErr != nil { 849 return false, errors.Wrapf(updateErr, "while updating error state for formation with ID %q", assignment.ID) 850 } 851 return false, err 852 } 853 854 return false, nil 855 } 856 857 func validateResponseState(newState, previousState string) bool { 858 if !model.SupportedFormationAssignmentStates[newState] { 859 return false 860 } 861 862 // handles synchronous "delete/unassign" statuses 863 if previousState == string(model.DeletingAssignmentState) && 864 (newState != string(model.DeleteErrorAssignmentState) && newState != string(model.ReadyAssignmentState)) { 865 return false 866 } 867 868 // handles synchronous "create/assign" statuses 869 if previousState == string(model.InitialAssignmentState) && 870 (newState != string(model.CreateErrorAssignmentState) && newState != string(model.ConfigPendingAssignmentState) && newState != string(model.ReadyAssignmentState)) { 871 return false 872 } 873 874 return true 875 } 876 877 func (s *service) SetAssignmentToErrorState(ctx context.Context, assignment *model.FormationAssignment, errorMessage string, errorCode AssignmentErrorCode, state model.FormationAssignmentState) error { 878 assignment.State = string(state) 879 assignmentError := AssignmentErrorWrapper{AssignmentError{ 880 Message: errorMessage, 881 ErrorCode: errorCode, 882 }} 883 marshaled, err := json.Marshal(assignmentError) 884 if err != nil { 885 return errors.Wrapf(err, "While preparing error message for assignment with ID %q", assignment.ID) 886 } 887 assignment.Value = marshaled 888 if err := s.Update(ctx, assignment.ID, assignment); err != nil { 889 return errors.Wrapf(err, "While updating formation assignment with id %q", assignment.ID) 890 } 891 log.C(ctx).Infof("Assignment with ID %s set to state %s", assignment.ID, assignment.State) 892 return nil 893 } 894 895 func (s *service) matchFormationAssignmentsWithRequests(ctx context.Context, assignments []*model.FormationAssignment, runtimeContextIDToRuntimeIDMapping map[string]string, applicationIDToApplicationTemplateIDMapping map[string]string, requests []*webhookclient.FormationAssignmentNotificationRequest) []*AssignmentMappingPair { 896 formationAssignmentMapping := make([]*FormationAssignmentRequestMapping, 0, len(assignments)) 897 for i, assignment := range assignments { 898 mappingObject := &FormationAssignmentRequestMapping{ 899 Request: nil, 900 FormationAssignment: assignments[i], 901 } 902 903 target := assignment.Target 904 if assignment.TargetType == model.FormationAssignmentTypeRuntimeContext { 905 log.C(ctx).Infof("Matching for runtime context, fetching associated runtime for runtime context with ID %s", target) 906 907 target = runtimeContextIDToRuntimeIDMapping[assignment.Target] 908 log.C(ctx).Infof("Fetched associated runtime with ID %s for runtime context with ID %s", target, assignment.Target) 909 } 910 911 assignment: 912 for j, request := range requests { 913 var objectID string 914 if request.Webhook.RuntimeID != nil { 915 objectID = *request.Webhook.RuntimeID 916 } 917 918 // It is possible for both the application and the application template to have registered webhooks. 919 // In such case the application webhook should be used. 920 if request.Webhook.ApplicationID != nil { 921 objectID = *request.Webhook.ApplicationID 922 } else if request.Webhook.ApplicationTemplateID != nil && 923 *request.Webhook.ApplicationTemplateID == applicationIDToApplicationTemplateIDMapping[target] { 924 objectID = target 925 } 926 927 if objectID != target { 928 continue 929 } 930 931 participants := request.Object.GetParticipantsIDs() 932 for _, id := range participants { 933 // We should not generate notifications for self 934 if assignment.Source == assignment.Target { 935 break assignment 936 } 937 if assignment.Source == id { 938 mappingObject.Request = requests[j] 939 break assignment 940 } 941 } 942 } 943 formationAssignmentMapping = append(formationAssignmentMapping, mappingObject) 944 } 945 946 log.C(ctx).Infof("Mapped %d formation assignments with %d notifications, %d assignments left with no notification", len(assignments), len(requests), len(assignments)-len(requests)) 947 sourceToTargetToMapping := make(map[string]map[string]*FormationAssignmentRequestMapping) 948 for _, mapping := range formationAssignmentMapping { 949 if _, ok := sourceToTargetToMapping[mapping.FormationAssignment.Source]; !ok { 950 sourceToTargetToMapping[mapping.FormationAssignment.Source] = make(map[string]*FormationAssignmentRequestMapping, len(assignments)/2) 951 } 952 sourceToTargetToMapping[mapping.FormationAssignment.Source][mapping.FormationAssignment.Target] = mapping 953 } 954 // Make mapping 955 assignmentMappingPairs := make([]*AssignmentMappingPair, 0, len(assignments)) 956 957 for _, mapping := range formationAssignmentMapping { 958 var reverseMapping *FormationAssignmentRequestMapping 959 if mappingsForTarget, ok := sourceToTargetToMapping[mapping.FormationAssignment.Target]; ok { 960 if actualReverseMapping, ok := mappingsForTarget[mapping.FormationAssignment.Source]; ok { 961 reverseMapping = actualReverseMapping 962 } 963 } 964 assignmentMappingPairs = append(assignmentMappingPairs, &AssignmentMappingPair{ 965 Assignment: mapping, 966 ReverseAssignment: reverseMapping, 967 }) 968 if mapping.Request != nil { 969 mapping.Request.Object.SetAssignment(mapping.FormationAssignment) 970 if reverseMapping != nil { 971 mapping.Request.Object.SetReverseAssignment(reverseMapping.FormationAssignment) 972 } 973 } 974 if reverseMapping != nil && reverseMapping.Request != nil { 975 reverseMapping.Request.Object.SetAssignment(reverseMapping.FormationAssignment) 976 reverseMapping.Request.Object.SetReverseAssignment(mapping.FormationAssignment) 977 } 978 } 979 return assignmentMappingPairs 980 } 981 982 // FormationAssignmentRequestMapping represents the mapping between the notification request and formation assignment 983 type FormationAssignmentRequestMapping struct { 984 Request *webhookclient.FormationAssignmentNotificationRequest 985 FormationAssignment *model.FormationAssignment 986 } 987 988 // Clone returns a copy of the FormationAssignmentRequestMapping 989 func (f *FormationAssignmentRequestMapping) Clone() *FormationAssignmentRequestMapping { 990 var request *webhookclient.FormationAssignmentNotificationRequest 991 if f.Request != nil { 992 request = f.Request.Clone() 993 } 994 return &FormationAssignmentRequestMapping{ 995 Request: request, 996 FormationAssignment: &model.FormationAssignment{ 997 ID: f.FormationAssignment.ID, 998 FormationID: f.FormationAssignment.FormationID, 999 TenantID: f.FormationAssignment.TenantID, 1000 Source: f.FormationAssignment.Source, 1001 SourceType: f.FormationAssignment.SourceType, 1002 Target: f.FormationAssignment.Target, 1003 TargetType: f.FormationAssignment.TargetType, 1004 State: f.FormationAssignment.State, 1005 Value: f.FormationAssignment.Value, 1006 }, 1007 } 1008 } 1009 1010 // AssignmentErrorCode represents error code used to differentiate the source of the error 1011 type AssignmentErrorCode int 1012 1013 const ( 1014 // TechnicalError indicates that the reason for the error is technical - for example networking issue 1015 TechnicalError = 1 1016 // ClientError indicates that the error was returned from the client 1017 ClientError = 2 1018 ) 1019 1020 // AssignmentMappingPair represents a pair of FormationAssignmentRequestMapping and its reverse 1021 type AssignmentMappingPair struct { 1022 Assignment *FormationAssignmentRequestMapping 1023 ReverseAssignment *FormationAssignmentRequestMapping 1024 } 1025 1026 // AssignmentMappingPairWithOperation represents a AssignmentMappingPair and the formation operation 1027 type AssignmentMappingPairWithOperation struct { 1028 *AssignmentMappingPair 1029 Operation model.FormationOperation 1030 } 1031 1032 // AssignmentError error struct used for storing the errors that occur during the FormationAssignment processing 1033 type AssignmentError struct { 1034 Message string `json:"message"` 1035 ErrorCode AssignmentErrorCode `json:"errorCode"` 1036 } 1037 1038 // AssignmentErrorWrapper wrapper for AssignmentError 1039 type AssignmentErrorWrapper struct { 1040 Error AssignmentError `json:"error"` 1041 }