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  }