Emit root-relative Galaxy pagination links on local ansible repos
This commit is contained in:
@@ -122,6 +122,10 @@ func TestAnsiblePublishAndServe(t *testing.T) {
|
||||
t.Fatalf("page 1 = %+v", page)
|
||||
}
|
||||
next := *page.Links.Next
|
||||
if !strings.HasPrefix(next, "/api/v1/local/"+repo+"/") {
|
||||
t.Fatalf("next = %q, want root-relative", next)
|
||||
}
|
||||
next = srv.URL + next
|
||||
page.Links.Next = nil
|
||||
getJSON(t, next, &page)
|
||||
if len(page.Data) != 1 || page.Data[0].Version != "1.0.0" || page.Links.Next != nil {
|
||||
|
||||
@@ -238,12 +238,14 @@ func (e Entry) Ref() VersionRef {
|
||||
// CollectionDoc describes a collection from its versions; base is the repo
|
||||
// URL the hrefs are built on.
|
||||
func CollectionDoc(base, ns, name string, refs []VersionRef) map[string]any {
|
||||
created, updated := "", ""
|
||||
var created, updated time.Time
|
||||
for _, r := range refs {
|
||||
if created == "" || r.CreatedAt < created {
|
||||
created = r.CreatedAt
|
||||
if t, err := time.Parse(time.RFC3339Nano, r.CreatedAt); err == nil && (created.IsZero() || t.Before(created)) {
|
||||
created = t
|
||||
}
|
||||
if t, err := time.Parse(time.RFC3339Nano, r.UpdatedAt); err == nil && t.After(updated) {
|
||||
updated = t
|
||||
}
|
||||
updated = max(updated, r.UpdatedAt)
|
||||
}
|
||||
return map[string]any{
|
||||
"href": base + CollectionPath(ns, name),
|
||||
@@ -251,11 +253,18 @@ func CollectionDoc(base, ns, name string, refs []VersionRef) map[string]any {
|
||||
"name": name,
|
||||
"deprecated": false,
|
||||
"versions_url": base + CollectionPath(ns, name) + "versions/",
|
||||
"created_at": created,
|
||||
"updated_at": updated,
|
||||
"created_at": formatTime(created),
|
||||
"updated_at": formatTime(updated),
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
|
||||
// Page reads limit/offset as the remote forwards them, defaulting to 100/0.
|
||||
func Page(q url.Values) (limit, offset int) {
|
||||
limit, offset = maxPageLimit, 0
|
||||
@@ -268,9 +277,11 @@ func Page(q url.Values) (limit, offset int) {
|
||||
return limit, offset
|
||||
}
|
||||
|
||||
// VersionsPage is one page of refs in Galaxy v3 list form.
|
||||
// VersionsPage is one page of refs in Galaxy v3 list form. Links are
|
||||
// root-relative like Galaxy's, since ansible-galaxy splices them onto the
|
||||
// server URL.
|
||||
func VersionsPage(base, ns, name string, refs []VersionRef, limit, offset int) map[string]any {
|
||||
list := base + CollectionPath(ns, name) + "versions/"
|
||||
list := rootPath(base) + CollectionPath(ns, name) + "versions/"
|
||||
link := func(off int) any {
|
||||
return list + "?" + url.Values{"limit": {strconv.Itoa(limit)}, "offset": {strconv.Itoa(off)}}.Encode()
|
||||
}
|
||||
@@ -294,11 +305,19 @@ func VersionsPage(base, ns, name string, refs []VersionRef, limit, offset int) m
|
||||
}
|
||||
return map[string]any{
|
||||
"meta": map[string]any{"count": len(refs)},
|
||||
"links": map[string]any{"first": link(0), "previous": previous, "next": next, "last": link(max(len(refs)-limit, 0))},
|
||||
"links": map[string]any{"first": link(0), "previous": previous, "next": next, "last": link(max(len(refs)-1, 0) / limit * limit)},
|
||||
"data": data,
|
||||
}
|
||||
}
|
||||
|
||||
// rootPath drops the scheme and host from base.
|
||||
func rootPath(base string) string {
|
||||
if u, err := url.Parse(base); err == nil {
|
||||
return u.EscapedPath()
|
||||
}
|
||||
return base
|
||||
}
|
||||
|
||||
// VersionDoc is a version's detail; the tarball is served from the repo at
|
||||
// artifacts/<filename>.
|
||||
func VersionDoc(base string, e Entry) map[string]any {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -145,7 +146,7 @@ func TestParseAPIPath(t *testing.T) {
|
||||
|
||||
func TestVersionsPage(t *testing.T) {
|
||||
refs := []VersionRef{{Version: "3.0.0"}, {Version: "2.0.0"}, {Version: "1.0.0"}}
|
||||
list := "https://h/r/api/v3/collections/a/b/versions/"
|
||||
list := "/r/api/v3/collections/a/b/versions/"
|
||||
links := func(p map[string]any) map[string]any { return p["links"].(map[string]any) }
|
||||
versions := func(p map[string]any) (out []string) {
|
||||
for _, d := range p["data"].([]map[string]any) {
|
||||
@@ -158,7 +159,7 @@ func TestVersionsPage(t *testing.T) {
|
||||
if got := versions(p); strings.Join(got, ",") != "3.0.0,2.0.0" {
|
||||
t.Fatalf("page 1 = %v", got)
|
||||
}
|
||||
if l := links(p); l["previous"] != nil || l["next"] != list+"?limit=2&offset=2" || l["last"] != list+"?limit=2&offset=1" {
|
||||
if l := links(p); l["previous"] != nil || l["next"] != list+"?limit=2&offset=2" || l["last"] != list+"?limit=2&offset=2" {
|
||||
t.Fatalf("page 1 links = %v", l)
|
||||
}
|
||||
if p["meta"].(map[string]any)["count"] != 3 {
|
||||
@@ -178,6 +179,65 @@ func TestVersionsPage(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestVersionsPageFollow pages through more than 100 versions the way
|
||||
// ansible-galaxy does: old clients splice next onto the versions URL in place
|
||||
// of its path, new ones urljoin it onto the server.
|
||||
func TestVersionsPageFollow(t *testing.T) {
|
||||
var refs []VersionRef
|
||||
for i := 104; i >= 0; i-- {
|
||||
refs = append(refs, VersionRef{Version: fmt.Sprintf("1.0.%d", i)})
|
||||
}
|
||||
server := "https://h:8443/api/v1/local/r%2Bx/"
|
||||
base := strings.TrimSuffix(server, "/")
|
||||
versionsURL := server + "api/v3/collections/a/b/versions/"
|
||||
for _, follow := range map[string]func(string) string{
|
||||
"replace": func(link string) string {
|
||||
u, _ := url.Parse(versionsURL)
|
||||
return strings.Replace(versionsURL, u.EscapedPath(), link, 1)
|
||||
},
|
||||
"urljoin": func(link string) string {
|
||||
s, _ := url.Parse(server)
|
||||
l, _ := url.Parse(link)
|
||||
return s.ResolveReference(l).String()
|
||||
},
|
||||
} {
|
||||
var got []string
|
||||
next := versionsURL + "?limit=100"
|
||||
for pages := 0; next != ""; pages++ {
|
||||
if pages > 3 {
|
||||
t.Fatal("pagination does not terminate")
|
||||
}
|
||||
u, err := url.Parse(next)
|
||||
if err != nil || u.Host != "h:8443" || !strings.HasPrefix(u.EscapedPath(), "/api/v1/local/r%2Bx/api/v3/") {
|
||||
t.Fatalf("followed link %q is not on the server", next)
|
||||
}
|
||||
limit, offset := Page(u.Query())
|
||||
p := VersionsPage(base, "a", "b", refs, limit, offset)
|
||||
for _, d := range p["data"].([]map[string]any) {
|
||||
got = append(got, d["version"].(string))
|
||||
}
|
||||
next = ""
|
||||
if l, ok := p["links"].(map[string]any)["next"].(string); ok {
|
||||
next = follow(l)
|
||||
}
|
||||
}
|
||||
if len(got) != 105 || got[0] != "1.0.104" || got[104] != "1.0.0" {
|
||||
t.Fatalf("collected %d versions, first %v", len(got), got[:1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollectionDocTimes(t *testing.T) {
|
||||
d := CollectionDoc("", "a", "b", []VersionRef{
|
||||
{CreatedAt: "2026-01-01T00:00:05Z", UpdatedAt: "2026-01-01T00:00:05Z"},
|
||||
{CreatedAt: "2026-01-01T00:00:05.5Z", UpdatedAt: "2026-01-01T00:00:05.5Z"},
|
||||
{CreatedAt: "2026-01-01T00:00:04.9Z", UpdatedAt: "2026-01-01T00:00:04.9Z"},
|
||||
})
|
||||
if d["created_at"] != "2026-01-01T00:00:04.9Z" || d["updated_at"] != "2026-01-01T00:00:05.5Z" {
|
||||
t.Fatalf("created/updated = %v/%v", d["created_at"], d["updated_at"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPage(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
q string
|
||||
|
||||
Reference in New Issue
Block a user