Add puppet virtual repositories
This commit is contained in:
@@ -215,6 +215,10 @@ func (h *ProxyHandler) handleVirtual(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveVirtualAnsible(w, r, *virt, path, proxyBaseURL)
|
||||
return
|
||||
}
|
||||
if virt.PackageType == models.PackagePuppet {
|
||||
h.serveVirtualPuppet(w, r, *virt, path, proxyBaseURL)
|
||||
return
|
||||
}
|
||||
if virt.PackageType == models.PackageGoProxy {
|
||||
h.serveVirtualGoProxy(w, r, *virt, path)
|
||||
return
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider/puppet"
|
||||
"git.unkin.net/unkin/artifactapi/internal/virtual"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
// serveVirtualPuppet serves the merged Forge v3 module and release listings
|
||||
// and 302s release details and downloads to the member owning that release.
|
||||
func (h *ProxyHandler) serveVirtualPuppet(w http.ResponseWriter, r *http.Request, virt models.Virtual, path, proxyBaseURL string) {
|
||||
var doc any
|
||||
var err error
|
||||
switch {
|
||||
case path == "v3/releases":
|
||||
q := r.URL.Query()
|
||||
slug := puppet.NormalizeSlug(q.Get("module"))
|
||||
if !puppet.ValidSlug(slug) {
|
||||
forgeError(w, http.StatusBadRequest, "module is required")
|
||||
return
|
||||
}
|
||||
var d *virtual.PuppetDoc
|
||||
d, err = h.virtualEngine.PuppetReleases(r.Context(), virt, slug)
|
||||
releases := []any{}
|
||||
if errors.Is(err, virtual.ErrNotFound) {
|
||||
err = nil
|
||||
} else if err == nil {
|
||||
for _, rel := range d.Releases {
|
||||
releases = append(releases, rel)
|
||||
}
|
||||
}
|
||||
doc = puppet.ReleasesPage(slug, releases, q)
|
||||
case strings.HasPrefix(path, "v3/modules/"):
|
||||
slug := puppet.NormalizeSlug(strings.TrimPrefix(path, "v3/modules/"))
|
||||
if !puppet.ValidSlug(slug) {
|
||||
puppet.NotFound(w)
|
||||
return
|
||||
}
|
||||
var d *virtual.PuppetDoc
|
||||
if d, err = h.virtualEngine.PuppetReleases(r.Context(), virt, slug); err == nil {
|
||||
doc, err = virtual.PuppetModule(d)
|
||||
}
|
||||
default:
|
||||
var loc string
|
||||
loc, err = h.virtualEngine.PuppetRedirect(r.Context(), virt, path, proxyBaseURL)
|
||||
if err == nil {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Vary", "Host, X-Forwarded-Proto")
|
||||
http.Redirect(w, r, loc, http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, virtual.ErrNotFound):
|
||||
puppet.NotFound(w)
|
||||
case errors.Is(err, virtual.ErrBadPath):
|
||||
forgeError(w, http.StatusBadRequest, err.Error())
|
||||
case err != nil:
|
||||
slog.Error("virtual puppet fetch failed", "virtual", virt.Name, "path", path, "error", err)
|
||||
forgeError(w, http.StatusBadGateway, "bad gateway")
|
||||
default:
|
||||
w.Header().Set("X-Artifact-Source", "virtual")
|
||||
puppet.WriteJSON(w, doc)
|
||||
}
|
||||
}
|
||||
|
||||
func forgeError(w http.ResponseWriter, code int, msg string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"message": http.StatusText(code), "errors": []string{msg}})
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.unkin.net/unkin/artifactapi/internal/provider"
|
||||
"git.unkin.net/unkin/artifactapi/internal/virtual"
|
||||
"git.unkin.net/unkin/artifactapi/pkg/models"
|
||||
)
|
||||
|
||||
func TestPuppetVirtualOverLocals(t *testing.T) {
|
||||
h, srv := npmTestServer(t)
|
||||
h.virtualEngine = virtual.NewEngine(h.db, nil)
|
||||
a, b := npmRepo(t, h, models.PackagePuppet), npmRepo(t, h, models.PackagePuppet)
|
||||
for _, p := range []struct{ repo, version string }{{a, "0.1.0"}, {b, "0.1.0"}, {b, "0.2.0"}} {
|
||||
file := "v3/files/unkin-hello-" + p.version + ".tar.gz"
|
||||
meta := &provider.PuppetMetadata{RepoName: p.repo, FilePath: file, ContentHash: "sha256:00", Slug: "unkin-hello", Version: p.version, MetadataJSON: `{"name":"unkin-hello","version":"` + p.version + `"}`}
|
||||
if err := h.db.InsertPuppetMetadata(context.Background(), meta); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
virt := "v" + a
|
||||
if err := h.db.CreateVirtual(context.Background(), &models.Virtual{Name: virt, PackageType: models.PackagePuppet, Members: []string{a, b}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
base := srv.URL + "/api/v1/virtual/" + virt
|
||||
|
||||
var page struct {
|
||||
Pagination struct {
|
||||
Total int
|
||||
Next *string
|
||||
}
|
||||
Results []struct {
|
||||
Version string
|
||||
FileURI string `json:"file_uri"`
|
||||
}
|
||||
}
|
||||
getJSON(t, base+"/v3/releases?module=unkin/hello&limit=1", &page)
|
||||
if page.Pagination.Total != 2 || len(page.Results) != 1 || page.Results[0].Version != "0.2.0" || page.Pagination.Next == nil {
|
||||
t.Fatalf("page 1 = %+v", page)
|
||||
}
|
||||
if *page.Pagination.Next != "/v3/releases?limit=1&module=unkin-hello&offset=1" || page.Results[0].FileURI != "/v3/files/unkin-hello-0.2.0.tar.gz" {
|
||||
t.Fatalf("links not relative to the forge root: next %q file_uri %q", *page.Pagination.Next, page.Results[0].FileURI)
|
||||
}
|
||||
getJSON(t, base+*page.Pagination.Next, &page)
|
||||
if len(page.Results) != 1 || page.Results[0].Version != "0.1.0" || page.Pagination.Next != nil {
|
||||
t.Fatalf("page 2 = %+v", page)
|
||||
}
|
||||
|
||||
var mod struct {
|
||||
CurrentRelease struct{ Version string } `json:"current_release"`
|
||||
Releases []struct{ Version string }
|
||||
}
|
||||
getJSON(t, base+"/v3/modules/unkin-hello", &mod)
|
||||
if mod.CurrentRelease.Version != "0.2.0" || len(mod.Releases) != 2 {
|
||||
t.Fatalf("module = %+v", mod)
|
||||
}
|
||||
|
||||
noRedirect := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
|
||||
for p, want := range map[string]string{
|
||||
"/v3/files/unkin-hello-0.1.0.tar.gz": "/api/v1/local/" + a + "/v3/files/unkin-hello-0.1.0.tar.gz",
|
||||
"/v3/releases/unkin-hello-0.2.0": "/api/v1/local/" + b + "/v3/releases/unkin-hello-0.2.0",
|
||||
} {
|
||||
resp, err := noRedirect.Get(base + p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusFound || resp.Header.Get("Location") != srv.URL+want || resp.Header.Get("Cache-Control") != "no-store" {
|
||||
t.Errorf("%s = %d %q %q, want 302 to %s", p, resp.StatusCode, resp.Header.Get("Location"), resp.Header.Get("Cache-Control"), want)
|
||||
}
|
||||
}
|
||||
|
||||
getJSON(t, base+"/v3/releases?module=unkin-absent", &page)
|
||||
if page.Pagination.Total != 0 || len(page.Results) != 0 {
|
||||
t.Fatalf("absent module = %+v", page)
|
||||
}
|
||||
for p, want := range map[string]int{
|
||||
"/v3/releases": http.StatusBadRequest,
|
||||
"/v3/modules/unkin-absent": http.StatusNotFound,
|
||||
"/v3/files/unkin-hello-0.9.0.tar.gz": http.StatusNotFound,
|
||||
} {
|
||||
if code, body := npmDo(t, http.MethodGet, base+p, ""); code != want || !strings.Contains(body, `"errors"`) {
|
||||
t.Errorf("%s = %d %s, want %d", p, code, body, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user