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 }