Skip to content

Commit

Permalink
Implement content packages discovery (#1235)
Browse files Browse the repository at this point in the history
Add a new discovery parameter to `/search` and `/categories` endpoints, to filter packages
based on their discovery fields. It supports a list of fields that has to be passed in the form
`fields:<comma-separated list of fields>`. To pass the filter, packages must include discovery
fields in their manifest, and all of them must be included in the list included in the parameter.

For example the following request will return prerelease packages of content type, whose
discovery subfields are a subset of ['http.method', 'apache.status.total_bytes'].
```
GET /search?prerelease=true&type=content&discovery=fields:http.method,apache.status.total_bytes
```
  • Loading branch information
jsoriano authored Oct 8, 2024
1 parent 9c30214 commit 8899500
Show file tree
Hide file tree
Showing 21 changed files with 209 additions and 12 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

* Add new `discovery` parameter to search and category endpoints. [#1235](https://github.com/elastic/package-registry/pull/1235)

### Deprecated

### Known Issues
Expand Down
10 changes: 9 additions & 1 deletion categories.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,14 @@ func newCategoriesFilterFromQuery(query url.Values) (*packages.Filter, error) {
}
}

if v := query.Get("discovery"); v != "" {
discovery, err := packages.NewDiscoveryFilter(v)
if err != nil {
return nil, fmt.Errorf("invalid 'discovery' query param: '%s': %w", v, err)
}
filter.Discovery = discovery
}

return &filter, nil
}

Expand Down Expand Up @@ -219,7 +227,7 @@ func getCategoriesOutput(ctx context.Context, categories map[string]*packages.Ca
}
sort.Strings(keys)

