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 }