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

Implement repo priorities. #112

Merged
merged 8 commits into from
Jul 8, 2024
86 changes: 46 additions & 40 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,19 +92,28 @@ func (ps *PackageState) Match(pi goolib.PackageInfo) bool {
return ps.PackageSpec.Name == pi.Name && (ps.PackageSpec.Arch == pi.Arch || pi.Arch == "") && (ps.PackageSpec.Version == pi.Ver || pi.Ver == "")
}

// Repo represents a single downloaded repo.
type Repo struct {
Priority int
Packages []goolib.RepoSpec
}

// RepoMap describes each repo's packages as seen from a client.
type RepoMap map[string][]goolib.RepoSpec
type RepoMap map[string]Repo

// AvailableVersions builds a RepoMap from a list of sources.
func AvailableVersions(ctx context.Context, srcs []string, cacheDir string, cacheLife time.Duration, proxyServer string) RepoMap {
func AvailableVersions(ctx context.Context, srcs map[string]int, cacheDir string, cacheLife time.Duration, proxyServer string) RepoMap {
rm := make(RepoMap)
for _, r := range srcs {
for r, pri := range srcs {
rf, err := unmarshalRepoPackages(ctx, r, cacheDir, cacheLife, proxyServer)
if err != nil {
logger.Errorf("error reading repo %q: %v", r, err)
continue
}
rm[r] = rf
rm[r] = Repo{
Priority: pri,
Packages: rf,
}
}
return rm
}
Expand Down Expand Up @@ -157,7 +166,7 @@ func decode(index io.ReadCloser, ct, url, cf string) ([]goolib.RepoSpec, error)

// unmarshalRepoPackages gets and unmarshals a repository URL or uses the cached contents
// if mtime is less than cacheLife.
// Sucessfully unmarshalled contents will be written to a cache.
// Successfully unmarshalled contents will be written to a cache.
func unmarshalRepoPackages(ctx context.Context, p, cacheDir string, cacheLife time.Duration, proxyServer string) ([]goolib.RepoSpec, error) {
pName := strings.TrimPrefix(p, "oauth-")

Expand Down Expand Up @@ -305,9 +314,9 @@ func unmarshalRepoPackagesGCS(ctx context.Context, bucket, object, url, cf strin
return decode(r, "application/json", url, cf)
}

// FindRepoSpec returns the element of pl whose PackageSpec matches pi.
func FindRepoSpec(pi goolib.PackageInfo, pl []goolib.RepoSpec) (goolib.RepoSpec, error) {
for _, p := range pl {
// FindRepoSpec returns the RepoSpec in repo whose PackageSpec matches pi.
func FindRepoSpec(pi goolib.PackageInfo, repo Repo) (goolib.RepoSpec, error) {
for _, p := range repo.Packages {
ps := p.PackageSpec
if ps.Name == pi.Name && ps.Arch == pi.Arch && ps.Version == pi.Ver {
return p, nil
Expand All @@ -316,66 +325,63 @@ func FindRepoSpec(pi goolib.PackageInfo, pl []goolib.RepoSpec) (goolib.RepoSpec,
return goolib.RepoSpec{}, fmt.Errorf("no match found for package %s.%s.%s in repo", pi.Name, pi.Arch, pi.Ver)
}

func latest(psm map[string][]*goolib.PkgSpec) (ver, repo string) {
// latest returns the version and repo having the greatest (priority, version) from the set of
// package specs in psm.
func latest(psm map[string][]*goolib.PkgSpec, rm RepoMap) (string, string) {
var ver, repo string
var pri int
for r, pl := range psm {
for _, p := range pl {
if ver == "" {
repo = r
ver = p.Version
continue
}
c, err := goolib.Compare(p.Version, ver)
if err != nil {
logger.Errorf("compare of %s to %s failed with error: %v", p.Version, ver, err)
for _, pkg := range pl {
q := rm[r].Priority
c := 1
if ver != "" {
var err error
if c, err = goolib.ComparePriorityVersion(q, pkg.Version, pri, ver); err != nil {
logger.Errorf("compare of %s to %s failed with error: %v", pkg.Version, ver, err)
continue
}
}
if c == 1 {
repo = r
ver = p.Version
ver = pkg.Version
pri = q
}
}
}
return
return ver, repo
}

// FindRepoLatest returns the latest version of a package along with its repo and arch.
func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (ver, repo, arch string, err error) {
// The archs are searched in order; if a matching package is found for any arch, it is
// returned immediately even if a later arch might have a later version.
func FindRepoLatest(pi goolib.PackageInfo, rm RepoMap, archs []string) (string, string, string, error) {
psm := make(map[string][]*goolib.PkgSpec)
name := pi.Name
if pi.Arch != "" {
for r, pl := range rm {
for _, p := range pl {
if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == pi.Arch {
psm[r] = append(psm[r], p.PackageSpec)
}
}
}
if len(psm) != 0 {
v, r := latest(psm)
return v, r, pi.Arch, nil
}
return "", "", "", fmt.Errorf("no versions of package %s.%s found in any repo", pi.Name, pi.Arch)
archs = []string{pi.Arch}
name = fmt.Sprintf("%s.%s", pi.Name, pi.Arch)
}

for _, a := range archs {
for r, pl := range rm {
for _, p := range pl {
for r, repo := range rm {
for _, p := range repo.Packages {
if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == a {
psm[r] = append(psm[r], p.PackageSpec)
}
}
}
if len(psm) != 0 {
v, r := latest(psm)
v, r := latest(psm, rm)
return v, r, a, nil
}
}
return "", "", "", fmt.Errorf("no versions of package %s found in any repo", pi.Name)
return "", "", "", fmt.Errorf("no versions of package %s found in any repo", name)
}

// WhatRepo returns what repo a package is in.
// Name, Arch, and Ver fields of PackageInfo must be provided.
func WhatRepo(pi goolib.PackageInfo, rm RepoMap) (string, error) {
for r, pl := range rm {
for _, p := range pl {
for r, repo := range rm {
for _, p := range repo.Packages {
if p.PackageSpec.Name == pi.Name && p.PackageSpec.Arch == pi.Arch && p.PackageSpec.Version == pi.Ver {
return r, nil
}
Expand Down
185 changes: 124 additions & 61 deletions client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,14 @@ func TestGetPackageStateNoMatch(t *testing.T) {

func TestWhatRepo(t *testing.T) {
rm := RepoMap{
"foo_repo": []goolib.RepoSpec{
{
PackageSpec: &goolib.PkgSpec{
Name: "foo_pkg",
Version: "1.2.3@4",
Arch: "noarch",
"foo_repo": Repo{
Packages: []goolib.RepoSpec{
{
PackageSpec: &goolib.PkgSpec{
Name: "foo_pkg",
Version: "1.2.3@4",
Arch: "noarch",
},
},
},
},
Expand All @@ -117,62 +119,123 @@ func TestWhatRepo(t *testing.T) {
}

func TestFindRepoLatest(t *testing.T) {
archs := []string{"noarch", "x86_64"}
rm := RepoMap{
"foo_repo": []goolib.RepoSpec{
{
PackageSpec: &goolib.PkgSpec{
Name: "foo_pkg",
Version: "1.2.3@4",
Arch: "noarch",
},
for _, tt := range []struct {
desc string
pi goolib.PackageInfo
archs []string
rm RepoMap
wantVersion string
wantArch string
wantRepo string
wantErr bool
}{
{
desc: "name and arch",
pi: goolib.PackageInfo{Name: "foo_pkg", Arch: "noarch"},
archs: []string{"noarch", "x86_64"},
rm: RepoMap{
"foo_repo": Repo{Packages: []goolib.RepoSpec{
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.2.3@4", Arch: "noarch"}},
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "x86_64"}},
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}},
{PackageSpec: &goolib.PkgSpec{Name: "bar_pkg", Version: "2.3.0@1", Arch: "noarch"}},
}},
},
{
PackageSpec: &goolib.PkgSpec{
Name: "foo_pkg",
Version: "1.0.0@1",
Arch: "noarch",
wantVersion: "1.2.3@4",
wantArch: "noarch",
wantRepo: "foo_repo",
},
{
desc: "name only",
pi: goolib.PackageInfo{Name: "foo_pkg"},
archs: []string{"noarch", "x86_64"},
rm: RepoMap{
"foo_repo": Repo{Packages: []goolib.RepoSpec{
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.2.3@4", Arch: "noarch"}},
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.0.0@1", Arch: "x86_64"}},
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}},
{PackageSpec: &goolib.PkgSpec{Name: "bar_pkg", Version: "2.3.0@1", Arch: "noarch"}},
}},
},
wantVersion: "1.2.3@4",
wantArch: "noarch",
wantRepo: "foo_repo",
},
{
desc: "specified arch not present",
pi: goolib.PackageInfo{Name: "foo_pkg", Arch: "x86_64"},
archs: []string{"noarch", "x86_64"},
rm: RepoMap{
"foo_repo": Repo{Packages: []goolib.RepoSpec{
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.2.3@4", Arch: "noarch"}},
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.0.0@1", Arch: "noarch"}},
{PackageSpec: &goolib.PkgSpec{Name: "bar_pkg", Version: "2.3.0@1", Arch: "noarch"}},
}},
},
wantErr: true,
},
{
desc: "multiple repos with same priority",
pi: goolib.PackageInfo{Name: "foo_pkg", Arch: "noarch"},
archs: []string{"noarch", "x86_64"},
rm: RepoMap{
"foo_repo": Repo{
Priority: 500,
Packages: []goolib.RepoSpec{
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.2.3@4", Arch: "noarch"}},
},
},
"bar_repo": Repo{
Priority: 500,
Packages: []goolib.RepoSpec{
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.4.5@1", Arch: "noarch"}},
},
},
},
{
PackageSpec: &goolib.PkgSpec{
Name: "bar_pkg",
Version: "1.0.0@1",
Arch: "noarch",
wantVersion: "2.4.5@1",
wantArch: "noarch",
wantRepo: "bar_repo",
},
{
desc: "multiple repos with different priority",
pi: goolib.PackageInfo{Name: "foo_pkg", Arch: "noarch"},
archs: []string{"noarch", "x86_64"},
rm: RepoMap{
"high_priority_repo": Repo{
Priority: 1500,
Packages: []goolib.RepoSpec{
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "1.2.3@4", Arch: "noarch"}},
},
},
"low_priority_repo": Repo{
Priority: 500,
Packages: []goolib.RepoSpec{
{PackageSpec: &goolib.PkgSpec{Name: "foo_pkg", Version: "2.4.5@1", Arch: "noarch"}},
},
},
},
wantVersion: "1.2.3@4",
wantArch: "noarch",
wantRepo: "high_priority_repo",
},
}

table := []struct {
pkg string
arch string
wVer string
wArch string
wRepo string
}{
{"foo_pkg", "noarch", "1.2.3@4", "noarch", "foo_repo"},
{"foo_pkg", "", "1.2.3@4", "noarch", "foo_repo"},
}
for _, tt := range table {
gotVer, gotRepo, gotArch, err := FindRepoLatest(goolib.PackageInfo{Name: tt.pkg, Arch: tt.arch, Ver: ""}, rm, archs)
if err != nil {
t.Fatalf("FindRepoLatest failed: %v", err)
}
if gotVer != tt.wVer {
t.Errorf("FindRepoLatest for %q, %q returned version: %q, want %q", tt.pkg, tt.arch, gotVer, tt.wVer)
}
if gotArch != tt.wArch {
t.Errorf("FindRepoLatest for %q, %q returned arch: %q, want %q", tt.pkg, tt.arch, gotArch, tt.wArch)
}
if gotRepo != tt.wRepo {
t.Errorf("FindRepoLatest for %q, %q returned repo: %q, want %q", tt.pkg, tt.arch, gotRepo, tt.wRepo)
}
}

werr := "no versions of package bar_pkg.x86_64 found in any repo"
if _, _, _, err := FindRepoLatest(goolib.PackageInfo{Name: "bar_pkg", Arch: "x86_64", Ver: ""}, rm, archs); err.Error() != werr {
t.Errorf("did not get expected error: got %q, want %q", err, werr)
} {
t.Run(tt.desc, func(t *testing.T) {
gotVersion, gotRepo, gotArch, err := FindRepoLatest(tt.pi, tt.rm, tt.archs)
if err != nil && !tt.wantErr {
t.Fatalf("FindRepoLatest(%v, %v, %v) failed: %v", tt.pi, tt.rm, tt.archs, err)
} else if err == nil && tt.wantErr {
t.Fatalf("FindRepoLatest(%v, %v, %v) got nil error, wanted non-nil", tt.pi, tt.rm, tt.archs)
}
if gotVersion != tt.wantVersion {
t.Errorf("FindRepoLatest(%v, %v, %v) got version: %q, want %q", tt.pi, tt.rm, tt.archs, gotVersion, tt.wantVersion)
}
if gotArch != tt.wantArch {
t.Errorf("FindRepoLatest(%v, %v, %v) got arch: %q, want %q", tt.pi, tt.rm, tt.archs, gotArch, tt.wantArch)
}
if gotRepo != tt.wantRepo {
t.Errorf("FindRepoLatest(%v, %v, %v) got repo: %q, want %q", tt.pi, tt.rm, tt.archs, gotRepo, tt.wantRepo)
}
})
}
}

Expand Down Expand Up @@ -298,12 +361,12 @@ func TestUnmarshalRepoPackagesCache(t *testing.T) {

func TestFindRepoSpec(t *testing.T) {
want := goolib.RepoSpec{PackageSpec: &goolib.PkgSpec{Name: "test"}}
rs := []goolib.RepoSpec{
repo := Repo{Packages: []goolib.RepoSpec{
want,
{PackageSpec: &goolib.PkgSpec{Name: "test2"}},
}
}}

got, err := FindRepoSpec(goolib.PackageInfo{Name: "test", Arch: "", Ver: ""}, rs)
got, err := FindRepoSpec(goolib.PackageInfo{Name: "test", Arch: "", Ver: ""}, repo)
if err != nil {
t.Errorf("error running FindRepoSpec: %v", err)
}
Expand All @@ -313,9 +376,9 @@ func TestFindRepoSpec(t *testing.T) {
}

func TestFindRepoSpecNoMatch(t *testing.T) {
rs := []goolib.RepoSpec{{PackageSpec: &goolib.PkgSpec{Name: "test2"}}}
repo := Repo{Packages: []goolib.RepoSpec{{PackageSpec: &goolib.PkgSpec{Name: "test2"}}}}

if _, err := FindRepoSpec(goolib.PackageInfo{Name: "test", Arch: "", Ver: ""}, rs); err == nil {
if _, err := FindRepoSpec(goolib.PackageInfo{Name: "test", Arch: "", Ver: ""}, repo); err == nil {
t.Error("did not get expected error when running FindRepoSpec")
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/blang/semver v3.5.1+incompatible
github.com/dustin/go-humanize v1.0.0
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/google/go-cmp v0.6.0
github.com/google/logger v1.1.1
github.com/google/subcommands v1.2.0
github.com/olekukonko/tablewriter v0.0.5
Expand Down
Loading