var outputCategories []*packages.Category
outputCategories := []*packages.Category{}
for _, k := range keys {
c := categories[k]
if category, ok := packages.Categories[c.Title]; ok {
Expand Down
3 changes: 3 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func TestEndpoints(t *testing.T) {
{"/categories?spec.min=1.1&spec.max=2.10&prerelease=true", "/categories", "categories-spec-min-1.1.0-max-2.10.0.json", categoriesHandler(testLogger, indexer, testCacheTime)},
{"/categories?spec.max=2.10&prerelease=true", "/categories", "categories-spec-max-2.10.0.json", categoriesHandler(testLogger, indexer, testCacheTime)},
{"/categories?spec.max=2.10.1&prerelease=true", "/categories", "categories-spec-max-error.txt", categoriesHandler(testLogger, indexer, testCacheTime)},
{"/categories?discovery=fields:process.pid&prerelease=true", "/categories", "categories-discovery-fields-process-pid.txt", categoriesHandler(testLogger, indexer, testCacheTime)},
{"/search?kibana.version=6.5.2", "/search", "search-kibana652.json", searchHandler(testLogger, indexer, testCacheTime)},
{"/search?kibana.version=7.2.1", "/search", "search-kibana721.json", searchHandler(testLogger, indexer, testCacheTime)},
{"/search?kibana.version=8.0.0", "/search", "search-kibana800.json", searchHandler(testLogger, indexer, testCacheTime)},
Expand All @@ -101,6 +102,8 @@ func TestEndpoints(t *testing.T) {
{"/search?spec.min=1.1&spec.max=2.10&prerelease=true", "/search", "search-spec-min-1.1.0-max-2.10.0.json", searchHandler(testLogger, indexer, testCacheTime)},
{"/search?spec.max=2.10&prerelease=true", "/search", "search-spec-max-2.10.0.json", searchHandler(testLogger, indexer, testCacheTime)},
{"/search?spec.max=2.10.1&prerelease=true", "/search", "search-spec-max-error.txt", searchHandler(testLogger, indexer, testCacheTime)},
{"/search?prerelease=true&discovery=fields:process.pid", "/search", "search-discovery-fields-process-pid.txt", searchHandler(testLogger, indexer, testCacheTime)},
{"/search?prerelease=true&discovery=fields:non.existing.field", "/search", "search-discovery-fields-empty.txt", searchHandler(testLogger, indexer, testCacheTime)},
{"/favicon.ico", "", "favicon.ico", faviconHandleFunc},

// Removed flags, kept to ensure that they don't break requests from old versions.
Expand Down
56 changes: 56 additions & 0 deletions packages/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,11 +297,63 @@ type Filter struct {
Capabilities []string
SpecMin *semver.Version
SpecMax *semver.Version
Discovery *discoveryFilter

// Deprecated, release tags to be removed.
Experimental bool
}

type discoveryFilter struct {
Fields discoveryFilterFields
}

func NewDiscoveryFilter(filter string) (*discoveryFilter, error) {
filterType, args, found := strings.Cut(filter, ":")
if !found {
return nil, fmt.Errorf("could not parse filter %q", filter)
}

var result discoveryFilter
switch filterType {
case "fields":
for _, name := range strings.Split(args, ",") {
result.Fields = append(result.Fields, DiscoveryField{
Name: name,
})
}
default:
return nil, fmt.Errorf("unknown discovery filter %q", filterType)
}

return &result, nil
}

func (f *discoveryFilter) Matches(p *Package) bool {
if f == nil {
return true
}
return f.Fields.Matches(p)
}

type discoveryFilterFields []DiscoveryField

// Matches implements matching for a collection of fields used as discovery filter.
// It matches if all fields in the package are included in the list of fields in the query.
func (fields discoveryFilterFields) Matches(p *Package) bool {
// If the package doesn't define this filter, it doesn't match.
if p.Discovery == nil || len(p.Discovery.Fields) == 0 {
return false
}

for _, packageField := range p.Discovery.Fields {
if !slices.Contains([]DiscoveryField(fields), packageField) {
return false
}
}

return true
}

// Apply applies the filter to the list of packages, if the filter is nil, no filtering is done.
func (f *Filter) Apply(ctx context.Context, packages Packages) (Packages, error) {
if f == nil {
Expand Down Expand Up @@ -352,6 +404,10 @@ func (f *Filter) Apply(ctx context.Context, packages Packages) (Packages, error)
}
}

if f.Discovery != nil && !f.Discovery.Matches(p) {
continue
}

if f.SpecMin != nil || f.SpecMax != nil {
valid, err := p.HasCompatibleSpec(f.SpecMin, f.SpecMax, f.KibanaVersion)
if err != nil {
Expand Down
56 changes: 49 additions & 7 deletions packages/packages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,10 @@ func TestPackagesSpecMinMaxFilter(t *testing.T) {
Version: "2.0.0",
Type: "integration",
KibanaVersion: "^7.17.0 || ^8.0.0",
DiscoveryFields: []string{
"host.ip",
"nginx.stubstatus.hostname",
},
},
{
FormatVersion: "1.0.0",
Expand Down Expand Up @@ -667,6 +671,26 @@ func TestPackagesSpecMinMaxFilter(t *testing.T) {
{Name: "redisenterprise", Version: "1.0.0"},
},
},
{
Title: "use fields discovery filter that no packages match",
Filter: Filter{
AllVersions: true,
Prerelease: true,
Discovery: mustBuildDiscoveryFilter("fields:apache.status.total_bytes"),
},
Expected: []filterTestPackage{},
},
{
Title: "use fields discovery filter for the nginx package",
Filter: Filter{
AllVersions: true,
Prerelease: true,
Discovery: mustBuildDiscoveryFilter("fields:host.ip,nginx.stubstatus.hostname"),
},
Expected: []filterTestPackage{
{Name: "nginx", Version: "2.0.0"},
},
},
}

for _, c := range cases {
Expand All @@ -678,14 +702,23 @@ func TestPackagesSpecMinMaxFilter(t *testing.T) {
}
}

func mustBuildDiscoveryFilter(filter string) *discoveryFilter {
f, err := NewDiscoveryFilter(filter)
if err != nil {
panic(err)
}
return f
}

type filterTestPackage struct {
FormatVersion string
Name string
Version string
Release string
Type string
KibanaVersion string
Capabilities []string
FormatVersion string
Name string
Version string
Release string
Type string
KibanaVersion string
Capabilities []string
DiscoveryFields []string
}

func (p filterTestPackage) Build() *Package {
Expand Down Expand Up @@ -727,6 +760,15 @@ func (p filterTestPackage) Build() *Package {
}
}

for _, name := range p.DiscoveryFields {
if build.Discovery == nil {
build.Discovery = &Discovery{}
}
build.Discovery.Fields = append(build.Discovery.Fields, DiscoveryField{
Name: name,
})
}

// set spec semver.Version variables
build.setRuntimeFields()
return &build
Expand Down
3 changes: 3 additions & 0 deletions packages/testdata/marshaler/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,9 @@
"type": "elastic",
"github": "elastic/ecosystem"
},
"categories": [
"web"
],
"discovery": {
"fields": [
{
Expand Down
8 changes: 8 additions & 0 deletions search.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,14 @@ func newSearchFilterFromQuery(query url.Values) (*packages.Filter, error) {
}
}

if v := query.Get("discovery"); v != "" {
discovery, err := packages.NewDiscoveryFilter(v)
if err != nil {
return nil, fmt.Errorf("invalid 'discovery' query param: '%s': %w", v, err)
}
filter.Discovery = discovery
}

return &filter, nil
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[
{
"id": "web",
"title": "Web Server",
"count": 1,
"parent_id": "observability",
"parent_title": "Observability"
}
]
2 changes: 1 addition & 1 deletion testdata/generated/categories-experimental.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
{
"id": "web",
"title": "Web Server",
"count": 3,
"count": 4,
"parent_id": "observability",
"parent_title": "Observability"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
{
"id": "web",
"title": "Web Server",
"count": 3,
"count": 4,
"parent_id": "observability",
"parent_title": "Observability"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
{
"id": "web",
"title": "Web Server",
"count": 3,
"count": 4,
"parent_id": "observability",
"parent_title": "Observability"
}
Expand Down
2 changes: 1 addition & 1 deletion testdata/generated/categories-prerelease.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
{
"id": "web",
"title": "Web Server",
"count": 3,
"count": 4,
"parent_id": "observability",
"parent_title": "Observability"
}
Expand Down
3 changes: 3 additions & 0 deletions testdata/generated/package/good_content/0.1.0/index.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
"type": "elastic",
"github": "elastic/ecosystem"
},
"categories": [
"web"
],
"discovery": {
"fields": [
{
Expand Down
3 changes: 3 additions & 0 deletions testdata/generated/search-content-packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
"type": "elastic",
"github": "elastic/ecosystem"
},
"categories": [
"web"
],
"discovery": {
"fields": [
{
Expand Down
1 change: 1 addition & 0 deletions testdata/generated/search-discovery-fields-empty.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
46 changes: 46 additions & 0 deletions testdata/generated/search-discovery-fields-process-pid.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
[
{
"name": "good_content",
"title": "Good content package",
"version": "0.1.0",
"release": "beta",
"source": {
"license": "Apache-2.0"
},
"description": "This package is a dummy example for packages with the content type. These packages contain resources that are useful with data ingested by other integrations. They are not used to configure data sources.\n",
"type": "content",
"download": "/epr/good_content/good_content-0.1.0.zip",
"path": "/package/good_content/0.1.0",
"icons": [
{
"src": "/img/system.svg",
"path": "/package/good_content/0.1.0/img/system.svg",
"title": "system",
"size": "1000x1000",
"type": "image/svg+xml"
}
],
"conditions": {
"kibana": {
"version": "^8.16.0"
},
"elastic": {
"subscription": "basic"
}
},
"owner": {
"type": "elastic",
"github": "elastic/ecosystem"
},
"categories": [
"web"
],
"discovery": {
"fields": [
{
"name": "process.pid"
}
]
}
}
]
3 changes: 3 additions & 0 deletions testdata/generated/search-package-experimental.json
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,9 @@
"type": "elastic",
"github": "elastic/ecosystem"
},
"categories": [
"web"
],
"discovery": {
"fields": [
{
Expand Down
3 changes: 3 additions & 0 deletions testdata/generated/search-package-prerelease.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@
"type": "elastic",
"github": "elastic/ecosystem"
},
"categories": [
"web"
],
"discovery": {
"fields": [
{
Expand Down
3 changes: 3 additions & 0 deletions testdata/generated/search-prerelease-capabilities-none.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@
"type": "elastic",
"github": "elastic/ecosystem"
},
"categories": [
"web"
],
"discovery": {
"fields": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@
"type": "elastic",
"github": "elastic/ecosystem"
},
"categories": [
"web"
],
"discovery": {
"fields": [
{
Expand Down
1 change: 1 addition & 0 deletions testdata/package/good_content/0.1.0/manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ version: 0.1.0
type: content
source:
license: "Apache-2.0"
categories: ["web"]
conditions:
kibana:
version: '^8.16.0' #TBD
Expand Down

0 comments on commit 8899500

Please sign in to comment.