Add ansible virtual repositories (#158)
ci/woodpecker/tag/docker Pipeline was successful

Clients need one Galaxy server to install both internal collections and galaxy.ansible.com ones; ansible has no virtual type.

- merge member collection version lists in member order, the earlier member winning a duplicate version, cached with the shared virtual merge cache
- paginate limit/offset over the merged list, following remote member pages
- serve version detail from the owning member with href and download_url via the virtual; downloads 302 to the member through the member-redirect validation
- fail closed with 502 on a member error or a member versions list shorter than its count
- add docs/ansible/virtual.md and an e2e-docker case driving real ansible-galaxy

requires unkin/artifactapi#157

Reviewed-on: #158
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
This commit was merged in pull request #158.
This commit is contained in:
2026-10-18 18:09:08 +11:00
committed by BenVincent
parent a00e45c3a7
commit 0cfde41612
11 changed files with 622 additions and 6 deletions
+11 -4
View File
@@ -38,6 +38,11 @@ type Engine struct {
// ErrNotFound.
GemsLocal func(ctx context.Context, repo, path string) ([]byte, error)
// AnsibleLocal returns a local ansible repo's collection, full versions list
// or version detail with root-relative URLs, or ErrNotFound.
AnsibleLocal func(ctx context.Context, repo, path string) ([]byte, error)
ansibleDoc func(ctx context.Context, remote models.Remote, path string) ([]byte, error)
mergeTTL time.Duration
sf singleflight.Group
@@ -68,6 +73,7 @@ func NewEngine(db *database.DB, proxyEngine *proxy.Engine) *Engine {
e.rpmMember = e.fetchRPMMember
e.npmMember = e.fetchNPMMember
e.gemsMember = e.fetchGemsMember
e.ansibleDoc = e.ansibleMemberDoc
return e
}
@@ -192,16 +198,17 @@ var (
ErrBadPath = errors.New("invalid path")
)
// MemberRedirect maps an rpm or npm virtual package path (<member>/<href>, as
// written by MergeRPM/MergeNPM) to the owning member's route. ok is false for
// any other path; ErrBadPath means the path is absolute or escapes the member.
// MemberRedirect maps an rpm, npm or ansible virtual package path
// (<member>/<href>, as written by MergeRPM/MergeNPM/AnsibleVersion) to the
// owning member's route. ok is false for any other path; ErrBadPath means the
// path is absolute or escapes the member.
func (e *Engine) MemberRedirect(ctx context.Context, virt models.Virtual, path, rawQuery, proxyBaseURL string) (string, bool, error) {
switch virt.PackageType {
case models.PackageRPM:
if strings.HasPrefix(path, "repodata/") {
return "", false, nil
}
case models.PackageNPM:
case models.PackageNPM, models.PackageAnsible:
default:
return "", false, nil
}