github.com/kyma-incubator/compass/components/director@v0.0.0-20230623144113-d764f56ff805/internal/open_resource_discovery/ord_document.go (about) 1 package ord 2 3 import ( 4 "encoding/json" 5 "net/url" 6 "path" 7 "regexp" 8 9 "github.com/imdario/mergo" 10 11 "github.com/hashicorp/go-multierror" 12 13 "github.com/kyma-incubator/compass/components/director/pkg/accessstrategy" 14 15 validation "github.com/go-ozzo/ozzo-validation/v4" 16 17 "github.com/kyma-incubator/compass/components/director/internal/model" 18 "github.com/pkg/errors" 19 "github.com/tidwall/gjson" 20 "github.com/tidwall/sjson" 21 ) 22 23 // WellKnownEndpoint is the single entry point for the discovery. 24 const WellKnownEndpoint = "/.well-known/open-resource-discovery" 25 26 // DocumentPerspective represents the perspective of the document 27 type DocumentPerspective string 28 29 const ( 30 // SystemVersionPerspective represents a dynamic document 31 SystemVersionPerspective DocumentPerspective = "system-version" 32 // SystemInstancePerspective represents a static document 33 SystemInstancePerspective DocumentPerspective = "system-instance" 34 ) 35 36 // WellKnownConfig represents the whole config object 37 type WellKnownConfig struct { 38 Schema string `json:"$schema"` 39 BaseURL string `json:"baseUrl"` 40 OpenResourceDiscoveryV1 OpenResourceDiscoveryV1 `json:"openResourceDiscoveryV1"` 41 } 42 43 // OpenResourceDiscoveryV1 contains all Documents' details 44 type OpenResourceDiscoveryV1 struct { 45 Documents []DocumentDetails `json:"documents"` 46 } 47 48 // DocumentDetails contains fields related to the fetching of each Document 49 type DocumentDetails struct { 50 URL string `json:"url"` 51 // TODO: Currently we cannot differentiate between system instance types reliably, therefore we cannot make use of the systemInstanceAware optimization (store it once per system type and reuse it for each system instance of that type). 52 // Once we have system landscape discovery and stable system types we can make use of this optimization. Until then we store all the information for a system instance as it is provided in the documents. 53 // Therefore we treat every resource as SystemInstanceAware = true 54 SystemInstanceAware bool `json:"systemInstanceAware"` 55 AccessStrategies accessstrategy.AccessStrategies `json:"accessStrategies"` 56 Perspective DocumentPerspective `json:"perspective"` 57 } 58 59 // Document represents an ORD Document 60 type Document struct { 61 Schema string `json:"$schema"` 62 OpenResourceDiscovery string `json:"openResourceDiscovery"` 63 Description string `json:"description"` 64 65 // TODO: In the current state of ORD and it's implementation we are missing system landscape discovery and an id correlation in the system instances. Because of that in the first phase we will rely on: 66 // - DescribedSystemInstance is the application in our DB and it's baseURL should match with the one in the webhook. 67 DescribedSystemInstance *model.Application `json:"describedSystemInstance"` 68 69 DescribedSystemVersion *model.ApplicationTemplateVersionInput `json:"describedSystemVersion"` 70 71 Perspective DocumentPerspective `json:"-"` 72 73 Packages []*model.PackageInput `json:"packages"` 74 ConsumptionBundles []*model.BundleCreateInput `json:"consumptionBundles"` 75 Products []*model.ProductInput `json:"products"` 76 APIResources []*model.APIDefinitionInput `json:"apiResources"` 77 EventResources []*model.EventDefinitionInput `json:"eventResources"` 78 Tombstones []*model.TombstoneInput `json:"tombstones"` 79 Vendors []*model.VendorInput `json:"vendors"` 80 } 81 82 // Validate validates if the Config object complies with the spec requirements 83 func (c WellKnownConfig) Validate(baseURL string) error { 84 if err := validation.Validate(c.BaseURL, validation.Match(regexp.MustCompile(ConfigBaseURLRegex))); err != nil { 85 return err 86 } 87 88 if err := validation.Validate(c.OpenResourceDiscoveryV1.Documents, validation.Required); err != nil { 89 return err 90 } 91 92 for _, docDetails := range c.OpenResourceDiscoveryV1.Documents { 93 if err := validateDocDetails(docDetails); err != nil { 94 return err 95 } 96 } 97 98 areDocsWithRelativeURLs, err := checkForRelativeDocURLs(c.OpenResourceDiscoveryV1.Documents) 99 if err != nil { 100 return err 101 } 102 103 if baseURL == "" && areDocsWithRelativeURLs { 104 return errors.New("there are relative document URls but no baseURL provided neither in config nor through /well-known endpoint") 105 } 106 107 return nil 108 } 109 110 // Documents is a slice of Document objects 111 type Documents []*Document 112 113 // ResourcesFromDB holds some of the ORD data from the database 114 type ResourcesFromDB struct { 115 APIs map[string]*model.APIDefinition 116 Events map[string]*model.EventDefinition 117 Packages map[string]*model.Package 118 Bundles map[string]*model.Bundle 119 } 120 121 // ResourceIDs holds some of the ORD entities' IDs 122 type ResourceIDs struct { 123 PackageIDs map[string]bool 124 PackagePolicyLevels map[string]string 125 BundleIDs map[string]bool 126 ProductIDs map[string]bool 127 APIIDs map[string]bool 128 EventIDs map[string]bool 129 VendorIDs map[string]bool 130 } 131 132 // Validate validates all the documents for a system instance 133 func (docs Documents) Validate(calculatedBaseURL string, resourcesFromDB ResourcesFromDB, resourceHashes map[string]uint64, globalResourcesOrdIDs map[string]bool, credentialExchangeStrategyTenantMappings map[string]CredentialExchangeStrategyTenantMapping) error { 134 var ( 135 errs *multierror.Error 136 baseURL = calculatedBaseURL 137 isBaseURLConfigured = len(calculatedBaseURL) > 0 138 ) 139 140 for _, doc := range docs { 141 if !isBaseURLConfigured && (doc.DescribedSystemInstance == nil || doc.DescribedSystemInstance.BaseURL == nil) { 142 errs = multierror.Append(errs, errors.New("no baseURL was provided neither from /well-known URL, nor from config, nor from describedSystemInstance")) 143 continue 144 } 145 146 if len(baseURL) == 0 { 147 baseURL = *doc.DescribedSystemInstance.BaseURL 148 } 149 150 if doc.DescribedSystemInstance != nil { 151 if err := ValidateSystemInstanceInput(doc.DescribedSystemInstance); err != nil { 152 errs = multierror.Append(errs, errors.Wrap(err, "error validating system instance")) 153 } 154 } 155 156 if doc.DescribedSystemVersion != nil { 157 if err := ValidateSystemVersionInput(doc.DescribedSystemVersion); err != nil { 158 errs = multierror.Append(errs, errors.Wrapf(err, "error validating system version")) 159 } 160 } 161 if doc.DescribedSystemInstance != nil && doc.DescribedSystemInstance.BaseURL != nil && *doc.DescribedSystemInstance.BaseURL != baseURL { 162 errs = multierror.Append(errs, errors.Errorf("describedSystemInstance should be the same as the one providing the documents - %s : %s", *doc.DescribedSystemInstance.BaseURL, baseURL)) 163 } 164 } 165 166 resourceIDs := ResourceIDs{ 167 PackageIDs: make(map[string]bool), 168 PackagePolicyLevels: make(map[string]string), 169 BundleIDs: make(map[string]bool), 170 ProductIDs: make(map[string]bool), 171 APIIDs: make(map[string]bool), 172 EventIDs: make(map[string]bool), 173 VendorIDs: make(map[string]bool), 174 } 175 176 for _, doc := range docs { 177 for _, pkg := range doc.Packages { 178 if _, ok := resourceIDs.PackageIDs[pkg.OrdID]; ok { 179 errs = multierror.Append(errs, errors.Errorf("found duplicate package with ord id %q", pkg.OrdID)) 180 continue 181 } 182 resourceIDs.PackageIDs[pkg.OrdID] = true 183 resourceIDs.PackagePolicyLevels[pkg.OrdID] = pkg.PolicyLevel 184 } 185 } 186 187 invalidApisIndices := make([]int, 0) 188 invalidEventsIndices := make([]int, 0) 189 190 r1, e1 := docs.validateAndCheckForDuplications(SystemVersionPerspective, true, resourcesFromDB, resourceIDs, resourceHashes, credentialExchangeStrategyTenantMappings) 191 r2, e2 := docs.validateAndCheckForDuplications(SystemInstancePerspective, true, resourcesFromDB, resourceIDs, resourceHashes, credentialExchangeStrategyTenantMappings) 192 r3, e3 := docs.validateAndCheckForDuplications("", false, resourcesFromDB, resourceIDs, resourceHashes, credentialExchangeStrategyTenantMappings) 193 errs = multierror.Append(errs, e1) 194 errs = multierror.Append(errs, e2) 195 errs = multierror.Append(errs, e3) 196 197 if err := mergo.Merge(&resourceIDs, r1); err != nil { 198 return err 199 } 200 if err := mergo.Merge(&resourceIDs, r2); err != nil { 201 return err 202 } 203 if err := mergo.Merge(&resourceIDs, r3); err != nil { 204 return err 205 } 206 207 // Validate entity relations 208 for _, doc := range docs { 209 for _, pkg := range doc.Packages { 210 if pkg.Vendor != nil && !resourceIDs.VendorIDs[*pkg.Vendor] && !globalResourcesOrdIDs[*pkg.Vendor] { 211 errs = multierror.Append(errs, errors.Errorf("package with id %q has a reference to unknown vendor %q", pkg.OrdID, *pkg.Vendor)) 212 } 213 ordIDs := gjson.ParseBytes(pkg.PartOfProducts).Array() 214 for _, productID := range ordIDs { 215 if !resourceIDs.ProductIDs[productID.String()] && !globalResourcesOrdIDs[productID.String()] { 216 errs = multierror.Append(errs, errors.Errorf("package with id %q has a reference to unknown product %q", pkg.OrdID, productID.String())) 217 } 218 } 219 } 220 for _, product := range doc.Products { 221 if !resourceIDs.VendorIDs[product.Vendor] && !globalResourcesOrdIDs[product.Vendor] { 222 errs = multierror.Append(errs, errors.Errorf("product with id %q has a reference to unknown vendor %q", product.OrdID, product.Vendor)) 223 } 224 } 225 for i, api := range doc.APIResources { 226 if api.OrdPackageID != nil && !resourceIDs.PackageIDs[*api.OrdPackageID] { 227 errs = multierror.Append(errs, errors.Errorf("api with id %q has a REFERENCEe to unknown package %q", *api.OrdID, *api.OrdPackageID)) 228 invalidApisIndices = append(invalidApisIndices, i) 229 } 230 if api.PartOfConsumptionBundles != nil { 231 for _, apiBndlRef := range api.PartOfConsumptionBundles { 232 if !resourceIDs.BundleIDs[apiBndlRef.BundleOrdID] { 233 errs = multierror.Append(errs, errors.Errorf("api with id %q has a reference to unknown bundle %q", *api.OrdID, apiBndlRef.BundleOrdID)) 234 } 235 } 236 } 237 238 ordIDs := gjson.ParseBytes(api.PartOfProducts).Array() 239 for _, productID := range ordIDs { 240 if !resourceIDs.ProductIDs[productID.String()] && !globalResourcesOrdIDs[productID.String()] { 241 errs = multierror.Append(errs, errors.Errorf("api with id %q has a reference to unknown product %q", *api.OrdID, productID.String())) 242 } 243 } 244 } 245 246 for i, event := range doc.EventResources { 247 if event.OrdPackageID != nil && !resourceIDs.PackageIDs[*event.OrdPackageID] { 248 errs = multierror.Append(errs, errors.Errorf("event with id %q has a reference to unknown package %q", *event.OrdID, *event.OrdPackageID)) 249 invalidEventsIndices = append(invalidEventsIndices, i) 250 } 251 if event.PartOfConsumptionBundles != nil { 252 for _, eventBndlRef := range event.PartOfConsumptionBundles { 253 if !resourceIDs.BundleIDs[eventBndlRef.BundleOrdID] { 254 errs = multierror.Append(errs, errors.Errorf("event with id %q has a reference to unknown bundle %q", *event.OrdID, eventBndlRef.BundleOrdID)) 255 } 256 } 257 } 258 259 ordIDs := gjson.ParseBytes(event.PartOfProducts).Array() 260 for _, productID := range ordIDs { 261 if !resourceIDs.ProductIDs[productID.String()] && !globalResourcesOrdIDs[productID.String()] { 262 errs = multierror.Append(errs, errors.Errorf("event with id %q has a reference to unknown product %q", *event.OrdID, productID.String())) 263 } 264 } 265 } 266 267 doc.APIResources = deleteInvalidInputObjects(invalidApisIndices, doc.APIResources) 268 doc.EventResources = deleteInvalidInputObjects(invalidEventsIndices, doc.EventResources) 269 invalidApisIndices = nil 270 invalidEventsIndices = nil 271 } 272 273 return errs.ErrorOrNil() 274 } 275 276 func (docs Documents) validateAndCheckForDuplications(perspectiveConstraint DocumentPerspective, forbidDuplications bool, resourcesFromDB ResourcesFromDB, resourceID ResourceIDs, resourceHashes map[string]uint64, credentialExchangeStrategyTenantMappings map[string]CredentialExchangeStrategyTenantMapping) (ResourceIDs, *multierror.Error) { 277 errs := &multierror.Error{} 278 279 resourceIDs := ResourceIDs{ 280 PackageIDs: make(map[string]bool), 281 PackagePolicyLevels: resourceID.PackagePolicyLevels, 282 BundleIDs: make(map[string]bool), 283 ProductIDs: make(map[string]bool), 284 APIIDs: make(map[string]bool), 285 EventIDs: make(map[string]bool), 286 VendorIDs: make(map[string]bool), 287 } 288 for _, doc := range docs { 289 if doc.Perspective == perspectiveConstraint { 290 continue 291 } 292 invalidPackagesIndices := make([]int, 0) 293 invalidBundlesIndices := make([]int, 0) 294 invalidProductsIndices := make([]int, 0) 295 invalidVendorsIndices := make([]int, 0) 296 invalidTombstonesIndices := make([]int, 0) 297 invalidApisIndices := make([]int, 0) 298 invalidEventsIndices := make([]int, 0) 299 300 if err := validateDocumentInput(doc); err != nil { 301 errs = multierror.Append(errs, errors.Wrap(err, "error validating document")) 302 } 303 304 for i, pkg := range doc.Packages { 305 if err := validatePackageInput(pkg, resourcesFromDB.Packages, resourceHashes); err != nil { 306 errs = multierror.Append(errs, errors.Wrapf(err, "error validating package with ord id %q", pkg.OrdID)) 307 invalidPackagesIndices = append(invalidPackagesIndices, i) 308 resourceIDs.PackageIDs[pkg.OrdID] = false 309 } 310 } 311 312 for i, bndl := range doc.ConsumptionBundles { 313 if err := validateBundleInput(bndl, resourcesFromDB.Bundles, resourceHashes, credentialExchangeStrategyTenantMappings); err != nil { 314 errs = multierror.Append(errs, errors.Wrapf(err, "error validating bundle with ord id %q", stringPtrToString(bndl.OrdID))) 315 invalidBundlesIndices = append(invalidBundlesIndices, i) 316 continue 317 } 318 if bndl.OrdID != nil { 319 if _, ok := resourceIDs.BundleIDs[*bndl.OrdID]; ok && forbidDuplications { 320 errs = multierror.Append(errs, errors.Errorf("found duplicate bundle with ord id %q", *bndl.OrdID)) 321 } 322 resourceIDs.BundleIDs[*bndl.OrdID] = true 323 } 324 } 325 326 for i, product := range doc.Products { 327 if err := validateProductInput(product); err != nil { 328 errs = multierror.Append(errs, errors.Wrapf(err, "error validating product with ord id %q", product.OrdID)) 329 invalidProductsIndices = append(invalidProductsIndices, i) 330 continue 331 } 332 if _, ok := resourceIDs.ProductIDs[product.OrdID]; ok && forbidDuplications { 333 errs = multierror.Append(errs, errors.Errorf("found duplicate product with ord id %q", product.OrdID)) 334 } 335 resourceIDs.ProductIDs[product.OrdID] = true 336 } 337 338 for i, api := range doc.APIResources { 339 if err := validateAPIInput(api, resourceIDs.PackagePolicyLevels, resourcesFromDB.APIs, resourceHashes); err != nil { 340 errs = multierror.Append(errs, errors.Wrapf(err, "error validating api with ord id %q", stringPtrToString(api.OrdID))) 341 invalidApisIndices = append(invalidApisIndices, i) 342 continue 343 } 344 if api.OrdID != nil { 345 if _, ok := resourceIDs.APIIDs[*api.OrdID]; ok && forbidDuplications { 346 errs = multierror.Append(errs, errors.Errorf("found duplicate api with ord id %q", *api.OrdID)) 347 } 348 resourceIDs.APIIDs[*api.OrdID] = true 349 } 350 } 351 352 for i, event := range doc.EventResources { 353 if err := validateEventInput(event, resourceIDs.PackagePolicyLevels, resourcesFromDB.Events, resourceHashes); err != nil { 354 errs = multierror.Append(errs, errors.Wrapf(err, "error validating event with ord id %q", stringPtrToString(event.OrdID))) 355 invalidEventsIndices = append(invalidEventsIndices, i) 356 continue 357 } 358 359 if event.OrdID != nil { 360 if _, ok := resourceIDs.EventIDs[*event.OrdID]; ok && forbidDuplications { 361 errs = multierror.Append(errs, errors.Errorf("found duplicate event with ord id %q", *event.OrdID)) 362 } 363 364 resourceIDs.EventIDs[*event.OrdID] = true 365 } 366 } 367 368 for i, vendor := range doc.Vendors { 369 if err := validateVendorInput(vendor); err != nil { 370 errs = multierror.Append(errs, errors.Wrapf(err, "error validating vendor with ord id %q", vendor.OrdID)) 371 invalidVendorsIndices = append(invalidVendorsIndices, i) 372 continue 373 } 374 if _, ok := resourceIDs.VendorIDs[vendor.OrdID]; ok && forbidDuplications { 375 errs = multierror.Append(errs, errors.Errorf("found duplicate vendor with ord id %q", vendor.OrdID)) 376 } 377 resourceIDs.VendorIDs[vendor.OrdID] = true 378 } 379 380 for i, tombstone := range doc.Tombstones { 381 if err := validateTombstoneInput(tombstone); err != nil { 382 errs = multierror.Append(errs, errors.Wrapf(err, "error validating tombstone with ord id %q", tombstone.OrdID)) 383 invalidTombstonesIndices = append(invalidTombstonesIndices, i) 384 } 385 } 386 387 doc.Packages = deleteInvalidInputObjects(invalidPackagesIndices, doc.Packages) 388 doc.ConsumptionBundles = deleteInvalidInputObjects(invalidBundlesIndices, doc.ConsumptionBundles) 389 doc.Products = deleteInvalidInputObjects(invalidProductsIndices, doc.Products) 390 doc.APIResources = deleteInvalidInputObjects(invalidApisIndices, doc.APIResources) 391 doc.EventResources = deleteInvalidInputObjects(invalidEventsIndices, doc.EventResources) 392 doc.Vendors = deleteInvalidInputObjects(invalidVendorsIndices, doc.Vendors) 393 doc.Tombstones = deleteInvalidInputObjects(invalidTombstonesIndices, doc.Tombstones) 394 } 395 396 return ResourceIDs{ 397 PackageIDs: resourceIDs.PackageIDs, 398 ProductIDs: resourceIDs.ProductIDs, 399 APIIDs: resourceIDs.APIIDs, 400 EventIDs: resourceIDs.EventIDs, 401 VendorIDs: resourceIDs.VendorIDs, 402 BundleIDs: resourceIDs.BundleIDs, 403 PackagePolicyLevels: resourceIDs.PackagePolicyLevels, 404 }, errs 405 } 406 407 // Sanitize performs all the merging and rewriting rules defined in ORD. This method should be invoked after Documents are validated with the Validate method. 408 // - Rewrite all relative URIs using the baseURL from the Described System Instance. If the Described System Instance baseURL is missing the provider baseURL (from the webhook) is used. 409 // - Package's partOfProducts, tags, countries, industry, lineOfBusiness, labels are inherited by the resources in the package. 410 // - Ensure to assign `defaultEntryPoint` if missing and there are available `entryPoints` to API's `PartOfConsumptionBundles` 411 func (docs Documents) Sanitize(baseURL string) error { 412 var err error 413 414 // Rewrite relative URIs 415 for _, doc := range docs { 416 for _, pkg := range doc.Packages { 417 if pkg.PackageLinks, err = rewriteRelativeURIsInJSON(pkg.PackageLinks, baseURL, "url"); err != nil { 418 return err 419 } 420 if pkg.Links, err = rewriteRelativeURIsInJSON(pkg.Links, baseURL, "url"); err != nil { 421 return err 422 } 423 } 424 425 for _, bndl := range doc.ConsumptionBundles { 426 if bndl.Links, err = rewriteRelativeURIsInJSON(bndl.Links, baseURL, "url"); err != nil { 427 return err 428 } 429 if bndl.CredentialExchangeStrategies, err = rewriteRelativeURIsInJSON(bndl.CredentialExchangeStrategies, baseURL, "callbackUrl"); err != nil { 430 return err 431 } 432 } 433 434 for _, api := range doc.APIResources { 435 for _, definition := range api.ResourceDefinitions { 436 if !isAbsoluteURL(definition.URL) { 437 definition.URL = baseURL + definition.URL 438 } 439 } 440 if api.APIResourceLinks, err = rewriteRelativeURIsInJSON(api.APIResourceLinks, baseURL, "url"); err != nil { 441 return err 442 } 443 if api.Links, err = rewriteRelativeURIsInJSON(api.Links, baseURL, "url"); err != nil { 444 return err 445 } 446 if api.ChangeLogEntries, err = rewriteRelativeURIsInJSON(api.ChangeLogEntries, baseURL, "url"); err != nil { 447 return err 448 } 449 if api.TargetURLs, err = rewriteRelativeURIsInJSONArray(api.TargetURLs, baseURL); err != nil { 450 return err 451 } 452 rewriteDefaultTargetURL(api.PartOfConsumptionBundles, baseURL) 453 } 454 455 for _, event := range doc.EventResources { 456 if event.ChangeLogEntries, err = rewriteRelativeURIsInJSON(event.ChangeLogEntries, baseURL, "url"); err != nil { 457 return err 458 } 459 if event.Links, err = rewriteRelativeURIsInJSON(event.Links, baseURL, "url"); err != nil { 460 return err 461 } 462 for _, definition := range event.ResourceDefinitions { 463 if !isAbsoluteURL(definition.URL) { 464 definition.URL = baseURL + definition.URL 465 } 466 } 467 } 468 } 469 470 // Package properties inheritance 471 packages := make(map[string]*model.PackageInput) 472 for _, doc := range docs { 473 for _, pkg := range doc.Packages { 474 packages[pkg.OrdID] = pkg 475 } 476 } 477 478 for _, doc := range docs { 479 for _, api := range doc.APIResources { 480 referredPkg, ok := packages[*api.OrdPackageID] 481 if !ok { 482 return errors.Errorf("api with ord id %q has a reference to unknown package %q", *api.OrdID, *api.OrdPackageID) 483 } 484 if api.PartOfProducts, err = mergeJSONArraysOfStrings(referredPkg.PartOfProducts, api.PartOfProducts); err != nil { 485 return errors.Wrapf(err, "error while merging partOfProducts for api with ord id %q", *api.OrdID) 486 } 487 if api.Tags, err = mergeJSONArraysOfStrings(referredPkg.Tags, api.Tags); err != nil { 488 return errors.Wrapf(err, "error while merging tags for api with ord id %q", *api.OrdID) 489 } 490 if api.Countries, err = mergeJSONArraysOfStrings(referredPkg.Countries, api.Countries); err != nil { 491 return errors.Wrapf(err, "error while merging countries for api with ord id %q", *api.OrdID) 492 } 493 if api.Industry, err = mergeJSONArraysOfStrings(referredPkg.Industry, api.Industry); err != nil { 494 return errors.Wrapf(err, "error while merging industry for api with ord id %q", *api.OrdID) 495 } 496 if api.LineOfBusiness, err = mergeJSONArraysOfStrings(referredPkg.LineOfBusiness, api.LineOfBusiness); err != nil { 497 return errors.Wrapf(err, "error while merging lineOfBusiness for api with ord id %q", *api.OrdID) 498 } 499 if api.Labels, err = mergeORDLabels(referredPkg.Labels, api.Labels); err != nil { 500 return errors.Wrapf(err, "error while merging labels for api with ord id %q", *api.OrdID) 501 } 502 assignDefaultEntryPointIfNeeded(api.PartOfConsumptionBundles, api.TargetURLs) 503 } 504 for _, event := range doc.EventResources { 505 referredPkg, ok := packages[*event.OrdPackageID] 506 if !ok { 507 return errors.Errorf("event with ord id %q has a reference to unknown package %q", *event.OrdID, *event.OrdPackageID) 508 } 509 if event.PartOfProducts, err = mergeJSONArraysOfStrings(referredPkg.PartOfProducts, event.PartOfProducts); err != nil { 510 return errors.Wrapf(err, "error while merging partOfProducts for event with ord id %q", *event.OrdID) 511 } 512 if event.Tags, err = mergeJSONArraysOfStrings(referredPkg.Tags, event.Tags); err != nil { 513 return errors.Wrapf(err, "error while merging tags for event with ord id %q", *event.OrdID) 514 } 515 if event.Countries, err = mergeJSONArraysOfStrings(referredPkg.Countries, event.Countries); err != nil { 516 return errors.Wrapf(err, "error while merging countries for event with ord id %q", *event.OrdID) 517 } 518 if event.Industry, err = mergeJSONArraysOfStrings(referredPkg.Industry, event.Industry); err != nil { 519 return errors.Wrapf(err, "error while merging industry for event with ord id %q", *event.OrdID) 520 } 521 if event.LineOfBusiness, err = mergeJSONArraysOfStrings(referredPkg.LineOfBusiness, event.LineOfBusiness); err != nil { 522 return errors.Wrapf(err, "error while merging lineOfBusiness for event with ord id %q", *event.OrdID) 523 } 524 if event.Labels, err = mergeORDLabels(referredPkg.Labels, event.Labels); err != nil { 525 return errors.Wrapf(err, "error while merging labels for event with ord id %q", *event.OrdID) 526 } 527 } 528 } 529 530 return err 531 } 532 533 // mergeORDLabels merges labels2 into labels1 534 func mergeORDLabels(labels1, labels2 json.RawMessage) (json.RawMessage, error) { 535 if len(labels2) == 0 { 536 return labels1, nil 537 } 538 parsedLabels1 := gjson.ParseBytes(labels1) 539 parsedLabels2 := gjson.ParseBytes(labels2) 540 if !parsedLabels1.IsObject() || !parsedLabels2.IsObject() { 541 return nil, errors.New("invalid arguments: expected two json objects") 542 } 543 544 labels1Map := parsedLabels1.Map() 545 labels2Map := parsedLabels2.Map() 546 547 for k, v := range labels1Map { 548 if v2, ok := labels2Map[k]; ok { 549 mergedValues, err := mergeJSONArraysOfStrings(json.RawMessage(v.Raw), json.RawMessage(v2.Raw)) 550 if err != nil { 551 return nil, errors.Wrapf(err, "while merging values for key %q", k) 552 } 553 labels1Map[k] = gjson.ParseBytes(mergedValues) 554 delete(labels2Map, k) 555 } 556 } 557 558 for k, v := range labels2Map { 559 labels1Map[k] = v 560 } 561 562 result := make(map[string]interface{}, len(labels1Map)) 563 for k, v := range labels1Map { 564 result[k] = v.Value() 565 } 566 567 return json.Marshal(result) 568 } 569 570 // mergeJSONArraysOfStrings merges arr2 in arr1 571 func mergeJSONArraysOfStrings(arr1, arr2 json.RawMessage) (json.RawMessage, error) { 572 if len(arr2) == 0 { 573 return arr1, nil 574 } 575 parsedArr1 := gjson.ParseBytes(arr1) 576 parsedArr2 := gjson.ParseBytes(arr2) 577 if !parsedArr1.IsArray() || !parsedArr2.IsArray() { 578 return nil, errors.New("invalid arguments: expected two json arrays") 579 } 580 resultJSONArr := append(parsedArr1.Array(), parsedArr2.Array()...) 581 result := make([]string, 0, len(resultJSONArr)) 582 for _, el := range resultJSONArr { 583 if el.Type != gjson.String { 584 return nil, errors.New("invalid arguments: expected json array of strings") 585 } 586 result = append(result, el.String()) 587 } 588 result = deduplicate(result) 589 return json.Marshal(result) 590 } 591 592 func validateDocDetails(docDetails DocumentDetails) error { 593 if err := validation.Validate(docDetails.URL, validation.Required); err != nil { 594 return err 595 } 596 597 if err := validation.Validate(docDetails.AccessStrategies, validation.Required); err != nil { 598 return err 599 } 600 601 for _, as := range docDetails.AccessStrategies { 602 if err := as.Validate(); err != nil { 603 return err 604 } 605 } 606 607 return nil 608 } 609 610 func checkForRelativeDocURLs(docs []DocumentDetails) (bool, error) { 611 for _, doc := range docs { 612 parsedDocURL, err := url.ParseRequestURI(doc.URL) 613 if err != nil { 614 return false, errors.New("error while parsing document url") 615 } 616 617 if parsedDocURL.Host == "" { 618 return true, nil 619 } 620 } 621 return false, nil 622 } 623 624 func deduplicate(s []string) []string { 625 if len(s) <= 1 { 626 return s 627 } 628 629 result := make([]string, 0, len(s)) 630 seen := make(map[string]bool) 631 for _, val := range s { 632 if !seen[val] { 633 result = append(result, val) 634 seen[val] = true 635 } 636 } 637 return result 638 } 639 640 func rewriteRelativeURIsInJSONArray(j json.RawMessage, baseURL string) (json.RawMessage, error) { 641 parsedJSON := gjson.ParseBytes(j) 642 643 items := make([]interface{}, 0) 644 for _, crrURI := range parsedJSON.Array() { 645 if !isAbsoluteURL(crrURI.String()) { 646 rewrittenURI := baseURL + crrURI.String() 647 648 items = append(items, rewrittenURI) 649 } else { 650 items = append(items, crrURI.String()) 651 } 652 } 653 654 rewrittenJSON, err := json.Marshal(items) 655 if err != nil { 656 return nil, err 657 } 658 659 return rewrittenJSON, nil 660 } 661 662 func rewriteDefaultTargetURL(bundleRefs []*model.ConsumptionBundleReference, baseURL string) { 663 for _, br := range bundleRefs { 664 if br.DefaultTargetURL != "" && !isAbsoluteURL(br.DefaultTargetURL) { 665 br.DefaultTargetURL = baseURL + br.DefaultTargetURL 666 } 667 } 668 } 669 670 func rewriteRelativeURIsInJSON(j json.RawMessage, baseURL, jsonPath string) (json.RawMessage, error) { 671 parsedJSON := gjson.ParseBytes(j) 672 if parsedJSON.IsArray() { 673 items := make([]interface{}, 0) 674 for _, jsonElement := range parsedJSON.Array() { 675 rewrittenElement, err := rewriteRelativeURIsInJSON(json.RawMessage(jsonElement.Raw), baseURL, jsonPath) 676 if err != nil { 677 return nil, err 678 } 679 items = append(items, gjson.ParseBytes(rewrittenElement).Value()) 680 } 681 return json.Marshal(items) 682 } else if parsedJSON.IsObject() { 683 uriProperty := gjson.GetBytes(j, jsonPath) 684 if uriProperty.Exists() && !isAbsoluteURL(uriProperty.String()) { 685 u, err := url.Parse(baseURL) 686 if err != nil { 687 return nil, err 688 } 689 u.Path = path.Join(u.Path, uriProperty.String()) 690 return sjson.SetBytes(j, jsonPath, u.String()) 691 } 692 } 693 return j, nil 694 } 695 696 func assignDefaultEntryPointIfNeeded(bundleReferences []*model.ConsumptionBundleReference, targetURLs json.RawMessage) { 697 lenTargetURLs := len(gjson.ParseBytes(targetURLs).Array()) 698 for _, br := range bundleReferences { 699 if br.DefaultTargetURL == "" && lenTargetURLs > 1 { 700 br.DefaultTargetURL = gjson.ParseBytes(targetURLs).Array()[0].String() 701 } 702 } 703 } 704 705 func isAbsoluteURL(str string) bool { 706 u, err := url.Parse(str) 707 return err == nil && u.Scheme != "" && u.Host != "" 708 } 709 710 func stringPtrToString(p *string) string { 711 if p != nil { 712 return *p 713 } 714 return "" 715 } 716 717 func deleteInvalidInputObjects[T any](invalidObjectsIndices []int, objects []T) []T { 718 decreaseIndexForDeleting := 0 719 for _, invalidObjectIndex := range invalidObjectsIndices { 720 deleteIndex := invalidObjectIndex - decreaseIndexForDeleting 721 objects = append(objects[:deleteIndex], objects[deleteIndex+1:]...) 722 decreaseIndexForDeleting++ 723 } 724 725 return objects 726 }