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  }