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

     1  package labeldef
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/kyma-incubator/compass/components/director/internal/model"
    10  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    11  	"github.com/kyma-incubator/compass/components/director/pkg/jsonschema"
    12  	"github.com/pkg/errors"
    13  )
    14  
    15  // Repository missing godoc
    16  //
    17  //go:generate mockery --name=Repository --output=automock --outpkg=automock --case=underscore --disable-version-string
    18  type Repository interface {
    19  	Create(ctx context.Context, def model.LabelDefinition) error
    20  	Upsert(ctx context.Context, def model.LabelDefinition) error
    21  	GetByKey(ctx context.Context, tenant string, key string) (*model.LabelDefinition, error)
    22  	Update(ctx context.Context, def model.LabelDefinition) error
    23  	Exists(ctx context.Context, tenant string, key string) (bool, error)
    24  	List(ctx context.Context, tenant string) ([]model.LabelDefinition, error)
    25  }
    26  
    27  // ScenarioAssignmentLister missing godoc
    28  //
    29  //go:generate mockery --name=ScenarioAssignmentLister --output=automock --outpkg=automock --case=underscore --disable-version-string
    30  type ScenarioAssignmentLister interface {
    31  	List(ctx context.Context, tenant string, pageSize int, cursor string) (*model.AutomaticScenarioAssignmentPage, error)
    32  }
    33  
    34  // LabelRepository missing godoc
    35  //
    36  //go:generate mockery --name=LabelRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    37  type LabelRepository interface {
    38  	GetByKey(ctx context.Context, tenant string, objectType model.LabelableObject, objectID, key string) (*model.Label, error)
    39  	ListForObject(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error)
    40  	ListByKey(ctx context.Context, tenant, key string) ([]*model.Label, error)
    41  	Delete(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string, key string) error
    42  	DeleteAll(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string) error
    43  	DeleteByKey(ctx context.Context, tenant string, key string) error
    44  }
    45  
    46  // TenantRepository missing godoc
    47  //
    48  //go:generate mockery --name=TenantRepository --output=automock --outpkg=automock --case=underscore --disable-version-string
    49  type TenantRepository interface {
    50  	Get(ctx context.Context, id string) (*model.BusinessTenantMapping, error)
    51  }
    52  
    53  // UIDService missing godoc
    54  //
    55  //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore --disable-version-string
    56  type UIDService interface {
    57  	Generate() string
    58  }
    59  
    60  type service struct {
    61  	repo                     Repository
    62  	labelRepo                LabelRepository
    63  	scenarioAssignmentLister ScenarioAssignmentLister
    64  	tenantRepo               TenantRepository
    65  	uidService               UIDService
    66  }
    67  
    68  // NewService creates new label definition service
    69  func NewService(repo Repository, labelRepo LabelRepository, scenarioAssignmentLister ScenarioAssignmentLister, tenantRepo TenantRepository, uidService UIDService) *service {
    70  	return &service{
    71  		repo:                     repo,
    72  		labelRepo:                labelRepo,
    73  		scenarioAssignmentLister: scenarioAssignmentLister,
    74  		tenantRepo:               tenantRepo,
    75  		uidService:               uidService,
    76  	}
    77  }
    78  
    79  // CreateWithFormations creates label definition with the provided formations
    80  func (s *service) CreateWithFormations(ctx context.Context, tnt string, formations []string) error {
    81  	schema, err := NewSchemaForFormations(formations)
    82  	if err != nil {
    83  		return errors.Wrapf(err, "while creaing new schema for key %s", model.ScenariosKey)
    84  	}
    85  
    86  	return s.repo.Create(ctx, model.LabelDefinition{
    87  		ID:      s.uidService.Generate(),
    88  		Tenant:  tnt,
    89  		Key:     model.ScenariosKey,
    90  		Schema:  &schema,
    91  		Version: 0,
    92  	})
    93  }
    94  
    95  // Get returns the tenant scoped label definition with the provided key
    96  func (s *service) Get(ctx context.Context, tenant string, key string) (*model.LabelDefinition, error) {
    97  	def, err := s.repo.GetByKey(ctx, tenant, key)
    98  	if err != nil {
    99  		return nil, errors.Wrap(err, "while fetching Label Definition")
   100  	}
   101  	return def, nil
   102  }
   103  
   104  // List returns all label definitions for the provided tenant
   105  func (s *service) List(ctx context.Context, tenant string) ([]model.LabelDefinition, error) {
   106  	defs, err := s.repo.List(ctx, tenant)
   107  	if err != nil {
   108  		return nil, errors.Wrap(err, "while fetching Label Definitions")
   109  	}
   110  	return defs, nil
   111  }
   112  
   113  // GetAvailableScenarios returns available scenarios based on scenario label definition
   114  func (s *service) GetAvailableScenarios(ctx context.Context, tenantID string) ([]string, error) {
   115  	def, err := s.repo.GetByKey(ctx, tenantID, model.ScenariosKey)
   116  	if err != nil {
   117  		return nil, errors.Wrapf(err, "while getting `%s` label definition", model.ScenariosKey)
   118  	}
   119  	if def.Schema == nil {
   120  		return nil, fmt.Errorf("missing schema for `%s` label definition", model.ScenariosKey)
   121  	}
   122  
   123  	return ParseFormationsFromSchema(def.Schema)
   124  }
   125  
   126  // ValidateExistingLabelsAgainstSchema validates the existing labels based on the provided schema
   127  func (s *service) ValidateExistingLabelsAgainstSchema(ctx context.Context, schema interface{}, tenant, key string) error {
   128  	existingLabels, err := s.labelRepo.ListByKey(ctx, tenant, key)
   129  	if err != nil {
   130  		return errors.Wrap(err, "while listing labels by key")
   131  	}
   132  
   133  	formationNames, err := ParseFormationsFromSchema(&schema)
   134  	if err != nil {
   135  		return err
   136  	}
   137  
   138  	if len(formationNames) == 0 && len(existingLabels) != 0 {
   139  		labelInfo := make([]string, 0, len(existingLabels))
   140  		for _, label := range existingLabels {
   141  			labelInfo = append(labelInfo, fmt.Sprintf("key=%q and value=%q", label.Key, label.Value))
   142  		}
   143  		return apperrors.NewInvalidDataError(fmt.Sprintf(`labels: %s are not valid against empty schema`, strings.Join(labelInfo, ",")))
   144  	}
   145  
   146  	validator, err := jsonschema.NewValidatorFromRawSchema(schema)
   147  	if err != nil {
   148  		return errors.Wrap(err, "while creating validator for new schema")
   149  	}
   150  
   151  	for _, label := range existingLabels {
   152  		result, err := validator.ValidateRaw(label.Value)
   153  		if err != nil {
   154  			return errors.Wrap(err, "while validating existing labels against new schema")
   155  		}
   156  
   157  		if !result.Valid {
   158  			return apperrors.NewInvalidDataError(fmt.Sprintf(`label with key="%s" and value="%s" is not valid against new schema for %s with ID="%s": %s`, label.Key, label.Value, label.ObjectType, label.ObjectID, result.Error))
   159  		}
   160  	}
   161  	return nil
   162  }
   163  
   164  // ValidateAutomaticScenarioAssignmentAgainstSchema validates the existing scenario assignments based on the provided schema
   165  func (s *service) ValidateAutomaticScenarioAssignmentAgainstSchema(ctx context.Context, schema interface{}, tenantID, key string) error {
   166  	if key != model.ScenariosKey {
   167  		return nil
   168  	}
   169  
   170  	validator, err := jsonschema.NewValidatorFromRawSchema(schema)
   171  	if err != nil {
   172  		return errors.Wrap(err, "while creating validator for a new schema")
   173  	}
   174  	inUse, err := s.fetchScenariosFromAssignments(ctx, tenantID)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	for _, used := range inUse {
   179  		res, err := validator.ValidateRaw([]interface{}{used})
   180  		if err != nil {
   181  			return errors.Wrapf(err, "while validating scenario assignment [scenario=%s] with a new schema", used)
   182  		}
   183  		if res.Error != nil {
   184  			return errors.Wrapf(res.Error, "Scenario Assignment [scenario=%s] is not valid against a new schema", used)
   185  		}
   186  	}
   187  	return nil
   188  }
   189  
   190  // NewSchemaForFormations returns new scenario schema with the provided formations
   191  func NewSchemaForFormations(formations []string) (interface{}, error) {
   192  	newSchema := model.NewScenariosSchema([]string{})
   193  	items, ok := newSchema["items"]
   194  	if !ok {
   195  		return nil, fmt.Errorf("mandatory property items is missing")
   196  	}
   197  	itemsMap, ok := items.(map[string]interface{})
   198  	if !ok {
   199  		return nil, fmt.Errorf("items property could not be converted")
   200  	}
   201  
   202  	itemsMap["enum"] = formations
   203  	return newSchema, nil
   204  }
   205  
   206  type formation struct {
   207  	Items struct {
   208  		Enum []string
   209  	}
   210  }
   211  
   212  // ParseFormationsFromSchema returns available scenarios from the provided schema
   213  func ParseFormationsFromSchema(schema *interface{}) ([]string, error) {
   214  	b, err := json.Marshal(schema)
   215  	if err != nil {
   216  		return nil, errors.Wrapf(err, "while marshaling schema")
   217  	}
   218  	f := formation{}
   219  	if err = json.Unmarshal(b, &f); err != nil {
   220  		return nil, errors.Wrapf(err, "while unmarshaling schema to %T", f)
   221  	}
   222  	return f.Items.Enum, nil
   223  }
   224  
   225  func (s *service) fetchScenariosFromAssignments(ctx context.Context, tenantID string) ([]string, error) {
   226  	m := make(map[string]struct{})
   227  	pageSize := 100
   228  	cursor := ""
   229  	for {
   230  		page, err := s.scenarioAssignmentLister.List(ctx, tenantID, pageSize, cursor)
   231  		if err != nil {
   232  			return nil, errors.Wrapf(err, "while getting page of Automatic Scenario Assignments")
   233  		}
   234  		for _, a := range page.Data {
   235  			m[a.ScenarioName] = struct{}{}
   236  		}
   237  		if !page.PageInfo.HasNextPage {
   238  			break
   239  		}
   240  		cursor = page.PageInfo.EndCursor
   241  	}
   242  
   243  	out := make([]string, 0, len(m))
   244  	for k := range m {
   245  		out = append(out, k)
   246  	}
   247  	return out, nil
   248  }