github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/runtime/service.go (about)

     1  package runtime
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"regexp"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/kyma-incubator/compass/components/director/pkg/consumer"
    11  
    12  	"github.com/kyma-incubator/compass/components/director/pkg/graphql"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/internal/domain/label"
    15  	"github.com/kyma-incubator/compass/components/director/internal/domain/scenarioassignment"
    16  	"github.com/kyma-incubator/compass/components/director/pkg/str"
    17  
    18  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    19  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    20  
    21  	"github.com/kyma-incubator/compass/components/director/internal/labelfilter"
    22  	"github.com/kyma-incubator/compass/components/director/internal/model"
    23  
    24  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    25  	"github.com/pkg/errors"
    26  )
    27  
    28  const (
    29  	// IsNormalizedLabel represents the label that is used to mark a runtime as normalized
    30  	IsNormalizedLabel = "isNormalized"
    31  
    32  	// RegionLabelKey is the key of the tenant label for region.
    33  	RegionLabelKey = "region"
    34  )
    35  
    36  //go:generate mockery --exported --name=runtimeRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    37  type runtimeRepository interface {
    38  	Exists(ctx context.Context, tenant, id string) (bool, error)
    39  	GetByID(ctx context.Context, tenant, id string) (*model.Runtime, error)
    40  	GetByFiltersGlobal(ctx context.Context, filter []*labelfilter.LabelFilter) (*model.Runtime, error)
    41  	List(ctx context.Context, tenant string, filter []*labelfilter.LabelFilter, pageSize int, cursor string) (*model.RuntimePage, error)
    42  	ListByFiltersGlobal(context.Context, []*labelfilter.LabelFilter) ([]*model.Runtime, error)
    43  	Create(ctx context.Context, tenant string, item *model.Runtime) error
    44  	Update(ctx context.Context, tenant string, item *model.Runtime) error
    45  	ListAll(context.Context, string, []*labelfilter.LabelFilter) ([]*model.Runtime, error)
    46  	Delete(ctx context.Context, tenant, id string) error
    47  	GetByFilters(ctx context.Context, tenant string, filter []*labelfilter.LabelFilter) (*model.Runtime, error)
    48  }
    49  
    50  //go:generate mockery --exported --name=labelRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    51  type labelRepository interface {
    52  	GetByKey(ctx context.Context, tenant string, objectType model.LabelableObject, objectID, key string) (*model.Label, error)
    53  	ListForObject(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error)
    54  	Delete(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string, key string) error
    55  	DeleteAll(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string) error
    56  	DeleteByKeyNegationPattern(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string, labelKeyPattern string) error
    57  }
    58  
    59  //go:generate mockery --exported --name=labelService --output=automock --outpkg=automock --case=underscore --disable-version-string
    60  type labelService interface {
    61  	UpsertMultipleLabels(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string, labels map[string]interface{}) error
    62  	UpsertLabel(ctx context.Context, tenant string, labelInput *model.LabelInput) error
    63  	GetByKey(ctx context.Context, tenant string, objectType model.LabelableObject, objectID, key string) (*model.Label, error)
    64  }
    65  
    66  //go:generate mockery --exported --name=tenantService --output=automock --outpkg=automock --case=underscore --disable-version-string
    67  type tenantService interface {
    68  	GetTenantByExternalID(ctx context.Context, id string) (*model.BusinessTenantMapping, error)
    69  	GetTenantByID(ctx context.Context, id string) (*model.BusinessTenantMapping, error)
    70  }
    71  
    72  //go:generate mockery --exported --name=uidService --output=automock --outpkg=automock --case=underscore --disable-version-string
    73  type uidService interface {
    74  	Generate() string
    75  }
    76  
    77  type service struct {
    78  	repo      runtimeRepository
    79  	labelRepo labelRepository
    80  
    81  	labelService          labelService
    82  	uidService            uidService
    83  	formationService      formationService
    84  	tenantSvc             tenantService
    85  	webhookService        WebhookService
    86  	runtimeContextService RuntimeContextService
    87  
    88  	protectedLabelPattern         string
    89  	immutableLabelPattern         string
    90  	runtimeTypeLabelKey           string
    91  	kymaRuntimeTypeLabelValue     string
    92  	kymaApplicationNamespaceValue string
    93  }
    94  
    95  // NewService missing godoc
    96  func NewService(repo runtimeRepository,
    97  	labelRepo labelRepository,
    98  	labelService labelService,
    99  	uidService uidService,
   100  	formationService formationService,
   101  	tenantService tenantService,
   102  	webhookService WebhookService,
   103  	runtimeContextService RuntimeContextService,
   104  	protectedLabelPattern, immutableLabelPattern, runtimeTypeLabelKey, kymaRuntimeTypeLabelValue, kymaApplicationNamespaceValue string) *service {
   105  	return &service{
   106  		repo:                          repo,
   107  		labelRepo:                     labelRepo,
   108  		labelService:                  labelService,
   109  		uidService:                    uidService,
   110  		formationService:              formationService,
   111  		tenantSvc:                     tenantService,
   112  		webhookService:                webhookService,
   113  		runtimeContextService:         runtimeContextService,
   114  		protectedLabelPattern:         protectedLabelPattern,
   115  		immutableLabelPattern:         immutableLabelPattern,
   116  		runtimeTypeLabelKey:           runtimeTypeLabelKey,
   117  		kymaRuntimeTypeLabelValue:     kymaRuntimeTypeLabelValue,
   118  		kymaApplicationNamespaceValue: kymaApplicationNamespaceValue,
   119  	}
   120  }
   121  
   122  // List missing godoc
   123  func (s *service) List(ctx context.Context, filter []*labelfilter.LabelFilter, pageSize int, cursor string) (*model.RuntimePage, error) {
   124  	rtmTenant, err := tenant.LoadFromContext(ctx)
   125  	if err != nil {
   126  		return nil, errors.Wrapf(err, "while loading tenant from context")
   127  	}
   128  
   129  	if pageSize < 1 || pageSize > 200 {
   130  		return nil, apperrors.NewInvalidDataError("page size must be between 1 and 200")
   131  	}
   132  
   133  	return s.repo.List(ctx, rtmTenant, filter, pageSize, cursor)
   134  }
   135  
   136  // Get missing godoc
   137  func (s *service) Get(ctx context.Context, id string) (*model.Runtime, error) {
   138  	rtmTenant, err := tenant.LoadFromContext(ctx)
   139  	if err != nil {
   140  		return nil, errors.Wrapf(err, "while loading tenant from context")
   141  	}
   142  
   143  	runtime, err := s.repo.GetByID(ctx, rtmTenant, id)
   144  	if err != nil {
   145  		return nil, errors.Wrapf(err, "while getting Runtime with ID %s", id)
   146  	}
   147  
   148  	return runtime, nil
   149  }
   150  
   151  // GetByTokenIssuer missing godoc
   152  func (s *service) GetByTokenIssuer(ctx context.Context, issuer string) (*model.Runtime, error) {
   153  	const (
   154  		consoleURLLabelKey = "runtime_consoleUrl"
   155  		dexSubdomain       = "dex"
   156  		consoleSubdomain   = "console"
   157  	)
   158  	consoleURL := strings.Replace(issuer, dexSubdomain, consoleSubdomain, 1)
   159  
   160  	filters := []*labelfilter.LabelFilter{
   161  		labelfilter.NewForKeyWithQuery(consoleURLLabelKey, fmt.Sprintf(`"%s"`, consoleURL)),
   162  	}
   163  
   164  	runtime, err := s.repo.GetByFiltersGlobal(ctx, filters)
   165  	if err != nil {
   166  		return nil, errors.Wrapf(err, "while getting the Runtime by the console URL label (%s)", consoleURL)
   167  	}
   168  
   169  	return runtime, nil
   170  }
   171  
   172  // GetByFiltersGlobal missing godoc
   173  func (s *service) GetByFiltersGlobal(ctx context.Context, filters []*labelfilter.LabelFilter) (*model.Runtime, error) {
   174  	runtimes, err := s.repo.GetByFiltersGlobal(ctx, filters)
   175  	if err != nil {
   176  		return nil, errors.Wrapf(err, "while getting runtimes by filters from repo")
   177  	}
   178  	return runtimes, nil
   179  }
   180  
   181  // GetByFilters retrieves model.Runtime matching on the given label filters
   182  func (s *service) GetByFilters(ctx context.Context, filters []*labelfilter.LabelFilter) (*model.Runtime, error) {
   183  	rtmTenant, err := tenant.LoadFromContext(ctx)
   184  	if err != nil {
   185  		return nil, errors.Wrapf(err, "while loading tenant from context")
   186  	}
   187  
   188  	runtime, err := s.repo.GetByFilters(ctx, rtmTenant, filters)
   189  	if err != nil {
   190  		return nil, errors.Wrapf(err, "while getting runtime by filters from repo")
   191  	}
   192  	return runtime, nil
   193  }
   194  
   195  // ListByFiltersGlobal missing godoc
   196  func (s *service) ListByFiltersGlobal(ctx context.Context, filters []*labelfilter.LabelFilter) ([]*model.Runtime, error) {
   197  	runtimes, err := s.repo.ListByFiltersGlobal(ctx, filters)
   198  	if err != nil {
   199  		return nil, errors.Wrapf(err, "while getting runtimes by filters from repo")
   200  	}
   201  	return runtimes, nil
   202  }
   203  
   204  // ListByFilters lists all runtimes in a given tenant that match given label filter.
   205  func (s *service) ListByFilters(ctx context.Context, filters []*labelfilter.LabelFilter) ([]*model.Runtime, error) {
   206  	rtmTenant, err := tenant.LoadFromContext(ctx)
   207  	if err != nil {
   208  		return nil, errors.Wrapf(err, "while loading tenant from context")
   209  	}
   210  
   211  	runtimes, err := s.repo.ListAll(ctx, rtmTenant, filters)
   212  	if err != nil {
   213  		return nil, errors.Wrapf(err, "while getting runtimes by filters from repo")
   214  	}
   215  	return runtimes, nil
   216  }
   217  
   218  // Exist missing godoc
   219  func (s *service) Exist(ctx context.Context, id string) (bool, error) {
   220  	rtmTenant, err := tenant.LoadFromContext(ctx)
   221  	if err != nil {
   222  		return false, errors.Wrapf(err, "while loading tenant from context")
   223  	}
   224  
   225  	exist, err := s.repo.Exists(ctx, rtmTenant, id)
   226  	if err != nil {
   227  		return false, errors.Wrapf(err, "while getting Runtime with ID %s", id)
   228  	}
   229  
   230  	return exist, nil
   231  }
   232  
   233  // Create creates a runtime in a given tenant.
   234  // If the runtime has a global_subaccount_id label which value is a valid external subaccount from our DB and a child of the caller tenant. The subaccount is used to register the runtime.
   235  // After successful registration, the ASAs in the parent of the caller tenant are processed to add all matching scenarios for the runtime in the parent tenant.
   236  func (s *service) Create(ctx context.Context, in model.RuntimeRegisterInput) (string, error) {
   237  	labels := make(map[string]interface{})
   238  	id := s.uidService.Generate()
   239  	return id, s.CreateWithMandatoryLabels(ctx, in, id, labels)
   240  }
   241  
   242  // CreateWithMandatoryLabels creates a runtime in a given tenant and also adds mandatory labels to it.
   243  func (s *service) CreateWithMandatoryLabels(ctx context.Context, in model.RuntimeRegisterInput, id string, mandatoryLabels map[string]interface{}) error {
   244  	var subaccountTnt string
   245  	if saVal, ok := in.Labels[scenarioassignment.SubaccountIDKey]; ok { // TODO: <backwards-compatibility>: Should be deleted once the provisioner start creating runtimes in a subaccount
   246  		tnt, err := s.extractTenantFromSubaccountLabel(ctx, saVal)
   247  		if err != nil {
   248  			return err
   249  		}
   250  		subaccountTnt = tnt.ID
   251  		ctx = tenant.SaveToContext(ctx, tnt.ID, tnt.ExternalTenant)
   252  	}
   253  
   254  	rtmTenant, err := tenant.LoadFromContext(ctx)
   255  	if err != nil {
   256  		return errors.Wrapf(err, "while loading tenant from context")
   257  	}
   258  
   259  	consumerInfo, err := consumer.LoadFromContext(ctx)
   260  	if err != nil {
   261  		return errors.Wrapf(err, "while loading consumer")
   262  	}
   263  
   264  	isConsumerIntegrationSystem := consumerInfo.ConsumerType == consumer.IntegrationSystem
   265  	if isConsumerIntegrationSystem {
   266  		in.ApplicationNamespace = &s.kymaApplicationNamespaceValue
   267  	}
   268  
   269  	rtm := in.ToRuntime(id, time.Now(), time.Now())
   270  
   271  	if err = s.repo.Create(ctx, rtmTenant, rtm); err != nil {
   272  		return errors.Wrapf(err, "while creating Runtime")
   273  	}
   274  
   275  	if in.Labels == nil || in.Labels[IsNormalizedLabel] == nil {
   276  		if in.Labels == nil {
   277  			in.Labels = make(map[string]interface{}, 1)
   278  		}
   279  		in.Labels[IsNormalizedLabel] = "true"
   280  	}
   281  
   282  	log.C(ctx).Debugf("Removing protected labels. Labels before: %+v", in.Labels)
   283  	if in.Labels, err = s.UnsafeExtractModifiableLabels(in.Labels); err != nil {
   284  		return err
   285  	}
   286  	log.C(ctx).Debugf("Successfully stripped protected labels. Resulting labels after operation are: %+v", in.Labels)
   287  
   288  	for key, value := range mandatoryLabels {
   289  		in.Labels[key] = value
   290  	}
   291  
   292  	var scenariosToAssign interface{} = nil
   293  	if _, areScenariosInLabels := in.Labels[model.ScenariosKey]; areScenariosInLabels {
   294  		scenariosToAssign = in.Labels[model.ScenariosKey]
   295  		delete(in.Labels, model.ScenariosKey)
   296  	}
   297  
   298  	if isConsumerIntegrationSystem {
   299  		in.Labels[s.runtimeTypeLabelKey] = s.kymaRuntimeTypeLabelValue
   300  
   301  		region, err := s.extractRegionFromSubaccountTenant(ctx, subaccountTnt)
   302  		if err != nil {
   303  			return err
   304  		}
   305  		in.Labels[RegionLabelKey] = region
   306  	}
   307  
   308  	if err = s.labelService.UpsertMultipleLabels(ctx, rtmTenant, model.RuntimeLabelableObject, id, in.Labels); err != nil {
   309  		return errors.Wrapf(err, "while creating multiple labels for Runtime")
   310  	}
   311  
   312  	if scenariosToAssign != nil {
   313  		if err := s.assignRuntimeScenarios(ctx, rtmTenant, id, scenariosToAssign); err != nil {
   314  			return err
   315  		}
   316  	}
   317  
   318  	for _, w := range in.Webhooks {
   319  		if _, err = s.webhookService.Create(ctx, rtm.ID, *w, model.RuntimeWebhookReference); err != nil {
   320  			return errors.Wrap(err, "while Creating Webhook for Runtime")
   321  		}
   322  	}
   323  
   324  	// The runtime is created successfully, however there can be ASAs in the parent that should be processed.
   325  	tnt, err := s.tenantSvc.GetTenantByID(ctx, rtmTenant)
   326  	if err != nil {
   327  		return errors.Wrapf(err, "while getting tenant with id %s", rtmTenant)
   328  	}
   329  
   330  	if len(tnt.Parent) == 0 {
   331  		return nil
   332  	}
   333  
   334  	ctxWithParentTenant := tenant.SaveToContext(ctx, tnt.Parent, "")
   335  
   336  	mergedScenarios, err := s.formationService.MergeScenariosFromInputLabelsAndAssignments(ctxWithParentTenant, map[string]interface{}{}, id)
   337  	if err != nil {
   338  		return errors.Wrap(err, "while merging scenarios from input and assignments")
   339  	}
   340  
   341  	if err := s.assignRuntimeScenarios(ctxWithParentTenant, tnt.Parent, id, mergedScenarios); err != nil {
   342  		return errors.Wrapf(err, "while assigning merged formations")
   343  	}
   344  
   345  	return nil
   346  }
   347  
   348  // Update updates Runtime and its labels
   349  func (s *service) Update(ctx context.Context, id string, in model.RuntimeUpdateInput) error {
   350  	rtmTenant, err := tenant.LoadFromContext(ctx)
   351  	if err != nil {
   352  		return errors.Wrapf(err, "while loading tenant from context")
   353  	}
   354  
   355  	rtm, err := s.repo.GetByID(ctx, rtmTenant, id)
   356  	if err != nil {
   357  		return errors.Wrapf(err, "while getting Runtime with id %s", id)
   358  	}
   359  
   360  	rtm.SetFromUpdateInput(in, id, rtm.CreationTimestamp, time.Now())
   361  
   362  	if err = s.repo.Update(ctx, rtmTenant, rtm); err != nil {
   363  		return errors.Wrap(err, "while updating Runtime")
   364  	}
   365  
   366  	if in.Labels == nil || in.Labels[IsNormalizedLabel] == nil {
   367  		if in.Labels == nil {
   368  			in.Labels = make(map[string]interface{}, 1)
   369  		}
   370  		in.Labels[IsNormalizedLabel] = "true"
   371  	}
   372  
   373  	if err := s.updateScenariosLabel(ctx, rtmTenant, id, in.Labels); err != nil {
   374  		return errors.Wrap(err, "while updating scenarios label")
   375  	}
   376  	delete(in.Labels, model.ScenariosKey)
   377  
   378  	log.C(ctx).Debugf("Removing protected labels. Labels before: %+v", in.Labels)
   379  	if in.Labels, err = s.UnsafeExtractModifiableLabels(in.Labels); err != nil {
   380  		return err
   381  	}
   382  	log.C(ctx).Debugf("Successfully stripped protected labels. Resulting labels after operation are: %+v", in.Labels)
   383  
   384  	unmodifiablePattern := s.protectedLabelPattern + "|^" + model.ScenariosKey + "$" + "|" + s.immutableLabelPattern
   385  	// NOTE: The db layer does not support OR currently so multiple label patterns can't be implemented easily
   386  	if err = s.labelRepo.DeleteByKeyNegationPattern(ctx, rtmTenant, model.RuntimeLabelableObject, id, unmodifiablePattern); err != nil {
   387  		return errors.Wrapf(err, "while deleting all labels for Runtime")
   388  	}
   389  
   390  	if err = s.labelService.UpsertMultipleLabels(ctx, rtmTenant, model.RuntimeLabelableObject, id, in.Labels); err != nil {
   391  		return errors.Wrapf(err, "while creating multiple labels for Runtime")
   392  	}
   393  
   394  	return nil
   395  }
   396  
   397  // Delete deletes all RuntimeContexts associated with the runtime with ID `id` and then deletes the runtime and its labels
   398  func (s *service) Delete(ctx context.Context, id string) error {
   399  	rtmTenant, err := tenant.LoadFromContext(ctx)
   400  	if err != nil {
   401  		return errors.Wrapf(err, "while loading tenant from context")
   402  	}
   403  
   404  	runtimeContexts, err := s.runtimeContextService.ListAllForRuntime(ctx, id)
   405  	if err != nil {
   406  		return errors.Wrapf(err, "while listing runtimeContexts for runtime with ID %q", id)
   407  	}
   408  
   409  	for _, rc := range runtimeContexts {
   410  		if err = s.runtimeContextService.Delete(ctx, rc.ID); err != nil {
   411  			return errors.Wrapf(err, "while deleting runtimeContext with ID %q", rc.ID)
   412  		}
   413  	}
   414  
   415  	if err = s.unassignRuntimeScenarios(ctx, rtmTenant, id); err != nil {
   416  		return err
   417  	}
   418  
   419  	if err = s.repo.Delete(ctx, rtmTenant, id); err != nil {
   420  		return errors.Wrapf(err, "while deleting Runtime")
   421  	}
   422  
   423  	// All labels are deleted (cascade delete)
   424  
   425  	return nil
   426  }
   427  
   428  // SetLabel sets Runtime label from a given input
   429  func (s *service) SetLabel(ctx context.Context, labelInput *model.LabelInput) error {
   430  	rtmTenant, err := tenant.LoadFromContext(ctx)
   431  	if err != nil {
   432  		return errors.Wrapf(err, "while loading tenant from context")
   433  	}
   434  
   435  	if err = s.ensureRuntimeExists(ctx, rtmTenant, labelInput.ObjectID); err != nil {
   436  		return err
   437  	}
   438  
   439  	if modifiable, err := isLabelModifiable(labelInput.Key, s.protectedLabelPattern, s.immutableLabelPattern); err != nil {
   440  		return err
   441  	} else if !modifiable {
   442  		return apperrors.NewInvalidDataError("could not set unmodifiable label with key %s", labelInput.Key)
   443  	}
   444  
   445  	id := labelInput.ObjectID
   446  
   447  	if labelInput.Key == model.ScenariosKey {
   448  		if err := s.updateScenariosLabel(ctx, rtmTenant, id, map[string]interface{}{model.ScenariosKey: labelInput.Value}); err != nil {
   449  			return errors.Wrap(err, "while updating scenarios label")
   450  		}
   451  	} else {
   452  		if err = s.labelService.UpsertLabel(ctx, rtmTenant, labelInput); err != nil {
   453  			return errors.Wrapf(err, "while creating label for Runtime")
   454  		}
   455  	}
   456  
   457  	return nil
   458  }
   459  
   460  // GetLabel missing godoc
   461  func (s *service) GetLabel(ctx context.Context, runtimeID string, key string) (*model.Label, error) {
   462  	rtmTenant, err := tenant.LoadFromContext(ctx)
   463  	if err != nil {
   464  		return nil, errors.Wrapf(err, "while loading tenant from context")
   465  	}
   466  
   467  	rtmExists, err := s.repo.Exists(ctx, rtmTenant, runtimeID)
   468  	if err != nil {
   469  		return nil, errors.Wrap(err, "while checking Runtime existence")
   470  	}
   471  	if !rtmExists {
   472  		return nil, fmt.Errorf("Runtime with ID %s doesn't exist", runtimeID)
   473  	}
   474  
   475  	label, err := s.labelRepo.GetByKey(ctx, rtmTenant, model.RuntimeLabelableObject, runtimeID, key)
   476  	if err != nil {
   477  		return nil, errors.Wrap(err, "while getting label for Runtime")
   478  	}
   479  
   480  	return label, nil
   481  }
   482  
   483  // ListLabels missing godoc
   484  func (s *service) ListLabels(ctx context.Context, runtimeID string) (map[string]*model.Label, error) {
   485  	rtmTenant, err := tenant.LoadFromContext(ctx)
   486  	if err != nil {
   487  		return nil, errors.Wrapf(err, "while loading tenant from context")
   488  	}
   489  
   490  	rtmExists, err := s.repo.Exists(ctx, rtmTenant, runtimeID)
   491  	if err != nil {
   492  		return nil, errors.Wrap(err, "while checking Runtime existence")
   493  	}
   494  
   495  	if !rtmExists {
   496  		return nil, fmt.Errorf("Runtime with ID %s doesn't exist", runtimeID)
   497  	}
   498  
   499  	labels, err := s.labelRepo.ListForObject(ctx, rtmTenant, model.RuntimeLabelableObject, runtimeID)
   500  	if err != nil {
   501  		return nil, errors.Wrap(err, "while getting label for Runtime")
   502  	}
   503  
   504  	return extractUnProtectedLabels(labels, s.protectedLabelPattern)
   505  }
   506  
   507  // DeleteLabel deletes Runtime label from a given label key
   508  func (s *service) DeleteLabel(ctx context.Context, runtimeID string, key string) error {
   509  	rtmTenant, err := tenant.LoadFromContext(ctx)
   510  	if err != nil {
   511  		return errors.Wrapf(err, "while loading tenant from context")
   512  	}
   513  
   514  	if err = s.ensureRuntimeExists(ctx, rtmTenant, runtimeID); err != nil {
   515  		return err
   516  	}
   517  
   518  	if modifiable, err := isLabelModifiable(key, s.protectedLabelPattern, s.immutableLabelPattern); err != nil {
   519  		return err
   520  	} else if !modifiable {
   521  		return apperrors.NewInvalidDataError("could not delete unmodifiable label with key %s", key)
   522  	}
   523  
   524  	if key == model.ScenariosKey {
   525  		if err := s.unassignRuntimeScenarios(ctx, rtmTenant, runtimeID); err != nil {
   526  			return err
   527  		}
   528  	} else {
   529  		if err = s.labelRepo.Delete(ctx, rtmTenant, model.RuntimeLabelableObject, runtimeID, key); err != nil {
   530  			return errors.Wrapf(err, "while deleting Runtime label")
   531  		}
   532  	}
   533  
   534  	return nil
   535  }
   536  
   537  // UnsafeExtractModifiableLabels returns all labels except the protected and immutable labels
   538  func (s *service) UnsafeExtractModifiableLabels(labels map[string]interface{}) (map[string]interface{}, error) {
   539  	result := make(map[string]interface{})
   540  	for labelKey, lbl := range labels {
   541  		modifiable, err := isLabelModifiable(labelKey, s.protectedLabelPattern, s.immutableLabelPattern)
   542  		if err != nil {
   543  			return nil, err
   544  		}
   545  		if modifiable {
   546  			result[labelKey] = lbl
   547  		}
   548  	}
   549  	return result, nil
   550  }
   551  
   552  func (s *service) ensureRuntimeExists(ctx context.Context, tnt string, runtimeID string) error {
   553  	rtmExists, err := s.repo.Exists(ctx, tnt, runtimeID)
   554  	if err != nil {
   555  		return errors.Wrap(err, "while checking Runtime existence")
   556  	}
   557  	if !rtmExists {
   558  		return fmt.Errorf("Runtime with ID %s doesn't exist", runtimeID)
   559  	}
   560  
   561  	return nil
   562  }
   563  
   564  func (s *service) assignRuntimeScenarios(ctx context.Context, rtmTenant, id string, scenarios interface{}) error {
   565  	scenariosStr, err := label.ValueToStringsSlice(scenarios)
   566  	if err != nil {
   567  		return errors.Wrapf(err, "while converting scenarios: %+v to slice of strings", scenarios)
   568  	}
   569  
   570  	for _, scenario := range scenariosStr {
   571  		if _, err = s.formationService.AssignFormation(ctx, rtmTenant, id, graphql.FormationObjectTypeRuntime, model.Formation{Name: scenario}); err != nil {
   572  			return errors.Wrapf(err, "while assigning formation %q from runtime with ID %q", scenario, id)
   573  		}
   574  	}
   575  
   576  	return nil
   577  }
   578  
   579  func (s *service) unassignRuntimeScenarios(ctx context.Context, rtmTenant, runtimeID string) error {
   580  	currentRuntimeLabels, err := s.getCurrentLabelsForRuntime(ctx, rtmTenant, runtimeID)
   581  	if err != nil {
   582  		return err
   583  	}
   584  
   585  	if scenarios, areScenariosInLabels := currentRuntimeLabels[model.ScenariosKey]; areScenariosInLabels {
   586  		scenariosStr, err := label.ValueToStringsSlice(scenarios)
   587  		if err != nil {
   588  			return errors.Wrapf(err, "while converting scenarios: %+v to slice of strings", scenarios)
   589  		}
   590  
   591  		for _, scenario := range scenariosStr {
   592  			if _, err = s.formationService.UnassignFormation(ctx, rtmTenant, runtimeID, graphql.FormationObjectTypeRuntime, model.Formation{Name: scenario}); err != nil {
   593  				return errors.Wrapf(err, "while unassigning formation %q from runtime with ID %q", scenario, runtimeID)
   594  			}
   595  		}
   596  	}
   597  
   598  	return nil
   599  }
   600  
   601  func (s *service) updateScenariosLabel(ctx context.Context, rtmTenant, rtmID string, inputLabels map[string]interface{}) error {
   602  	mergedScenarios, err := s.formationService.MergeScenariosFromInputLabelsAndAssignments(ctx, inputLabels, rtmID)
   603  	if err != nil {
   604  		return errors.Wrap(err, "while merging scenarios from input and assignments")
   605  	}
   606  
   607  	mergedScenariosSlice, err := label.ValueToStringsSlice(mergedScenarios)
   608  	if err != nil {
   609  		return errors.Wrapf(err, "while converting merged scenarios: %+v to slice of strings", mergedScenarios)
   610  	}
   611  
   612  	currentRuntimeLabels, err := s.getCurrentLabelsForRuntime(ctx, rtmTenant, rtmID)
   613  	if err != nil {
   614  		return err
   615  	}
   616  
   617  	currentScenarios, areCurrentScenariosInLabels := currentRuntimeLabels[model.ScenariosKey]
   618  	if !areCurrentScenariosInLabels {
   619  		currentScenarios = []interface{}{}
   620  	}
   621  
   622  	currentScenariosSlice, err := label.ValueToStringsSlice(currentScenarios)
   623  	if err != nil {
   624  		return errors.Wrapf(err, "while converting current runtime scenarios: %+v to slice of strings", currentScenarios)
   625  	}
   626  
   627  	currentScenariosMap := make(map[string]struct{}, len(currentScenariosSlice))
   628  	for _, s := range currentScenariosSlice {
   629  		currentScenariosMap[s] = struct{}{}
   630  	}
   631  
   632  	mergedScenariosMap := make(map[string]struct{}, len(mergedScenariosSlice))
   633  	for _, s := range mergedScenariosSlice {
   634  		mergedScenariosMap[s] = struct{}{}
   635  	}
   636  
   637  	for scenario := range currentScenariosMap {
   638  		if _, found := mergedScenariosMap[scenario]; !found {
   639  			if _, err = s.formationService.UnassignFormation(ctx, rtmTenant, rtmID, graphql.FormationObjectTypeRuntime, model.Formation{Name: scenario}); err != nil {
   640  				return errors.Wrapf(err, "while unassigning formation %q from runtime with ID %q", scenario, rtmID)
   641  			}
   642  		}
   643  	}
   644  
   645  	for scenario := range mergedScenariosMap {
   646  		if _, found := currentScenariosMap[scenario]; !found {
   647  			if _, err = s.formationService.AssignFormation(ctx, rtmTenant, rtmID, graphql.FormationObjectTypeRuntime, model.Formation{Name: scenario}); err != nil {
   648  				return errors.Wrapf(err, "while assigning formation %q from runtime with ID %q", scenario, rtmID)
   649  			}
   650  		}
   651  	}
   652  
   653  	return nil
   654  }
   655  
   656  func (s *service) getCurrentLabelsForRuntime(ctx context.Context, tenantID, runtimeID string) (map[string]interface{}, error) {
   657  	labels, err := s.labelRepo.ListForObject(ctx, tenantID, model.RuntimeLabelableObject, runtimeID)
   658  	if err != nil {
   659  		return nil, err
   660  	}
   661  
   662  	currentLabels := make(map[string]interface{})
   663  	for _, v := range labels {
   664  		currentLabels[v.Key] = v.Value
   665  	}
   666  	return currentLabels, nil
   667  }
   668  
   669  func (s *service) extractTenantFromSubaccountLabel(ctx context.Context, value interface{}) (*model.BusinessTenantMapping, error) {
   670  	callingTenant, err := tenant.LoadFromContext(ctx)
   671  	if err != nil {
   672  		return nil, errors.Wrapf(err, "while loading tenant from context")
   673  	}
   674  
   675  	sa, err := convertLabelValue(value)
   676  	if err != nil {
   677  		return nil, errors.Wrapf(err, "while converting %s label", scenarioassignment.SubaccountIDKey)
   678  	}
   679  
   680  	log.C(ctx).Infof("Runtime registered by tenant %s with %s label with value %s. Will proceed with the subaccount as tenant...", callingTenant, scenarioassignment.SubaccountIDKey, sa)
   681  
   682  	tnt, err := s.tenantSvc.GetTenantByExternalID(ctx, sa)
   683  	if err != nil {
   684  		return nil, errors.Wrapf(err, "while getting tenant %s", sa)
   685  	}
   686  
   687  	if callingTenant != tnt.ID && callingTenant != tnt.Parent {
   688  		log.C(ctx).Errorf("Caller tenant %s is not parent of the subaccount %s in the %s label", callingTenant, sa, scenarioassignment.SubaccountIDKey)
   689  		return nil, apperrors.NewInvalidOperationError(fmt.Sprintf("Tenant provided in %s label should be child of the caller tenant", scenarioassignment.SubaccountIDKey))
   690  	}
   691  	return tnt, nil
   692  }
   693  
   694  func (s *service) extractRegionFromSubaccountTenant(ctx context.Context, subaccountTnt string) (string, error) {
   695  	if subaccountTnt == "" {
   696  		return "", nil
   697  	}
   698  
   699  	regionLabel, err := s.labelService.GetByKey(ctx, subaccountTnt, model.TenantLabelableObject, subaccountTnt, RegionLabelKey)
   700  	if err != nil {
   701  		if !apperrors.IsNotFoundError(err) {
   702  			return "", errors.Wrapf(err, "while getting label %q for %q with id %q", RegionLabelKey, model.TenantLabelableObject, subaccountTnt)
   703  		}
   704  	}
   705  
   706  	regionValue := ""
   707  	if regionLabel != nil && regionLabel.Value != nil {
   708  		if regionLabelValue, ok := regionLabel.Value.(string); ok {
   709  			regionValue = regionLabelValue
   710  		}
   711  	}
   712  
   713  	return regionValue, nil
   714  }
   715  
   716  func extractUnProtectedLabels(labels map[string]*model.Label, protectedLabelsKeyPattern string) (map[string]*model.Label, error) {
   717  	result := make(map[string]*model.Label)
   718  	for labelKey, label := range labels {
   719  		protected, err := regexp.MatchString(protectedLabelsKeyPattern, labelKey)
   720  		if err != nil {
   721  			return nil, err
   722  		}
   723  		if !protected {
   724  			result[labelKey] = label
   725  		}
   726  	}
   727  	return result, nil
   728  }
   729  
   730  func isLabelModifiable(labelKey, protectedLabelsKeyPattern, immutableLabelsKeyPattern string) (bool, error) {
   731  	protected, err := regexp.MatchString(protectedLabelsKeyPattern, labelKey)
   732  	if err != nil {
   733  		return false, err
   734  	}
   735  	immutable, err := regexp.MatchString(immutableLabelsKeyPattern, labelKey)
   736  	if err != nil {
   737  		return false, err
   738  	}
   739  	return !protected && !immutable, err
   740  }
   741  
   742  func convertLabelValue(value interface{}) (string, error) {
   743  	values, err := label.ValueToStringsSlice(value)
   744  	if err != nil {
   745  		result := str.CastOrEmpty(value)
   746  		if len(result) == 0 {
   747  			return "", errors.New("cannot cast label value: expected []string or string")
   748  		}
   749  		return result, nil
   750  	}
   751  	if len(values) != 1 {
   752  		return "", errors.New("expected single value for label")
   753  	}
   754  	return values[0], nil
   755  }