github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/formation/resolver.go (about) 1 package formation 2 3 import ( 4 "context" 5 "encoding/json" 6 7 "github.com/kyma-incubator/compass/components/director/pkg/log" 8 9 "github.com/kyma-incubator/compass/components/director/internal/domain/formationassignment" 10 11 webhookclient "github.com/kyma-incubator/compass/components/director/pkg/webhook_client" 12 13 dataloader "github.com/kyma-incubator/compass/components/director/internal/dataloaders" 14 15 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 16 "github.com/kyma-incubator/compass/components/director/internal/model" 17 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 18 "github.com/kyma-incubator/compass/components/director/pkg/graphql" 19 "github.com/kyma-incubator/compass/components/director/pkg/persistence" 20 "github.com/pkg/errors" 21 ) 22 23 // Service missing godoc 24 // 25 //go:generate mockery --name=Service --output=automock --outpkg=automock --case=underscore --disable-version-string 26 type Service interface { 27 Get(ctx context.Context, id string) (*model.Formation, error) 28 GetFormationByName(ctx context.Context, formationName, tnt string) (*model.Formation, error) 29 List(ctx context.Context, pageSize int, cursor string) (*model.FormationPage, error) 30 CreateFormation(ctx context.Context, tnt string, formation model.Formation, templateName string) (*model.Formation, error) 31 DeleteFormation(ctx context.Context, tnt string, formation model.Formation) (*model.Formation, error) 32 AssignFormation(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation model.Formation) (*model.Formation, error) 33 UnassignFormation(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation model.Formation) (*model.Formation, error) 34 ResynchronizeFormationNotifications(ctx context.Context, formationID string) (*model.Formation, error) 35 } 36 37 // Converter missing godoc 38 // 39 //go:generate mockery --name=Converter --output=automock --outpkg=automock --case=underscore --disable-version-string 40 type Converter interface { 41 FromGraphQL(i graphql.FormationInput) model.Formation 42 ToGraphQL(i *model.Formation) (*graphql.Formation, error) 43 MultipleToGraphQL(in []*model.Formation) ([]*graphql.Formation, error) 44 } 45 46 //go:generate mockery --exported --name=formationAssignmentService --output=automock --outpkg=automock --case=underscore --disable-version-string 47 type formationAssignmentService interface { 48 Delete(ctx context.Context, id string) error 49 DeleteAssignmentsForObjectID(ctx context.Context, formationID, objectID string) error 50 ListByFormationIDs(ctx context.Context, formationIDs []string, pageSize int, cursor string) ([]*model.FormationAssignmentPage, error) 51 ListByFormationIDsNoPaging(ctx context.Context, formationIDs []string) ([][]*model.FormationAssignment, error) 52 GetForFormation(ctx context.Context, id, formationID string) (*model.FormationAssignment, error) 53 ListFormationAssignmentsForObjectID(ctx context.Context, formationID, objectID string) ([]*model.FormationAssignment, error) 54 ProcessFormationAssignments(ctx context.Context, formationAssignmentsForObject []*model.FormationAssignment, runtimeContextIDToRuntimeIDMapping map[string]string, applicationIDToApplicationTemplateIDMapping map[string]string, requests []*webhookclient.FormationAssignmentNotificationRequest, operation func(context.Context, *formationassignment.AssignmentMappingPairWithOperation) (bool, error), formationOperation model.FormationOperation) error 55 ProcessFormationAssignmentPair(ctx context.Context, mappingPair *formationassignment.AssignmentMappingPairWithOperation) (bool, error) 56 GenerateAssignments(ctx context.Context, tnt, objectID string, objectType graphql.FormationObjectType, formation *model.Formation) ([]*model.FormationAssignment, error) 57 CleanupFormationAssignment(ctx context.Context, mappingPair *formationassignment.AssignmentMappingPairWithOperation) (bool, error) 58 GetAssignmentsForFormationWithStates(ctx context.Context, tenantID, formationID string, states []string) ([]*model.FormationAssignment, error) 59 GetReverseBySourceAndTarget(ctx context.Context, formationID, sourceID, targetID string) (*model.FormationAssignment, error) 60 } 61 62 // FormationAssignmentConverter converts FormationAssignment between the model.FormationAssignment service-layer representation and graphql.FormationAssignment. 63 // 64 //go:generate mockery --name=FormationAssignmentConverter --output=automock --outpkg=automock --case=underscore --disable-version-string 65 type FormationAssignmentConverter interface { 66 MultipleToGraphQL(in []*model.FormationAssignment) ([]*graphql.FormationAssignment, error) 67 ToGraphQL(in *model.FormationAssignment) (*graphql.FormationAssignment, error) 68 } 69 70 // TenantFetcher calls an API which fetches details for the given tenant from an external tenancy service, stores the tenant in the Compass DB and returns 200 OK if the tenant was successfully created. 71 // 72 //go:generate mockery --name=TenantFetcher --output=automock --outpkg=automock --case=underscore --disable-version-string 73 type TenantFetcher interface { 74 FetchOnDemand(tenant, parentTenant string) error 75 } 76 77 // Resolver is the formation resolver 78 type Resolver struct { 79 transact persistence.Transactioner 80 service Service 81 conv Converter 82 formationAssignmentSvc formationAssignmentService 83 formationAssignmentConv FormationAssignmentConverter 84 fetcher TenantFetcher 85 } 86 87 // NewResolver creates formation resolver 88 func NewResolver(transact persistence.Transactioner, service Service, conv Converter, formationAssignmentSvc formationAssignmentService, formationAssignmentConv FormationAssignmentConverter, fetcher TenantFetcher) *Resolver { 89 return &Resolver{ 90 transact: transact, 91 service: service, 92 conv: conv, 93 formationAssignmentSvc: formationAssignmentSvc, 94 formationAssignmentConv: formationAssignmentConv, 95 fetcher: fetcher, 96 } 97 } 98 99 func (r *Resolver) getFormation(ctx context.Context, get func(context.Context) (*model.Formation, error)) (*graphql.Formation, error) { 100 tx, err := r.transact.Begin() 101 if err != nil { 102 return nil, err 103 } 104 defer r.transact.RollbackUnlessCommitted(ctx, tx) 105 106 ctx = persistence.SaveToContext(ctx, tx) 107 108 formation, err := get(ctx) 109 if err != nil { 110 return nil, err 111 } 112 113 if err = tx.Commit(); err != nil { 114 return nil, err 115 } 116 117 return r.conv.ToGraphQL(formation) 118 } 119 120 // FormationByName returns a Formation by its name 121 func (r *Resolver) FormationByName(ctx context.Context, name string) (*graphql.Formation, error) { 122 return r.getFormation(ctx, func(ctx context.Context) (*model.Formation, error) { 123 tnt, err := tenant.LoadFromContext(ctx) 124 if err != nil { 125 return nil, err 126 } 127 128 return r.service.GetFormationByName(ctx, name, tnt) 129 }) 130 } 131 132 // Formation returns a Formation by its id 133 func (r *Resolver) Formation(ctx context.Context, id string) (*graphql.Formation, error) { 134 return r.getFormation(ctx, func(ctx context.Context) (*model.Formation, error) { 135 return r.service.Get(ctx, id) 136 }) 137 } 138 139 // Formations returns paginated Formations based on first and after 140 func (r *Resolver) Formations(ctx context.Context, first *int, after *graphql.PageCursor) (*graphql.FormationPage, error) { 141 var cursor string 142 if after != nil { 143 cursor = string(*after) 144 } 145 if first == nil { 146 return nil, apperrors.NewInvalidDataError("missing required parameter 'first'") 147 } 148 149 tx, err := r.transact.Begin() 150 if err != nil { 151 return nil, err 152 } 153 defer r.transact.RollbackUnlessCommitted(ctx, tx) 154 155 ctx = persistence.SaveToContext(ctx, tx) 156 157 formationPage, err := r.service.List(ctx, *first, cursor) 158 if err != nil { 159 return nil, err 160 } 161 if err = tx.Commit(); err != nil { 162 return nil, err 163 } 164 165 formations, err := r.conv.MultipleToGraphQL(formationPage.Data) 166 if err != nil { 167 return nil, err 168 } 169 170 return &graphql.FormationPage{ 171 Data: formations, 172 TotalCount: formationPage.TotalCount, 173 PageInfo: &graphql.PageInfo{ 174 StartCursor: graphql.PageCursor(formationPage.PageInfo.StartCursor), 175 EndCursor: graphql.PageCursor(formationPage.PageInfo.EndCursor), 176 HasNextPage: formationPage.PageInfo.HasNextPage, 177 }, 178 }, nil 179 } 180 181 // CreateFormation creates new formation for the caller tenant 182 func (r *Resolver) CreateFormation(ctx context.Context, formationInput graphql.FormationInput) (*graphql.Formation, error) { 183 tnt, err := tenant.LoadFromContext(ctx) 184 if err != nil { 185 return nil, err 186 } 187 188 tx, err := r.transact.Begin() 189 if err != nil { 190 return nil, err 191 } 192 defer r.transact.RollbackUnlessCommitted(ctx, tx) 193 194 ctx = persistence.SaveToContext(ctx, tx) 195 196 templateName := model.DefaultTemplateName 197 if formationInput.TemplateName != nil && *formationInput.TemplateName != "" { 198 templateName = *formationInput.TemplateName 199 } 200 201 newFormation, err := r.service.CreateFormation(ctx, tnt, r.conv.FromGraphQL(formationInput), templateName) 202 if err != nil { 203 return nil, err 204 } 205 206 if err = tx.Commit(); err != nil { 207 return nil, errors.Wrap(err, "while committing transaction") 208 } 209 210 return r.conv.ToGraphQL(newFormation) 211 } 212 213 // DeleteFormation deletes the formation from the caller tenant formations 214 func (r *Resolver) DeleteFormation(ctx context.Context, formation graphql.FormationInput) (*graphql.Formation, error) { 215 tnt, err := tenant.LoadFromContext(ctx) 216 if err != nil { 217 return nil, err 218 } 219 220 tx, err := r.transact.Begin() 221 if err != nil { 222 return nil, err 223 } 224 defer r.transact.RollbackUnlessCommitted(ctx, tx) 225 226 ctx = persistence.SaveToContext(ctx, tx) 227 228 deletedFormation, err := r.service.DeleteFormation(ctx, tnt, r.conv.FromGraphQL(formation)) 229 if err != nil { 230 return nil, err 231 } 232 233 if err = tx.Commit(); err != nil { 234 return nil, errors.Wrap(err, "while committing transaction") 235 } 236 237 return r.conv.ToGraphQL(deletedFormation) 238 } 239 240 // AssignFormation assigns object to the provided formation 241 func (r *Resolver) AssignFormation(ctx context.Context, objectID string, objectType graphql.FormationObjectType, formation graphql.FormationInput) (*graphql.Formation, error) { 242 tnt, err := tenant.LoadFromContext(ctx) 243 if err != nil { 244 return nil, err 245 } 246 247 if objectType == graphql.FormationObjectTypeTenant { 248 if err := r.fetcher.FetchOnDemand(objectID, tnt); err != nil { 249 return nil, errors.Wrapf(err, "while trying to create if not exists subaccount %s", objectID) 250 } 251 } 252 253 tx, err := r.transact.Begin() 254 if err != nil { 255 return nil, err 256 } 257 defer r.transact.RollbackUnlessCommitted(ctx, tx) 258 259 ctx = persistence.SaveToContext(ctx, tx) 260 261 newFormation, err := r.service.AssignFormation(ctx, tnt, objectID, objectType, r.conv.FromGraphQL(formation)) 262 if err != nil { 263 return nil, err 264 } 265 266 if err = tx.Commit(); err != nil { 267 return nil, errors.Wrap(err, "while committing transaction") 268 } 269 270 return r.conv.ToGraphQL(newFormation) 271 } 272 273 // UnassignFormation unassigns the object from the provided formation 274 func (r *Resolver) UnassignFormation(ctx context.Context, objectID string, objectType graphql.FormationObjectType, formation graphql.FormationInput) (*graphql.Formation, error) { 275 tnt, err := tenant.LoadFromContext(ctx) 276 if err != nil { 277 return nil, err 278 } 279 280 if objectType != graphql.FormationObjectTypeTenant { 281 err = r.deleteSelfReferencedFormationAssignment(ctx, tnt, formation.Name, objectID) 282 if err != nil { 283 return nil, errors.Wrapf(err, "while deleting self referenced formation assignment for formation with name %q and object ID %q", formation.Name, objectID) 284 } 285 } 286 287 tx, err := r.transact.Begin() 288 if err != nil { 289 return nil, err 290 } 291 defer r.transact.RollbackUnlessCommitted(ctx, tx) 292 293 ctx = persistence.SaveToContext(ctx, tx) 294 295 newFormation, err := r.service.UnassignFormation(ctx, tnt, objectID, objectType, r.conv.FromGraphQL(formation)) 296 if err != nil { 297 return nil, err 298 } 299 300 if err = tx.Commit(); err != nil { 301 return nil, errors.Wrap(err, "while committing transaction") 302 } 303 304 return r.conv.ToGraphQL(newFormation) 305 } 306 307 // FormationAssignments retrieves a page of FormationAssignments for the specified Formation 308 func (r *Resolver) FormationAssignments(ctx context.Context, obj *graphql.Formation, first *int, after *graphql.PageCursor) (*graphql.FormationAssignmentPage, error) { 309 param := dataloader.ParamFormationAssignment{ID: obj.ID, Ctx: ctx, First: first, After: after} 310 return dataloader.FormationFor(ctx).FormationAssignmentByID.Load(param) 311 } 312 313 // FormationAssignmentsDataLoader retrieves a page of FormationAssignments for each Formation ID in the keys argument 314 func (r *Resolver) FormationAssignmentsDataLoader(keys []dataloader.ParamFormationAssignment) ([]*graphql.FormationAssignmentPage, []error) { 315 if len(keys) == 0 { 316 return nil, []error{apperrors.NewInternalError("No Formations found")} 317 } 318 319 ctx := keys[0].Ctx 320 formationIDs := make([]string, 0, len(keys)) 321 for _, key := range keys { 322 formationIDs = append(formationIDs, key.ID) 323 } 324 325 var cursor string 326 if keys[0].After != nil { 327 cursor = string(*keys[0].After) 328 } 329 330 if keys[0].First == nil { 331 return nil, []error{apperrors.NewInvalidDataError("missing required parameter 'first'")} 332 } 333 334 tx, err := r.transact.Begin() 335 if err != nil { 336 return nil, []error{err} 337 } 338 defer r.transact.RollbackUnlessCommitted(ctx, tx) 339 340 ctx = persistence.SaveToContext(ctx, tx) 341 342 formationAssignmentPages, err := r.formationAssignmentSvc.ListByFormationIDs(ctx, formationIDs, *keys[0].First, cursor) 343 if err != nil { 344 return nil, []error{err} 345 } 346 347 gqlFormationAssignments := make([]*graphql.FormationAssignmentPage, 0, len(formationAssignmentPages)) 348 for _, page := range formationAssignmentPages { 349 fas, err := r.formationAssignmentConv.MultipleToGraphQL(page.Data) 350 if err != nil { 351 return nil, []error{err} 352 } 353 354 gqlFormationAssignments = append(gqlFormationAssignments, &graphql.FormationAssignmentPage{Data: fas, TotalCount: page.TotalCount, PageInfo: &graphql.PageInfo{ 355 StartCursor: graphql.PageCursor(page.PageInfo.StartCursor), 356 EndCursor: graphql.PageCursor(page.PageInfo.EndCursor), 357 HasNextPage: page.PageInfo.HasNextPage, 358 }}) 359 } 360 361 if err = tx.Commit(); err != nil { 362 return nil, []error{err} 363 } 364 365 return gqlFormationAssignments, nil 366 } 367 368 // FormationAssignment missing godoc 369 func (r *Resolver) FormationAssignment(ctx context.Context, obj *graphql.Formation, id string) (*graphql.FormationAssignment, error) { 370 if obj == nil { 371 return nil, apperrors.NewInternalError("Formation cannot be empty") 372 } 373 374 tx, err := r.transact.Begin() 375 if err != nil { 376 return nil, err 377 } 378 defer r.transact.RollbackUnlessCommitted(ctx, tx) 379 380 ctx = persistence.SaveToContext(ctx, tx) 381 382 formationAssignment, err := r.formationAssignmentSvc.GetForFormation(ctx, id, obj.ID) 383 if err != nil { 384 if apperrors.IsNotFoundError(err) { 385 return nil, tx.Commit() 386 } 387 return nil, err 388 } 389 390 if err = tx.Commit(); err != nil { 391 return nil, err 392 } 393 394 return r.formationAssignmentConv.ToGraphQL(formationAssignment) 395 } 396 397 // Status retrieves a Status for the specified Formation 398 func (r *Resolver) Status(ctx context.Context, obj *graphql.Formation) (*graphql.FormationStatus, error) { 399 param := dataloader.ParamFormationStatus{ID: obj.ID, State: obj.State, Message: obj.Error.Message, ErrorCode: obj.Error.ErrorCode, Ctx: ctx} 400 return dataloader.FormationStatusFor(ctx).FormationStatusByID.Load(param) 401 } 402 403 // StatusDataLoader retrieves a Status for each Formation ID in the keys argument 404 func (r *Resolver) StatusDataLoader(keys []dataloader.ParamFormationStatus) ([]*graphql.FormationStatus, []error) { 405 if len(keys) == 0 { 406 return nil, []error{apperrors.NewInternalError("No Formations found")} 407 } 408 409 ctx := keys[0].Ctx 410 formationIDs := make([]string, 0, len(keys)) 411 for _, key := range keys { 412 formationIDs = append(formationIDs, key.ID) 413 } 414 415 tx, err := r.transact.Begin() 416 if err != nil { 417 return nil, []error{err} 418 } 419 defer r.transact.RollbackUnlessCommitted(ctx, tx) 420 421 ctx = persistence.SaveToContext(ctx, tx) 422 423 formationAssignmentsPerFormation, err := r.formationAssignmentSvc.ListByFormationIDsNoPaging(ctx, formationIDs) 424 if err != nil { 425 return nil, []error{err} 426 } 427 gqlFormationStatuses := make([]*graphql.FormationStatus, 0, len(formationAssignmentsPerFormation)) 428 for i := 0; i < len(keys); i++ { 429 formationAssignments := formationAssignmentsPerFormation[i] 430 431 var condition graphql.FormationStatusCondition 432 var formationStatusErrors []*graphql.FormationStatusError 433 434 switch formationState := keys[i].State; formationState { 435 case string(model.ReadyFormationState): 436 condition = graphql.FormationStatusConditionReady 437 case string(model.InitialFormationState), string(model.DeletingFormationState): 438 condition = graphql.FormationStatusConditionInProgress 439 case string(model.CreateErrorFormationState), string(model.DeleteErrorFormationState): 440 condition = graphql.FormationStatusConditionError 441 formationStatusErrors = append(formationStatusErrors, &graphql.FormationStatusError{Message: keys[i].Message, ErrorCode: keys[i].ErrorCode}) 442 } 443 444 for _, fa := range formationAssignments { 445 if isInErrorState(fa.State) { 446 condition = graphql.FormationStatusConditionError 447 448 if fa.Value == nil { 449 formationStatusErrors = append(formationStatusErrors, &graphql.FormationStatusError{AssignmentID: &fa.ID}) 450 continue 451 } 452 var assignmentError formationassignment.AssignmentErrorWrapper 453 if err = json.Unmarshal(fa.Value, &assignmentError); err != nil { 454 return nil, []error{errors.Wrapf(err, "while unmarshalling formation assignment error with assignment ID %q", fa.ID)} 455 } 456 457 formationStatusErrors = append(formationStatusErrors, &graphql.FormationStatusError{ 458 AssignmentID: &fa.ID, 459 Message: assignmentError.Error.Message, 460 ErrorCode: int(assignmentError.Error.ErrorCode), 461 }) 462 } else if condition != graphql.FormationStatusConditionError && isInProgressState(fa.State) { 463 condition = graphql.FormationStatusConditionInProgress 464 } 465 } 466 467 gqlFormationStatuses = append(gqlFormationStatuses, &graphql.FormationStatus{ 468 Condition: condition, 469 Errors: formationStatusErrors, 470 }) 471 } 472 473 if err = tx.Commit(); err != nil { 474 return nil, []error{err} 475 } 476 477 return gqlFormationStatuses, nil 478 } 479 480 // ResynchronizeFormationNotifications sends all notifications that are in error or initial state 481 func (r *Resolver) ResynchronizeFormationNotifications(ctx context.Context, formationID string) (*graphql.Formation, error) { 482 tx, err := r.transact.Begin() 483 if err != nil { 484 return nil, err 485 } 486 defer r.transact.RollbackUnlessCommitted(ctx, tx) 487 488 ctx = persistence.SaveToContext(ctx, tx) 489 490 updatedFormation, err := r.service.ResynchronizeFormationNotifications(ctx, formationID) 491 if err != nil { 492 return nil, err 493 } 494 495 if err = tx.Commit(); err != nil { 496 return nil, err 497 } 498 499 return r.conv.ToGraphQL(updatedFormation) 500 } 501 502 func (r *Resolver) deleteSelfReferencedFormationAssignment(ctx context.Context, tnt, formationName, objectID string) error { 503 selfFATx, err := r.transact.Begin() 504 if err != nil { 505 return err 506 } 507 selfFATransactionCtx := persistence.SaveToContext(ctx, selfFATx) 508 defer r.transact.RollbackUnlessCommitted(selfFATransactionCtx, selfFATx) 509 510 formationFromDB, err := r.service.GetFormationByName(selfFATransactionCtx, formationName, tnt) 511 if err != nil { 512 log.C(ctx).Errorf("An error occurred while getting formation by name: %q: %v", formationName, err) 513 return errors.Wrapf(err, "An error occurred while getting formation by name: %q", formationName) 514 } 515 516 fa, err := r.formationAssignmentSvc.GetReverseBySourceAndTarget(selfFATransactionCtx, formationFromDB.ID, objectID, objectID) 517 if err == nil { 518 _ = r.formationAssignmentSvc.Delete(selfFATransactionCtx, fa.ID) 519 } 520 521 err = selfFATx.Commit() 522 if err != nil { 523 return errors.Wrapf(err, "while committing transaction") 524 } 525 return nil 526 } 527 528 func isInErrorState(state string) bool { 529 return state == string(model.CreateErrorAssignmentState) || state == string(model.DeleteErrorAssignmentState) 530 } 531 532 func isInProgressState(state string) bool { 533 return state == string(model.InitialAssignmentState) || 534 state == string(model.DeletingAssignmentState) || 535 state == string(model.ConfigPendingAssignmentState) 536 }