Skip to content

Commit

Permalink
apk/client: add LatestPackage to get the latest named package in an i…
Browse files Browse the repository at this point in the history
…ndex

Signed-off-by: Jason Hall <jason@chainguard.dev>
  • Loading branch information
imjasonh committed Nov 12, 2024
1 parent ef8f78a commit 8227e18
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 1 deletion.
4 changes: 4 additions & 0 deletions pkg/apk/apk/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,12 @@ type Package struct {
}

func (p *Package) String() string {
if p == nil {
return "<nil>"
}
return fmt.Sprintf("%s (ver:%s arch:%s)", p.Name, p.Version, p.Arch)
}

func (p *Package) PackageName() string { return p.Name }

// Filename returns the package filename as it's named in a repository.
Expand Down
44 changes: 43 additions & 1 deletion pkg/apk/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package client
import (
"context"
"fmt"
"log"
"net/http"
"net/url"
"sync"

"chainguard.dev/apko/pkg/apk/apk"
"chainguard.dev/apko/pkg/apk/auth"
Expand All @@ -24,6 +26,10 @@ const (
// Client is a client for interacting with an APK package repository.
type Client struct {
httpClient *http.Client

// map of package name to latest package with that name.
latestMap map[string]*apk.Package
once sync.Once
}

// New creates a new Client, suitable for accessing remote APK indexes and
Expand All @@ -42,7 +48,7 @@ func New(httpClient *http.Client) *Client {
// "https://packages.wolfi.dev/os".
//
// `arch` is the architecture of the index, e.g. "x86_64" or "aarch64".
func (c Client) GetRemoteIndex(ctx context.Context, apkRepo, arch string) (*apk.APKIndex, error) {
func (c *Client) GetRemoteIndex(ctx context.Context, apkRepo, arch string) (*apk.APKIndex, error) {
indexURL := apk.IndexURL(apkRepo, arch)

u, err := url.Parse(indexURL)
Expand All @@ -69,3 +75,39 @@ func (c Client) GetRemoteIndex(ctx context.Context, apkRepo, arch string) (*apk.

return apk.IndexFromArchive(resp.Body)
}

func (c *Client) LatestPackage(idx *apk.APKIndex, name string) *apk.Package {
c.once.Do(func() { c.latestMap = onlyLatest(idx.Packages) })
return c.latestMap[name]
}

func onlyLatest(packages []*apk.Package) map[string]*apk.Package {
highest := map[string]*apk.Package{}
for _, pkg := range packages {
got, err := apk.ParseVersion(pkg.Version)
if err != nil {
// TODO: We should really fail here.
log.Printf("parsing %q: %v", pkg.Filename(), err)
continue
}

have, ok := highest[pkg.Name]
if !ok {
highest[pkg.Name] = pkg
continue
}

// TODO: We re-parse this for no reason.
parsed, err := apk.ParseVersion(have.Version)
if err != nil {
// TODO: We should really fail here.
log.Printf("parsing %q: %v", have.Version, err)
continue
}

if apk.CompareVersions(got, parsed) > 0 {
highest[pkg.Name] = pkg
}
}
return highest
}
32 changes: 32 additions & 0 deletions pkg/apk/client/client_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package client

import (
"testing"

"chainguard.dev/apko/pkg/apk/apk"
)

func TestLatestPackage(t *testing.T) {
arch := "x86_64"
idx := &apk.APKIndex{
Packages: []*apk.Package{
{Name: "foo", Version: "1.0.0", Arch: arch},
{Name: "foo", Version: "1.0.1", Arch: arch}, // latest
{Name: "bar", Version: "1.0.0", Arch: arch}, // only
},
}

for _, c := range []struct {
name string
want *apk.Package
}{
{"foo", &apk.Package{Name: "foo", Version: "1.0.1", Arch: arch}},
{"bar", &apk.Package{Name: "bar", Version: "1.0.0", Arch: arch}},
{"baz", nil}, // not found
} {
got := (&Client{}).LatestPackage(idx, c.name)
if got.String() != c.want.String() {
t.Errorf("LatestPackage(%q) = %v, want %v", c.name, got, c.want)
}
}
}

0 comments on commit 8227e18

Please sign in to comment.