github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/systemfetcher/service.go (about) 1 package systemfetcher 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 "sync" 9 "time" 10 11 "github.com/kyma-incubator/compass/components/director/pkg/str" 12 "github.com/tidwall/gjson" 13 14 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 15 16 "github.com/google/uuid" 17 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 18 tenantEntity "github.com/kyma-incubator/compass/components/director/pkg/tenant" 19 20 "github.com/kyma-incubator/compass/components/director/internal/model" 21 "github.com/kyma-incubator/compass/components/director/pkg/log" 22 "github.com/kyma-incubator/compass/components/director/pkg/persistence" 23 "github.com/pkg/errors" 24 ) 25 26 const ( 27 // LifecycleAttributeName is the lifecycle status attribute of the application in the external source response for applications retrieval. 28 LifecycleAttributeName string = "lifecycleStatus" 29 // LifecycleDeleted is the string matching the deleted lifecycle state of the application in the external source. 30 LifecycleDeleted string = "DELETED" 31 32 // ConcurrentDeleteOperationErrMsg is the error message returned by the Compass Director, when we try to delete an application, which is already undergoing a delete operation. 33 ConcurrentDeleteOperationErrMsg = "Concurrent operation [reason=delete operation is in progress]" 34 mainURLKey = "mainUrl" 35 productIDKey = "productId" 36 displayNameKey = "displayName" 37 systemNumberKey = "systemNumber" 38 additionalAttributesKey = "additionalAttributes" 39 productDescriptionKey = "productDescription" 40 infrastructureProviderKey = "infrastructureProvider" 41 additionalUrlsKey = "additionalUrls" 42 ppmsProductVersionIDKey = "ppmsProductVersionId" 43 businessTypeIDKey = "businessTypeId" 44 businessTypeDescriptionKey = "businessTypeDescription" 45 ) 46 47 //go:generate mockery --name=tenantService --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string 48 type tenantService interface { 49 ListByType(ctx context.Context, tenantType tenantEntity.Type) ([]*model.BusinessTenantMapping, error) 50 GetTenantByExternalID(ctx context.Context, id string) (*model.BusinessTenantMapping, error) 51 GetInternalTenant(ctx context.Context, externalTenant string) (string, error) 52 } 53 54 //go:generate mockery --name=systemsService --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string 55 type systemsService interface { 56 TrustedUpsert(ctx context.Context, in model.ApplicationRegisterInput) error 57 TrustedUpsertFromTemplate(ctx context.Context, in model.ApplicationRegisterInput, appTemplateID *string) error 58 GetBySystemNumber(ctx context.Context, systemNumber string) (*model.Application, error) 59 } 60 61 // SystemsSyncService is the service for managing systems synchronization timestamps 62 // 63 //go:generate mockery --name=SystemsSyncService --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string 64 type SystemsSyncService interface { 65 List(ctx context.Context) ([]*model.SystemSynchronizationTimestamp, error) 66 Upsert(ctx context.Context, in *model.SystemSynchronizationTimestamp) error 67 } 68 69 //go:generate mockery --name=tenantBusinessTypeService --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string 70 type tenantBusinessTypeService interface { 71 Create(ctx context.Context, in *model.TenantBusinessTypeInput) (string, error) 72 GetByID(ctx context.Context, id string) (*model.TenantBusinessType, error) 73 ListAll(ctx context.Context) ([]*model.TenantBusinessType, error) 74 } 75 76 //go:generate mockery --name=systemsAPIClient --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string 77 type systemsAPIClient interface { 78 FetchSystemsForTenant(ctx context.Context, tenant string, mutex *sync.Mutex) ([]System, error) 79 } 80 81 //go:generate mockery --name=directorClient --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string 82 type directorClient interface { 83 DeleteSystemAsync(ctx context.Context, id, tenant string) error 84 } 85 86 //go:generate mockery --name=templateRenderer --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string 87 type templateRenderer interface { 88 ApplicationRegisterInputFromTemplate(ctx context.Context, sc System) (*model.ApplicationRegisterInput, error) 89 } 90 91 // Config holds the configuration available for the SystemFetcher. 92 type Config struct { 93 SystemsQueueSize int `envconfig:"default=100,APP_SYSTEM_INFORMATION_QUEUE_SIZE"` 94 FetcherParallellism int `envconfig:"default=30,APP_SYSTEM_INFORMATION_PARALLELLISM"` 95 DirectorGraphqlURL string `envconfig:"APP_DIRECTOR_GRAPHQL_URL"` 96 DirectorRequestTimeout time.Duration `envconfig:"default=30s,APP_DIRECTOR_REQUEST_TIMEOUT"` 97 DirectorSkipSSLValidation bool `envconfig:"default=false,APP_DIRECTOR_SKIP_SSL_VALIDATION"` 98 99 EnableSystemDeletion bool `envconfig:"default=true,APP_ENABLE_SYSTEM_DELETION"` 100 OperationalMode string `envconfig:"APP_OPERATIONAL_MODE"` 101 TemplatesFileLocation string `envconfig:"optional,APP_TEMPLATES_FILE_LOCATION"` 102 VerifyTenant string `envconfig:"optional,APP_VERIFY_TENANT"` 103 } 104 105 // SystemFetcher is responsible for synchronizing the existing applications in Compass and a pre-defined external source. 106 type SystemFetcher struct { 107 transaction persistence.Transactioner 108 tenantService tenantService 109 systemsService systemsService 110 systemsSyncService SystemsSyncService 111 tbtService tenantBusinessTypeService 112 templateRenderer templateRenderer 113 systemsAPIClient systemsAPIClient 114 directorClient directorClient 115 116 config Config 117 workers chan struct{} 118 } 119 120 // NewSystemFetcher returns a new SystemFetcher. 121 func NewSystemFetcher(tx persistence.Transactioner, ts tenantService, ss systemsService, sSync SystemsSyncService, tbts tenantBusinessTypeService, tr templateRenderer, sac systemsAPIClient, directorClient directorClient, config Config) *SystemFetcher { 122 return &SystemFetcher{ 123 transaction: tx, 124 tenantService: ts, 125 systemsService: ss, 126 systemsSyncService: sSync, 127 tbtService: tbts, 128 templateRenderer: tr, 129 systemsAPIClient: sac, 130 directorClient: directorClient, 131 workers: make(chan struct{}, config.FetcherParallellism), 132 config: config, 133 } 134 } 135 136 type tenantSystems struct { 137 tenant *model.BusinessTenantMapping 138 systems []System 139 } 140 141 func splitBusinessTenantMappingsToChunks(slice []*model.BusinessTenantMapping, chunkSize int) [][]*model.BusinessTenantMapping { 142 var chunks [][]*model.BusinessTenantMapping 143 for { 144 if len(slice) == 0 { 145 break 146 } 147 148 if len(slice) < chunkSize { 149 chunkSize = len(slice) 150 } 151 152 chunks = append(chunks, slice[0:chunkSize]) 153 slice = slice[chunkSize:] 154 } 155 156 return chunks 157 } 158 159 // SyncSystems synchronizes applications between Compass and external source. It deletes the applications with deleted state in the external source from Compass, 160 // and creates any new applications present in the external source. 161 func (s *SystemFetcher) SyncSystems(ctx context.Context) error { 162 tenants, err := s.listTenants(ctx) 163 if err != nil { 164 return errors.Wrap(err, "failed to list tenants") 165 } 166 167 tenantBusinessTypes, err := s.getTenantBusinessTypes(ctx) 168 if err != nil { 169 return errors.Wrap(err, "failed to get tenant business types") 170 } 171 172 systemsQueue := make(chan tenantSystems, s.config.SystemsQueueSize) 173 wgDB := sync.WaitGroup{} 174 wgDB.Add(1) 175 var mutex sync.Mutex 176 go func() { 177 defer func() { 178 wgDB.Done() 179 }() 180 for tenantSystems := range systemsQueue { 181 entry := log.C(ctx) 182 entry = entry.WithField(log.FieldRequestID, uuid.New().String()) 183 ctx = log.ContextWithLogger(ctx, entry) 184 185 if err = s.processSystemsForTenant(ctx, tenantSystems.tenant, tenantSystems.systems, tenantBusinessTypes); err != nil { 186 log.C(ctx).Error(errors.Wrap(err, fmt.Sprintf("failed to save systems for tenant %s", tenantSystems.tenant.ExternalTenant))) 187 continue 188 } 189 190 mutex.Lock() 191 if SystemSynchronizationTimestamps == nil { 192 SystemSynchronizationTimestamps = make(map[string]map[string]SystemSynchronizationTimestamp, 0) 193 } 194 195 for _, i := range tenantSystems.systems { 196 currentTenant := tenantSystems.tenant.ExternalTenant 197 currentTimestamp := SystemSynchronizationTimestamp{ 198 ID: uuid.NewString(), 199 LastSyncTimestamp: time.Now().UTC(), 200 } 201 202 if _, ok := SystemSynchronizationTimestamps[currentTenant]; !ok { 203 SystemSynchronizationTimestamps[currentTenant] = make(map[string]SystemSynchronizationTimestamp, 0) 204 } 205 206 systemPayload, err := json.Marshal(i.SystemPayload) 207 if err != nil { 208 log.C(ctx).Error(errors.Wrapf(err, "failed to marshal a system payload for tenant %s", tenantSystems.tenant.ExternalTenant)) 209 return 210 } 211 productID := gjson.GetBytes(systemPayload, productIDKey).String() 212 213 if v, ok1 := SystemSynchronizationTimestamps[currentTenant][productID]; ok1 { 214 currentTimestamp.ID = v.ID 215 } 216 217 SystemSynchronizationTimestamps[currentTenant][productID] = currentTimestamp 218 } 219 mutex.Unlock() 220 221 log.C(ctx).Info(fmt.Sprintf("Successfully synced systems for tenant %s", tenantSystems.tenant.ExternalTenant)) 222 } 223 }() 224 225 chunks := splitBusinessTenantMappingsToChunks(tenants, 15) 226 227 for _, chunk := range chunks { 228 time.Sleep(time.Second * 1) 229 230 wg := sync.WaitGroup{} 231 for _, t := range chunk { 232 wg.Add(1) 233 s.workers <- struct{}{} 234 go func(t *model.BusinessTenantMapping) { 235 defer func() { 236 wg.Done() 237 <-s.workers 238 }() 239 systems, err := s.systemsAPIClient.FetchSystemsForTenant(ctx, t.ExternalTenant, &mutex) 240 if err != nil { 241 log.C(ctx).Error(errors.Wrap(err, fmt.Sprintf("failed to fetch systems for tenant %s", t.ExternalTenant))) 242 return 243 } 244 245 log.C(ctx).Infof("found %d systems for tenant %s", len(systems), t.ExternalTenant) 246 if len(s.config.VerifyTenant) > 0 { 247 log.C(ctx).Infof("systems: %#v", systems) 248 } 249 250 if len(systems) > 0 { 251 systemsQueue <- tenantSystems{ 252 tenant: t, 253 systems: systems, 254 } 255 } 256 }(t) 257 } 258 259 wg.Wait() 260 } 261 close(systemsQueue) 262 wgDB.Wait() 263 264 return nil 265 } 266 267 // UpsertSystemsSyncTimestamps updates the synchronization timestamps of the systems for each tenant or creates new ones if they don't exist in the database 268 func (s *SystemFetcher) UpsertSystemsSyncTimestamps(ctx context.Context, transact persistence.Transactioner) error { 269 tx, err := transact.Begin() 270 if err != nil { 271 return errors.Wrap(err, "Error while beginning transaction") 272 } 273 defer transact.RollbackUnlessCommitted(ctx, tx) 274 275 ctx = persistence.SaveToContext(ctx, tx) 276 277 for tnt, v := range SystemSynchronizationTimestamps { 278 err := s.upsertSystemsSyncTimestampsForTenant(ctx, tnt, v) 279 if err != nil { 280 return errors.Wrapf(err, "failed to upsert systems sync timestamps for tenant %s", tnt) 281 } 282 } 283 284 err = tx.Commit() 285 if err != nil { 286 return errors.Wrap(err, "failed to commit transaction") 287 } 288 289 return nil 290 } 291 292 func (s *SystemFetcher) upsertSystemsSyncTimestampsForTenant(ctx context.Context, tenant string, timestamps map[string]SystemSynchronizationTimestamp) error { 293 for productID, timestamp := range timestamps { 294 in := &model.SystemSynchronizationTimestamp{ 295 ID: timestamp.ID, 296 TenantID: tenant, 297 ProductID: productID, 298 LastSyncTimestamp: timestamp.LastSyncTimestamp, 299 } 300 301 err := s.systemsSyncService.Upsert(ctx, in) 302 if err != nil { 303 return err 304 } 305 } 306 307 return nil 308 } 309 310 func (s *SystemFetcher) listTenants(ctx context.Context) ([]*model.BusinessTenantMapping, error) { 311 tx, err := s.transaction.Begin() 312 if err != nil { 313 return nil, errors.Wrap(err, "failed to begin transaction") 314 } 315 defer s.transaction.RollbackUnlessCommitted(ctx, tx) 316 317 ctx = persistence.SaveToContext(ctx, tx) 318 319 var tenants []*model.BusinessTenantMapping 320 if len(s.config.VerifyTenant) > 0 { 321 singleTenant, err := s.tenantService.GetTenantByExternalID(ctx, s.config.VerifyTenant) 322 if err != nil { 323 return nil, errors.Wrapf(err, "failed to retrieve tenant %s", s.config.VerifyTenant) 324 } 325 tenants = append(tenants, singleTenant) 326 } else { 327 tenants, err = s.tenantService.ListByType(ctx, tenantEntity.Account) 328 if err != nil { 329 return nil, errors.Wrap(err, "failed to retrieve tenants") 330 } 331 } 332 333 err = tx.Commit() 334 if err != nil { 335 return nil, errors.Wrap(err, "failed to commit while retrieving tenants") 336 } 337 338 return tenants, nil 339 } 340 341 func (s *SystemFetcher) processSystemsForTenant(ctx context.Context, tenantMapping *model.BusinessTenantMapping, systems []System, tenantBusinessTypes map[string]*model.TenantBusinessType) error { 342 log.C(ctx).Infof("Saving %d systems for tenant %s", len(systems), tenantMapping.Name) 343 344 for _, system := range systems { 345 err := func() error { 346 tx, err := s.transaction.Begin() 347 if err != nil { 348 return errors.Wrap(err, "failed to begin transaction") 349 } 350 ctx = tenant.SaveToContext(ctx, tenantMapping.ID, tenantMapping.ExternalTenant) 351 ctx = persistence.SaveToContext(ctx, tx) 352 defer s.transaction.RollbackUnlessCommitted(ctx, tx) 353 354 systemPayload, err := json.Marshal(system.SystemPayload) 355 if err != nil { 356 log.C(ctx).Error(errors.Wrapf(err, "failed to marshal a system payload for tenant %s", tenantMapping.ExternalTenant)) 357 return err 358 } 359 displayName := gjson.GetBytes(systemPayload, displayNameKey).String() 360 systemNumber := gjson.GetBytes(systemPayload, systemNumberKey).String() 361 lifecycleStatus := gjson.GetBytes(systemPayload, additionalAttributesKey+"."+LifecycleAttributeName).String() 362 363 log.C(ctx).Infof("Getting system by name %s and system number %s", displayName, systemNumber) 364 365 system.StatusCondition = model.ApplicationStatusConditionInitial 366 app, err := s.systemsService.GetBySystemNumber(ctx, systemNumber) 367 if err != nil { 368 if !apperrors.IsNotFoundError(err) { 369 log.C(ctx).WithError(err).Errorf("Could not get system with name %s and system number %s", displayName, systemNumber) 370 return nil 371 } 372 } 373 374 if lifecycleStatus == LifecycleDeleted && s.config.EnableSystemDeletion { 375 if app == nil { 376 log.C(ctx).Warnf("System with system number %s is not present. Skipping deletion.", systemNumber) 377 return nil 378 } 379 380 if !app.Ready && !app.GetDeletedAt().IsZero() { 381 log.C(ctx).Infof("System with id %s is currently being deleted", app.ID) 382 return nil 383 } 384 if err := s.directorClient.DeleteSystemAsync(ctx, app.ID, tenantMapping.ID); err != nil { 385 if strings.Contains(err.Error(), ConcurrentDeleteOperationErrMsg) { 386 log.C(ctx).Warnf("Delete operation is in progress for system with id %s", app.ID) 387 } else { 388 log.C(ctx).WithError(err).Errorf("Could not delete system with id %s", app.ID) 389 } 390 return nil 391 } 392 log.C(ctx).Infof("Started asynchronously delete for system with id %s", app.ID) 393 return nil 394 } 395 396 if app != nil && app.Status != nil { 397 system.StatusCondition = app.Status.Condition 398 } 399 400 log.C(ctx).Infof("Started processing tenant business type for system with system number %s", systemNumber) 401 tenantBusinessType, err := s.processSystemTenantBusinessType(ctx, systemPayload, tenantBusinessTypes) 402 if err != nil { 403 return err 404 } 405 406 appInput, err := s.convertSystemToAppRegisterInput(ctx, system) 407 if err != nil { 408 return err 409 } 410 if tenantBusinessType != nil { 411 appInput.TenantBusinessTypeID = &tenantBusinessType.ID 412 } 413 414 if appInput.TemplateID == "" { 415 if err = s.systemsService.TrustedUpsert(ctx, appInput.ApplicationRegisterInput); err != nil { 416 return errors.Wrap(err, "while upserting application") 417 } 418 } else { 419 if err = s.systemsService.TrustedUpsertFromTemplate(ctx, appInput.ApplicationRegisterInput, &appInput.TemplateID); err != nil { 420 return errors.Wrap(err, "while upserting application") 421 } 422 } 423 424 if err = tx.Commit(); err != nil { 425 return errors.Wrap(err, fmt.Sprintf("failed to commit applications for tenant %s", tenantMapping.ExternalTenant)) 426 } 427 return nil 428 }() 429 if err != nil { 430 return err 431 } 432 } 433 return nil 434 } 435 436 func (s *SystemFetcher) convertSystemToAppRegisterInput(ctx context.Context, sc System) (*model.ApplicationRegisterInputWithTemplate, error) { 437 input, err := s.appRegisterInput(ctx, sc) 438 if err != nil { 439 return nil, err 440 } 441 442 return &model.ApplicationRegisterInputWithTemplate{ 443 ApplicationRegisterInput: *input, 444 TemplateID: sc.TemplateID, 445 }, nil 446 } 447 448 func (s *SystemFetcher) appRegisterInput(ctx context.Context, sc System) (*model.ApplicationRegisterInput, error) { 449 if len(sc.TemplateID) > 0 { 450 return s.templateRenderer.ApplicationRegisterInputFromTemplate(ctx, sc) 451 } 452 453 payload, err := json.Marshal(sc.SystemPayload) 454 if err != nil { 455 return nil, err 456 } 457 458 return &model.ApplicationRegisterInput{ 459 Name: gjson.GetBytes(payload, displayNameKey).String(), 460 Description: str.Ptr(gjson.GetBytes(payload, productDescriptionKey).String()), 461 StatusCondition: &sc.StatusCondition, 462 ProviderName: str.Ptr(gjson.GetBytes(payload, infrastructureProviderKey).String()), 463 BaseURL: str.Ptr(gjson.GetBytes(payload, additionalUrlsKey+"."+mainURLKey).String()), 464 SystemNumber: str.Ptr(gjson.GetBytes(payload, systemNumberKey).String()), 465 Labels: map[string]interface{}{ 466 "managed": "true", 467 "productId": str.Ptr(gjson.GetBytes(payload, productIDKey).String()), 468 "ppmsProductVersionId": str.Ptr(gjson.GetBytes(payload, ppmsProductVersionIDKey).String()), 469 }, 470 }, nil 471 } 472 473 func (s *SystemFetcher) getTenantBusinessTypes(ctx context.Context) (map[string]*model.TenantBusinessType, error) { 474 tx, err := s.transaction.Begin() 475 if err != nil { 476 return nil, errors.Wrap(err, "failed to begin transaction") 477 } 478 defer s.transaction.RollbackUnlessCommitted(ctx, tx) 479 480 ctx = persistence.SaveToContext(ctx, tx) 481 482 tenantBusinessTypes, err := s.tbtService.ListAll(ctx) 483 if err != nil { 484 return nil, errors.Wrap(err, "failed to retrieve tenant business types") 485 } 486 487 err = tx.Commit() 488 if err != nil { 489 return nil, errors.Wrap(err, "failed to commit while retrieving tenant business types") 490 } 491 492 tbtMap := make(map[string]*model.TenantBusinessType, 0) 493 for _, tbt := range tenantBusinessTypes { 494 tbtMap[tbt.Code] = tbt 495 } 496 497 return tbtMap, nil 498 } 499 500 func (s *SystemFetcher) processSystemTenantBusinessType(ctx context.Context, systemPayload []byte, tenantBusinessTypes map[string]*model.TenantBusinessType) (*model.TenantBusinessType, error) { 501 businessTypeID := gjson.GetBytes(systemPayload, businessTypeIDKey).String() 502 businessTypeDescription := gjson.GetBytes(systemPayload, businessTypeDescriptionKey).String() 503 tbt, exists := tenantBusinessTypes[businessTypeID] 504 if businessTypeID != "" && businessTypeDescription != "" { 505 if !exists { 506 log.C(ctx).Infof("Creating tenant business type with code: %q", businessTypeID) 507 createdTbtID, err := s.tbtService.Create(ctx, &model.TenantBusinessTypeInput{Code: businessTypeID, Name: businessTypeDescription}) 508 if err != nil { 509 return nil, err 510 } 511 createdTbt, err := s.tbtService.GetByID(ctx, createdTbtID) 512 if err != nil { 513 return nil, err 514 } 515 tenantBusinessTypes[createdTbt.Code] = createdTbt 516 return createdTbt, nil 517 } 518 } 519 return tbt, nil 520 }