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  }