github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/tenantfetchersvc/resync/tenant_manager.go (about)

     1  package resync
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  	"strconv"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/avast/retry-go/v4"
    12  	"github.com/kyma-incubator/compass/components/director/internal/model"
    13  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    14  	"github.com/kyma-incubator/compass/components/director/pkg/graphql"
    15  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    16  	tenantpkg "github.com/kyma-incubator/compass/components/director/pkg/tenant"
    17  	"github.com/pkg/errors"
    18  	"github.com/tidwall/gjson"
    19  )
    20  
    21  // EventAPIClient missing godoc
    22  //
    23  //go:generate mockery --name=EventAPIClient --output=automock --outpkg=automock --case=underscore --disable-version-string
    24  type EventAPIClient interface {
    25  	FetchTenantEventsPage(ctx context.Context, eventsType EventsType, additionalQueryParams QueryParams) (*EventsPage, error)
    26  }
    27  
    28  // TenantConverter expects tenant converter implementation
    29  //
    30  //go:generate mockery --name=TenantConverter --output=automock --outpkg=automock --case=underscore --disable-version-string
    31  type TenantConverter interface {
    32  	MultipleInputToGraphQLInput([]model.BusinessTenantMappingInput) []graphql.BusinessTenantMappingInput
    33  	ToGraphQLInput(model.BusinessTenantMappingInput) graphql.BusinessTenantMappingInput
    34  }
    35  
    36  // DirectorGraphQLClient expects graphql implementation
    37  //
    38  //go:generate mockery --name=DirectorGraphQLClient --output=automock --outpkg=automock --case=underscore --disable-version-string
    39  type DirectorGraphQLClient interface {
    40  	WriteTenants(context.Context, []graphql.BusinessTenantMappingInput) error
    41  	DeleteTenants(ctx context.Context, tenants []graphql.BusinessTenantMappingInput) error
    42  	UpdateTenant(ctx context.Context, id string, tenant graphql.BusinessTenantMappingInput) error
    43  }
    44  
    45  type supportedEvents struct {
    46  	createdTenantEvent EventsType
    47  	updatedTenantEvent EventsType
    48  	deletedTenantEvent EventsType
    49  }
    50  
    51  type externalTenantsManager struct {
    52  	config         EventsConfig
    53  	tenantProvider string
    54  
    55  	gqlClient      DirectorGraphQLClient
    56  	eventAPIClient EventAPIClient
    57  
    58  	tenantConverter TenantConverter
    59  }
    60  
    61  // TenantsManager is responsible for creating, updating and deleting tenants associated with an external tenants registry
    62  type TenantsManager struct {
    63  	externalTenantsManager
    64  
    65  	regionalClients     map[string]EventAPIClient
    66  	supportedEventTypes supportedEvents
    67  }
    68  
    69  // NewTenantsManager returns a new tenants manager built with the provided configurations and API clients
    70  func NewTenantsManager(jobConfig JobConfig, directorClient DirectorGraphQLClient, universalClient EventAPIClient, regionalDetails map[string]EventAPIClient, tenantConverter TenantConverter) (*TenantsManager, error) {
    71  	supportedEvents, err := supportedEventTypes(jobConfig.TenantType)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  
    76  	tenantsManager := &TenantsManager{
    77  		regionalClients:     regionalDetails,
    78  		supportedEventTypes: supportedEvents,
    79  		externalTenantsManager: externalTenantsManager{
    80  			gqlClient:       directorClient,
    81  			eventAPIClient:  universalClient,
    82  			config:          jobConfig.EventsConfig,
    83  			tenantConverter: tenantConverter,
    84  			tenantProvider:  jobConfig.TenantProvider,
    85  		},
    86  	}
    87  	return tenantsManager, nil
    88  }
    89  
    90  // TenantsToCreate returns all tenants that are missing in Compass but are present in the external tenants registry
    91  func (tm *TenantsManager) TenantsToCreate(ctx context.Context, region, fromTimestamp string) ([]model.BusinessTenantMappingInput, error) {
    92  	fetchedTenants, err := tm.fetchTenantsForEventType(ctx, region, fromTimestamp, tm.supportedEventTypes.createdTenantEvent)
    93  	if err != nil {
    94  		return nil, errors.Wrapf(err, "while fetching created tenants for region %s ", region)
    95  	}
    96  	updatedTenants, err := tm.fetchTenantsForEventType(ctx, region, fromTimestamp, tm.supportedEventTypes.updatedTenantEvent)
    97  	if err != nil {
    98  		return nil, errors.Wrapf(err, "while fetching updated tenants for region %s ", region)
    99  	}
   100  	updatedTenants = excludeTenants(updatedTenants, fetchedTenants)
   101  	fetchedTenants = append(fetchedTenants, updatedTenants...)
   102  	return fetchedTenants, nil
   103  }
   104  
   105  // TenantsToDelete returns all tenants that are no longer associated with their parent tenant and should be moved from one parent tenant to another
   106  func (tm *TenantsManager) TenantsToDelete(ctx context.Context, region, fromTimestamp string) ([]model.BusinessTenantMappingInput, error) {
   107  	res, err := tm.fetchTenantsForEventType(ctx, region, fromTimestamp, tm.supportedEventTypes.deletedTenantEvent)
   108  	if err != nil {
   109  		return nil, errors.Wrapf(err, "while fetching deleted tenants for region %s ", region)
   110  	}
   111  	return res, nil
   112  }
   113  
   114  // FetchTenant retrieves a given tenant from all available regions and updates or creates it in Compass
   115  func (tm *TenantsManager) FetchTenant(ctx context.Context, externalTenantID string) (*model.BusinessTenantMappingInput, error) {
   116  	additionalFields := map[string]string{
   117  		tm.config.QueryConfig.EntityField: externalTenantID,
   118  	}
   119  	configProvider := eventsQueryConfigProviderWithAdditionalFields(tm.config, additionalFields)
   120  
   121  	fetchedTenants, err := fetchCreatedTenantsWithRetries(ctx, tm.eventAPIClient, tm.config.RetryAttempts, tm.supportedEventTypes, configProvider)
   122  	if err != nil {
   123  		return nil, err
   124  	}
   125  
   126  	if len(fetchedTenants) >= 1 {
   127  		log.C(ctx).Infof("Tenant found from central region with universal client")
   128  		return &fetchedTenants[0], err
   129  	}
   130  
   131  	log.C(ctx).Infof("Tenant not found from central region, checking regional APIs")
   132  
   133  	tenantChan := make(chan *model.BusinessTenantMappingInput, len(tm.regionalClients))
   134  	for region, regionalClient := range tm.regionalClients {
   135  		go func(ctx context.Context, region string, regionalClient EventAPIClient, ch chan *model.BusinessTenantMappingInput) {
   136  			ctx = context.WithValue(ctx, TenantRegionCtxKey, region)
   137  			createdRegionalTenants, err := fetchCreatedTenantsWithRetries(ctx, regionalClient, tm.config.RetryAttempts, tm.supportedEventTypes, configProvider)
   138  			if err != nil {
   139  				log.C(ctx).WithError(err).Errorf("Failed to fetch created tenants from region %s: %v", region, err)
   140  			}
   141  
   142  			if len(createdRegionalTenants) == 1 {
   143  				log.C(ctx).Infof("Tenant found in region %s", region)
   144  				ch <- &createdRegionalTenants[0]
   145  			} else {
   146  				log.C(ctx).Warnf("Tenant not found in region %s", region)
   147  				ch <- nil
   148  			}
   149  		}(ctx, region, regionalClient, tenantChan)
   150  	}
   151  
   152  	pendingRegionalInfo := len(tm.regionalClients)
   153  	if pendingRegionalInfo == 0 {
   154  		// TODO return error when lazy store is reverted
   155  		log.C(ctx).Error("no regions are configured")
   156  		return nil, nil
   157  	}
   158  
   159  	var tenant *model.BusinessTenantMappingInput
   160  	for result := range tenantChan {
   161  		if result != nil {
   162  			tenant = result
   163  			break
   164  		}
   165  		pendingRegionalInfo--
   166  		if pendingRegionalInfo == 0 {
   167  			// TODO return error when lazy store is reverted
   168  			log.C(ctx).Error("tenant not found in all configured regions")
   169  			return nil, nil
   170  		}
   171  	}
   172  
   173  	return tenant, nil
   174  }
   175  
   176  // CreateTenants takes care of creating all missing tenants in Compass by calling Director in chunks
   177  func (tm *TenantsManager) CreateTenants(ctx context.Context, tenants []model.BusinessTenantMappingInput) error {
   178  	tenantsToCreateGQL := tm.tenantConverter.MultipleInputToGraphQLInput(tenants)
   179  	return runInChunks(ctx, tm.config.TenantOperationChunkSize, tenantsToCreateGQL, func(ctx context.Context, chunk []graphql.BusinessTenantMappingInput) error {
   180  		return tm.gqlClient.WriteTenants(ctx, chunk)
   181  	})
   182  }
   183  
   184  // DeleteTenants takes care of deleting all  tenants marked as deleted in the external tenants registry
   185  func (tm *TenantsManager) DeleteTenants(ctx context.Context, tenantsToDelete []model.BusinessTenantMappingInput) error {
   186  	tenantsToDeleteGQL := tm.tenantConverter.MultipleInputToGraphQLInput(tenantsToDelete)
   187  	return runInChunks(ctx, tm.config.TenantOperationChunkSize, tenantsToDeleteGQL, func(ctx context.Context, chunk []graphql.BusinessTenantMappingInput) error {
   188  		return tm.gqlClient.DeleteTenants(ctx, chunk)
   189  	})
   190  }
   191  
   192  func (tm *TenantsManager) fetchTenantsForEventType(ctx context.Context, region, fromTimestamp string, eventsType EventsType) ([]model.BusinessTenantMappingInput, error) {
   193  	configProvider := eventsQueryConfigProviderWithRegion(tm.config, fromTimestamp, region)
   194  	fetchedTenants, err := fetchTenantsWithRetries(ctx, tm.eventAPIClient, tm.config.RetryAttempts, eventsType, configProvider)
   195  	if err != nil {
   196  		return nil, errors.Wrap(err, "while fetching tenants with universal client")
   197  	}
   198  
   199  	regionClient, ok := tm.regionalClients[region]
   200  	if !ok {
   201  		log.C(ctx).Infof("Region %s does not have local events client enabled", region)
   202  		return fetchedTenants, nil
   203  	}
   204  	configProvider = eventsQueryConfigProvider(tm.config, fromTimestamp)
   205  	createdRegionalTenants, err := fetchTenantsWithRetries(ctx, regionClient, tm.config.RetryAttempts, eventsType, configProvider)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	createdRegionalTenants = excludeTenants(createdRegionalTenants, fetchedTenants)
   211  	fetchedTenants = append(fetchedTenants, createdRegionalTenants...)
   212  
   213  	return fetchedTenants, nil
   214  }
   215  
   216  func supportedEventTypes(tenantType tenantpkg.Type) (supportedEvents, error) {
   217  	switch tenantType {
   218  	case tenantpkg.Account:
   219  		return supportedEvents{
   220  			createdTenantEvent: CreatedAccountType,
   221  			updatedTenantEvent: UpdatedAccountType,
   222  			deletedTenantEvent: DeletedAccountType,
   223  		}, nil
   224  	case tenantpkg.Subaccount:
   225  		return supportedEvents{
   226  			createdTenantEvent: CreatedSubaccountType,
   227  			updatedTenantEvent: UpdatedSubaccountType,
   228  			deletedTenantEvent: DeletedSubaccountType,
   229  		}, nil
   230  	}
   231  	return supportedEvents{}, errors.Errorf("Tenant events for type %s are not supported", tenantType)
   232  }
   233  
   234  func eventsQueryConfigProvider(config EventsConfig, fromTimestamp string) func() (QueryParams, PageConfig) {
   235  	return eventsQueryConfigProviderWithRegion(config, fromTimestamp, "")
   236  }
   237  
   238  func eventsQueryConfigProviderWithRegion(config EventsConfig, fromTimestamp, region string) func() (QueryParams, PageConfig) {
   239  	additionalFields := map[string]string{
   240  		config.QueryConfig.TimestampField: fromTimestamp,
   241  	}
   242  	if len(region) > 0 && len(config.QueryConfig.RegionField) > 0 {
   243  		additionalFields[config.QueryConfig.RegionField] = region
   244  	}
   245  	return eventsQueryConfigProviderWithAdditionalFields(config, additionalFields)
   246  }
   247  
   248  func eventsQueryConfigProviderWithAdditionalFields(config EventsConfig, additionalQueryParams map[string]string) func() (QueryParams, PageConfig) {
   249  	return func() (QueryParams, PageConfig) {
   250  		qp := QueryParams{
   251  			config.QueryConfig.PageNumField:  config.QueryConfig.PageStartValue,
   252  			config.QueryConfig.PageSizeField: config.QueryConfig.PageSizeValue,
   253  		}
   254  		for field, value := range additionalQueryParams {
   255  			qp[field] = value
   256  		}
   257  
   258  		pc := PageConfig{
   259  			TotalPagesField:   config.PagingConfig.TotalPagesField,
   260  			TotalResultsField: config.PagingConfig.TotalResultsField,
   261  			PageNumField:      config.QueryConfig.PageNumField,
   262  			PageWorkers:       config.PageWorkers,
   263  		}
   264  		return qp, pc
   265  	}
   266  }
   267  
   268  func fetchCreatedTenantsWithRetries(ctx context.Context, eventAPIClient EventAPIClient, retryNumber uint, supportedEvents supportedEvents, configProvider func() (QueryParams, PageConfig)) ([]model.BusinessTenantMappingInput, error) {
   269  	var fetchedTenants []model.BusinessTenantMappingInput
   270  
   271  	createdTenants, err := fetchTenantsWithRetries(ctx, eventAPIClient, retryNumber, supportedEvents.createdTenantEvent, configProvider)
   272  	if err != nil {
   273  		return nil, fmt.Errorf("while fetching created tenants: %v", err)
   274  	}
   275  	fetchedTenants = append(fetchedTenants, createdTenants...)
   276  
   277  	updatedTenants, err := fetchTenantsWithRetries(ctx, eventAPIClient, retryNumber, supportedEvents.updatedTenantEvent, configProvider)
   278  	if err != nil {
   279  		return nil, fmt.Errorf("while fetching updated tenants: %v", err)
   280  	}
   281  
   282  	updatedTenants = excludeTenants(updatedTenants, createdTenants)
   283  	fetchedTenants = append(fetchedTenants, updatedTenants...)
   284  	return fetchedTenants, nil
   285  }
   286  
   287  func fetchTenantsWithRetries(ctx context.Context, eventAPIClient EventAPIClient, retryNumber uint, eventsType EventsType, configProvider func() (QueryParams, PageConfig)) ([]model.BusinessTenantMappingInput, error) {
   288  	var tenants []model.BusinessTenantMappingInput
   289  	if err := fetchWithRetries(retryNumber, func() error {
   290  		fetchedTenants, err := fetchTenants(ctx, eventAPIClient, eventsType, configProvider)
   291  		if err != nil {
   292  			return fmt.Errorf("while fetching tenants: %v", err)
   293  		}
   294  		tenants = fetchedTenants
   295  		return nil
   296  	}); err != nil {
   297  		return nil, err
   298  	}
   299  
   300  	return tenants, nil
   301  }
   302  
   303  func fetchTenants(ctx context.Context, eventAPIClient EventAPIClient, eventsType EventsType, configProvider func() (QueryParams, PageConfig)) ([]model.BusinessTenantMappingInput, error) {
   304  	tenants := make([]model.BusinessTenantMappingInput, 0)
   305  	if err := walkThroughPages(ctx, eventAPIClient, eventsType, configProvider, func(page *EventsPage) error {
   306  		mappings := page.GetTenantMappings(ctx, eventsType)
   307  		tenants = append(tenants, mappings...)
   308  		return nil
   309  	}); err != nil {
   310  		return nil, fmt.Errorf("while walking through pages: %v", err)
   311  	}
   312  
   313  	return tenants, nil
   314  }
   315  
   316  func fetchWithRetries(retryAttempts uint, applyFunc func() error) error {
   317  	return retry.Do(applyFunc, retry.Attempts(retryAttempts), retry.Delay(retryDelaySeconds*time.Second))
   318  }
   319  
   320  func walkThroughPages(ctx context.Context, eventAPIClient EventAPIClient, eventsType EventsType, configProvider func() (QueryParams, PageConfig), applyFunc func(*EventsPage) error) error {
   321  	params, pageConfig := configProvider()
   322  	firstPage, err := eventAPIClient.FetchTenantEventsPage(ctx, eventsType, params)
   323  	if err != nil {
   324  		return errors.Wrap(err, "while fetching tenant events page")
   325  	}
   326  	if firstPage == nil {
   327  		return nil
   328  	}
   329  
   330  	if err = applyFunc(firstPage); err != nil {
   331  		return fmt.Errorf("while applyfunc on event page: %v", err)
   332  	}
   333  
   334  	initialCount := gjson.GetBytes(firstPage.Payload, pageConfig.TotalResultsField).Int()
   335  	totalPages := gjson.GetBytes(firstPage.Payload, pageConfig.TotalPagesField).Int()
   336  
   337  	pageStart, err := strconv.ParseInt(params[pageConfig.PageNumField], 10, 64)
   338  	if err != nil {
   339  		return err
   340  	}
   341  
   342  	region := getRegionFromCtx(ctx)
   343  	log.C(ctx).Infof("Starting processing for %d events across %d pages for event type %v in region %s", initialCount, totalPages, eventsType, region)
   344  
   345  	start := time.Now()
   346  	var m sync.Mutex
   347  	eventPages := make([]*EventsPage, 0)
   348  	queryParamsQueue := make(chan QueryParams)
   349  	wg := sync.WaitGroup{}
   350  	var globalErr safeError
   351  
   352  	log.C(ctx).Infof("Initializing %d page fetching workers: starting concurrent retrieval of pages.", pageConfig.PageWorkers)
   353  	for worker := 0; worker < pageConfig.PageWorkers; worker++ {
   354  		wg.Add(1)
   355  		go func() {
   356  			defer wg.Done()
   357  
   358  			for qParams := range queryParamsQueue {
   359  				if globalErr.GetError() != nil {
   360  					continue
   361  				}
   362  
   363  				var res *EventsPage
   364  				err := retry.Do(func() error {
   365  					res, err = eventAPIClient.FetchTenantEventsPage(ctx, eventsType, qParams)
   366  					if err != nil {
   367  						return errors.Wrap(err, "while fetching tenant events page")
   368  					}
   369  					if res == nil {
   370  						return apperrors.NewInternalError("next page was expected but response was empty")
   371  					}
   372  
   373  					return nil
   374  				}, []retry.Option{retry.Attempts(5)}...)
   375  
   376  				if err != nil {
   377  					log.C(ctx).WithError(err).Errorf("Error occurred while fetching page.")
   378  					if globalErr.GetError() == nil {
   379  						globalErr.SetError(err)
   380  					}
   381  					continue
   382  				}
   383  
   384  				m.Lock()
   385  				eventPages = append(eventPages, &EventsPage{
   386  					FieldMapping:                 res.FieldMapping,
   387  					MovedSubaccountsFieldMapping: res.MovedSubaccountsFieldMapping,
   388  					ProviderName:                 res.ProviderName,
   389  					Payload:                      res.Payload,
   390  				})
   391  				m.Unlock()
   392  			}
   393  		}()
   394  	}
   395  
   396  	wgParams := sync.WaitGroup{}
   397  	wgParams.Add(1)
   398  
   399  	log.C(ctx).Infof("Starting a goroutine to prepare query parameters.")
   400  	go createParamsForFetching(&wgParams, pageStart, totalPages, params, pageConfig, queryParamsQueue)
   401  
   402  	log.C(ctx).Infof("Waiting for the goroutine to prepare all query parameters.")
   403  	wgParams.Wait()
   404  
   405  	close(queryParamsQueue)
   406  	log.C(ctx).Infof("Waiting for all page fetching workers to finish.")
   407  	wg.Wait()
   408  
   409  	if globalErr.GetError() != nil {
   410  		return globalErr.GetError()
   411  	}
   412  
   413  	log.C(ctx).Infof("Successfully fetched %d event pages for region %s. Starting processing...", len(eventPages), region)
   414  	for _, res := range eventPages {
   415  		if err = applyFunc(res); err != nil {
   416  			return err
   417  		}
   418  	}
   419  
   420  	log.C(ctx).Infof("Processing of %d events across %d pages for event type %v in region %s took %v", initialCount, totalPages, eventsType, region, time.Since(start))
   421  	return nil
   422  }
   423  
   424  func getRegionFromCtx(ctx context.Context) string {
   425  	region, ok := ctx.Value(TenantRegionCtxKey).(string)
   426  	if !ok {
   427  		return ""
   428  	}
   429  	return region
   430  }
   431  
   432  func createParamsForFetching(wg *sync.WaitGroup, pageStart int64, totalPages int64, params QueryParams, config PageConfig, queue chan QueryParams) {
   433  	defer wg.Done()
   434  
   435  	for i := pageStart + 1; i <= totalPages; i++ {
   436  		qParams := make(map[string]string)
   437  		for k, v := range params {
   438  			qParams[k] = v
   439  		}
   440  		qParams[config.PageNumField] = strconv.FormatInt(i, 10)
   441  		queue <- qParams
   442  	}
   443  }
   444  
   445  func runInChunks(ctx context.Context, maxChunkSize int, tenants []graphql.BusinessTenantMappingInput, storeTenantsFunc func(ctx context.Context, chunk []graphql.BusinessTenantMappingInput) error) error {
   446  	for len(tenants) > 0 {
   447  		chunkSize := int(math.Min(float64(len(tenants)), float64(maxChunkSize)))
   448  		tenantsChunk := tenants[:chunkSize]
   449  		if err := storeTenantsFunc(ctx, tenantsChunk); err != nil {
   450  			return err
   451  		}
   452  		tenants = tenants[chunkSize:]
   453  	}
   454  
   455  	return nil
   456  }
   457  
   458  type safeError struct {
   459  	mu    sync.Mutex
   460  	Error error
   461  }
   462  
   463  func (se *safeError) SetError(err error) {
   464  	se.mu.Lock()
   465  	defer se.mu.Unlock()
   466  	se.Error = err
   467  }
   468  
   469  func (se *safeError) GetError() error {
   470  	se.mu.Lock()
   471  	defer se.mu.Unlock()
   472  	return se.Error
   473  }