Forward module and pagination params on puppet release listings
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful

This commit is contained in:
2026-10-20 00:26:53 +11:00
parent 4649878a6d
commit 732403cb23
3 changed files with 58 additions and 0 deletions
+3
View File
@@ -5,3 +5,6 @@ Shared classification, TTL and eviction rules: [caching](../common/caching.md).
| Type | Mutable (auto-detected) | Immutable (auto-detected) |
|---|---|---|
| `puppet` | `v3/modules/*`, `v3/releases*` | `.tar.gz` |
`/v3/releases` forwards the `module`, `limit` and `offset` query params
upstream; set `base_url` to the Forge API root, e.g. `https://forgeapi.puppet.com`.
+30
View File
@@ -3,6 +3,8 @@ package puppet
import (
"context"
"net/http"
"regexp"
"strconv"
"strings"
"git.unkin.net/unkin/artifactapi/internal/auth"
@@ -39,6 +41,34 @@ func (p *Provider) UpstreamURL(remote models.Remote, path string) string {
return strings.TrimRight(remote.BaseURL, "/") + "/" + strings.TrimLeft(path, "/")
}
// ForwardedQuery keeps the module filter and pagination on release listings.
func (p *Provider) ForwardedQuery(path string) map[string]provider.QueryValidator {
if path != "v3/releases" {
return nil
}
return map[string]provider.QueryValidator{"module": moduleParam, "limit": pageLimit, "offset": pageOffset}
}
var slugRe = regexp.MustCompile(`^[a-z0-9]+-[a-z][a-z0-9_]*$`)
func moduleParam(v string) (string, bool) {
v = normalizeSlug(v)
return v, slugRe.MatchString(v)
}
func pageLimit(v string) (string, bool) {
n, err := strconv.ParseUint(v, 10, 32)
if err != nil || n == 0 {
return "", false
}
return strconv.FormatUint(min(n, 100), 10), true
}
func pageOffset(v string) (string, bool) {
n, err := strconv.ParseUint(v, 10, 32)
return strconv.FormatUint(n, 10), err == nil
}
func (p *Provider) RewritesResponse() {}
func (p *Provider) RewriteResponse(body []byte, remote models.Remote, proxyBaseURL string) ([]byte, error) {
+25
View File
@@ -76,3 +76,28 @@ func TestAuthHeaders(t *testing.T) {
t.Error("no credentials, no header")
}
}
func TestForwardedQuery(t *testing.T) {
p := &Provider{}
if p.ForwardedQuery("v3/files/a-b-1.0.0.tar.gz") != nil || p.ForwardedQuery("v3/modules/a-b") != nil {
t.Fatal("only release listings forward a query")
}
f := p.ForwardedQuery("v3/releases")
for name, c := range map[string]struct {
in, want string
ok bool
}{
"module": {"Puppetlabs/stdlib", "puppetlabs-stdlib", true},
"limit": {"500", "100", true},
"offset": {"40", "40", true},
} {
if got, ok := f[name](c.in); got != c.want || ok != c.ok {
t.Errorf("%s(%q) = %q %v, want %q %v", name, c.in, got, ok, c.want, c.ok)
}
}
for name, bad := range map[string]string{"module": "../x", "limit": "0", "offset": "-1"} {
if _, ok := f[name](bad); ok {
t.Errorf("%s(%q) accepted", name, bad)
}
}
}