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  }