github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/tenantfetchersvc/resync/synchronizer.go (about) 1 package resync 2 3 import ( 4 "context" 5 "fmt" 6 "time" 7 8 "github.com/kyma-incubator/compass/components/director/internal/model" 9 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 10 "github.com/kyma-incubator/compass/components/director/pkg/log" 11 "github.com/kyma-incubator/compass/components/director/pkg/persistence" 12 "github.com/kyma-incubator/compass/components/director/pkg/tenant" 13 "github.com/pkg/errors" 14 ) 15 16 type contextKey string 17 18 const ( 19 retryDelaySeconds = 3 20 // TenantOnDemandProvider is the name of the business tenant mapping provider used when the tenant is not found in the events service 21 TenantOnDemandProvider = "lazily-tenant-fetcher" 22 // TenantRegionCtxKey region context key 23 TenantRegionCtxKey contextKey = "tenantsRegion" 24 ) 25 26 // TenantStorageService missing godoc 27 // 28 //go:generate mockery --name=TenantStorageService --output=automock --outpkg=automock --case=underscore --disable-version-string 29 type TenantStorageService interface { 30 List(ctx context.Context) ([]*model.BusinessTenantMapping, error) 31 GetTenantByExternalID(ctx context.Context, id string) (*model.BusinessTenantMapping, error) 32 ListsByExternalIDs(ctx context.Context, ids []string) ([]*model.BusinessTenantMapping, error) 33 } 34 35 // TenantCreator takes care of retrieving tenants from external tenant registry and storing them in Director 36 // 37 //go:generate mockery --name=TenantCreator --output=automock --outpkg=automock --case=underscore --disable-version-string 38 type TenantCreator interface { 39 FetchTenant(ctx context.Context, externalTenantID string) (*model.BusinessTenantMappingInput, error) 40 TenantsToCreate(ctx context.Context, region, fromTimestamp string) ([]model.BusinessTenantMappingInput, error) 41 CreateTenants(ctx context.Context, eventsTenants []model.BusinessTenantMappingInput) error 42 } 43 44 // TenantDeleter takes care of retrieving no longer used tenants from external tenant registry and delete them from Director 45 // 46 //go:generate mockery --name=TenantDeleter --output=automock --outpkg=automock --case=underscore --disable-version-string 47 type TenantDeleter interface { 48 TenantsToDelete(ctx context.Context, region, fromTimestamp string) ([]model.BusinessTenantMappingInput, error) 49 DeleteTenants(ctx context.Context, eventsTenants []model.BusinessTenantMappingInput) error 50 } 51 52 // TenantMover takes care of moving tenants from one parent tenant to another. 53 // 54 //go:generate mockery --name=TenantMover --output=automock --outpkg=automock --case=underscore --disable-version-string 55 type TenantMover interface { 56 TenantsToMove(ctx context.Context, region, fromTimestamp string) ([]model.MovedSubaccountMappingInput, error) 57 MoveTenants(ctx context.Context, movedSubaccountMappings []model.MovedSubaccountMappingInput) error 58 } 59 60 // AggregationFailurePusher takes care of pushing aggregation failures to Prometheus. 61 // 62 //go:generate mockery --name=AggregationFailurePusher --output=automock --outpkg=automock --case=underscore --disable-version-string 63 type AggregationFailurePusher interface { 64 ReportAggregationFailure(ctx context.Context, err error) 65 } 66 67 // TenantsSynchronizer takes care of synchronizing tenants with external tenant registry. 68 // It creates, updates, deletes and moves tenants that were created, updated, deleted or moved in that external registry. 69 type TenantsSynchronizer struct { 70 supportedRegions []string 71 72 transact persistence.Transactioner 73 tenantStorageService TenantStorageService 74 75 creator TenantCreator 76 mover TenantMover 77 deleter TenantDeleter 78 79 kubeClient KubeClient 80 config JobConfig 81 82 metricsReporter AggregationFailurePusher 83 } 84 85 // NewTenantSynchronizer returns a new tenants synchronizer. 86 func NewTenantSynchronizer(config JobConfig, transact persistence.Transactioner, tenantStorageService TenantStorageService, creator TenantCreator, mover TenantMover, deleter TenantDeleter, kubeClient KubeClient, metricsReporter AggregationFailurePusher) *TenantsSynchronizer { 87 return &TenantsSynchronizer{ 88 supportedRegions: supportedRegions(config), 89 transact: transact, 90 tenantStorageService: tenantStorageService, 91 creator: creator, 92 mover: mover, 93 deleter: deleter, 94 kubeClient: kubeClient, 95 config: config, 96 metricsReporter: metricsReporter, 97 } 98 } 99 100 func supportedRegions(config JobConfig) []string { 101 regionNames := make([]string, 0) 102 for _, regionDetails := range config.RegionalAPIConfigs { 103 regionNames = append(regionNames, regionDetails.RegionName) 104 } 105 if len(regionNames) == 0 { 106 log.D().Infof("Job %s has only one central region: %s", config.JobName, config.APIConfig.RegionName) 107 regionNames = append(regionNames, config.APIConfig.RegionName) 108 } 109 return regionNames 110 } 111 112 // Name returns the name set to the tenants synchronizer. 113 func (ts *TenantsSynchronizer) Name() string { 114 return ts.config.JobName 115 } 116 117 // TenantType returns the tenant type the tenants synchronizer is responsible for. 118 func (ts *TenantsSynchronizer) TenantType() tenant.Type { 119 return ts.config.TenantType 120 } 121 122 // ResyncInterval returns the interval that the synchronizer is supposed to make a regular tenants resync with the external tenants registry. 123 func (ts *TenantsSynchronizer) ResyncInterval() time.Duration { 124 return ts.config.TenantFetcherJobIntervalMins 125 } 126 127 // Synchronize is responsible for synchronizing the tenants of the configured type in Compass and the configured external tenants registry. 128 // When a tenant is created in the external registry, it is also greated in Compass. Same applies for updated, deleted and moved tenants. 129 func (ts *TenantsSynchronizer) Synchronize(ctx context.Context) error { 130 var err error 131 if err = ts.synchronizeTenants(ctx); err != nil { 132 ts.metricsReporter.ReportAggregationFailure(ctx, err) 133 } 134 return err 135 } 136 137 func (ts *TenantsSynchronizer) synchronizeTenants(ctx context.Context) error { 138 startTime, lastConsumedTenantTimestamp, lastResyncTimestamp, err := resyncTimestamps(ctx, ts.kubeClient, ts.config.FullResyncInterval) 139 if err != nil { 140 return err 141 } 142 143 for _, region := range ts.supportedRegions { 144 ctx = context.WithValue(ctx, TenantRegionCtxKey, region) 145 log.C(ctx).Printf("Processing new events for region: %s...", region) 146 tenantsToCreate, err := ts.creator.TenantsToCreate(ctx, region, lastConsumedTenantTimestamp) 147 if err != nil { 148 return err 149 } 150 151 tenantsToMove, err := ts.mover.TenantsToMove(ctx, region, lastConsumedTenantTimestamp) 152 if err != nil { 153 return err 154 } 155 156 tenantsToDelete, err := ts.deleter.TenantsToDelete(ctx, region, lastConsumedTenantTimestamp) 157 if err != nil { 158 return err 159 } 160 161 tenantsToCreate = dedupeTenants(tenantsToCreate) 162 tenantsToCreate = excludeTenants(tenantsToCreate, tenantsToDelete) 163 164 totalNewEvents := len(tenantsToCreate) + len(tenantsToDelete) + len(tenantsToMove) 165 log.C(ctx).Printf("Amount of new events for region %s: %d", region, totalNewEvents) 166 if totalNewEvents == 0 { 167 log.C(ctx).Printf("No new events for processing, resync completed for region %s", region) 168 continue 169 } 170 171 currentTenants := make(map[string]string) 172 if len(tenantsToCreate) > 0 || len(tenantsToDelete) > 0 { 173 currentTenantsIDs := getTenantsIDs(tenantsToCreate, tenantsToDelete) 174 currentTenants, err = ts.currentTenants(ctx, currentTenantsIDs) 175 if err != nil { 176 return err 177 } 178 } 179 180 // Order of event processing matters - we want the most destructive operation to be last 181 if len(tenantsToCreate) > 0 { 182 if err := ts.createTenants(ctx, currentTenants, tenantsToCreate, region); err != nil { 183 return errors.Wrap(err, "while creating tenants") 184 } 185 } 186 187 if len(tenantsToMove) > 0 { 188 if err := ts.mover.MoveTenants(ctx, tenantsToMove); err != nil { 189 return errors.Wrap(err, "while moving tenants") 190 } 191 } 192 193 if len(tenantsToDelete) > 0 { 194 if err := ts.deleteTenants(ctx, currentTenants, tenantsToDelete); err != nil { 195 return errors.Wrap(err, "while deleting tenants") 196 } 197 } 198 199 log.C(ctx).Printf("Processed all new events for region: %s", region) 200 } 201 202 return ts.kubeClient.UpdateTenantFetcherConfigMapData(ctx, convertTimeToUnixMilliSecondString(*startTime), lastResyncTimestamp) 203 } 204 205 // SynchronizeTenant is responsible for updating the given tenant with the values available in the external registry, 206 // or creating it if it does not exist in Compass. 207 // All available regions are checked for the existence of the tenant. 208 func (ts *TenantsSynchronizer) SynchronizeTenant(ctx context.Context, parentTenantID, tenantID string) error { 209 tnt, err := ts.fetchFromDirector(ctx, tenantID) 210 if err != nil { 211 return errors.Wrapf(err, "while checking if tenant eith ID %s already exists", tenantID) 212 } 213 214 if tnt != nil { 215 log.C(ctx).Infof("Tenant with external ID %s already exists", tenantID) 216 return nil 217 } 218 219 fetchedTenant, err := ts.creator.FetchTenant(ctx, tenantID) 220 if err != nil { 221 return err 222 } 223 224 if fetchedTenant == nil { 225 log.C(ctx).Infof("Tenant with ID %s was not found, it will be stored lazily", tenantID) 226 fetchedTenant := model.BusinessTenantMappingInput{ 227 Name: tenantID, 228 ExternalTenant: tenantID, 229 Parent: parentTenantID, 230 Subdomain: "", 231 Region: "", 232 Type: string(tenant.Subaccount), 233 Provider: TenantOnDemandProvider, 234 } 235 return ts.creator.CreateTenants(ctx, []model.BusinessTenantMappingInput{fetchedTenant}) 236 } 237 238 parentTenantID = fetchedTenant.Parent 239 if len(parentTenantID) == 0 { 240 return fmt.Errorf("parent tenant not found of tenant with ID %s", tenantID) 241 } 242 243 parent, err := ts.fetchFromDirector(ctx, fetchedTenant.Parent) 244 if err != nil { 245 return errors.Wrapf(err, "while checking if parent tenant with ID %s exists", fetchedTenant.Parent) 246 } 247 248 fetchedTenant.Parent = parent.ID 249 return ts.creator.CreateTenants(ctx, []model.BusinessTenantMappingInput{*fetchedTenant}) 250 } 251 252 func (ts *TenantsSynchronizer) fetchFromDirector(ctx context.Context, tenantID string) (*model.BusinessTenantMapping, error) { 253 tx, err := ts.transact.Begin() 254 if err != nil { 255 return nil, err 256 } 257 defer ts.transact.RollbackUnlessCommitted(ctx, tx) 258 ctx = persistence.SaveToContext(ctx, tx) 259 260 log.C(ctx).Infof("Checking if tenant with external tenant ID %s already exists...", tenantID) 261 tnt, err := ts.tenantStorageService.GetTenantByExternalID(ctx, tenantID) 262 if err != nil && !apperrors.IsNotFoundError(err) { 263 return nil, errors.Wrapf(err, "while checking if tenant with external ID %s already exists", tenantID) 264 } 265 266 if err := tx.Commit(); err != nil { 267 return nil, err 268 } 269 return tnt, nil 270 } 271 272 func (ts *TenantsSynchronizer) currentTenants(ctx context.Context, tenantsIDs []string) (map[string]string, error) { 273 tx, err := ts.transact.Begin() 274 if err != nil { 275 return nil, err 276 } 277 defer ts.transact.RollbackUnlessCommitted(ctx, tx) 278 ctx = persistence.SaveToContext(ctx, tx) 279 280 currentTenants, listErr := ts.tenantStorageService.ListsByExternalIDs(ctx, tenantsIDs) 281 if listErr != nil { 282 return nil, errors.Wrap(listErr, "while listing tenants") 283 } 284 285 currentTenantsMap := make(map[string]string) 286 for _, ct := range currentTenants { 287 currentTenantsMap[ct.ExternalTenant] = ct.ID 288 } 289 290 if err = tx.Commit(); err != nil { 291 return nil, err 292 } 293 294 return currentTenantsMap, nil 295 } 296 297 func (ts *TenantsSynchronizer) createTenants(ctx context.Context, currentTenants map[string]string, newTenants []model.BusinessTenantMappingInput, region string) error { 298 fullRegionName := ts.config.RegionPrefix + region 299 // create missing parent tenants 300 tenantsToCreate := missingParentTenants(currentTenants, newTenants, ts.config.TenantProvider, fullRegionName) 301 for _, eventTenant := range newTenants { 302 // use internal ID of parent for pre-existing targetParentTenants 303 if parentGUID, ok := currentTenants[eventTenant.Parent]; ok { 304 eventTenant.Parent = parentGUID 305 } 306 307 eventTenant.Region = fullRegionName 308 tenantsToCreate = append(tenantsToCreate, eventTenant) 309 } 310 311 return ts.creator.CreateTenants(ctx, tenantsToCreate) 312 } 313 314 func (ts *TenantsSynchronizer) deleteTenants(ctx context.Context, currTenants map[string]string, eventsTenants []model.BusinessTenantMappingInput) error { 315 tenantsToDelete := make([]model.BusinessTenantMappingInput, 0) 316 for _, toDelete := range eventsTenants { 317 if _, ok := currTenants[toDelete.ExternalTenant]; ok { 318 tenantsToDelete = append(tenantsToDelete, toDelete) 319 } 320 } 321 322 if len(tenantsToDelete) > 0 { 323 return ts.deleter.DeleteTenants(ctx, tenantsToDelete) 324 } 325 326 return nil 327 } 328 329 func missingParentTenants(currTenants map[string]string, eventsTenants []model.BusinessTenantMappingInput, providerName, region string) []model.BusinessTenantMappingInput { 330 parentsToCreate := make([]model.BusinessTenantMappingInput, 0) 331 for _, eventTenant := range eventsTenants { 332 if len(eventTenant.Parent) > 0 { 333 if _, ok := currTenants[eventTenant.Parent]; !ok { 334 parentTenant := model.BusinessTenantMappingInput{ 335 Name: eventTenant.Parent, 336 ExternalTenant: eventTenant.Parent, 337 Parent: "", 338 Type: getTenantParentType(eventTenant.Type), 339 Provider: providerName, 340 Region: region, 341 } 342 parentsToCreate = append(parentsToCreate, parentTenant) 343 } 344 } 345 } 346 347 return dedupeTenants(parentsToCreate) 348 } 349 350 func getTenantParentType(tenantType string) string { 351 if tenantType == tenant.TypeToStr(tenant.Account) { 352 return tenant.TypeToStr(tenant.Customer) 353 } 354 return tenant.TypeToStr(tenant.Account) 355 } 356 357 func dedupeTenants(tenants []model.BusinessTenantMappingInput) []model.BusinessTenantMappingInput { 358 tenantsByExtID := make(map[string]model.BusinessTenantMappingInput) 359 for _, t := range tenants { 360 tenantsByExtID[t.ExternalTenant] = t 361 } 362 363 tenants = make([]model.BusinessTenantMappingInput, 0, len(tenantsByExtID)) 364 for _, t := range tenantsByExtID { 365 // cleaning up missingParentTenants of self referencing tenants 366 if t.ExternalTenant == t.Parent { 367 t.Parent = "" 368 } 369 370 tenants = append(tenants, t) 371 } 372 return tenants 373 } 374 375 func excludeTenants(source, target []model.BusinessTenantMappingInput) []model.BusinessTenantMappingInput { 376 deleteTenantsMap := make(map[string]model.BusinessTenantMappingInput) 377 for _, ct := range target { 378 deleteTenantsMap[ct.ExternalTenant] = ct 379 } 380 381 result := append([]model.BusinessTenantMappingInput{}, source...) 382 383 for i := len(result) - 1; i >= 0; i-- { 384 if _, found := deleteTenantsMap[result[i].ExternalTenant]; found { 385 result = append(result[:i], result[i+1:]...) 386 } 387 } 388 389 return result 390 } 391 392 func getTenantsIDs(tenants ...[]model.BusinessTenantMappingInput) []string { 393 var currentTenantsIDs []string 394 for _, tenantsList := range tenants { 395 for _, t := range tenantsList { 396 if len(t.Parent) > 0 { 397 currentTenantsIDs = append(currentTenantsIDs, t.Parent) 398 } 399 if len(t.ExternalTenant) > 0 { 400 currentTenantsIDs = append(currentTenantsIDs, t.ExternalTenant) 401 } 402 } 403 } 404 return currentTenantsIDs 405 }