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  }