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

     1  package systemfetcher
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"os"
     8  	"path/filepath"
     9  	"reflect"
    10  	"strings"
    11  
    12  	"github.com/kyma-incubator/compass/components/director/pkg/str"
    13  
    14  	"github.com/kyma-incubator/compass/components/director/internal/domain/tenant"
    15  	"github.com/kyma-incubator/compass/components/director/internal/model"
    16  	"github.com/kyma-incubator/compass/components/director/pkg/apperrors"
    17  	"github.com/kyma-incubator/compass/components/director/pkg/log"
    18  	"github.com/kyma-incubator/compass/components/director/pkg/persistence"
    19  	"github.com/pkg/errors"
    20  )
    21  
    22  const (
    23  	applicationTemplatesDirectoryPath   = "/data/templates/"
    24  	integrationSystemJSONKey            = "intSystem"
    25  	integrationSystemNameJSONKey        = "name"
    26  	integrationSystemDescriptionJSONKey = "description"
    27  	managedAppProvisioningLabelKey      = "managed_app_provisioning"
    28  	integrationSystemIDLabelKey         = "integrationSystemID"
    29  )
    30  
    31  //go:generate mockery --name=appTmplService --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string
    32  type appTmplService interface {
    33  	GetByNameAndRegion(ctx context.Context, name string, region interface{}) (*model.ApplicationTemplate, error)
    34  	Create(ctx context.Context, in model.ApplicationTemplateInput) (string, error)
    35  	Update(ctx context.Context, id string, in model.ApplicationTemplateUpdateInput) error
    36  }
    37  
    38  //go:generate mockery --name=intSysSvc --output=automock --outpkg=automock --case=underscore --exported=true --disable-version-string
    39  type intSysSvc interface {
    40  	Create(ctx context.Context, in model.IntegrationSystemInput) (string, error)
    41  	List(ctx context.Context, pageSize int, cursor string) (model.IntegrationSystemPage, error)
    42  }
    43  
    44  // DataLoader loads and creates all the necessary data needed by system-fetcher
    45  type DataLoader struct {
    46  	transaction persistence.Transactioner
    47  	appTmplSvc  appTmplService
    48  	intSysSvc   intSysSvc
    49  	cfg         Config
    50  }
    51  
    52  // NewDataLoader creates new DataLoader
    53  func NewDataLoader(tx persistence.Transactioner, cfg Config, appTmplSvc appTmplService, intSysSvc intSysSvc) *DataLoader {
    54  	return &DataLoader{
    55  		transaction: tx,
    56  		appTmplSvc:  appTmplSvc,
    57  		intSysSvc:   intSysSvc,
    58  		cfg:         cfg,
    59  	}
    60  }
    61  
    62  // LoadData loads and creates all the necessary data needed by system-fetcher
    63  func (d *DataLoader) LoadData(ctx context.Context, readDir func(dirname string) ([]os.DirEntry, error), readFile func(filename string) ([]byte, error)) error {
    64  	appTemplateInputsMap, err := d.loadAppTemplates(ctx, readDir, readFile)
    65  	if err != nil {
    66  		return errors.Wrap(err, "failed while loading application templates")
    67  	}
    68  
    69  	tx, err := d.transaction.Begin()
    70  	if err != nil {
    71  		return errors.Wrap(err, "Error while beginning transaction")
    72  	}
    73  	defer d.transaction.RollbackUnlessCommitted(ctx, tx)
    74  	ctxWithTx := persistence.SaveToContext(ctx, tx)
    75  
    76  	appTemplateInputs, err := d.createAppTemplatesDependentEntities(ctxWithTx, appTemplateInputsMap)
    77  	if err != nil {
    78  		return errors.Wrap(err, "failed while creating application templates dependent entities")
    79  	}
    80  
    81  	if err = d.upsertAppTemplates(ctxWithTx, appTemplateInputs); err != nil {
    82  		return errors.Wrap(err, "failed while upserting application templates")
    83  	}
    84  
    85  	if err := tx.Commit(); err != nil {
    86  		return errors.Wrap(err, "while committing transaction")
    87  	}
    88  
    89  	return nil
    90  }
    91  
    92  func (d *DataLoader) loadAppTemplates(ctx context.Context, readDir func(dirname string) ([]os.DirEntry, error), readFile func(filename string) ([]byte, error)) ([]map[string]interface{}, error) {
    93  	appTemplatesFileLocation := applicationTemplatesDirectoryPath
    94  	if len(d.cfg.TemplatesFileLocation) > 0 {
    95  		appTemplatesFileLocation = d.cfg.TemplatesFileLocation
    96  	}
    97  
    98  	files, err := readDir(appTemplatesFileLocation)
    99  	if err != nil {
   100  		return nil, errors.Wrapf(err, "while reading directory with application templates files [%s]", appTemplatesFileLocation)
   101  	}
   102  
   103  	var appTemplateInputs []map[string]interface{}
   104  	for _, f := range files {
   105  		log.C(ctx).Infof("Loading application templates from file: %s", f.Name())
   106  
   107  		if filepath.Ext(f.Name()) != ".json" {
   108  			return nil, apperrors.NewInternalError(fmt.Sprintf("unsupported file format %q, supported format: json", filepath.Ext(f.Name())))
   109  		}
   110  
   111  		bytes, err := readFile(appTemplatesFileLocation + f.Name())
   112  		if err != nil {
   113  			return nil, errors.Wrapf(err, "while reading application templates file %q", appTemplatesFileLocation+f.Name())
   114  		}
   115  
   116  		var templatesFromFile []map[string]interface{}
   117  		if err := json.Unmarshal(bytes, &templatesFromFile); err != nil {
   118  			return nil, errors.Wrapf(err, "while unmarshalling application templates from file %s", appTemplatesFileLocation+f.Name())
   119  		}
   120  		log.C(ctx).Infof("Successfully loaded application templates from file: %s", f.Name())
   121  		appTemplateInputs = append(appTemplateInputs, templatesFromFile...)
   122  	}
   123  
   124  	return appTemplateInputs, nil
   125  }
   126  
   127  func (d *DataLoader) createAppTemplatesDependentEntities(ctx context.Context, appTmplInputs []map[string]interface{}) ([]model.ApplicationTemplateInput, error) {
   128  	appTemplateInputs := make([]model.ApplicationTemplateInput, 0, len(appTmplInputs))
   129  	for _, appTmplInput := range appTmplInputs {
   130  		var input model.ApplicationTemplateInput
   131  		appTmplInputJSON, err := json.Marshal(appTmplInput)
   132  		if err != nil {
   133  			return nil, errors.Wrap(err, "while marshaling application template input")
   134  		}
   135  
   136  		if err = json.Unmarshal(appTmplInputJSON, &input); err != nil {
   137  			return nil, errors.Wrap(err, "while unmarshalling application template input")
   138  		}
   139  
   140  		intSystem, ok := appTmplInput[integrationSystemJSONKey]
   141  		if ok {
   142  			intSystemData, ok := intSystem.(map[string]interface{})
   143  			if !ok {
   144  				return nil, fmt.Errorf("the type of the integration system is %T instead of map[string]interface{}. %v", intSystem, intSystemData)
   145  			}
   146  
   147  			appTmplIntSystem, err := extractIntegrationSystem(intSystemData)
   148  			if err != nil {
   149  				return nil, err
   150  			}
   151  
   152  			intSystemsFromDB, err := d.listIntegrationSystems(ctx)
   153  			if err != nil {
   154  				return nil, err
   155  			}
   156  
   157  			var intSysID string
   158  			for _, is := range intSystemsFromDB {
   159  				if is.Name == appTmplIntSystem.Name && str.PtrStrToStr(is.Description) == str.PtrStrToStr(appTmplIntSystem.Description) {
   160  					intSysID = is.ID
   161  					break
   162  				}
   163  			}
   164  			if intSysID == "" {
   165  				id, err := d.intSysSvc.Create(ctx, *appTmplIntSystem)
   166  				if err != nil {
   167  					return nil, errors.Wrapf(err, "while creating integration system with name %s", appTmplIntSystem.Name)
   168  				}
   169  				intSysID = id
   170  				log.C(ctx).Infof("Successfully created integration system with id: %s", intSysID)
   171  			}
   172  			input.ApplicationInputJSON, err = enrichWithIntegrationSystemIDLabel(input.ApplicationInputJSON, intSysID)
   173  			if err != nil {
   174  				return nil, err
   175  			}
   176  		}
   177  		appTemplateInputs = append(appTemplateInputs, input)
   178  	}
   179  
   180  	return enrichApplicationTemplateInput(appTemplateInputs), nil
   181  }
   182  
   183  func (d *DataLoader) listIntegrationSystems(ctx context.Context) ([]*model.IntegrationSystem, error) {
   184  	pageSize := 200
   185  	pageCursor := ""
   186  	hasNextPage := true
   187  
   188  	var integrationSystems []*model.IntegrationSystem
   189  	for hasNextPage {
   190  		intSystemsPage, err := d.intSysSvc.List(ctx, pageSize, pageCursor)
   191  		if err != nil {
   192  			return nil, errors.Wrapf(err, "while listing integration systems")
   193  		}
   194  
   195  		integrationSystems = append(integrationSystems, intSystemsPage.Data...)
   196  
   197  		pageCursor = intSystemsPage.PageInfo.EndCursor
   198  		hasNextPage = intSystemsPage.PageInfo.HasNextPage
   199  	}
   200  
   201  	return integrationSystems, nil
   202  }
   203  
   204  func (d *DataLoader) upsertAppTemplates(ctx context.Context, appTemplateInputs []model.ApplicationTemplateInput) error {
   205  	for _, appTmplInput := range appTemplateInputs {
   206  		var region interface{}
   207  		region, err := retrieveRegion(appTmplInput.Labels)
   208  		if err != nil {
   209  			return err
   210  		}
   211  		if region == "" {
   212  			region = nil
   213  		}
   214  
   215  		log.C(ctx).Infof("Retrieving application template with name %q and region %s", appTmplInput.Name, region)
   216  		appTemplate, err := d.appTmplSvc.GetByNameAndRegion(ctx, appTmplInput.Name, region)
   217  		if err != nil {
   218  			if !strings.Contains(err.Error(), "Object not found") {
   219  				return errors.Wrapf(err, "error while getting application template with name %q and region %s", appTmplInput.Name, region)
   220  			}
   221  
   222  			log.C(ctx).Infof("Cannot find application template with name %q and region %s. Creation triggered...", appTmplInput.Name, region)
   223  			templateID, err := d.appTmplSvc.Create(ctx, appTmplInput)
   224  			if err != nil {
   225  				return errors.Wrapf(err, "error while creating application template with name %q and region %s", appTmplInput.Name, region)
   226  			}
   227  			log.C(ctx).Infof("Successfully created application template with id: %q", templateID)
   228  			continue
   229  		}
   230  
   231  		if !areAppTemplatesEqual(appTemplate, appTmplInput) {
   232  			log.C(ctx).Infof("Updating application template with id %q", appTemplate.ID)
   233  			appTemplateUpdateInput := model.ApplicationTemplateUpdateInput{
   234  				Name:                 appTmplInput.Name,
   235  				Description:          appTmplInput.Description,
   236  				ApplicationNamespace: appTmplInput.ApplicationNamespace,
   237  				ApplicationInputJSON: appTmplInput.ApplicationInputJSON,
   238  				Placeholders:         appTmplInput.Placeholders,
   239  				AccessLevel:          appTmplInput.AccessLevel,
   240  				Labels:               appTmplInput.Labels,
   241  				Webhooks:             appTmplInput.Webhooks,
   242  			}
   243  			if err := d.appTmplSvc.Update(ctx, appTemplate.ID, appTemplateUpdateInput); err != nil {
   244  				return errors.Wrapf(err, "while updating application template with id %q", appTemplate.ID)
   245  			}
   246  			log.C(ctx).Infof("Successfully updated application template with id %q", appTemplate.ID)
   247  		}
   248  	}
   249  
   250  	return nil
   251  }
   252  
   253  func enrichApplicationTemplateInput(appTemplateInputs []model.ApplicationTemplateInput) []model.ApplicationTemplateInput {
   254  	enriched := make([]model.ApplicationTemplateInput, 0, len(appTemplateInputs))
   255  	for _, appTemplateInput := range appTemplateInputs {
   256  		if appTemplateInput.Description == nil {
   257  			appTemplateInput.Description = str.Ptr(appTemplateInput.Name)
   258  		}
   259  
   260  		if appTemplateInput.Placeholders == nil || len(appTemplateInput.Placeholders) == 0 {
   261  			appTemplateInput.Placeholders = []model.ApplicationTemplatePlaceholder{
   262  				{
   263  					Name:        "name",
   264  					Description: str.Ptr("Application’s technical name"),
   265  					JSONPath:    str.Ptr("$.displayName"),
   266  				},
   267  				{
   268  					Name:        "display-name",
   269  					Description: str.Ptr("Application’s display name"),
   270  					JSONPath:    str.Ptr("$.displayName"),
   271  				},
   272  			}
   273  		}
   274  
   275  		if appTemplateInput.AccessLevel == "" {
   276  			appTemplateInput.AccessLevel = model.GlobalApplicationTemplateAccessLevel
   277  		}
   278  
   279  		if appTemplateInput.Labels == nil {
   280  			appTemplateInput.Labels = map[string]interface{}{managedAppProvisioningLabelKey: false}
   281  		}
   282  		enriched = append(enriched, appTemplateInput)
   283  	}
   284  	return enriched
   285  }
   286  
   287  func enrichWithIntegrationSystemIDLabel(applicationInputJSON, intSystemID string) (string, error) {
   288  	var appInput map[string]interface{}
   289  
   290  	if err := json.Unmarshal([]byte(applicationInputJSON), &appInput); err != nil {
   291  		return "", errors.Wrapf(err, "while unmarshaling application input json")
   292  	}
   293  
   294  	appInput[integrationSystemIDLabelKey] = intSystemID
   295  
   296  	inputJSON, err := json.Marshal(appInput)
   297  	if err != nil {
   298  		return "", errors.Wrapf(err, "while marshalling app input")
   299  	}
   300  	return string(inputJSON), nil
   301  }
   302  
   303  func extractIntegrationSystem(intSysMap map[string]interface{}) (*model.IntegrationSystemInput, error) {
   304  	intSysName, ok := intSysMap[integrationSystemNameJSONKey]
   305  	if !ok {
   306  		return nil, fmt.Errorf("integration system name is missing")
   307  	}
   308  	intSysNameValue, ok := intSysName.(string)
   309  	if !ok {
   310  		return nil, fmt.Errorf("integration system name value must be string")
   311  	}
   312  
   313  	intSysDesc, ok := intSysMap[integrationSystemDescriptionJSONKey]
   314  	if !ok {
   315  		return nil, fmt.Errorf("integration system description is missing")
   316  	}
   317  	intSysDescValue, ok := intSysDesc.(string)
   318  	if !ok {
   319  		return nil, fmt.Errorf("integration system description value must be string")
   320  	}
   321  
   322  	return &model.IntegrationSystemInput{
   323  		Name:        intSysNameValue,
   324  		Description: str.Ptr(intSysDescValue),
   325  	}, nil
   326  }
   327  
   328  func retrieveRegion(labels map[string]interface{}) (string, error) {
   329  	if labels == nil {
   330  		return "", nil
   331  	}
   332  
   333  	region, exists := labels[tenant.RegionLabelKey]
   334  	if !exists {
   335  		return "", nil
   336  	}
   337  
   338  	regionValue, ok := region.(string)
   339  	if !ok {
   340  		return "", fmt.Errorf("%q label value must be string", tenant.RegionLabelKey)
   341  	}
   342  	return regionValue, nil
   343  }
   344  
   345  func areAppTemplatesEqual(appTemplate *model.ApplicationTemplate, appTemplateInput model.ApplicationTemplateInput) bool {
   346  	if appTemplate == nil {
   347  		return false
   348  	}
   349  
   350  	isAppInputJSONEqual := appTemplate.ApplicationInputJSON == appTemplateInput.ApplicationInputJSON
   351  	isLabelEqual := reflect.DeepEqual(appTemplate.Labels, appTemplateInput.Labels)
   352  	isPlaceholderEqual := reflect.DeepEqual(appTemplate.Placeholders, appTemplateInput.Placeholders)
   353  	areWebhooksEq := areWebhooksEqual(appTemplate.Webhooks, appTemplateInput.Webhooks)
   354  
   355  	if isAppInputJSONEqual && isLabelEqual && isPlaceholderEqual && areWebhooksEq {
   356  		return true
   357  	}
   358  
   359  	return false
   360  }
   361  
   362  func areWebhooksEqual(webhooksModel []model.Webhook, webhooksInput []*model.WebhookInput) bool {
   363  	if len(webhooksModel) != len(webhooksInput) {
   364  		return false
   365  	}
   366  	foundWebhooksCounter := 0
   367  	for _, whModel := range webhooksModel {
   368  		for _, whInput := range webhooksInput {
   369  			if reflect.DeepEqual(whModel, *whInput.ToWebhook(whModel.ID, whModel.ObjectID, whModel.ObjectType)) {
   370  				foundWebhooksCounter++
   371  				break
   372  			}
   373  		}
   374  	}
   375  	return foundWebhooksCounter == len(webhooksModel)
   376  }