github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/domain/apptemplate/service.go (about) 1 package apptemplate 2 3 import ( 4 "context" 5 "encoding/json" 6 "fmt" 7 "strings" 8 9 "github.com/kyma-incubator/compass/components/director/internal/domain/tenant" 10 11 "github.com/kyma-incubator/compass/components/director/pkg/resource" 12 13 "github.com/kyma-incubator/compass/components/director/internal/labelfilter" 14 15 "github.com/kyma-incubator/compass/components/director/pkg/str" 16 17 "github.com/kyma-incubator/compass/components/director/pkg/log" 18 19 "github.com/kyma-incubator/compass/components/director/pkg/apperrors" 20 21 "github.com/pkg/errors" 22 23 "github.com/kyma-incubator/compass/components/director/internal/model" 24 ) 25 26 const applicationTypeLabelKey = "applicationType" 27 const otherSystemType = "Other System Type" 28 const providerSAP = "SAP" 29 const labelsKey = "labels" 30 31 // ApplicationTemplateRepository missing godoc 32 // 33 //go:generate mockery --name=ApplicationTemplateRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 34 type ApplicationTemplateRepository interface { 35 Create(ctx context.Context, item model.ApplicationTemplate) error 36 Get(ctx context.Context, id string) (*model.ApplicationTemplate, error) 37 GetByFilters(ctx context.Context, filter []*labelfilter.LabelFilter) (*model.ApplicationTemplate, error) 38 Exists(ctx context.Context, id string) (bool, error) 39 List(ctx context.Context, filter []*labelfilter.LabelFilter, pageSize int, cursor string) (model.ApplicationTemplatePage, error) 40 ListByName(ctx context.Context, id string) ([]*model.ApplicationTemplate, error) 41 ListByFilters(ctx context.Context, filter []*labelfilter.LabelFilter) ([]*model.ApplicationTemplate, error) 42 Update(ctx context.Context, model model.ApplicationTemplate) error 43 Delete(ctx context.Context, id string) error 44 } 45 46 // UIDService missing godoc 47 // 48 //go:generate mockery --name=UIDService --output=automock --outpkg=automock --case=underscore --disable-version-string 49 type UIDService interface { 50 Generate() string 51 } 52 53 // WebhookRepository missing godoc 54 // 55 //go:generate mockery --name=WebhookRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 56 type WebhookRepository interface { 57 CreateMany(ctx context.Context, tenant string, items []*model.Webhook) error 58 DeleteAllByApplicationTemplateID(ctx context.Context, applicationTemplateID string) error 59 } 60 61 // LabelUpsertService missing godoc 62 // 63 //go:generate mockery --name=LabelUpsertService --output=automock --outpkg=automock --case=underscore --disable-version-string 64 type LabelUpsertService interface { 65 UpsertMultipleLabels(ctx context.Context, tenant string, objectType model.LabelableObject, objectID string, labels map[string]interface{}) error 66 UpsertLabelGlobal(ctx context.Context, labelInput *model.LabelInput) error 67 } 68 69 // LabelRepository missing godoc 70 // 71 //go:generate mockery --name=LabelRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 72 type LabelRepository interface { 73 ListForGlobalObject(ctx context.Context, objectType model.LabelableObject, objectID string) (map[string]*model.Label, error) 74 GetByKey(ctx context.Context, tenant string, objectType model.LabelableObject, objectID, key string) (*model.Label, error) 75 } 76 77 // ApplicationRepository missing godoc 78 // 79 //go:generate mockery --name=ApplicationRepository --output=automock --outpkg=automock --case=underscore --disable-version-string 80 type ApplicationRepository interface { 81 ListAllByApplicationTemplateID(ctx context.Context, applicationTemplateID string) ([]*model.Application, error) 82 } 83 84 type service struct { 85 appTemplateRepo ApplicationTemplateRepository 86 webhookRepo WebhookRepository 87 uidService UIDService 88 labelUpsertService LabelUpsertService 89 labelRepo LabelRepository 90 appRepo ApplicationRepository 91 } 92 93 // NewService missing godoc 94 func NewService(appTemplateRepo ApplicationTemplateRepository, webhookRepo WebhookRepository, uidService UIDService, labelUpsertService LabelUpsertService, labelRepo LabelRepository, appRepo ApplicationRepository) *service { 95 return &service{ 96 appTemplateRepo: appTemplateRepo, 97 webhookRepo: webhookRepo, 98 uidService: uidService, 99 labelUpsertService: labelUpsertService, 100 labelRepo: labelRepo, 101 appRepo: appRepo, 102 } 103 } 104 105 // Create missing godoc 106 func (s *service) Create(ctx context.Context, in model.ApplicationTemplateInput) (string, error) { 107 appTemplateID := s.uidService.Generate() 108 if len(str.PtrStrToStr(in.ID)) > 0 { 109 appTemplateID = *in.ID 110 } 111 112 if in.Labels == nil { 113 in.Labels = map[string]interface{}{} 114 } 115 116 log.C(ctx).Debugf("ID %s generated for Application Template with name %s", appTemplateID, in.Name) 117 118 appInputJSON, err := enrichWithApplicationTypeLabel(in.ApplicationInputJSON, in.Name) 119 if err != nil { 120 return "", err 121 } 122 in.ApplicationInputJSON = appInputJSON 123 124 region := in.Labels[tenant.RegionLabelKey] 125 _, err = s.GetByNameAndRegion(ctx, in.Name, region) 126 if err != nil && !apperrors.IsNotFoundError(err) { 127 return "", errors.Wrapf(err, "while checking if application template with name %q and region %v exists", in.Name, region) 128 } 129 if err == nil { 130 return "", fmt.Errorf("application template with name %q and region %v already exists", in.Name, region) 131 } 132 133 appTemplate := in.ToApplicationTemplate(appTemplateID) 134 135 err = s.appTemplateRepo.Create(ctx, appTemplate) 136 if err != nil { 137 return "", errors.Wrapf(err, "while creating Application Template with name %s", in.Name) 138 } 139 140 webhooks := make([]*model.Webhook, 0, len(in.Webhooks)) 141 for _, item := range in.Webhooks { 142 webhooks = append(webhooks, item.ToWebhook(s.uidService.Generate(), appTemplateID, model.ApplicationTemplateWebhookReference)) 143 } 144 if err = s.webhookRepo.CreateMany(ctx, "", webhooks); err != nil { 145 return "", errors.Wrapf(err, "while creating Webhooks for applicationTemplate") 146 } 147 148 err = s.labelUpsertService.UpsertMultipleLabels(ctx, "", model.AppTemplateLabelableObject, appTemplateID, in.Labels) 149 if err != nil { 150 return appTemplateID, errors.Wrapf(err, "while creating multiple labels for Application Template with id %s", appTemplateID) 151 } 152 153 return appTemplateID, nil 154 } 155 156 // CreateWithLabels Creates an AppTemplate with provided labels 157 func (s *service) CreateWithLabels(ctx context.Context, in model.ApplicationTemplateInput, labels map[string]interface{}) (string, error) { 158 for key, val := range labels { 159 in.Labels[key] = val 160 } 161 162 appTemplateID, err := s.Create(ctx, in) 163 if err != nil { 164 return "", errors.Wrapf(err, "while creating Application Template") 165 } 166 167 return appTemplateID, nil 168 } 169 170 // Get missing godoc 171 func (s *service) Get(ctx context.Context, id string) (*model.ApplicationTemplate, error) { 172 appTemplate, err := s.appTemplateRepo.Get(ctx, id) 173 if err != nil { 174 return nil, errors.Wrapf(err, "while getting Application Template with id %s", id) 175 } 176 177 return appTemplate, nil 178 } 179 180 // GetByFilters gets a model.ApplicationTemplate by given slice of labelfilter.LabelFilter 181 func (s *service) GetByFilters(ctx context.Context, filter []*labelfilter.LabelFilter) (*model.ApplicationTemplate, error) { 182 appTemplate, err := s.appTemplateRepo.GetByFilters(ctx, filter) 183 if err != nil { 184 return nil, errors.Wrap(err, "while getting Application Template by filters") 185 } 186 187 return appTemplate, nil 188 } 189 190 // ListByName retrieves all Application Templates by given name 191 func (s *service) ListByName(ctx context.Context, name string) ([]*model.ApplicationTemplate, error) { 192 appTemplates, err := s.appTemplateRepo.ListByName(ctx, name) 193 if err != nil { 194 return nil, errors.Wrapf(err, "while listing application templates with name %q", name) 195 } 196 197 return appTemplates, nil 198 } 199 200 // ListByFilters retrieves all Application Templates by given slice of labelfilter.LabelFilter 201 func (s *service) ListByFilters(ctx context.Context, filter []*labelfilter.LabelFilter) ([]*model.ApplicationTemplate, error) { 202 appTemplates, err := s.appTemplateRepo.ListByFilters(ctx, filter) 203 if err != nil { 204 return nil, errors.Wrap(err, "while listing application templates by filters") 205 } 206 207 return appTemplates, nil 208 } 209 210 // GetByNameAndRegion retrieves Application Template by given name and region 211 func (s *service) GetByNameAndRegion(ctx context.Context, name string, region interface{}) (*model.ApplicationTemplate, error) { 212 appTemplates, err := s.appTemplateRepo.ListByName(ctx, name) 213 if err != nil { 214 return nil, errors.Wrapf(err, "while listing application templates with name %q", name) 215 } 216 217 for _, appTemplate := range appTemplates { 218 appTmplRegion, err := s.retrieveLabel(ctx, appTemplate.ID, tenant.RegionLabelKey) 219 if err != nil && !apperrors.IsNotFoundError(err) { 220 return nil, err 221 } 222 223 if region == appTmplRegion { 224 log.C(ctx).Infof("Found Application Template with name %q and region label %v", name, region) 225 return appTemplate, nil 226 } 227 } 228 229 return nil, apperrors.NewNotFoundErrorWithType(resource.ApplicationTemplate) 230 } 231 232 // ListLabels retrieves all labels for application template 233 func (s *service) ListLabels(ctx context.Context, appTemplateID string) (map[string]*model.Label, error) { 234 appTemplateExists, err := s.appTemplateRepo.Exists(ctx, appTemplateID) 235 if err != nil { 236 return nil, errors.Wrap(err, "while checking Application Template existence") 237 } 238 239 if !appTemplateExists { 240 return nil, fmt.Errorf("application template with ID %s doesn't exist", appTemplateID) 241 } 242 243 labels, err := s.labelRepo.ListForGlobalObject(ctx, model.AppTemplateLabelableObject, appTemplateID) // tenent is not needed for AppTemplateLabelableObject 244 if err != nil { 245 return nil, errors.Wrap(err, "while getting labels for Application Template") 246 } 247 248 return labels, nil 249 } 250 251 // GetLabel gets a given label for application template 252 func (s *service) GetLabel(ctx context.Context, appTemplateID string, key string) (*model.Label, error) { 253 labels, err := s.ListLabels(ctx, appTemplateID) 254 if err != nil { 255 return nil, err 256 } 257 258 label, ok := labels[key] 259 if !ok { 260 return nil, apperrors.NewNotFoundErrorWithMessage(resource.Label, "", fmt.Sprintf("label %s for application template with ID %s doesn't exist", key, appTemplateID)) 261 } 262 263 return label, nil 264 } 265 266 // Exists missing godoc 267 func (s *service) Exists(ctx context.Context, id string) (bool, error) { 268 exist, err := s.appTemplateRepo.Exists(ctx, id) 269 if err != nil { 270 return false, errors.Wrapf(err, "while getting Application Template with ID %s", id) 271 } 272 273 return exist, nil 274 } 275 276 // List missing godoc 277 func (s *service) List(ctx context.Context, filter []*labelfilter.LabelFilter, pageSize int, cursor string) (model.ApplicationTemplatePage, error) { 278 if pageSize < 1 || pageSize > 200 { 279 return model.ApplicationTemplatePage{}, apperrors.NewInvalidDataError("page size must be between 1 and 200") 280 } 281 282 return s.appTemplateRepo.List(ctx, filter, pageSize, cursor) 283 } 284 285 // Update missing godoc 286 func (s *service) Update(ctx context.Context, id string, in model.ApplicationTemplateUpdateInput) error { 287 oldAppTemplate, err := s.Get(ctx, id) 288 if err != nil { 289 return err 290 } 291 292 region, err := s.retrieveLabel(ctx, id, tenant.RegionLabelKey) 293 if err != nil && !apperrors.IsNotFoundError(err) { 294 return err 295 } 296 297 appInputJSON, err := enrichWithApplicationTypeLabel(in.ApplicationInputJSON, in.Name) 298 if err != nil { 299 return err 300 } 301 in.ApplicationInputJSON = appInputJSON 302 303 if oldAppTemplate.Name != in.Name { 304 _, err := s.GetByNameAndRegion(ctx, in.Name, region) 305 if err != nil && !apperrors.IsNotFoundError(err) { 306 return errors.Wrapf(err, "while checking if application template with name %q and region %v exists", in.Name, region) 307 } 308 if err == nil { 309 return fmt.Errorf("application template with name %q and region %v already exists", in.Name, region) 310 } 311 } 312 313 appTemplate := in.ToApplicationTemplate(id) 314 err = s.appTemplateRepo.Update(ctx, appTemplate) 315 if err != nil { 316 return errors.Wrapf(err, "while updating Application Template with ID %s", id) 317 } 318 319 if err = s.webhookRepo.DeleteAllByApplicationTemplateID(ctx, appTemplate.ID); err != nil { 320 return errors.Wrapf(err, "while deleting Webhooks for applicationTemplate") 321 } 322 323 webhooks := make([]*model.Webhook, 0, len(in.Webhooks)) 324 for _, item := range in.Webhooks { 325 webhooks = append(webhooks, item.ToWebhook(s.uidService.Generate(), appTemplate.ID, model.ApplicationTemplateWebhookReference)) 326 } 327 if err = s.webhookRepo.CreateMany(ctx, "", webhooks); err != nil { 328 return errors.Wrapf(err, "while creating Webhooks for applicationTemplate") 329 } 330 331 err = s.labelUpsertService.UpsertMultipleLabels(ctx, "", model.AppTemplateLabelableObject, id, in.Labels) 332 if err != nil { 333 return errors.Wrapf(err, "while upserting labels for Application Template with id %s", id) 334 } 335 336 if oldAppTemplate.Name != appTemplate.Name { 337 log.C(ctx).Infof("Listing applications registered from application template with id %s", id) 338 appsByAppTemplate, err := s.appRepo.ListAllByApplicationTemplateID(ctx, id) 339 if err != nil { 340 return errors.Wrapf(err, "while listing applications for app template with id %s", id) 341 } 342 343 for _, app := range appsByAppTemplate { 344 log.C(ctx).Infof("Updating %s label for application with id %s", applicationTypeLabelKey, app.ID) 345 err = s.labelUpsertService.UpsertLabelGlobal(ctx, &model.LabelInput{ 346 Key: applicationTypeLabelKey, 347 Value: appTemplate.Name, 348 ObjectID: app.ID, 349 ObjectType: model.ApplicationLabelableObject, 350 }) 351 if err != nil { 352 return errors.Wrapf(err, "while updating %s label of application with id %s", applicationTypeLabelKey, app.ID) 353 } 354 } 355 } 356 357 return nil 358 } 359 360 // Delete missing godoc 361 func (s *service) Delete(ctx context.Context, id string) error { 362 err := s.appTemplateRepo.Delete(ctx, id) 363 if err != nil { 364 return errors.Wrapf(err, "while deleting Application Template with ID %s", id) 365 } 366 367 return nil 368 } 369 370 // PrepareApplicationCreateInputJSON missing godoc 371 func (s *service) PrepareApplicationCreateInputJSON(appTemplate *model.ApplicationTemplate, values model.ApplicationFromTemplateInputValues) (string, error) { 372 appCreateInputJSON := appTemplate.ApplicationInputJSON 373 for _, placeholder := range appTemplate.Placeholders { 374 newValue, err := values.FindPlaceholderValue(placeholder.Name) 375 isOptional := false 376 if placeholder.Optional != nil { 377 isOptional = *placeholder.Optional 378 } 379 380 if err != nil && !isOptional { 381 return "", errors.Wrap(err, "required placeholder not provided") 382 } 383 384 err = validatePlaceholderValue(placeholder, newValue) 385 if err != nil { 386 return "", errors.Wrap(err, "value of placeholder is invalid") 387 } 388 389 appCreateInputJSON = strings.ReplaceAll(appCreateInputJSON, fmt.Sprintf("{{%s}}", placeholder.Name), newValue) 390 appCreateInputJSON, err = removeEmptyKeyFromLabels(appCreateInputJSON, placeholder.Name) 391 if err != nil { 392 return "", errors.Wrap(err, "error while clear optional empty value") 393 } 394 } 395 return appCreateInputJSON, nil 396 } 397 398 func removeEmptyKeyFromLabels(stringInput string, keyName string) (string, error) { 399 var objMap map[string]interface{} 400 err := json.Unmarshal([]byte(stringInput), &objMap) 401 if err != nil { 402 return "", errors.Wrap(err, "error while unmarshal input") 403 } 404 processMap(&objMap, keyName, true) 405 406 output, err := json.Marshal(objMap) 407 if err != nil { 408 return "", errors.Wrap(err, "error while marshal output") 409 } 410 return string(output), nil 411 } 412 413 func processMap(input *map[string]interface{}, keyName string, rootObject bool) { 414 for key, value := range *input { 415 if _, ok := value.(string); ok { 416 // String value 417 if value == "" && key == keyName { 418 if !rootObject { 419 delete(*input, key) 420 } 421 } 422 } else if mapValue, ok := value.(map[string]interface{}); ok { 423 // Object value - process only labels object 424 if key == labelsKey { 425 processMap(&mapValue, keyName, false) 426 } 427 } 428 } 429 } 430 431 func (s *service) retrieveLabel(ctx context.Context, id string, labelKey string) (interface{}, error) { 432 label, err := s.labelRepo.GetByKey(ctx, "", model.AppTemplateLabelableObject, id, labelKey) 433 if err != nil { 434 return nil, err 435 } 436 return label.Value, nil 437 } 438 439 func validatePlaceholderValue(placeholder model.ApplicationTemplatePlaceholder, value string) error { 440 if placeholder.Name == "provider" { 441 valueRemovedWhitespaces := strings.Fields(value) 442 443 for _, i := range valueRemovedWhitespaces { 444 if i == providerSAP { 445 return errors.New("provider cannot contain \"SAP\"") 446 } 447 } 448 } 449 450 if placeholder.Name == "application-type" { 451 currentValue := value 452 if len(value) >= 4 { 453 firstFour := value[:4] 454 currentValue = strings.Trim(firstFour, " \t\n") 455 } 456 457 if currentValue == providerSAP { 458 return errors.New("your application type cannot start with \"SAP\"") 459 } 460 } 461 462 return nil 463 } 464 465 func enrichWithApplicationTypeLabel(applicationInputJSON, applicationType string) (string, error) { 466 var appInput map[string]interface{} 467 468 if err := json.Unmarshal([]byte(applicationInputJSON), &appInput); err != nil { 469 return "", errors.Wrapf(err, "while unmarshaling application input json") 470 } 471 472 labels, ok := appInput[labelsKey] 473 if ok && labels != nil { 474 labelsMap, ok := labels.(map[string]interface{}) 475 if !ok { 476 return "", fmt.Errorf("app input json labels are type %T instead of map[string]interface{}. %v", labelsMap, labels) 477 } 478 479 if appType, ok := labelsMap[applicationTypeLabelKey]; ok { 480 appTypeValue, ok := appType.(string) 481 if !ok { 482 return "", fmt.Errorf("%q label value must be string", applicationTypeLabelKey) 483 } 484 if applicationType != otherSystemType && appTypeValue != applicationType { 485 return "", fmt.Errorf("%q label value does not match the application template name", applicationTypeLabelKey) 486 } 487 return applicationInputJSON, nil 488 } 489 490 labelsMap[applicationTypeLabelKey] = applicationType 491 appInput[labelsKey] = labelsMap 492 } else { 493 appInput[labelsKey] = map[string]interface{}{applicationTypeLabelKey: applicationType} 494 } 495 496 inputJSON, err := json.Marshal(appInput) 497 if err != nil { 498 return "", errors.Wrapf(err, "while marshalling app input") 499 } 500 return string(inputJSON), nil 501 }