Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return single package when multiple indexers are used #849

Merged
merged 6 commits into from
Jul 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Bugfixes

* Return only the latest version of each package when a combined index is used. [#849](https://github.com/elastic/package-registry/pull/849)
* Return only first appearance of the same version of a package when it is available in multiple indexes. [#849](https://github.com/elastic/package-registry/pull/849)

### Added

* Add `elastic.subscription` condition to package index metadata, use this value for backwards compatibility with previous `license` field. [#826](https://github.com/elastic/package-registry/pull/826)
Expand Down
42 changes: 42 additions & 0 deletions indexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ package main

import (
"context"
"sort"
"time"

"github.com/Masterminds/semver/v3"

"github.com/elastic/package-registry/metrics"
"github.com/elastic/package-registry/packages"
)
Expand Down Expand Up @@ -44,5 +47,44 @@ func (c CombinedIndexer) Get(ctx context.Context, opts *packages.GetOptions) (pa
}
packages = packages.Join(p)
}

if !opts.Filter.AllVersions {
return latestPackagesVersion(packages), nil
}

return packages, nil
}

func latestPackagesVersion(source packages.Packages) (result packages.Packages) {
sort.Sort(byNameVersion(source))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you call sort.Sort on the source, it won't alter the original packages slice? I'm double-checking if this is concurrency-safe.

Copy link
Member Author

@jsoriano jsoriano Jul 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It will modify the packages slice defined in the CombinerIndexer.Get, that is built on every call to this method and shouldn't be used concurrently in any other place.


current := ""
for _, p := range source {
if p.Name == current {
continue
}

current = p.Name
result = append(result, p)
}

return result
}

type byNameVersion packages.Packages

func (p byNameVersion) Len() int { return len(p) }
func (p byNameVersion) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p byNameVersion) Less(i, j int) bool {
if p[i].Name != p[j].Name {
return p[i].Name < p[j].Name
}

// Newer versions first.
iSemVer, _ := semver.NewVersion(p[i].Version)
jSemVer, _ := semver.NewVersion(p[j].Version)
if iSemVer != nil && jSemVer != nil {
return jSemVer.LessThan(iSemVer)
}
return p[j].Version < p[i].Version
}
83 changes: 83 additions & 0 deletions indexer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package main

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/elastic/package-registry/packages"
)

