github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/formation/service.go (about)

     1  package formation
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  
     8  	"github.com/hashicorp/go-multierror"
     9  
    10  	"github.com/kyma-incubator/compass/components/director/internal/domain/formationassignment"
    11  	webhookdir "github.com/kyma-incubator/compass/components/director/pkg/webhook"
    12  
    13  	"github.com/kyma-incubator/compass/components/director/internal/domain/label"
    14  	"github.com/kyma-incubator/compass/components/director/internal/domain/labeldef"
    15  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    16  	"github.com/kyma-incubator/compass/components/director/pkg/formationconstraint"
    17  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    18  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    19  	"github.com/kyma-incubator/compass/components/director/pkg/resource"
    20  	"github.com/pkg/errors"
    21  
    22  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    23  	"github.com/kyma-incubator/compass/components/director/internal/labelfilter"
    24  	"github.com/kyma-incubator/compass/components/director/internal/model"
    25  	"github.com/kyma-incubator/compass/components/director/pkg/graphql"
    26  	webhookclient "github.com/kyma-incubator/compass/components/director/pkg/webhook_client"
    27  )
    28  
    29  //go:generate mockery --exported --name=labelDefRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    30  type labelDefRepository interface {
    31  	GetByKey(ctx context.Context, tenant string, key string) (*model.LabelDefinition, error)
    32  	UpdateWithVersion(ctx context.Context, def model.LabelDefinition) error
    33  }
    34  
    35  //go:generate mockery --exported --name=labelRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    36  type labelRepository interface {
    37  	Delete(context.Context, string, model.LabelableObject, string, string) error
    38  	ListForObjectIDs(ctx context.Context, tenant string, objectType model.LabelableObject, objectIDs []string) (map[string]map[string]interface{}, error)
    39  	ListForObject(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error)
    40  }
    41  
    42  //go:generate mockery --exported --name=runtimeRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    43  type runtimeRepository interface {
    44  	GetByFiltersAndIDUsingUnion(ctx context.Context, tenant, id string, filter []*labelfilter.LabelFilter) (*model.Runtime, error)
    45  	ListAll(ctx context.Context, tenant string, filter []*labelfilter.LabelFilter) ([]*model.Runtime, error)
    46  	ListAllWithUnionSetCombination(ctx context.Context, tenant string, filter []*labelfilter.LabelFilter) ([]*model.Runtime, error)
    47  	ListOwnedRuntimes(ctx context.Context, tenant string, filter []*labelfilter.LabelFilter) ([]*model.Runtime, error)
    48  	ListByScenariosAndIDs(ctx context.Context, tenant string, scenarios []string, ids []string) ([]*model.Runtime, error)
    49  	ListByScenarios(ctx context.Context, tenant string, scenarios []string) ([]*model.Runtime, error)
    50  	ListByIDs(ctx context.Context, tenant string, ids []string) ([]*model.Runtime, error)
    51  	GetByID(ctx context.Context, tenant, id string) (*model.Runtime, error)
    52  	OwnerExistsByFiltersAndID(ctx context.Context, tenant, id string, filter []*labelfilter.LabelFilter) (bool, error)
    53  }
    54  
    55  //go:generate mockery --exported --name=runtimeContextRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    56  type runtimeContextRepository interface {
    57  	GetByRuntimeID(ctx context.Context, tenant, runtimeID string) (*model.RuntimeContext, error)
    58  	ListByIDs(ctx context.Context, tenant string, ids []string) ([]*model.RuntimeContext, error)
    59  	ListByScenariosAndRuntimeIDs(ctx context.Context, tenant string, scenarios []string, runtimeIDs []string) ([]*model.RuntimeContext, error)
    60  	ListByScenarios(ctx context.Context, tenant string, scenarios []string) ([]*model.RuntimeContext, error)
    61  	GetByID(ctx context.Context, tenant, id string) (*model.RuntimeContext, error)
    62  	ExistsByRuntimeID(ctx context.Context, tenant, rtmID string) (bool, error)
    63  }
    64  
    65  // FormationRepository represents the Formations repository layer
    66  //go:generate mockery --name=FormationRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    67  type FormationRepository interface {
    68  	Get(ctx context.Context, id, tenantID string) (*model.Formation, error)
    69  	GetByName(ctx context.Context, name, tenantID string) (*model.Formation, error)
    70  	GetGlobalByID(ctx context.Context, id string) (*model.Formation, error)
    71  	List(ctx context.Context, tenant string, pageSize int, cursor string) (*model.FormationPage, error)
    72  	Create(ctx context.Context, item *model.Formation) error
    73  	DeleteByName(ctx context.Context, tenantID, name string) error
    74  	Update(ctx context.Context, model *model.Formation) error
    75  }
    76  
    77  // FormationTemplateRepository represents the FormationTemplate repository layer
    78  //go:generate mockery --name=FormationTemplateRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    79  type FormationTemplateRepository interface {
    80  	Get(ctx context.Context, id string) (*model.FormationTemplate, error)
    81  	GetByNameAndTenant(ctx context.Context, templateName, tenantID string) (*model.FormationTemplate, error)
    82  }
    83  
    84  // NotificationsService represents the notification service for generating and sending notifications
    85  //go:generate mockery --name=NotificationsService --output=automock --outpkg=automock --case=underscore --disable-version-string
    86  type NotificationsService interface {
    87  	GenerateFormationAssignmentNotifications(ctx context.Context, tenant, objectID string, formation *model.Formation, operation model.FormationOperation, objectType graphql.FormationObjectType) ([]*webhookclient.FormationAssignmentNotificationRequest, error)
    88  	GenerateFormationNotifications(ctx context.Context, formationTemplateWebhooks []*model.Webhook, tenantID string, formation *model.Formation, formationTemplateName, formationTemplateID string, formationOperation model.FormationOperation) ([]*webhookclient.FormationNotificationRequest, error)
    89  	SendNotification(ctx context.Context, webhookNotificationReq webhookclient.WebhookExtRequest) (*webhookdir.Response, error)
    90  	PrepareDetailsForNotificationStatusReturned(ctx context.Context, formation *model.Formation, operation model.FormationOperation) (*formationconstraint.NotificationStatusReturnedOperationDetails, error)
    91  }
    92  
    93  //go:generate mockery --exported --name=statusService --output=automock --outpkg=automock --case=underscore --disable-version-string
    94  type statusService interface {
    95  	UpdateWithConstraints(ctx context.Context, formation *model.Formation, operation model.FormationOperation) error
    96  	SetFormationToErrorStateWithConstraints(ctx context.Context, formation *model.Formation, errorMessage string, errorCode formationassignment.AssignmentErrorCode, state model.FormationState, operation model.FormationOperation) error
    97  }
    98  
    99  // FormationAssignmentNotificationsService represents the notification service for generating and sending notifications
   100  //go:generate mockery --name=FormationAssignmentNotificationsService --output=automock --outpkg=automock --case=underscore --disable-version-string
   101  type FormationAssignmentNotificationsService interface {
   102  	GenerateFormationAssignmentNotification(ctx context.Context, formationAssignment *model.FormationAssignment, operation model.FormationOperation) (*webhookclient.FormationAssignmentNotificationRequest, error)
   103  }
   104  
   105  //go:generate mockery --exported --name=labelDefService --output=automock --outpkg=automock --case=underscore --disable-version-string
   106  type labelDefService interface {
   107  	CreateWithFormations(ctx context.Context, tnt string, formations []string) error
   108  	ValidateExistingLabelsAgainstSchema(ctx context.Context, schema interface{}, tenant, key string) error
   109  	ValidateAutomaticScenarioAssignmentAgainstSchema(ctx context.Context, schema interface{}, tenantID, key string) error
   110  	GetAvailableScenarios(ctx context.Context, tenantID string) ([]string, error)
   111  }
   112  
   113  //go:generate mockery --exported --name=labelService --output=automock --outpkg=automock --case=underscore --disable-version-string
   114  type labelService interface {
   115  	CreateLabel(ctx context.Context, tenant, id string, labelInput *model.LabelInput) error
   116  	UpdateLabel(ctx context.Context, tenant, id string, labelInput *model.LabelInput) error
   117  	GetLabel(ctx context.Context, tenant string, labelInput *model.LabelInput) (*model.Label, error)
   118  }
   119  
   120  //go:generate mockery --exported --name=uuidService --output=automock --outpkg=automock --case=underscore --disable-version-string
   121  type uuidService interface {
   122  	Generate() string
   123  }
   124  
   125  //go:generate mockery --exported --name=automaticFormationAssignmentService --output=automock --outpkg=automock --case=underscore --disable-version-string
   126  type automaticFormationAssignmentService interface {
   127  	GetForScenarioName(ctx context.Context, scenarioName string) (model.AutomaticScenarioAssignment, error)
   128  }
   129  
   130  //go:generate mockery --exported --name=automaticFormationAssignmentRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
   131  type automaticFormationAssignmentRepository interface {
   132  	Create(ctx context.Context, model model.AutomaticScenarioAssignment) error
   133  	DeleteForTargetTenant(ctx context.Context, tenantID string, targetTenantID string) error
   134  	DeleteForScenarioName(ctx context.Context, tenantID string, scenarioName string) error
   135  	ListAll(ctx context.Context, tenantID string) ([]*model.AutomaticScenarioAssignment, error)
   136  }
   137  
   138  //go:generate mockery --exported --name=tenantService --output=automock --outpkg=automock --case=underscore --disable-version-string
   139  type tenantService interface {
   140  	GetInternalTenant(ctx context.Context, externalTenant string) (string, error)
   141  	GetTenantByExternalID(ctx context.Context, id string) (*model.BusinessTenantMapping, error)
   142  	GetTenantByID(ctx context.Context, id string) (*model.BusinessTenantMapping, error)
   143  }
   144  
   145  //go:generate mockery --exported --name=constraintEngine --output=automock --outpkg=automock --case=underscore --disable-version-string
   146  type constraintEngine interface {
   147  	EnforceConstraints(ctx context.Context, location formationconstraint.JoinPointLocation, details formationconstraint.JoinPointDetails, formationTemplateID string) error
   148  }
   149  
   150  //go:generate mockery --exported --name=asaEngine --output=automock --outpkg=automock --case=underscore --disable-version-string
   151  type asaEngine interface {
   152  	EnsureScenarioAssigned(ctx context.Context, in model.AutomaticScenarioAssignment, processScenarioFunc ProcessScenarioFunc) error
   153  	RemoveAssignedScenario(ctx context.Context, in model.AutomaticScenarioAssignment, processScenarioFunc ProcessScenarioFunc) error
   154  	GetMatchingFuncByFormationObjectType(objType graphql.FormationObjectType) (MatchingFunc, error)
   155  	GetScenariosFromMatchingASAs(ctx context.Context, objectID string, objType graphql.FormationObjectType) ([]string, error)
   156  	IsFormationComingFromASA(ctx context.Context, objectID, formation string, objectType graphql.FormationObjectType) (bool, error)
   157  }
   158  
   159  type service struct {
   160  	applicationRepository                  applicationRepository
   161  	labelDefRepository                     labelDefRepository
   162  	labelRepository                        labelRepository
   163  	formationRepository                    FormationRepository
   164  	formationTemplateRepository            FormationTemplateRepository
   165  	labelService                           labelService
   166  	labelDefService                        labelDefService
   167  	asaService                             automaticFormationAssignmentService
   168  	uuidService                            uuidService
   169  	tenantSvc                              tenantService
   170  	repo                                   automaticFormationAssignmentRepository
   171  	runtimeRepo                            runtimeRepository
   172  	runtimeContextRepo                     runtimeContextRepository
   173  	formationAssignmentService             formationAssignmentService
   174  	formationAssignmentNotificationService FormationAssignmentNotificationsService
   175  	notificationsService                   NotificationsService
   176  	constraintEngine                       constraintEngine
   177  	webhookRepository                      webhookRepository
   178  	transact                               persistence.Transactioner
   179  	asaEngine                              asaEngine
   180  	statusService                          statusService
   181  	runtimeTypeLabelKey                    string
   182  	applicationTypeLabelKey                string
   183  }
   184  
   185  // NewService creates formation service
   186  func NewService(
   187  	transact persistence.Transactioner,
   188  	applicationRepository applicationRepository,
   189  	labelDefRepository labelDefRepository,
   190  	labelRepository labelRepository,
   191  	formationRepository FormationRepository,
   192  	formationTemplateRepository FormationTemplateRepository,
   193  	labelService labelService,
   194  	uuidService uuidService,
   195  	labelDefService labelDefService,
   196  	asaRepo automaticFormationAssignmentRepository,
   197  	asaService automaticFormationAssignmentService,
   198  	tenantSvc tenantService, runtimeRepo runtimeRepository,
   199  	runtimeContextRepo runtimeContextRepository,
   200  	formationAssignmentService formationAssignmentService,
   201  	formationAssignmentNotificationService FormationAssignmentNotificationsService,
   202  	notificationsService NotificationsService,
   203  	constraintEngine constraintEngine,
   204  	webhookRepository webhookRepository,
   205  	statusService statusService,
   206  	runtimeTypeLabelKey, applicationTypeLabelKey string) *service {
   207  	return &service{
   208  		transact:                               transact,
   209  		applicationRepository:                  applicationRepository,
   210  		labelDefRepository:                     labelDefRepository,
   211  		labelRepository:                        labelRepository,
   212  		formationRepository:                    formationRepository,
   213  		formationTemplateRepository:            formationTemplateRepository,
   214  		labelService:                           labelService,
   215  		labelDefService:                        labelDefService,
   216  		asaService:                             asaService,
   217  		uuidService:                            uuidService,
   218  		tenantSvc:                              tenantSvc,
   219  		repo:                                   asaRepo,
   220  		runtimeRepo:                            runtimeRepo,
   221  		runtimeContextRepo:                     runtimeContextRepo,
   222  		formationAssignmentNotificationService: formationAssignmentNotificationService,
   223  		formationAssignmentService:             formationAssignmentService,
   224  		notificationsService:                   notificationsService,
   225  		constraintEngine:                       constraintEngine,
   226  		runtimeTypeLabelKey:                    runtimeTypeLabelKey,
   227  		applicationTypeLabelKey:                applicationTypeLabelKey,
   228  		asaEngine:                              NewASAEngine(asaRepo, runtimeRepo, runtimeContextRepo, formationRepository, formationTemplateRepository, runtimeTypeLabelKey, applicationTypeLabelKey),
   229  		webhookRepository:                      webhookRepository,
   230  		statusService:                          statusService,
   231  	}
   232  }
   233  
   234  // Used for testing
   235  //nolint
   236  //go:generate mockery --exported --name=processFunc --output=automock --outpkg=automock --case=underscore --disable-version-string
   237  type processFunc interface {
   238  	ProcessScenarioFunc(context.Context, string, string, graphql.FormationObjectType, model.Formation) (*model.Formation, error)
   239  }
   240  
   241  // ProcessScenarioFunc provides the signature for functions that process scenarios
   242  type ProcessScenarioFunc func(context.Context, string, string, graphql.FormationObjectType, model.Formation) (*model.Formation, error)
   243  
   244  // List returns paginated Formations based on pageSize and cursor
   245  func (s *service) List(ctx context.Context, pageSize int, cursor string) (*model.FormationPage, error) {
   246  	formationTenant, err := tenant.LoadFromContext(ctx)
   247  	if err != nil {
   248  		return nil, errors.Wrapf(err, "while loading tenant from context")
   249  	}
   250  
   251  	if pageSize < 1 || pageSize > 200 {
   252  		return nil, apperrors.NewInvalidDataError("page size must be between 1 and 200")
   253  	}
   254  
   255  	return s.formationRepository.List(ctx, formationTenant, pageSize, cursor)
   256  }
   257  
   258  // Get returns the Formation by its id
   259  func (s *service) Get(ctx context.Context, id string) (*model.Formation, error) {
   260  	tnt, err := tenant.LoadFromContext(ctx)
   261  	if err != nil {
   262  		return nil, errors.Wrapf(err, "while loading tenant from context")
   263  	}
   264  
   265  	formation, err := s.formationRepository.Get(ctx, id, tnt)
   266  	if err != nil {
   267  		return nil, errors.Wrapf(err, "while getting Formation with ID %q", id)
   268  	}
   269  
   270  	return formation, nil
   271  }
   272  
   273  // GetFormationByName returns the Formation by its name
   274  func (s *service) GetFormationByName(ctx context.Context, formationName, tnt string) (*model.Formation, error) {
   275  	f, err := s.formationRepository.GetByName(ctx, formationName, tnt)
   276  	if err != nil {
   277  		log.C(ctx).Errorf("An error occurred while getting formation by name: %q: %v", formationName, err)
   278  		return nil, errors.Wrapf(err, "An error occurred while getting formation by name: %q", formationName)
   279  	}
   280  
   281  	return f, nil
   282  }
   283  
   284  // GetGlobalByID retrieves formation by `id` globally
   285  func (s *service) GetGlobalByID(ctx context.Context, id string) (*model.Formation, error) {
   286  	f, err := s.formationRepository.GetGlobalByID(ctx, id)
   287  	if err != nil {
   288  		log.C(ctx).Errorf("An error occurred while getting formation by ID: %q globally", id)
   289  		return nil, errors.Wrapf(err, "An error occurred while getting formation by ID: %q globally", id)
   290  	}
   291  
   292  	return f, nil
   293  }
   294  
   295  func (s *service) Update(ctx context.Context, model *model.Formation) error {
   296  	if err := s.formationRepository.Update(ctx, model); err != nil {
   297  		log.C(ctx).Errorf("An error occurred while updating formation with ID: %q", model.ID)
   298  		return errors.Wrapf(err, "An error occurred while updating formation with ID: %q", model.ID)
   299  	}
   300  	return nil
   301  }
   302  
   303  // GetFormationsForObject returns slice of formations for entity with ID objID and type objType
   304  func (s *service) GetFormationsForObject(ctx context.Context, tnt string, objType model.LabelableObject, objID string) ([]string, error) {
   305  	labelInput := &model.LabelInput{
   306  		Key:        model.ScenariosKey,
   307  		ObjectID:   objID,
   308  		ObjectType: objType,
   309  	}
   310  	existingLabel, err := s.labelService.GetLabel(ctx, tnt, labelInput)
   311  	if err != nil {
   312  		return nil, errors.Wrapf(err, "while fetching scenario label for %q with id %q", objType, objID)
   313  	}
   314  
   315  	return label.ValueToStringsSlice(existingLabel.Value)
   316  }
   317  
   318  // CreateFormation is responsible for a couple of things:
   319  //  - Enforce any "pre" and "post" operation formation constraints
   320  //  - Adds the provided formation to the scenario label definitions of the given tenant, if the scenario label definition does not exist it will be created
   321  //  - Creates a new Formation entity based on the provided template name or the default one is used if it's not provided
   322  //  - Generate and send notification(s) if the template from which the formation is created has a webhook attached. And maintain a state based on the executed formation notification(s) - either synchronous or asynchronous
   323  func (s *service) CreateFormation(ctx context.Context, tnt string, formation model.Formation, templateName string) (*model.Formation, error) {
   324  	fTmpl, err := s.formationTemplateRepository.GetByNameAndTenant(ctx, templateName, tnt)
   325  	if err != nil {
   326  		log.C(ctx).Errorf("An error occurred while getting formation template by name: %q: %v", templateName, err)
   327  		return nil, errors.Wrapf(err, "An error occurred while getting formation template by name: %q", templateName)
   328  	}
   329  
   330  	formationName := formation.Name
   331  	formationTemplateID := fTmpl.ID
   332  	formationTemplateName := fTmpl.Name
   333  
   334  	CRUDJoinPointDetails := &formationconstraint.CRUDFormationOperationDetails{
   335  		FormationType:       templateName,
   336  		FormationTemplateID: formationTemplateID,
   337  		FormationName:       formationName,
   338  		TenantID:            tnt,
   339  	}
   340  
   341  	if err = s.constraintEngine.EnforceConstraints(ctx, formationconstraint.PreCreate, CRUDJoinPointDetails, formationTemplateID); err != nil {
   342  		return nil, errors.Wrapf(err, "while enforcing constraints for target operation %q and constraint type %q", model.CreateFormationOperation, model.PreOperation)
   343  	}
   344  
   345  	if err := s.modifyFormations(ctx, tnt, formationName, addFormation); err != nil {
   346  		if !apperrors.IsNotFoundError(err) {
   347  			return nil, err
   348  		}
   349  		if err = s.labelDefService.CreateWithFormations(ctx, tnt, []string{formationName}); err != nil {
   350  			return nil, err
   351  		}
   352  	}
   353  
   354  	formationTemplateWebhooks, err := s.webhookRepository.ListByReferenceObjectIDGlobal(ctx, formationTemplateID, model.FormationTemplateWebhookReference)
   355  	if err != nil {
   356  		return nil, errors.Wrapf(err, "when listing formation lifecycle webhooks for formation template with ID: %q", formationTemplateID)
   357  	}
   358  
   359  	formationState := determineFormationState(ctx, formationTemplateID, formationTemplateName, formationTemplateWebhooks, formation.State)
   360  
   361  	// TODO:: Currently we need to support both mechanisms of formation creation/deletion(through label definitions and Formations entity) for backwards compatibility
   362  	newFormation, err := s.createFormation(ctx, tnt, formationTemplateID, formationName, formationState)
   363  	if err != nil {
   364  		return nil, err
   365  	}
   366  
   367  	formationReqs, err := s.notificationsService.GenerateFormationNotifications(ctx, formationTemplateWebhooks, tnt, newFormation, formationTemplateName, formationTemplateID, model.CreateFormation)
   368  	if err != nil {
   369  		return nil, errors.Wrapf(err, "while generating notifications for formation with ID: %q and name: %q", newFormation.ID, newFormation.Name)
   370  	}
   371  
   372  	for _, formationReq := range formationReqs {
   373  		if err := s.processFormationNotifications(ctx, newFormation, formationReq, model.CreateErrorFormationState); err != nil {
   374  			processErr := errors.Wrapf(err, "while processing notifications for formation with ID: %q and name: %q", newFormation.ID, newFormation.Name)
   375  			log.C(ctx).Error(processErr)
   376  			return nil, processErr
   377  		}
   378  	}
   379  
   380  	if err = s.constraintEngine.EnforceConstraints(ctx, formationconstraint.PostCreate, CRUDJoinPointDetails, formationTemplateID); err != nil {
   381  		return nil, errors.Wrapf(err, "while enforcing constraints for target operation %q and constraint type %q", model.CreateFormationOperation, model.PostOperation)
   382  	}
   383  
   384  	return newFormation, nil
   385  }
   386  
   387  // DeleteFormation is responsible for a couple of things:
   388  //  - Enforce any "pre" and "post" operation formation constraints
   389  //  - Generate and send notification(s) if the template from which the formation is created has a webhook attached. And maintain a state based on the executed formation notification(s) - either synchronous or asynchronous
   390  //  - Removes the provided formation from the scenario label definitions of the given tenant and deletes the formation entity from the DB
   391  func (s *service) DeleteFormation(ctx context.Context, tnt string, formation model.Formation) (*model.Formation, error) {
   392  	ft, err := s.getFormationWithTemplate(ctx, formation.Name, tnt)
   393  	if err != nil {
   394  		return nil, errors.Wrapf(err, "while deleting formation")
   395  	}
   396  
   397  	formationID := ft.formation.ID
   398  	formationName := ft.formation.Name
   399  	formationTemplateID := ft.formationTemplate.ID
   400  	formationTemplateName := ft.formationTemplate.Name
   401  
   402  	joinPointDetails := &formationconstraint.CRUDFormationOperationDetails{
   403  		FormationType:       formationTemplateName,
   404  		FormationTemplateID: formationTemplateID,
   405  		FormationName:       formationName,
   406  		TenantID:            tnt,
   407  	}
   408  
   409  	if err = s.constraintEngine.EnforceConstraints(ctx, formationconstraint.PreDelete, joinPointDetails, formationTemplateID); err != nil {
   410  		return nil, errors.Wrapf(err, "while enforcing constraints for target operation %q and constraint type %q", model.DeleteFormationOperation, model.PreOperation)
   411  	}
   412  
   413  	formationTemplateWebhooks, err := s.webhookRepository.ListByReferenceObjectIDGlobal(ctx, formationTemplateID, model.FormationTemplateWebhookReference)
   414  	if err != nil {
   415  		return nil, errors.Wrapf(err, "when listing formation lifecycle webhooks for formation template with ID: %q", formationTemplateID)
   416  	}
   417  
   418  	formationReqs, err := s.notificationsService.GenerateFormationNotifications(ctx, formationTemplateWebhooks, tnt, ft.formation, formationTemplateName, formationTemplateID, model.DeleteFormation)
   419  	if err != nil {
   420  		return nil, errors.Wrapf(err, "while generating notifications for formation with ID: %q and name: %q", formationID, formationName)
   421  	}
   422  
   423  	for _, formationReq := range formationReqs {
   424  		if err := s.processFormationNotifications(ctx, ft.formation, formationReq, model.DeleteErrorFormationState); err != nil {
   425  			processErr := errors.Wrapf(err, "while processing notifications for formation with ID: %q and name: %q", formationID, formationName)
   426  			log.C(ctx).Error(processErr)
   427  			return nil, processErr
   428  		}
   429  	}
   430  
   431  	if ft.formation.State == model.ReadyFormationState {
   432  		if err := s.DeleteFormationEntityAndScenarios(ctx, tnt, formationName); err != nil {
   433  			return nil, errors.Wrapf(err, "An error occurred while deleting formation entity with name: %q and its scenarios label", formationName)
   434  		}
   435  
   436  		if err = s.constraintEngine.EnforceConstraints(ctx, formationconstraint.PostDelete, joinPointDetails, formationTemplateID); err != nil {
   437  			return nil, errors.Wrapf(err, "while enforcing constraints for target operation %q and constraint type %q", model.DeleteFormationOperation, model.PostOperation)
   438  		}
   439  	}
   440  
   441  	return ft.formation, nil
   442  }
   443  
   444  // DeleteFormationEntityAndScenarios removes the formation name from scenarios label definitions and deletes the formation entity from the DB
   445  func (s *service) DeleteFormationEntityAndScenarios(ctx context.Context, tnt, formationName string) error {
   446  	if err := s.modifyFormations(ctx, tnt, formationName, deleteFormation); err != nil {
   447  		return err
   448  	}
   449  
   450  	// TODO:: Currently we need to support both mechanisms of formation creation/deletion(through label definitions and Formations entity) for backwards compatibility
   451  	if err := s.formationRepository.DeleteByName(ctx, tnt, formationName); err != nil {
   452  		log.C(ctx).Errorf("An error occurred while deleting formation with name: %q", formationName)
   453  		return errors.Wrapf(err, "An error occurred while deleting formation with name: %q", formationName)
   454  	}
   455  
   456  	return nil
   457  }
   458  
   459  // AssignFormation assigns object based on graphql.FormationObjectType.
   460  //
   461  // When objectType graphql.FormationObjectType is graphql.FormationObjectTypeApplication, graphql.FormationObjectTypeRuntime and
   462  // graphql.FormationObjectTypeRuntimeContext it adds the provided formation to the scenario label of the entity if such exists,
   463  // otherwise new scenario label is created for the entity with the provided formation.
   464  //
   465  // FormationAssignments for the object that is being assigned and the already assigned objects are generated and stored.
   466  // For each object X already part of the formation formationAssignment with source=X and target=objectID and formationAssignment
   467  // with source=objectID and target=X are generated.
   468  //
   469  // Additionally, notifications are sent to the interested participants for that formation change.
   470  // 		- If objectType is graphql.FormationObjectTypeApplication:
   471  //				- A notification about the assigned application is sent to all the runtimes that are in the formation (either directly or via runtimeContext) and has configuration change webhook.
   472  //  			- A notification about the assigned application is sent to all the applications that are in the formation and has application tenant mapping webhook.
   473  //				- If the assigned application has an application tenant mapping webhook, a notification about each application in the formation is sent to this application.
   474  //				- If the assigned application has a configuration change webhook, a notification about each runtime/runtimeContext in the formation is sent to this application.
   475  // 		- If objectType is graphql.FormationObjectTypeRuntime or graphql.FormationObjectTypeRuntimeContext:
   476  //				- If the assigned runtime/runtimeContext has configuration change webhook, a notification about each application in the formation is sent to this runtime.
   477  //				- A notification about the assigned runtime/runtimeContext is sent to all the applications that are in the formation and have configuration change webhook.
   478  //
   479  // If an error occurs during the formationAssignment processing the failed formationAssignment's value is updated with the error and the processing proceeds. The error should not
   480  // be returned but only logged. If the error is returned the assign operation will be rolled back and all the created resources(labels, formationAssignments etc.) will be rolled
   481  // back. On the other hand the participants in the formation will have been notified for the assignment and there is no mechanism for informing them that the assignment was not executed successfully.
   482  //
   483  // After the assigning there may be formationAssignments in CREATE_ERROR state. They can be fixed by assigning the object to the same formation again. This will result in retrying only
   484  // the formationAssignments that are in state different from READY.
   485  //
   486  // If the graphql.FormationObjectType is graphql.FormationObjectTypeTenant it will
   487  // create automatic scenario assignment with the caller and target tenant which then will assign the right Runtime / RuntimeContexts based on the formation template's runtimeType.
   488  func (s *service) AssignFormation(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation model.Formation) (*model.Formation, error) {
   489  	log.C(ctx).Infof("Assigning object with ID %q of type %q to formation %q", objectID, objectType, formation.Name)
   490  
   491  	ft, err := s.getFormationWithTemplate(ctx, formation.Name, tnt)
   492  	if err != nil {
   493  		return nil, errors.Wrapf(err, "while assigning formation with name %q", formation.Name)
   494  	}
   495  
   496  	if !isObjectTypeSupported(ft.formationTemplate, objectType) {
   497  		return nil, errors.Errorf("Formation %q of type %q does not support resources of type %q", ft.formation.Name, ft.formationTemplate.Name, objectType)
   498  	}
   499  
   500  	joinPointDetails, err := s.prepareDetailsForAssign(ctx, tnt, objectID, objectType, ft.formation, ft.formationTemplate)
   501  	if err != nil {
   502  		return nil, errors.Wrapf(err, "while preparing joinpoint details for target operation %q and constraint type %q", model.AssignFormationOperation, model.PreOperation)
   503  	}
   504  
   505  	if err = s.constraintEngine.EnforceConstraints(ctx, formationconstraint.PreAssign, joinPointDetails, ft.formationTemplate.ID); err != nil {
   506  		return nil, errors.Wrapf(err, "while enforcing constraints for target operation %q and constraint type %q", model.AssignFormationOperation, model.PreOperation)
   507  	}
   508  
   509  	formationFromDB := ft.formation
   510  	switch objectType {
   511  	case graphql.FormationObjectTypeApplication, graphql.FormationObjectTypeRuntime, graphql.FormationObjectTypeRuntimeContext:
   512  		// If we assign it to the label definitions when it is in deleting state we risk leaving incorrect data
   513  		// in the LabelDefinition and formation assignments and failing to delete the formation later on
   514  		if formationFromDB.State == model.DeletingFormationState || formationFromDB.State == model.DeleteErrorFormationState {
   515  			return nil, fmt.Errorf("cannot assign to formation with ID %q as it is in %q state", formationFromDB.ID, formationFromDB.State)
   516  		}
   517  		err := s.assign(ctx, tnt, objectID, objectType, formationFromDB, ft.formationTemplate)
   518  		if err != nil {
   519  			return nil, err
   520  		}
   521  
   522  		assignments, err := s.formationAssignmentService.GenerateAssignments(ctx, tnt, objectID, objectType, formationFromDB)
   523  		if err != nil {
   524  			return nil, err
   525  		}
   526  
   527  		// When it is in initial state, the notification generation will be handled by the async API via resynchronizing the formation later
   528  		// If we are in create error state, the formation is not ready, and we should not send notifications
   529  		if formationFromDB.State == model.InitialFormationState || formationFromDB.State == model.CreateErrorFormationState {
   530  			log.C(ctx).Infof("Formation with id %q is not in %q state. Waiting for response on status API before sending notifications...", formationFromDB.ID, model.ReadyFormationState)
   531  			return ft.formation, nil
   532  		}
   533  
   534  		rtmContextIDsMapping, err := s.getRuntimeContextIDToRuntimeIDMapping(ctx, tnt, assignments)
   535  		if err != nil {
   536  			return nil, err
   537  		}
   538  
   539  		applicationIDToApplicationTemplateIDMapping, err := s.getApplicationIDToApplicationTemplateIDMapping(ctx, tnt, assignments)
   540  		if err != nil {
   541  			return nil, err
   542  		}
   543  
   544  		requests, err := s.notificationsService.GenerateFormationAssignmentNotifications(ctx, tnt, objectID, formationFromDB, model.AssignFormation, objectType)
   545  		if err != nil {
   546  			return nil, errors.Wrapf(err, "while generating notifications for %s assignment", objectType)
   547  		}
   548  
   549  		if err = s.formationAssignmentService.ProcessFormationAssignments(ctx, assignments, rtmContextIDsMapping, applicationIDToApplicationTemplateIDMapping, requests, s.formationAssignmentService.ProcessFormationAssignmentPair, model.AssignFormation); err != nil {
   550  			log.C(ctx).Errorf("Error occurred while processing formationAssignments %s", err.Error())
   551  			return nil, err
   552  		}
   553  
   554  	case graphql.FormationObjectTypeTenant:
   555  		targetTenantID, err := s.tenantSvc.GetInternalTenant(ctx, objectID)
   556  		if err != nil {
   557  			return nil, err
   558  		}
   559  
   560  		if _, err = s.CreateAutomaticScenarioAssignment(ctx, newAutomaticScenarioAssignmentModel(formationFromDB.Name, tnt, targetTenantID)); err != nil {
   561  			return nil, err
   562  		}
   563  
   564  	default:
   565  		return nil, fmt.Errorf("unknown formation type %s", objectType)
   566  	}
   567  
   568  	if err = s.constraintEngine.EnforceConstraints(ctx, formationconstraint.PostAssign, joinPointDetails, ft.formationTemplate.ID); err != nil {
   569  		return nil, errors.Wrapf(err, "while enforcing constraints for target operation %q and constraint type %q", model.AssignFormationOperation, model.PostOperation)
   570  	}
   571  
   572  	return formationFromDB, nil
   573  }
   574  
   575  func (s *service) prepareDetailsForAssign(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation *model.Formation, formationTemplate *model.FormationTemplate) (*formationconstraint.AssignFormationOperationDetails, error) {
   576  	resourceSubtype, err := s.getObjectSubtype(ctx, tnt, objectID, objectType)
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  
   581  	joinPointDetails := &formationconstraint.AssignFormationOperationDetails{
   582  		ResourceType:        model.ResourceType(objectType),
   583  		ResourceSubtype:     resourceSubtype,
   584  		ResourceID:          objectID,
   585  		FormationType:       formationTemplate.Name,
   586  		FormationTemplateID: formationTemplate.ID,
   587  		FormationID:         formation.ID,
   588  		FormationName:       formation.Name,
   589  		TenantID:            tnt,
   590  	}
   591  	return joinPointDetails, nil
   592  }
   593  
   594  func (s *service) prepareDetailsForUnassign(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation *model.Formation, formationTemplate *model.FormationTemplate) (*formationconstraint.UnassignFormationOperationDetails, error) {
   595  	resourceSubtype, err := s.getObjectSubtype(ctx, tnt, objectID, objectType)
   596  	if err != nil {
   597  		return nil, err
   598  	}
   599  
   600  	joinPointDetails := &formationconstraint.UnassignFormationOperationDetails{
   601  		ResourceType:        model.ResourceType(objectType),
   602  		ResourceSubtype:     resourceSubtype,
   603  		ResourceID:          objectID,
   604  		FormationType:       formationTemplate.Name,
   605  		FormationTemplateID: formationTemplate.ID,
   606  		FormationID:         formation.ID,
   607  		TenantID:            tnt,
   608  	}
   609  	return joinPointDetails, nil
   610  }
   611  
   612  func (s *service) getObjectSubtype(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType) (string, error) {
   613  	switch objectType {
   614  	case graphql.FormationObjectTypeApplication:
   615  		applicationTypeLabel, err := s.labelService.GetLabel(ctx, tnt, &model.LabelInput{
   616  			Key:        s.applicationTypeLabelKey,
   617  			ObjectID:   objectID,
   618  			ObjectType: model.ApplicationLabelableObject,
   619  		})
   620  		if err != nil {
   621  			if apperrors.IsNotFoundError(err) {
   622  				return "", nil
   623  			}
   624  			return "", errors.Wrapf(err, "while getting label %q for application with ID %q", s.applicationTypeLabelKey, objectID)
   625  		}
   626  
   627  		applicationType, ok := applicationTypeLabel.Value.(string)
   628  		if !ok {
   629  			return "", errors.Errorf("Missing application type for application %q", objectID)
   630  		}
   631  		return applicationType, nil
   632  
   633  	case graphql.FormationObjectTypeRuntime:
   634  		runtimeTypeLabel, err := s.labelService.GetLabel(ctx, tnt, &model.LabelInput{
   635  			Key:        s.runtimeTypeLabelKey,
   636  			ObjectID:   objectID,
   637  			ObjectType: model.RuntimeLabelableObject,
   638  		})
   639  		if err != nil {
   640  			if apperrors.IsNotFoundError(err) {
   641  				return "", nil
   642  			}
   643  			return "", errors.Wrapf(err, "while getting label %q for runtime with ID %q", s.runtimeTypeLabelKey, objectID)
   644  		}
   645  
   646  		runtimeType, ok := runtimeTypeLabel.Value.(string)
   647  		if !ok {
   648  			return "", errors.Errorf("Missing runtime type for runtime %q", objectID)
   649  		}
   650  		return runtimeType, nil
   651  
   652  	case graphql.FormationObjectTypeRuntimeContext:
   653  		rtmCtx, err := s.runtimeContextRepo.GetByID(ctx, tnt, objectID)
   654  		if err != nil {
   655  			return "", errors.Wrapf(err, "while fetching runtime context with ID %q", objectID)
   656  		}
   657  
   658  		runtimeTypeLabel, err := s.labelService.GetLabel(ctx, tnt, &model.LabelInput{
   659  			Key:        s.runtimeTypeLabelKey,
   660  			ObjectID:   rtmCtx.RuntimeID,
   661  			ObjectType: model.RuntimeLabelableObject,
   662  		})
   663  		if err != nil {
   664  			return "", errors.Wrapf(err, "while getting label %q for runtime with ID %q", s.runtimeTypeLabelKey, objectID)
   665  		}
   666  
   667  		runtimeType, ok := runtimeTypeLabel.Value.(string)
   668  		if !ok {
   669  			return "", errors.Errorf("Missing runtime type for runtime %q", rtmCtx.RuntimeID)
   670  		}
   671  		return runtimeType, nil
   672  
   673  	case graphql.FormationObjectTypeTenant:
   674  		t, err := s.tenantSvc.GetTenantByExternalID(ctx, objectID)
   675  		if err != nil {
   676  			return "", errors.Wrapf(err, "while getting tenant by external ID")
   677  		}
   678  
   679  		return string(t.Type), nil
   680  
   681  	default:
   682  		return "", fmt.Errorf("unknown formation type %s", objectType)
   683  	}
   684  }
   685  
   686  func (s *service) assign(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation *model.Formation, formationTemplate *model.FormationTemplate) error {
   687  	if err := s.checkFormationTemplateTypes(ctx, tnt, objectID, objectType, formationTemplate); err != nil {
   688  		return err
   689  	}
   690  
   691  	if err := s.modifyAssignedFormations(ctx, tnt, objectID, formation.Name, objectTypeToLabelableObject(objectType), addFormation); err != nil {
   692  		if apperrors.IsNotFoundError(err) {
   693  			labelInput := newLabelInput(formation.Name, objectID, objectTypeToLabelableObject(objectType))
   694  			if err = s.labelService.CreateLabel(ctx, tnt, s.uuidService.Generate(), labelInput); err != nil {
   695  				return err
   696  			}
   697  			return nil
   698  		}
   699  		return err
   700  	}
   701  
   702  	return nil
   703  }
   704  
   705  func (s *service) unassign(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation *model.Formation) error {
   706  	switch objectType {
   707  	case graphql.FormationObjectTypeApplication:
   708  		if err := s.modifyAssignedFormations(ctx, tnt, objectID, formation.Name, objectTypeToLabelableObject(objectType), deleteFormation); err != nil {
   709  			return err
   710  		}
   711  	case graphql.FormationObjectTypeRuntime, graphql.FormationObjectTypeRuntimeContext:
   712  		if isFormationComingFromASA, err := s.asaEngine.IsFormationComingFromASA(ctx, objectID, formation.Name, objectType); err != nil {
   713  			return err
   714  		} else if isFormationComingFromASA {
   715  			return apperrors.NewCannotUnassignObjectComingFromASAError(objectID)
   716  		}
   717  
   718  		if err := s.modifyAssignedFormations(ctx, tnt, objectID, formation.Name, objectTypeToLabelableObject(objectType), deleteFormation); err != nil {
   719  			return err
   720  		}
   721  
   722  	default:
   723  		return nil
   724  	}
   725  	return nil
   726  }
   727  
   728  func (s *service) checkFormationTemplateTypes(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formationTemplate *model.FormationTemplate) error {
   729  	switch objectType {
   730  	case graphql.FormationObjectTypeApplication:
   731  		if err := s.isValidApplicationType(ctx, tnt, objectID, formationTemplate); err != nil {
   732  			return errors.Wrapf(err, "while validating application type for application %q", objectID)
   733  		}
   734  	case graphql.FormationObjectTypeRuntime:
   735  		if err := s.isValidRuntimeType(ctx, tnt, objectID, formationTemplate); err != nil {
   736  			return errors.Wrapf(err, "while validating runtime type")
   737  		}
   738  	case graphql.FormationObjectTypeRuntimeContext:
   739  		runtimeCtx, err := s.runtimeContextRepo.GetByID(ctx, tnt, objectID)
   740  		if err != nil {
   741  			return errors.Wrapf(err, "while getting runtime context")
   742  		}
   743  		if err = s.isValidRuntimeType(ctx, tnt, runtimeCtx.RuntimeID, formationTemplate); err != nil {
   744  			return errors.Wrapf(err, "while validating runtime type of runtime")
   745  		}
   746  	}
   747  	return nil
   748  }
   749  
   750  // UnassignFormation unassigns object base on graphql.FormationObjectType.
   751  //
   752  // For objectType graphql.FormationObjectTypeApplication it removes the provided formation from the
   753  // scenario label of the application.
   754  //
   755  // For objectTypes graphql.FormationObjectTypeRuntime and graphql.FormationObjectTypeRuntimeContext
   756  // it removes the formation from the scenario label of the runtime/runtime context if the provided
   757  // formation is NOT assigned from ASA and does nothing if it is assigned from ASA.
   758  //
   759  //  Additionally, notifications are sent to the interested participants for that formation change.
   760  // 		- If objectType is graphql.FormationObjectTypeApplication:
   761  //				- A notification about the unassigned application is sent to all the runtimes that are in the formation (either directly or via runtimeContext) and has configuration change webhook.
   762  //  			- A notification about the unassigned application is sent to all the applications that are in the formation and has application tenant mapping webhook.
   763  //				- If the unassigned application has an application tenant mapping webhook, a notification about each application in the formation is sent to this application.
   764  //				- If the unassigned application has a configuration change webhook, a notification about each runtime/runtimeContext in the formation is sent to this application.
   765  // 		- If objectType is graphql.FormationObjectTypeRuntime or graphql.FormationObjectTypeRuntimeContext:
   766  //				- If the unassigned runtime/runtimeContext has configuration change webhook, a notification about each application in the formation is sent to this runtime.
   767  //   			- A notification about the unassigned runtime/runtimeContext is sent to all the applications that are in the formation and have configuration change webhook.
   768  //
   769  // For the formationAssignments that have their source or target field set to objectID:
   770  // 		- If the formationAssignment does not have notification associated with it
   771  //				- the formation assignment is deleted
   772  //		- If the formationAssignment is associated with a notification
   773  //				- If the response from the notification is success
   774  //						- the formationAssignment is deleted
   775  // 				- If the response from the notification is different from success
   776  //						- the formation assignment is updated with an error
   777  //
   778  // After the processing of the formationAssignments the state is persisted regardless of whether there were any errors.
   779  // If an error has occurred during the formationAssignment processing the unassign operation is rolled back(the updated
   780  // with the error formationAssignments are already persisted in the database).
   781  //
   782  // For objectType graphql.FormationObjectTypeTenant it will
   783  // delete the automatic scenario assignment with the caller and target tenant which then will unassign the right Runtime / RuntimeContexts based on the formation template's runtimeType.
   784  func (s *service) UnassignFormation(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation model.Formation) (*model.Formation, error) {
   785  	log.C(ctx).Infof("Unassigning object with ID %q of type %q to formation %q", objectID, objectType, formation.Name)
   786  
   787  	formationName := formation.Name
   788  	ft, err := s.getFormationWithTemplate(ctx, formationName, tnt)
   789  	if err != nil {
   790  		return nil, errors.Wrapf(err, "while unassigning formation with name %q", formationName)
   791  	}
   792  
   793  	joinPointDetails, err := s.prepareDetailsForUnassign(ctx, tnt, objectID, objectType, ft.formation, ft.formationTemplate)
   794  	if err != nil {
   795  		return nil, errors.Wrapf(err, "while preparing joinpoint details for target operation %q and constraint type %q", model.UnassignFormationOperation, model.PreOperation)
   796  	}
   797  
   798  	if err = s.constraintEngine.EnforceConstraints(ctx, formationconstraint.PreUnassign, joinPointDetails, ft.formationTemplate.ID); err != nil {
   799  		return nil, errors.Wrapf(err, "while enforcing constraints for target operation %q and constraint type %q", model.UnassignFormationOperation, model.PreOperation)
   800  	}
   801  	formationFromDB := ft.formation
   802  
   803  	err = s.unassign(ctx, tnt, objectID, objectType, formationFromDB)
   804  	if err != nil && !apperrors.IsCannotUnassignObjectComingFromASAError(err) && !apperrors.IsNotFoundError(err) {
   805  		return nil, errors.Wrapf(err, "while unassigning from formation")
   806  	}
   807  	if apperrors.IsCannotUnassignObjectComingFromASAError(err) || apperrors.IsNotFoundError(err) {
   808  		// No need to enforce post-constraints as nothing is done
   809  		return formationFromDB, nil
   810  	}
   811  
   812  	switch objectType {
   813  	case graphql.FormationObjectTypeApplication:
   814  		// We can reach this only if we are in INITIAL state and there are assigned objects to the formation
   815  		// there are no notifications sent for them, and we have created formation assignments for them.
   816  		// We should clean those up because on an earlier step we have removed it from the LabelDefinition,
   817  		// and it would result in leftover formation assignments that could cause problems on future assigns
   818  		// If we by any chance reach it from ERROR state, the formation should be empty and the deletion shouldn't do anything.
   819  		if formationFromDB.State != model.ReadyFormationState {
   820  			log.C(ctx).Infof("Formation with id %q is not in %q state. Waiting for response on status API before sending notifications...", formationFromDB.ID, model.ReadyFormationState)
   821  			err = s.formationAssignmentService.DeleteAssignmentsForObjectID(ctx, formationFromDB.ID, objectID)
   822  			if err != nil {
   823  				return nil, errors.Wrapf(err, "while deleting formationAssignments for object with type %q and ID %q", objectType, objectID)
   824  			}
   825  			return ft.formation, nil
   826  		}
   827  
   828  		requests, err := s.notificationsService.GenerateFormationAssignmentNotifications(ctx, tnt, objectID, formationFromDB, model.UnassignFormation, objectType)
   829  		if err != nil {
   830  			return nil, errors.Wrapf(err, "while generating notifications for %s unassignment", objectType)
   831  		}
   832  
   833  		formationAssignmentsForObject, err := s.formationAssignmentService.ListFormationAssignmentsForObjectID(ctx, formationFromDB.ID, objectID)
   834  		if err != nil {
   835  			return nil, errors.Wrapf(err, "while listing formationAssignments for object with type %q and ID %q", objectType, objectID)
   836  		}
   837  
   838  		rtmContextIDsMapping, err := s.getRuntimeContextIDToRuntimeIDMapping(ctx, tnt, formationAssignmentsForObject)
   839  		if err != nil {
   840  			return nil, err
   841  		}
   842  
   843  		applicationIDToApplicationTemplateIDMapping, err := s.getApplicationIDToApplicationTemplateIDMapping(ctx, tnt, formationAssignmentsForObject)
   844  		if err != nil {
   845  			return nil, err
   846  		}
   847  
   848  		tx, err := s.transact.Begin()
   849  		if err != nil {
   850  			return nil, err
   851  		}
   852  		transactionCtx := persistence.SaveToContext(ctx, tx)
   853  		defer s.transact.RollbackUnlessCommitted(transactionCtx, tx)
   854  
   855  		if err = s.formationAssignmentService.ProcessFormationAssignments(transactionCtx, formationAssignmentsForObject, rtmContextIDsMapping, applicationIDToApplicationTemplateIDMapping, requests, s.formationAssignmentService.CleanupFormationAssignment, model.UnassignFormation); err != nil {
   856  			commitErr := tx.Commit()
   857  			if commitErr != nil {
   858  				return nil, errors.Wrapf(err, "while committing transaction with error")
   859  			}
   860  			return nil, err
   861  		}
   862  
   863  		// It is important to do the list in the inner transaction
   864  		pendingAsyncAssignments, err := s.formationAssignmentService.ListFormationAssignmentsForObjectID(transactionCtx, formationFromDB.ID, objectID)
   865  		if err != nil {
   866  			return nil, errors.Wrapf(err, "while listing formationAssignments for object with type %q and ID %q", objectType, objectID)
   867  		}
   868  
   869  		err = tx.Commit()
   870  		if err != nil {
   871  			return nil, errors.Wrapf(err, "while committing transaction")
   872  		}
   873  
   874  		if len(pendingAsyncAssignments) > 0 {
   875  			log.C(ctx).Infof("There is an async delete notification in progress. Re-assigning the object with type %q and ID %q to formation %q until status is reported by the notification receiver", objectType, objectID, formationName)
   876  			err := s.assign(ctx, tnt, objectID, objectType, formationFromDB, ft.formationTemplate) // It is important to do the re-assign in the outer transaction.
   877  			if err != nil {
   878  				return nil, errors.Wrapf(err, "while re-assigning the object with type %q and ID %q that is being unassigned asynchronously", objectType, objectID)
   879  			}
   880  		}
   881  
   882  	case graphql.FormationObjectTypeRuntime, graphql.FormationObjectTypeRuntimeContext:
   883  		// We can reach this only if we are in INITIAL state and there are assigned objects to the formation
   884  		// there are no notifications sent for them, and we have created formation assignments for them.
   885  		// We should clean those up because on an earlier step we have removed it from the LabelDefinition,
   886  		// and it would result in leftover formation assignments that could cause problems on future assigns
   887  		// If we by any chance reach it from ERROR state, the formation should be empty and the deletion shouldn't do anything.
   888  		if formationFromDB.State != model.ReadyFormationState {
   889  			log.C(ctx).Infof("Formation with id %q is not in %q state. Waiting for response on status API before sending notifications...", formationFromDB.ID, model.ReadyFormationState)
   890  			err = s.formationAssignmentService.DeleteAssignmentsForObjectID(ctx, formationFromDB.ID, objectID)
   891  			if err != nil {
   892  				return nil, errors.Wrapf(err, "while deleting formationAssignments for object with type %q and ID %q", objectType, objectID)
   893  			}
   894  			return ft.formation, nil
   895  		}
   896  		requests, err := s.notificationsService.GenerateFormationAssignmentNotifications(ctx, tnt, objectID, formationFromDB, model.UnassignFormation, objectType)
   897  		if err != nil {
   898  			return nil, errors.Wrapf(err, "while generating notifications for %s unassignment", objectType)
   899  		}
   900  
   901  		formationAssignmentsForObject, err := s.formationAssignmentService.ListFormationAssignmentsForObjectID(ctx, formationFromDB.ID, objectID)
   902  		if err != nil {
   903  			return nil, errors.Wrapf(err, "while listing formationAssignments for object with type %q and ID %q", objectType, objectID)
   904  		}
   905  
   906  		rtmContextIDsMapping, err := s.getRuntimeContextIDToRuntimeIDMapping(ctx, tnt, formationAssignmentsForObject)
   907  		if err != nil {
   908  			return nil, err
   909  		}
   910  
   911  		applicationIDToApplicationTemplateIDMapping, err := s.getApplicationIDToApplicationTemplateIDMapping(ctx, tnt, formationAssignmentsForObject)
   912  		if err != nil {
   913  			return nil, err
   914  		}
   915  
   916  		tx, err := s.transact.Begin()
   917  		if err != nil {
   918  			return nil, err
   919  		}
   920  
   921  		transactionCtx := persistence.SaveToContext(ctx, tx)
   922  		defer s.transact.RollbackUnlessCommitted(transactionCtx, tx)
   923  
   924  		if err = s.formationAssignmentService.ProcessFormationAssignments(transactionCtx, formationAssignmentsForObject, rtmContextIDsMapping, applicationIDToApplicationTemplateIDMapping, requests, s.formationAssignmentService.CleanupFormationAssignment, model.UnassignFormation); err != nil {
   925  			commitErr := tx.Commit()
   926  			if commitErr != nil {
   927  				return nil, errors.Wrapf(err, "while committing transaction with error")
   928  			}
   929  			return nil, err
   930  		}
   931  
   932  		// It is important to do the list in the inner transaction
   933  		pendingAsyncAssignments, err := s.formationAssignmentService.ListFormationAssignmentsForObjectID(transactionCtx, formationFromDB.ID, objectID)
   934  		if err != nil {
   935  			return nil, errors.Wrapf(err, "while listing formationAssignments for object with type %q and ID %q", objectType, objectID)
   936  		}
   937  
   938  		err = tx.Commit()
   939  		if err != nil {
   940  			return nil, errors.Wrapf(err, "while committing transaction")
   941  		}
   942  
   943  		if len(pendingAsyncAssignments) > 0 {
   944  			log.C(ctx).Infof("There is an async delete notification in progress. Re-assigning the object with type %q and ID %q to formation %q until status is reported by the notification receiver", objectType, objectID, formation.Name)
   945  			err := s.assign(ctx, tnt, objectID, objectType, formationFromDB, ft.formationTemplate) // It is importnat to do the re-assign in the outer transaction.
   946  			if err != nil {
   947  				return nil, errors.Wrapf(err, "while re-assigning the object with type %q and ID %q that is being unassigned asynchronously", objectType, objectID)
   948  			}
   949  		}
   950  
   951  	case graphql.FormationObjectTypeTenant:
   952  		asa, err := s.asaService.GetForScenarioName(ctx, formationName)
   953  		if err != nil {
   954  			return nil, err
   955  		}
   956  		if err = s.DeleteAutomaticScenarioAssignment(ctx, asa); err != nil {
   957  			return nil, err
   958  		}
   959  
   960  	default:
   961  		return nil, fmt.Errorf("unknown formation type %s", objectType)
   962  	}
   963  
   964  	if err = s.constraintEngine.EnforceConstraints(ctx, formationconstraint.PostUnassign, joinPointDetails, ft.formationTemplate.ID); err != nil {
   965  		return nil, errors.Wrapf(err, "while enforcing constraints for target operation %q and constraint type %q", model.UnassignFormationOperation, model.PostOperation)
   966  	}
   967  
   968  	return formationFromDB, nil
   969  }
   970  
   971  // ResynchronizeFormationNotifications sends all notifications that are in error or initial state
   972  func (s *service) ResynchronizeFormationNotifications(ctx context.Context, formationID string) (*model.Formation, error) {
   973  	log.C(ctx).Infof("Resynchronizing formation with ID: %q", formationID)
   974  	tenantID, err := tenant.LoadFromContext(ctx)
   975  	if err != nil {
   976  		return nil, errors.Wrapf(err, "while loading tenant from context")
   977  	}
   978  
   979  	formation, err := s.formationRepository.Get(ctx, formationID, tenantID)
   980  	if err != nil {
   981  		return nil, errors.Wrapf(err, "while getting formation with ID %q for tenant %q", tenantID, formationID)
   982  	}
   983  	if formation.State != model.ReadyFormationState {
   984  		previousState := formation.State
   985  		updatedFormation, err := s.resynchronizeFormationNotifications(ctx, tenantID, formation)
   986  		if err != nil {
   987  			return nil, errors.Wrapf(err, "while resynchronizing formation notifications for formation with ID %q", formationID)
   988  		}
   989  		if previousState == model.DeleteErrorFormationState && updatedFormation.State == model.ReadyFormationState {
   990  			return updatedFormation, nil
   991  		}
   992  		formation, err = s.formationRepository.Get(ctx, formationID, tenantID)
   993  		if err != nil {
   994  			return nil, errors.Wrapf(err, "while getting formation with ID %q for tenant %q", tenantID, formationID)
   995  		}
   996  		if updatedFormation.State != model.ReadyFormationState {
   997  			return updatedFormation, nil
   998  		}
   999  	}
  1000  
  1001  	return s.resynchronizeFormationAssignmentNotifications(ctx, tenantID, formation)
  1002  }
  1003  
  1004  func (s *service) resynchronizeFormationAssignmentNotifications(ctx context.Context, tenantID string, formation *model.Formation) (*model.Formation, error) {
  1005  	formationID := formation.ID
  1006  
  1007  	resyncableFormationAssignments, err := s.formationAssignmentService.GetAssignmentsForFormationWithStates(ctx, tenantID, formationID,
  1008  		[]string{string(model.InitialAssignmentState),
  1009  			string(model.DeletingAssignmentState),
  1010  			string(model.CreateErrorAssignmentState),
  1011  			string(model.DeleteErrorAssignmentState)})
  1012  	if err != nil {
  1013  		return nil, errors.Wrap(err, "while getting formation assignments with synchronizing and error states")
  1014  	}
  1015  
  1016  	failedDeleteErrorFormationAssignments := make([]*model.FormationAssignment, 0, len(resyncableFormationAssignments))
  1017  	var errs *multierror.Error
  1018  	for _, fa := range resyncableFormationAssignments {
  1019  		operation := model.AssignFormation
  1020  		if fa.State == string(model.DeleteErrorAssignmentState) || fa.State == string(model.DeletingAssignmentState) {
  1021  			operation = model.UnassignFormation
  1022  		}
  1023  		var notificationForReverseFA *webhookclient.FormationAssignmentNotificationRequest
  1024  		notificationForFA, err := s.formationAssignmentNotificationService.GenerateFormationAssignmentNotification(ctx, fa, operation)
  1025  		if err != nil {
  1026  			return nil, err
  1027  		}
  1028  
  1029  		reverseFA, err := s.formationAssignmentService.GetReverseBySourceAndTarget(ctx, fa.FormationID, fa.Source, fa.Target)
  1030  		if err != nil && !apperrors.IsNotFoundError(err) {
  1031  			return nil, err
  1032  		}
  1033  		if reverseFA != nil {
  1034  			notificationForReverseFA, err = s.formationAssignmentNotificationService.GenerateFormationAssignmentNotification(ctx, reverseFA, operation)
  1035  			if err != nil && !apperrors.IsNotFoundError(err) {
  1036  				return nil, err
  1037  			}
  1038  		}
  1039  
  1040  		assignmentPair := formationassignment.AssignmentMappingPairWithOperation{
  1041  			AssignmentMappingPair: &formationassignment.AssignmentMappingPair{
  1042  				Assignment: &formationassignment.FormationAssignmentRequestMapping{
  1043  					Request:             notificationForFA,
  1044  					FormationAssignment: fa,
  1045  				},
  1046  				ReverseAssignment: &formationassignment.FormationAssignmentRequestMapping{
  1047  					Request:             notificationForReverseFA,
  1048  					FormationAssignment: reverseFA,
  1049  				},
  1050  			},
  1051  		}
  1052  		switch fa.State {
  1053  		case string(model.InitialAssignmentState), string(model.CreateErrorAssignmentState):
  1054  			assignmentPair.Operation = model.AssignFormation
  1055  			if _, err = s.formationAssignmentService.ProcessFormationAssignmentPair(ctx, &assignmentPair); err != nil {
  1056  				errs = multierror.Append(errs, err)
  1057  			}
  1058  		case string(model.DeletingAssignmentState), string(model.DeleteErrorAssignmentState):
  1059  			assignmentPair.Operation = model.UnassignFormation
  1060  			if _, err = s.formationAssignmentService.CleanupFormationAssignment(ctx, &assignmentPair); err != nil {
  1061  				errs = multierror.Append(errs, err)
  1062  			}
  1063  		}
  1064  		if fa.State == string(model.DeleteErrorAssignmentState) {
  1065  			failedDeleteErrorFormationAssignments = append(failedDeleteErrorFormationAssignments, fa)
  1066  		}
  1067  	}
  1068  
  1069  	if len(failedDeleteErrorFormationAssignments) > 0 {
  1070  		objectIDToTypeMap := make(map[string]graphql.FormationObjectType, len(failedDeleteErrorFormationAssignments)*2)
  1071  		for _, assignment := range failedDeleteErrorFormationAssignments {
  1072  			objectIDToTypeMap[assignment.Source] = formationAssignmentTypeToFormationObjectType(assignment.SourceType)
  1073  			objectIDToTypeMap[assignment.Target] = formationAssignmentTypeToFormationObjectType(assignment.TargetType)
  1074  		}
  1075  
  1076  		for objectID, objectType := range objectIDToTypeMap {
  1077  			leftAssignmentsInFormation, err := s.formationAssignmentService.ListFormationAssignmentsForObjectID(ctx, formationID, objectID)
  1078  			if err != nil {
  1079  				return nil, errors.Wrapf(err, "while listing formationAssignments for object with type %q and ID %q", objectType, objectID)
  1080  			}
  1081  
  1082  			if len(leftAssignmentsInFormation) == 0 {
  1083  				log.C(ctx).Infof("There are no formation assignments left for formation with ID: %q. Unassigning the object with type %q and ID %q from formation %q", formationID, objectType, objectID, formationID)
  1084  				err = s.unassign(ctx, tenantID, objectID, objectType, formation)
  1085  				if err != nil && !apperrors.IsCannotUnassignObjectComingFromASAError(err) && !apperrors.IsNotFoundError(err) {
  1086  					return nil, errors.Wrapf(err, "while unassigning the object with type %q and ID %q", objectType, objectID)
  1087  				}
  1088  			}
  1089  		}
  1090  	}
  1091  
  1092  	return formation, errs.ErrorOrNil()
  1093  }
  1094  
  1095  func (s *service) resynchronizeFormationNotifications(ctx context.Context, tenantID string, formation *model.Formation) (*model.Formation, error) {
  1096  	fTmpl, err := s.formationTemplateRepository.Get(ctx, formation.FormationTemplateID)
  1097  	if err != nil {
  1098  		return nil, errors.Wrapf(err, "An error occurred while getting formation template with ID: %q", formation.FormationTemplateID)
  1099  	}
  1100  	formationTemplateID := fTmpl.ID
  1101  	formationTemplateName := fTmpl.Name
  1102  
  1103  	formationTemplateWebhooks, err := s.webhookRepository.ListByReferenceObjectIDGlobal(ctx, formationTemplateID, model.FormationTemplateWebhookReference)
  1104  	if err != nil {
  1105  		return nil, errors.Wrapf(err, "when listing formation lifecycle webhooks for formation template with ID: %q", formationTemplateID)
  1106  	}
  1107  	operation := determineFormationOperationFromState(formation.State)
  1108  	errorState := determineFormationErrorStateFromOperation(operation)
  1109  
  1110  	formationReqs, err := s.notificationsService.GenerateFormationNotifications(ctx, formationTemplateWebhooks, tenantID, formation, formationTemplateName, formationTemplateID, operation)
  1111  	if err != nil {
  1112  		return nil, errors.Wrapf(err, "while generating notifications for formation with ID: %q and name: %q", formation.ID, formation.Name)
  1113  	}
  1114  
  1115  	for _, formationReq := range formationReqs {
  1116  		if err = s.processFormationNotifications(ctx, formation, formationReq, errorState); err != nil {
  1117  			processErr := errors.Wrapf(err, "while processing notifications for formation with ID: %q and name: %q", formation.ID, formation.Name)
  1118  			log.C(ctx).Error(processErr)
  1119  			return nil, processErr
  1120  		}
  1121  		if errorState == model.DeleteErrorFormationState && formation.State == model.ReadyFormationState && formationReq.Webhook.Mode != nil && *formationReq.Webhook.Mode == graphql.WebhookModeSync {
  1122  			if err = s.DeleteFormationEntityAndScenarios(ctx, tenantID, formation.Name); err != nil {
  1123  				return nil, errors.Wrapf(err, "while deleting formation with name %s", formation.Name)
  1124  			}
  1125  		}
  1126  	}
  1127  	return formation, nil
  1128  }
  1129  
  1130  func (s *service) getRuntimeContextIDToRuntimeIDMapping(ctx context.Context, tnt string, formationAssignmentsForObject []*model.FormationAssignment) (map[string]string, error) {
  1131  	rtmContextIDsSet := make(map[string]bool, 0)
  1132  	for _, assignment := range formationAssignmentsForObject {
  1133  		if assignment.TargetType == model.FormationAssignmentTypeRuntimeContext {
  1134  			rtmContextIDsSet[assignment.Target] = true
  1135  		}
  1136  	}
  1137  	rtmContextIDs := setToSlice(rtmContextIDsSet)
  1138  
  1139  	rtmContexts, err := s.runtimeContextRepo.ListByIDs(ctx, tnt, rtmContextIDs)
  1140  	if err != nil {
  1141  		return nil, err
  1142  	}
  1143  	rtmContextIDsToRuntimeMap := make(map[string]string, len(rtmContexts))
  1144  	for _, rtmContext := range rtmContexts {
  1145  		rtmContextIDsToRuntimeMap[rtmContext.ID] = rtmContext.RuntimeID
  1146  	}
  1147  	return rtmContextIDsToRuntimeMap, nil
  1148  }
  1149  
  1150  func (s *service) getApplicationIDToApplicationTemplateIDMapping(ctx context.Context, tnt string, formationAssignmentsForObject []*model.FormationAssignment) (map[string]string, error) {
  1151  	appIDsMap := make(map[string]bool, 0)
  1152  	for _, assignment := range formationAssignmentsForObject {
  1153  		if assignment.TargetType == model.FormationAssignmentTypeApplication {
  1154  			appIDsMap[assignment.Target] = true
  1155  		}
  1156  	}
  1157  	applications, err := s.applicationRepository.ListAllByIDs(ctx, tnt, setToSlice(appIDsMap))
  1158  	if err != nil {
  1159  		return nil, err
  1160  	}
  1161  	appToAppTemplateMap := make(map[string]string, len(applications))
  1162  	for i := range applications {
  1163  		if applications[i].ApplicationTemplateID != nil {
  1164  			appToAppTemplateMap[applications[i].ID] = *applications[i].ApplicationTemplateID
  1165  		}
  1166  	}
  1167  	return appToAppTemplateMap, nil
  1168  }
  1169  
  1170  // CreateAutomaticScenarioAssignment creates a new AutomaticScenarioAssignment for a given ScenarioName, Tenant and TargetTenantID
  1171  // It also ensures that all runtimes(or/and runtime contexts) with given scenarios are assigned for the TargetTenantID
  1172  func (s *service) CreateAutomaticScenarioAssignment(ctx context.Context, in model.AutomaticScenarioAssignment) (model.AutomaticScenarioAssignment, error) {
  1173  	tenantID, err := tenant.LoadFromContext(ctx)
  1174  	if err != nil {
  1175  		return model.AutomaticScenarioAssignment{}, err
  1176  	}
  1177  
  1178  	in.Tenant = tenantID
  1179  	if err := s.validateThatScenarioExists(ctx, in); err != nil {
  1180  		return model.AutomaticScenarioAssignment{}, err
  1181  	}
  1182  
  1183  	if err = s.repo.Create(ctx, in); err != nil {
  1184  		if apperrors.IsNotUniqueError(err) {
  1185  			return model.AutomaticScenarioAssignment{}, apperrors.NewInvalidOperationError("a given scenario already has an assignment")
  1186  		}
  1187  
  1188  		return model.AutomaticScenarioAssignment{}, errors.Wrap(err, "while persisting Assignment")
  1189  	}
  1190  
  1191  	if err = s.asaEngine.EnsureScenarioAssigned(ctx, in, s.AssignFormation); err != nil {
  1192  		return model.AutomaticScenarioAssignment{}, errors.Wrap(err, "while assigning scenario to runtimes matching selector")
  1193  	}
  1194  
  1195  	return in, nil
  1196  }
  1197  
  1198  // DeleteAutomaticScenarioAssignment deletes the assignment for a given scenario in a scope of a tenant
  1199  // It also removes corresponding assigned scenarios for the ASA
  1200  func (s *service) DeleteAutomaticScenarioAssignment(ctx context.Context, in model.AutomaticScenarioAssignment) error {
  1201  	tenantID, err := tenant.LoadFromContext(ctx)
  1202  	if err != nil {
  1203  		return errors.Wrap(err, "while loading tenant from context")
  1204  	}
  1205  
  1206  	if err = s.repo.DeleteForScenarioName(ctx, tenantID, in.ScenarioName); err != nil {
  1207  		return errors.Wrap(err, "while deleting the Assignment")
  1208  	}
  1209  
  1210  	if err = s.asaEngine.RemoveAssignedScenario(ctx, in, s.UnassignFormation); err != nil {
  1211  		return errors.Wrap(err, "while unassigning scenario from runtimes")
  1212  	}
  1213  
  1214  	return nil
  1215  }
  1216  
  1217  // RemoveAssignedScenarios removes all the scenarios that are coming from any of the provided ASAs
  1218  func (s *service) RemoveAssignedScenarios(ctx context.Context, in []*model.AutomaticScenarioAssignment) error {
  1219  	for _, asa := range in {
  1220  		if err := s.asaEngine.RemoveAssignedScenario(ctx, *asa, s.UnassignFormation); err != nil {
  1221  			return errors.Wrapf(err, "while deleting automatic scenario assigment: %s", asa.ScenarioName)
  1222  		}
  1223  	}
  1224  	return nil
  1225  }
  1226  
  1227  // DeleteManyASAForSameTargetTenant deletes a list of ASAs for the same targetTenant
  1228  // It also removes corresponding scenario assignments coming from the ASAs
  1229  func (s *service) DeleteManyASAForSameTargetTenant(ctx context.Context, in []*model.AutomaticScenarioAssignment) error {
  1230  	tenantID, err := tenant.LoadFromContext(ctx)
  1231  	if err != nil {
  1232  		return err
  1233  	}
  1234  
  1235  	targetTenant, err := s.ensureSameTargetTenant(in)
  1236  	if err != nil {
  1237  		return errors.Wrap(err, "while ensuring input is valid")
  1238  	}
  1239  
  1240  	if err = s.repo.DeleteForTargetTenant(ctx, tenantID, targetTenant); err != nil {
  1241  		return errors.Wrap(err, "while deleting the Assignments")
  1242  	}
  1243  
  1244  	if err = s.RemoveAssignedScenarios(ctx, in); err != nil {
  1245  		return errors.Wrap(err, "while unassigning scenario from runtimes")
  1246  	}
  1247  
  1248  	return nil
  1249  }
  1250  
  1251  func (s *service) GetScenariosFromMatchingASAs(ctx context.Context, objectID string, objType graphql.FormationObjectType) ([]string, error) {
  1252  	return s.asaEngine.GetScenariosFromMatchingASAs(ctx, objectID, objType)
  1253  }
  1254  
  1255  // MergeScenariosFromInputLabelsAndAssignments merges all the scenarios that are part of the resource labels (already added + to be added with the current operation)
  1256  // with all the scenarios that should be assigned based on ASAs.
  1257  func (s *service) MergeScenariosFromInputLabelsAndAssignments(ctx context.Context, inputLabels map[string]interface{}, runtimeID string) ([]interface{}, error) {
  1258  	scenariosFromAssignments, err := s.asaEngine.GetScenariosFromMatchingASAs(ctx, runtimeID, graphql.FormationObjectTypeRuntime)
  1259  	scenariosSet := make(map[string]struct{}, len(scenariosFromAssignments))
  1260  
  1261  	if err != nil {
  1262  		return nil, errors.Wrapf(err, "while getting scenarios for selector labels")
  1263  	}
  1264  
  1265  	for _, scenario := range scenariosFromAssignments {
  1266  		scenariosSet[scenario] = struct{}{}
  1267  	}
  1268  
  1269  	scenariosFromInput, isScenarioLabelInInput := inputLabels[model.ScenariosKey]
  1270  
  1271  	if isScenarioLabelInInput {
  1272  		scenarioLabels, err := label.ValueToStringsSlice(scenariosFromInput)
  1273  		if err != nil {
  1274  			return nil, errors.Wrap(err, "while converting scenarios label to a string slice")
  1275  		}
  1276  
  1277  		for _, scenario := range scenarioLabels {
  1278  			scenariosSet[scenario] = struct{}{}
  1279  		}
  1280  	}
  1281  
  1282  	scenarios := make([]interface{}, 0, len(scenariosSet))
  1283  	for k := range scenariosSet {
  1284  		scenarios = append(scenarios, k)
  1285  	}
  1286  	return scenarios, nil
  1287  }
  1288  
  1289  func (s *service) SetFormationToErrorState(ctx context.Context, formation *model.Formation, errorMessage string, errorCode formationassignment.AssignmentErrorCode, state model.FormationState) error {
  1290  	log.C(ctx).Infof("Setting formation with ID: %q to state: %q", formation.ID, state)
  1291  	formation.State = state
  1292  
  1293  	formationError := formationassignment.AssignmentError{
  1294  		Message:   errorMessage,
  1295  		ErrorCode: errorCode,
  1296  	}
  1297  
  1298  	marshaledErr, err := json.Marshal(formationError)
  1299  	if err != nil {
  1300  		return errors.Wrapf(err, "While preparing error message for formation with ID: %q", formation.ID)
  1301  	}
  1302  	formation.Error = marshaledErr
  1303  
  1304  	if err := s.formationRepository.Update(ctx, formation); err != nil {
  1305  		return err
  1306  	}
  1307  	return nil
  1308  }
  1309  
  1310  // MatchingFunc provides signature for functions used for matching asa against runtimeID
  1311  type MatchingFunc func(ctx context.Context, asa *model.AutomaticScenarioAssignment, runtimeID string) (bool, error)
  1312  
  1313  func (s *service) modifyFormations(ctx context.Context, tnt, formationName string, modificationFunc modificationFunc) error {
  1314  	def, err := s.labelDefRepository.GetByKey(ctx, tnt, model.ScenariosKey)
  1315  	if err != nil {
  1316  		return errors.Wrapf(err, "while getting `%s` label definition", model.ScenariosKey)
  1317  	}
  1318  	if def.Schema == nil {
  1319  		return fmt.Errorf("missing schema for `%s` label definition", model.ScenariosKey)
  1320  	}
  1321  
  1322  	formationNames, err := labeldef.ParseFormationsFromSchema(def.Schema)
  1323  	if err != nil {
  1324  		return err
  1325  	}
  1326  
  1327  	formationNames = modificationFunc(formationNames, formationName)
  1328  
  1329  	schema, err := labeldef.NewSchemaForFormations(formationNames)
  1330  	if err != nil {
  1331  		return errors.Wrap(err, "while parsing scenarios")
  1332  	}
  1333  
  1334  	if err = s.labelDefService.ValidateExistingLabelsAgainstSchema(ctx, schema, tnt, model.ScenariosKey); err != nil {
  1335  		return err
  1336  	}
  1337  	if err = s.labelDefService.ValidateAutomaticScenarioAssignmentAgainstSchema(ctx, schema, tnt, model.ScenariosKey); err != nil {
  1338  		return errors.Wrap(err, "while validating Scenario Assignments against a new schema")
  1339  	}
  1340  
  1341  	return s.labelDefRepository.UpdateWithVersion(ctx, model.LabelDefinition{
  1342  		ID:      def.ID,
  1343  		Tenant:  tnt,
  1344  		Key:     model.ScenariosKey,
  1345  		Schema:  &schema,
  1346  		Version: def.Version,
  1347  	})
  1348  }
  1349  
  1350  func (s *service) modifyAssignedFormations(ctx context.Context, tnt, objectID, formationName string, objectType model.LabelableObject, modificationFunc modificationFunc) error {
  1351  	log.C(ctx).Infof("Modifying formation with name: %q for object with type: %q and ID: %q", formationName, objectType, objectID)
  1352  
  1353  	labelInput := newLabelInput(formationName, objectID, objectType)
  1354  	existingLabel, err := s.labelService.GetLabel(ctx, tnt, labelInput)
  1355  	if err != nil {
  1356  		return err
  1357  	}
  1358  
  1359  	existingFormations, err := label.ValueToStringsSlice(existingLabel.Value)
  1360  	if err != nil {
  1361  		return err
  1362  	}
  1363  
  1364  	formations := modificationFunc(existingFormations, formationName)
  1365  
  1366  	// can not set scenario label to empty value, violates the scenario label definition
  1367  	if len(formations) == 0 {
  1368  		log.C(ctx).Infof("After the modifications, the %q label is empty. Deleting empty label...", model.ScenariosKey)
  1369  		return s.labelRepository.Delete(ctx, tnt, objectType, objectID, model.ScenariosKey)
  1370  	}
  1371  
  1372  	labelInput.Value = formations
  1373  	labelInput.Version = existingLabel.Version
  1374  	log.C(ctx).Infof("Updating formations list to %q", formations)
  1375  	return s.labelService.UpdateLabel(ctx, tnt, existingLabel.ID, labelInput)
  1376  }
  1377  
  1378  type modificationFunc func(formationNames []string, formationName string) []string
  1379  
  1380  func addFormation(formationNames []string, formationName string) []string {
  1381  	for _, f := range formationNames {
  1382  		if f == formationName {
  1383  			return formationNames
  1384  		}
  1385  	}
  1386  
  1387  	return append(formationNames, formationName)
  1388  }
  1389  
  1390  func deleteFormation(formations []string, formation string) []string {
  1391  	filteredFormations := make([]string, 0, len(formations))
  1392  	for _, f := range formations {
  1393  		if f != formation {
  1394  			filteredFormations = append(filteredFormations, f)
  1395  		}
  1396  	}
  1397  
  1398  	return filteredFormations
  1399  }
  1400  
  1401  func newLabelInput(formation, objectID string, objectType model.LabelableObject) *model.LabelInput {
  1402  	return &model.LabelInput{
  1403  		Key:        model.ScenariosKey,
  1404  		Value:      []string{formation},
  1405  		ObjectID:   objectID,
  1406  		ObjectType: objectType,
  1407  		Version:    0,
  1408  	}
  1409  }
  1410  
  1411  func newAutomaticScenarioAssignmentModel(formation, callerTenant, targetTenant string) model.AutomaticScenarioAssignment {
  1412  	return model.AutomaticScenarioAssignment{
  1413  		ScenarioName:   formation,
  1414  		Tenant:         callerTenant,
  1415  		TargetTenantID: targetTenant,
  1416  	}
  1417  }
  1418  
  1419  func objectTypeToLabelableObject(objectType graphql.FormationObjectType) (labelableObj model.LabelableObject) {
  1420  	switch objectType {
  1421  	case graphql.FormationObjectTypeApplication:
  1422  		labelableObj = model.ApplicationLabelableObject
  1423  	case graphql.FormationObjectTypeRuntime:
  1424  		labelableObj = model.RuntimeLabelableObject
  1425  	case graphql.FormationObjectTypeTenant:
  1426  		labelableObj = model.TenantLabelableObject
  1427  	case graphql.FormationObjectTypeRuntimeContext:
  1428  		labelableObj = model.RuntimeContextLabelableObject
  1429  	}
  1430  	return labelableObj
  1431  }
  1432  
  1433  func formationAssignmentTypeToFormationObjectType(objectType model.FormationAssignmentType) (formationObjectType graphql.FormationObjectType) {
  1434  	switch objectType {
  1435  	case model.FormationAssignmentTypeApplication:
  1436  		formationObjectType = graphql.FormationObjectTypeApplication
  1437  	case model.FormationAssignmentTypeRuntime:
  1438  		formationObjectType = graphql.FormationObjectTypeRuntime
  1439  	case model.FormationAssignmentTypeRuntimeContext:
  1440  		formationObjectType = graphql.FormationObjectTypeRuntimeContext
  1441  	}
  1442  	return formationObjectType
  1443  }
  1444  
  1445  func (s *service) ensureSameTargetTenant(in []*model.AutomaticScenarioAssignment) (string, error) {
  1446  	if len(in) == 0 || in[0] == nil {
  1447  		return "", apperrors.NewInternalError("expected at least one item in Assignments slice")
  1448  	}
  1449  
  1450  	targetTenant := in[0].TargetTenantID
  1451  
  1452  	for _, item := range in {
  1453  		if item != nil && item.TargetTenantID != targetTenant {
  1454  			return "", apperrors.NewInternalError("all input items have to have the same target tenant")
  1455  		}
  1456  	}
  1457  
  1458  	return targetTenant, nil
  1459  }
  1460  
  1461  func (s *service) validateThatScenarioExists(ctx context.Context, in model.AutomaticScenarioAssignment) error {
  1462  	availableScenarios, err := s.getAvailableScenarios(ctx, in.Tenant)
  1463  	if err != nil {
  1464  		return err
  1465  	}
  1466  
  1467  	for _, av := range availableScenarios {
  1468  		if av == in.ScenarioName {
  1469  			return nil
  1470  		}
  1471  	}
  1472  
  1473  	return apperrors.NewNotFoundError(resource.AutomaticScenarioAssigment, in.ScenarioName)
  1474  }
  1475  
  1476  func (s *service) getAvailableScenarios(ctx context.Context, tenantID string) ([]string, error) {
  1477  	out, err := s.labelDefService.GetAvailableScenarios(ctx, tenantID)
  1478  	if err != nil {
  1479  		return nil, errors.Wrap(err, "while getting available scenarios")
  1480  	}
  1481  	return out, nil
  1482  }
  1483  
  1484  func (s *service) createFormation(ctx context.Context, tenant, templateID, formationName string, state model.FormationState) (*model.Formation, error) {
  1485  	formation := &model.Formation{
  1486  		ID:                  s.uuidService.Generate(),
  1487  		TenantID:            tenant,
  1488  		FormationTemplateID: templateID,
  1489  		Name:                formationName,
  1490  		State:               state,
  1491  	}
  1492  
  1493  	log.C(ctx).Debugf("Creating formation with name: %q and template ID: %q...", formationName, templateID)
  1494  	if err := s.formationRepository.Create(ctx, formation); err != nil {
  1495  		log.C(ctx).Errorf("An error occurred while creating formation with name: %q and template ID: %q", formationName, templateID)
  1496  		return nil, errors.Wrapf(err, "An error occurred while creating formation with name: %q and template ID: %q", formationName, templateID)
  1497  	}
  1498  
  1499  	return formation, nil
  1500  }
  1501  
  1502  type formationWithTemplate struct {
  1503  	formation         *model.Formation
  1504  	formationTemplate *model.FormationTemplate
  1505  }
  1506  
  1507  func (s *service) getFormationWithTemplate(ctx context.Context, formationName, tnt string) (*formationWithTemplate, error) {
  1508  	formation, err := s.formationRepository.GetByName(ctx, formationName, tnt)
  1509  	if err != nil {
  1510  		log.C(ctx).Errorf("An error occurred while getting formation by name: %q: %v", formationName, err)
  1511  		return nil, errors.Wrapf(err, "An error occurred while getting formation by name: %q", formationName)
  1512  	}
  1513  
  1514  	template, err := s.formationTemplateRepository.Get(ctx, formation.FormationTemplateID)
  1515  	if err != nil {
  1516  		log.C(ctx).Errorf("An error occurred while getting formation template by ID: %q: %v", formation.FormationTemplateID, err)
  1517  		return nil, errors.Wrapf(err, "An error occurred while getting formation template by ID: %q", formation.FormationTemplateID)
  1518  	}
  1519  
  1520  	return &formationWithTemplate{formation: formation, formationTemplate: template}, nil
  1521  }
  1522  
  1523  func (s *service) isValidRuntimeType(ctx context.Context, tnt string, runtimeID string, formationTemplate *model.FormationTemplate) error {
  1524  	runtimeTypeLabel, err := s.labelService.GetLabel(ctx, tnt, &model.LabelInput{
  1525  		Key:        s.runtimeTypeLabelKey,
  1526  		ObjectID:   runtimeID,
  1527  		ObjectType: model.RuntimeLabelableObject,
  1528  	})
  1529  	if err != nil {
  1530  		return errors.Wrapf(err, "while getting label %q for runtime with ID %q", s.runtimeTypeLabelKey, runtimeID)
  1531  	}
  1532  
  1533  	runtimeType, ok := runtimeTypeLabel.Value.(string)
  1534  	if !ok {
  1535  		return apperrors.NewInvalidOperationError(fmt.Sprintf("missing runtimeType for formation template %q, allowing only %q", formationTemplate.Name, formationTemplate.RuntimeTypes))
  1536  	}
  1537  	isAllowed := false
  1538  	for _, allowedType := range formationTemplate.RuntimeTypes {
  1539  		if allowedType == runtimeType {
  1540  			isAllowed = true
  1541  			break
  1542  		}
  1543  	}
  1544  	if !isAllowed {
  1545  		return apperrors.NewInvalidOperationError(fmt.Sprintf("unsupported runtimeType %q for formation template %q, allowing only %q", runtimeType, formationTemplate.Name, formationTemplate.RuntimeTypes))
  1546  	}
  1547  	return nil
  1548  }
  1549  
  1550  func (s *service) isValidApplicationType(ctx context.Context, tnt string, applicationID string, formationTemplate *model.FormationTemplate) error {
  1551  	applicationTypeLabel, err := s.labelService.GetLabel(ctx, tnt, &model.LabelInput{
  1552  		Key:        s.applicationTypeLabelKey,
  1553  		ObjectID:   applicationID,
  1554  		ObjectType: model.ApplicationLabelableObject,
  1555  	})
  1556  	if err != nil {
  1557  		return errors.Wrapf(err, "while getting label %q for application with ID %q", s.applicationTypeLabelKey, applicationID)
  1558  	}
  1559  
  1560  	applicationType, ok := applicationTypeLabel.Value.(string)
  1561  	if !ok {
  1562  		return apperrors.NewInvalidOperationError(fmt.Sprintf("missing %s label for formation template %q, allowing only %q", s.applicationTypeLabelKey, formationTemplate.Name, formationTemplate.ApplicationTypes))
  1563  	}
  1564  	isAllowed := false
  1565  	for _, allowedType := range formationTemplate.ApplicationTypes {
  1566  		if allowedType == applicationType {
  1567  			isAllowed = true
  1568  			break
  1569  		}
  1570  	}
  1571  	if !isAllowed {
  1572  		return apperrors.NewInvalidOperationError(fmt.Sprintf("unsupported applicationType %q for formation template %q, allowing only %q", applicationType, formationTemplate.Name, formationTemplate.ApplicationTypes))
  1573  	}
  1574  	return nil
  1575  }
  1576  
  1577  func setToSlice(set map[string]bool) []string {
  1578  	result := make([]string, 0, len(set))
  1579  	for key := range set {
  1580  		result = append(result, key)
  1581  	}
  1582  	return result
  1583  }
  1584  
  1585  func (s *service) processFormationNotifications(ctx context.Context, formation *model.Formation, formationReq *webhookclient.FormationNotificationRequest, errorState model.FormationState) error {
  1586  	response, err := s.notificationsService.SendNotification(ctx, formationReq)
  1587  	if err != nil {
  1588  		updateError := s.SetFormationToErrorState(ctx, formation, err.Error(), formationassignment.TechnicalError, errorState)
  1589  		if updateError != nil {
  1590  			return errors.Wrapf(updateError, "while updating error state: %s", errors.Wrapf(err, "while sending notification for formation with ID: %q", formation.ID).Error())
  1591  		}
  1592  		notificationErr := errors.Wrapf(err, "while sending notification for formation with ID: %q and name: %q", formation.ID, formation.Name)
  1593  		log.C(ctx).Error(notificationErr)
  1594  		return notificationErr
  1595  	}
  1596  
  1597  	if response.Error != nil && *response.Error != "" {
  1598  		if err = s.statusService.SetFormationToErrorStateWithConstraints(ctx, formation, *response.Error, formationassignment.ClientError, errorState, determineFormationOperationFromState(errorState)); err != nil {
  1599  			return errors.Wrapf(err, "while updating error state for formation with ID: %q and name: %q", formation.ID, formation.Name)
  1600  		}
  1601  
  1602  		log.C(ctx).Errorf("Received error from formation webhook response: %v", *response.Error)
  1603  		// This is the client error case, and we should not return an error,
  1604  		// because otherwise the transaction will be rolled back
  1605  		return nil
  1606  	}
  1607  
  1608  	if formationReq.Webhook.Mode != nil && *formationReq.Webhook.Mode == graphql.WebhookModeAsyncCallback {
  1609  		log.C(ctx).Infof("The webhook with ID: %q in the notification is in %q mode. Waiting for the receiver to report the status on the status API...", formationReq.Webhook.ID, graphql.WebhookModeAsyncCallback)
  1610  		if errorState == model.CreateErrorFormationState {
  1611  			formation.State = model.InitialFormationState
  1612  			formation.Error = nil
  1613  		}
  1614  		if errorState == model.DeleteErrorFormationState {
  1615  			formation.State = model.DeletingFormationState
  1616  			formation.Error = nil
  1617  		}
  1618  		log.C(ctx).Infof("Updating formation with ID: %q and name: %q to: %q state and waiting for the receiver to report the status on the status API...", formation.ID, formation.Name, formation.State)
  1619  		if err = s.formationRepository.Update(ctx, formation); err != nil {
  1620  			return errors.Wrapf(err, "while updating formation with id %q", formation.ID)
  1621  		}
  1622  		return nil
  1623  	}
  1624  
  1625  	if *response.ActualStatusCode == *response.SuccessStatusCode {
  1626  		formation.State = model.ReadyFormationState
  1627  		formation.Error = nil
  1628  		log.C(ctx).Infof("Updating formation with ID: %q and name: %q to: %q state", formation.ID, formation.Name, model.ReadyFormationState)
  1629  		if err := s.statusService.UpdateWithConstraints(ctx, formation, determineFormationOperationFromState(errorState)); err != nil {
  1630  			return errors.Wrapf(err, "while updating formation with ID: %q and name: %q to state: %s", formation.ID, formation.Name, model.ReadyFormationState)
  1631  		}
  1632  	}
  1633  
  1634  	return nil
  1635  }
  1636  
  1637  func determineFormationState(ctx context.Context, formationTemplateID, formationTemplateName string, formationTemplateWebhooks []*model.Webhook, externallyProvidedFormationState model.FormationState) model.FormationState {
  1638  	if len(formationTemplateWebhooks) == 0 {
  1639  		if len(externallyProvidedFormationState) > 0 {
  1640  			log.C(ctx).Infof("Formation template with ID: %q and name: %q does not have any webhooks. The formation will be created with %s state as it was provided externally", formationTemplateID, formationTemplateName, externallyProvidedFormationState)
  1641  			return externallyProvidedFormationState
  1642  		}
  1643  		log.C(ctx).Infof("Formation template with ID: %q and name: %q does not have any webhooks. The formation will be created with %s state", formationTemplateID, formationTemplateName, model.ReadyFormationState)
  1644  		return model.ReadyFormationState
  1645  	}
  1646  
  1647  	return model.InitialFormationState
  1648  }
  1649  
  1650  func determineFormationOperationFromState(state model.FormationState) model.FormationOperation {
  1651  	switch state {
  1652  	case model.InitialFormationState, model.CreateErrorFormationState:
  1653  		return model.CreateFormation
  1654  	case model.DeletingFormationState, model.DeleteErrorFormationState:
  1655  		return model.DeleteFormation
  1656  	default:
  1657  		return ""
  1658  	}
  1659  }
  1660  
  1661  func determineFormationErrorStateFromOperation(operation model.FormationOperation) model.FormationState {
  1662  	switch operation {
  1663  	case model.CreateFormation:
  1664  		return model.CreateErrorFormationState
  1665  	case model.DeleteFormation:
  1666  		return model.DeleteErrorFormationState
  1667  	default:
  1668  		return ""
  1669  	}
  1670  }
  1671  
  1672  func isObjectTypeSupported(formationTemplate *model.FormationTemplate, objectType graphql.FormationObjectType) bool {
  1673  	if formationTemplate.RuntimeArtifactKind == nil && formationTemplate.RuntimeTypeDisplayName == nil && len(formationTemplate.RuntimeTypes) == 0 {
  1674  		switch objectType {
  1675  		case graphql.FormationObjectTypeRuntime, graphql.FormationObjectTypeRuntimeContext, graphql.FormationObjectTypeTenant:
  1676  			return false
  1677  		default:
  1678  			return true
  1679  		}
  1680  	}
  1681  
  1682  	return true
  1683  }