9294686ea6
ci/woodpecker/tag/docker Pipeline was successful
Virtual repos cover rpm, deb, npm and others but not alpine, so a local alpine repo and github_alpine remotes cannot be served as one apk repository. - merge members' `<dir>/APKINDEX.tar.gz` per directory; member order wins duplicate (P, V, A); a member 404 is skipped - 302 `<dir>/<P>-<V>.apk` downloads to the member whose index listed the package - serve the merged index unsigned, like local and github_alpine indexes - cache merges per directory on the shared virtual cache; member errors fail with 502 - add docs/alpine/virtual.md, unit tests and an apk e2e with abuild-built packages Requires unkin/artifactapi#163. Reviewed-on: #164 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
141 lines
5.1 KiB
Go
141 lines
5.1 KiB
Go
package virtual
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
|
)
|
|
|
|
func apkRec(p, v, a string) string {
|
|
return "C:Q1" + p + v + "\nP:" + p + "\nV:" + v + "\nA:" + a + "\nS:10\n"
|
|
}
|
|
|
|
func apkTarGz(t *testing.T, name string, body []byte, closeTar bool) []byte {
|
|
t.Helper()
|
|
var tb bytes.Buffer
|
|
tw := tar.NewWriter(&tb)
|
|
if err := tw.WriteHeader(&tar.Header{Name: name, Mode: 0o644, Size: int64(len(body)), Typeflag: tar.TypeReg}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tw.Write(body)
|
|
if closeTar {
|
|
tw.Close()
|
|
} else {
|
|
tw.Flush()
|
|
}
|
|
var gb bytes.Buffer
|
|
gz := gzip.NewWriter(&gb)
|
|
gz.Write(tb.Bytes())
|
|
gz.Close()
|
|
return gb.Bytes()
|
|
}
|
|
|
|
func TestMergeAlpine(t *testing.T) {
|
|
repo := MergeAlpine([]AlpineMember{
|
|
{RemoteName: "a", Index: []byte(apkRec("foo", "1.0-r0", "x86_64") + "\n" + apkRec("bar", "1-r0", "noarch") + "\nC:Q1x\nV:9\n")},
|
|
{RemoteName: "b"},
|
|
{RemoteName: "c", Index: []byte(apkRec("foo", "1.0-r0", "x86_64") + "\n" + apkRec("foo", "2.0-r0", "x86_64") + "\n" + apkRec("bar", "1-r0", "x86_64"))},
|
|
})
|
|
idx, err := apkIndexText(repo.Index)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
recs := splitStanzas(idx)
|
|
if len(recs) != 4 || strings.Count(string(idx), "P:foo\nV:1.0-r0\n") != 1 {
|
|
t.Fatalf("want foo 1.0 once, foo 2.0 and bar for both arches, record without P dropped:\n%s", idx)
|
|
}
|
|
for file, want := range map[string]string{"foo-1.0-r0.apk": "a", "foo-2.0-r0.apk": "c", "bar-1-r0.apk": "a"} {
|
|
if got := string(repo.Owners[file]); got != want {
|
|
t.Errorf("%s owned by %q, want %q", file, got, want)
|
|
}
|
|
}
|
|
if again := MergeAlpine([]AlpineMember{{RemoteName: "a", Index: idx}}); !bytes.Equal(again.Index, MergeAlpine([]AlpineMember{{RemoteName: "a", Index: idx}}).Index) {
|
|
t.Fatal("merged index must be byte-stable across merges")
|
|
}
|
|
}
|
|
|
|
func TestApkIndexTextSkipsSignature(t *testing.T) {
|
|
signed := append(apkTarGz(t, ".SIGN.RSA.alpine.rsa.pub", []byte("sig"), false), apkTarGz(t, "APKINDEX", []byte(apkRec("foo", "1", "x86_64")), true)...)
|
|
idx, err := apkIndexText(signed)
|
|
if err != nil || !strings.Contains(string(idx), "P:foo\n") {
|
|
t.Fatalf("signed index: %v\n%s", err, idx)
|
|
}
|
|
if _, err := apkIndexText(apkTarGz(t, "DESCRIPTION", []byte("x"), true)); err == nil {
|
|
t.Fatal("index without APKINDEX entry should fail")
|
|
}
|
|
}
|
|
|
|
func alpineEngine(members map[string]string) *Engine {
|
|
e := fakeEngine(nil)
|
|
e.apkMember = func(_ context.Context, name, dir string) (*AlpineMember, error) {
|
|
idx, ok := members[name]
|
|
if !ok {
|
|
return nil, errors.New("upstream down")
|
|
}
|
|
if dir != "x86_64/" {
|
|
return &AlpineMember{RemoteName: name}, nil
|
|
}
|
|
return &AlpineMember{RemoteName: name, Index: []byte(idx)}, nil
|
|
}
|
|
return e
|
|
}
|
|
|
|
func alpineVirt(members ...string) models.Virtual {
|
|
return models.Virtual{Name: "v", PackageType: models.PackageAlpine, Members: members}
|
|
}
|
|
|
|
func TestFetchAlpineIndexAndRedirect(t *testing.T) {
|
|
e := alpineEngine(map[string]string{"loc": apkRec("foo", "1-r0", "x86_64"), "gh": apkRec("foo", "1-r0", "x86_64") + "\n" + apkRec("tool", "2-r0", "x86_64")})
|
|
virt := alpineVirt("loc", "gh")
|
|
|
|
body, ct, err := e.Fetch(context.Background(), virt, "./x86_64/APKINDEX.tar.gz", "")
|
|
if err != nil || ct != "application/gzip" {
|
|
t.Fatalf("fetch index: %v %q", err, ct)
|
|
}
|
|
if idx, _ := apkIndexText(body); strings.Count(string(idx), "P:") != 2 {
|
|
t.Fatalf("merged index:\n%s", idx)
|
|
}
|
|
for _, tc := range []struct {
|
|
path, want string
|
|
ok bool
|
|
}{
|
|
{path: "x86_64/foo-1-r0.apk", ok: true, want: "http://h/api/v1/remote/loc/x86_64/foo-1-r0.apk"},
|
|
{path: "./x86_64/tool-2-r0.apk", ok: true, want: "http://h/api/v1/remote/gh/x86_64/tool-2-r0.apk"},
|
|
{path: "x86_64/../x86_64/tool-2-r0.apk", ok: true, want: "http://h/api/v1/remote/gh/x86_64/tool-2-r0.apk"},
|
|
{path: "x86_64/missing-1-r0.apk"},
|
|
{path: "aarch64/foo-1-r0.apk"},
|
|
{path: "x86_64/APKINDEX.tar.gz"},
|
|
{path: "foo-1-r0.apk"},
|
|
} {
|
|
got, ok, err := e.MemberRedirect(context.Background(), virt, tc.path, "", "http://h/")
|
|
if got != tc.want || ok != tc.ok || err != nil {
|
|
t.Errorf("%s: got (%q, %v, %v), want (%q, %v)", tc.path, got, ok, err, tc.want, tc.ok)
|
|
}
|
|
}
|
|
for _, p := range []string{"x86_64/missing-1-r0.apk", "x86_64/DESCRIPTION", "APKINDEX.tar.gz"} {
|
|
if _, _, err := e.Fetch(context.Background(), virt, p, ""); !errors.Is(err, ErrNotFound) {
|
|
t.Errorf("%s: want not found, got %v", p, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFetchAlpineFailsClosed(t *testing.T) {
|
|
e := alpineEngine(map[string]string{"a": apkRec("foo", "1-r0", "x86_64")})
|
|
virt := alpineVirt("a", "down")
|
|
if _, _, err := e.Fetch(context.Background(), virt, "x86_64/APKINDEX.tar.gz", ""); err == nil || errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("index with a member down: want upstream error, got %v", err)
|
|
}
|
|
if _, ok, err := e.MemberRedirect(context.Background(), virt, "x86_64/foo-1-r0.apk", "", "http://h/"); ok || err != nil {
|
|
t.Fatalf("redirect with a member down must defer to Fetch: ok %v err %v", ok, err)
|
|
}
|
|
if _, _, err := e.Fetch(context.Background(), virt, "x86_64/foo-1-r0.apk", ""); err == nil || errors.Is(err, ErrNotFound) {
|
|
t.Fatalf("download with a member down: want upstream error, got %v", err)
|
|
}
|
|
}
|