func TestLatestPackagesVersion(t *testing.T) {
newPackage := func(name string, version string) *packages.Package {
p := new(packages.Package)
p.Name = name
p.Version = version
return p
}

cases := []struct {
title string
source packages.Packages
expected packages.Packages
}{
{
title: "single package",
source: packages.Packages{
newPackage("foo", "1.2.3"),
},
expected: packages.Packages{
newPackage("foo", "1.2.3"),
},
},
{
title: "single package sorted versions",
source: packages.Packages{
newPackage("foo", "1.2.3"),
newPackage("foo", "1.2.2"),
newPackage("foo", "1.2.1"),
},
expected: packages.Packages{
newPackage("foo", "1.2.3"),
},
},
{
title: "single package unsorted versions",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about adding a case where both indexers contain the same package version?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the case where both indexers contain the same package version, what would the behavior be for a query with ?all=true?

I would think even in this case the response should only contain one item for that package version. The reason being that the other APIs don't provide a way to differentiate between the two items (e.g. the only specifiers are name and version in the download request /epr/example/example-1.0.1.zip). Assuming the solution is to only return one in the search/?all=true response then it would be good to specify somewhere what the priority order is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that in that case we have to decide that some indexers have precendence over the others, I will take a look to this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added a change so indexes defined later have precedence over the ones defined earlier. I have made that because currently we are defining the zip indexer, with support for signatures, after the plain file indexer, so now if a package is in both indexers, the zip with signature will have precedence.

source: packages.Packages{
newPackage("foo", "1.2.2"),
newPackage("foo", "1.2.1"),
newPackage("foo", "1.2.3"),
},
expected: packages.Packages{
newPackage("foo", "1.2.3"),
},
},
{
title: "multiple packages with multiple versions",
source: packages.Packages{
newPackage("foo", "1.2.2"),
newPackage("bar", "1.2.1"),
newPackage("bar", "1.2.2"),
newPackage("foo", "1.2.1"),
newPackage("bar", "1.2.3"),
newPackage("foo", "1.2.3"),
},
expected: packages.Packages{
newPackage("bar", "1.2.3"),
newPackage("foo", "1.2.3"),
},
},
}

for _, c := range cases {
t.Run(c.title, func(t *testing.T) {
result := latestPackagesVersion(c.source)
assert.EqualValues(t, c.expected, result)
})
}

}
2 changes: 1 addition & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,8 @@ func initIndexers(ctx context.Context, logger *zap.Logger, config *Config) Combi
WatchInterval: storageIndexerWatchInterval,
}))
} else {
indexers = append(indexers, packages.NewFileSystemIndexer(packagesBasePaths...))
indexers = append(indexers, packages.NewZipFileSystemIndexer(packagesBasePaths...))
indexers = append(indexers, packages.NewFileSystemIndexer(packagesBasePaths...))
}
combinedIndexer := NewCombinedIndexer(indexers...)
ensurePackagesAvailable(ctx, logger, combinedIndexer)
Expand Down
13 changes: 8 additions & 5 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ func init() {

func TestEndpoints(t *testing.T) {
packagesBasePaths := []string{"./testdata/second_package_path", "./testdata/package"}
indexer := packages.NewFileSystemIndexer(packagesBasePaths...)
indexer := NewCombinedIndexer(
packages.NewZipFileSystemIndexer("./testdata/local-storage"),
packages.NewFileSystemIndexer(packagesBasePaths...),
)

err := indexer.Init(context.Background())
require.NoError(t, err)
Expand Down Expand Up @@ -246,8 +249,8 @@ func TestStaticsModifiedTime(t *testing.T) {
}

indexer := NewCombinedIndexer(
packages.NewFileSystemIndexer("./testdata/package"),
packages.NewZipFileSystemIndexer("./testdata/local-storage"),
packages.NewFileSystemIndexer("./testdata/package"),
)
err := indexer.Init(context.Background())
require.NoError(t, err)
Expand Down Expand Up @@ -307,8 +310,8 @@ func TestZippedArtifacts(t *testing.T) {

func TestPackageIndex(t *testing.T) {
indexer := NewCombinedIndexer(
packages.NewFileSystemIndexer("./testdata/package"),
packages.NewZipFileSystemIndexer("./testdata/local-storage"),
packages.NewFileSystemIndexer("./testdata/package"),
)

err := indexer.Init(context.Background())
Expand Down Expand Up @@ -425,8 +428,8 @@ func TestContentTypes(t *testing.T) {
}

indexer := NewCombinedIndexer(
packages.NewFileSystemIndexer("./testdata/package"),
packages.NewZipFileSystemIndexer("./testdata/local-storage"),
packages.NewFileSystemIndexer("./testdata/package"),
)

err := indexer.Init(context.Background())
Expand Down Expand Up @@ -454,8 +457,8 @@ func TestContentTypes(t *testing.T) {
// on different file systems.
func TestRangeDownloads(t *testing.T) {
indexer := NewCombinedIndexer(
packages.NewFileSystemIndexer("./testdata/package"),
packages.NewZipFileSystemIndexer("./testdata/local-storage"),
packages.NewFileSystemIndexer("./testdata/package"),
)

err := indexer.Init(context.Background())
Expand Down
39 changes: 36 additions & 3 deletions packages/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,43 @@ func (p Packages) Less(i, j int) bool {
return p[i].Version < p[j].Version
}

// Join returns a set of packages that combines both sets.
// Join returns a set of packages that combines both sets. If there is already
// a package in `p1` with the same name and version that a package in `p2`, the
// latter is not added.
func (p1 Packages) Join(p2 Packages) Packages {
// TODO: Avoid duplications?
return append(p1, p2...)
for _, p := range p2 {
if p1.contains(p) {
continue
}
p1 = append(p1, p)
}
return p1
}

// contains returns true if `ps` contains a package with the same name and version as `p`.
func (ps Packages) contains(p *Package) bool {
return ps.index(p) >= 0
}

// index finds if `ps` contains a package with the same name and version as `p` and
// returns its index. If it is not found, it returns -1.
func (ps Packages) index(p *Package) int {
for i, candidate := range ps {
if candidate.Name != p.Name {
continue
}
if cv, pv := candidate.versionSemVer, p.versionSemVer; cv != nil && pv != nil {
if !cv.Equal(pv) {
continue
}
}
if candidate.Version != p.Version {
continue
}

return i
}
return -1
}

// GetOptions can be used to pass options to Get.
Expand Down
96 changes: 96 additions & 0 deletions packages/testdata/marshaler/packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,102 @@
}
]
},
{
"name": "example",
"title": "Example Integration",
"version": "1.0.1",
"release": "ga",
"description": "This is the example integration",
"type": "integration",
"download": "/epr/example/example-1.0.1.zip",
"path": "/package/example/1.0.1",
"conditions": {
"kibana": {
"version": "~7.x.x"
}
},
"owner": {
"github": "ruflin"
},
"categories": [
"crm",
"azure"
],
"format_version": "1.0.0",
"readme": "/package/example/1.0.1/docs/README.md",
"license": "basic",
"screenshots": [
{
"src": "/img/kibana-envoyproxy.jpg",
"path": "/package/example/1.0.1/img/kibana-envoyproxy.jpg",
"title": "IP Tables Ubiquity Dashboard",
"size": "1492x1464",
"type": "image/png"
}
],
"assets": [
"/package/example/1.0.1/manifest.yml",
"/package/example/1.0.1/docs/README.md",
"/package/example/1.0.1/img/icon.png",
"/package/example/1.0.1/img/kibana-envoyproxy.jpg",
"/package/example/1.0.1/data_stream/foo/manifest.yml",
"/package/example/1.0.1/kibana/dashboard/0c610510-5cbd-11e9-8477-077ec9664dbd.json",
"/package/example/1.0.1/kibana/visualization/0a994af0-5c9d-11e9-8477-077ec9664dbd.json",
"/package/example/1.0.1/kibana/visualization/36f872a0-5c03-11e9-85b4-19d0072eb4f2.json",
"/package/example/1.0.1/kibana/visualization/38f96190-5c99-11e9-8477-077ec9664dbd.json",
"/package/example/1.0.1/kibana/visualization/7e4084e0-5c99-11e9-8477-077ec9664dbd.json",
"/package/example/1.0.1/kibana/visualization/80844540-5c97-11e9-8477-077ec9664dbd.json",
"/package/example/1.0.1/kibana/visualization/ab48c3f0-5ca6-11e9-8477-077ec9664dbd.json",
"/package/example/1.0.1/data_stream/foo/fields/base-fields.yml",
"/package/example/1.0.1/data_stream/foo/agent/stream/stream.yml.hbs",
"/package/example/1.0.1/data_stream/foo/elasticsearch/ingest_pipeline/pipeline-entry.json",
"/package/example/1.0.1/data_stream/foo/elasticsearch/ingest_pipeline/pipeline-http.json",
"/package/example/1.0.1/data_stream/foo/elasticsearch/ingest_pipeline/pipeline-json.json",
"/package/example/1.0.1/data_stream/foo/elasticsearch/ingest_pipeline/pipeline-plaintext.json",
"/package/example/1.0.1/data_stream/foo/elasticsearch/ingest_pipeline/pipeline-tcp.json"
],
"policy_templates": [
{
"name": "logs",
"title": "Logs datasource",
"description": "Datasource for your log files.",
"inputs": [
{
"type": "foo"
}
],
"multiple": true
}
],
"data_streams": [
{
"type": "logs",
"dataset": "example.foo",
"title": "Foo",
"release": "ga",
"ingest_pipeline": "pipeline-entry",
"streams": [
{
"input": "foo",
"vars": [
{
"name": "paths",
"type": "text",
"description": "Path to log files to be collected",
"multi": true,
"required": true,
"show_user": false
}
],
"template_path": "stream.yml.hbs",
"enabled": true
}
],
"package": "example",
"path": "foo"
}
]
},
{
"name": "example",
"title": "Example Integration",
Expand Down
4 changes: 2 additions & 2 deletions testdata/generated/categories-experimental.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
{
"id": "azure",
"title": "Azure",
"count": 1
"count": 2
},
{
"id": "cloud",
Expand All @@ -22,7 +22,7 @@
{
"id": "crm",
"title": "CRM",
"count": 1
"count": 2
},
{
"id": "custom",
Expand Down
4 changes: 2 additions & 2 deletions testdata/generated/categories-include-policy-templates.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
{
"id": "azure",
"title": "Azure",
"count": 2
"count": 3
},
{
"id": "crm",
"title": "CRM",
"count": 2
"count": 3
},
{
"id": "custom",
Expand Down
Loading