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 }