A mail client's release is about 80 MB per version, too large to keep in a repository at every upgrade, and an add-on whose licence forbids copying it to third parties may not be in a repository at all. An archive artifact may now name url, sha256 and version (and file or within) instead of from: the build seat fetches it once, refuses it when its digest is not the pin's and names both, keeps it in its workspace under the digest (a vendor that serves only its newest version cannot break a later build of the same pin), and packs it like any archive, so a node gets the mesh's own archive by digest and never asks the vendor. The build seat's image carries xz, which Go's standard library does not read. The installation check takes the vendor's address and version of such a pin for the world's, as a registry is an image's.
300 lines
10 KiB
Go
300 lines
10 KiB
Go
package builder
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
)
|
|
|
|
// An archive pinned by URL and sha256 (novox/hq ADR 0316 §2, §6; How it is checked: "an upstream artifact
|
|
// whose download does not match its sha256 fails the build and names both digests").
|
|
|
|
type tarEntry struct {
|
|
name, body string
|
|
mode int64
|
|
kind byte
|
|
}
|
|
|
|
func tarOf(t *testing.T, entries []tarEntry) []byte {
|
|
t.Helper()
|
|
var raw bytes.Buffer
|
|
w := tar.NewWriter(&raw)
|
|
for _, e := range entries {
|
|
kind := e.kind
|
|
if kind == 0 {
|
|
kind = tar.TypeReg
|
|
}
|
|
h := &tar.Header{Name: e.name, Mode: e.mode, Typeflag: kind, Size: int64(len(e.body))}
|
|
if kind == tar.TypeSymlink {
|
|
h.Linkname, h.Size = "/usr/share/hunspell", 0
|
|
}
|
|
if kind == tar.TypeDir {
|
|
h.Size = 0
|
|
}
|
|
if err := w.WriteHeader(h); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if kind == tar.TypeReg {
|
|
if _, err := w.Write([]byte(e.body)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw.Bytes()
|
|
}
|
|
|
|
func gzipped(t *testing.T, body []byte) []byte {
|
|
t.Helper()
|
|
var out bytes.Buffer
|
|
z := gzip.NewWriter(&out)
|
|
if _, err := z.Write(body); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := z.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return out.Bytes()
|
|
}
|
|
|
|
func sumHex(b []byte) string {
|
|
s := sha256.Sum256(b)
|
|
return hex.EncodeToString(s[:])
|
|
}
|
|
|
|
// vendor serves files by path over https and counts what it was asked.
|
|
type vendor struct {
|
|
files map[string][]byte
|
|
asked int
|
|
}
|
|
|
|
func serveVendor(t *testing.T, v *vendor) string {
|
|
t.Helper()
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
v.asked++
|
|
body, ok := v.files[r.URL.Path]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
w.Write(body)
|
|
}))
|
|
t.Cleanup(server.Close)
|
|
before := upstreamClient
|
|
upstreamClient = server.Client()
|
|
t.Cleanup(func() { upstreamClient = before })
|
|
return server.URL
|
|
}
|
|
|
|
// entriesOf lists a packed archive: each file with its mode and content.
|
|
func entriesOf(t *testing.T, packed []byte) map[string]string {
|
|
t.Helper()
|
|
z, err := gzip.NewReader(bytes.NewReader(packed))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := tar.NewReader(z)
|
|
out := map[string]string{}
|
|
for {
|
|
h, err := r.Next()
|
|
if err == io.EOF {
|
|
return out
|
|
}
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(r)
|
|
out[h.Name] = string(body) + " " + os.FileMode(h.Mode).String()
|
|
}
|
|
}
|
|
|
|
func release(t *testing.T) []byte {
|
|
return gzipped(t, tarOf(t, []tarEntry{
|
|
{name: "betterbird/", kind: tar.TypeDir, mode: 0o755},
|
|
{name: "betterbird/application.ini", body: "[App]\nVersion=153.4.0\n", mode: 0o644},
|
|
{name: "betterbird/betterbird", body: "#!/bin/sh\n", mode: 0o755},
|
|
{name: "betterbird/defaults/pref/channel-prefs.js", body: "pref(\"app.update.channel\", \"default\");\n", mode: 0o644},
|
|
}))
|
|
}
|
|
|
|
func pinnedManifest(base, sum string) string {
|
|
return `{"module":"betterbird","version":"1",
|
|
"build":{"artifacts":[
|
|
{"name":"archive","kind":"archive","url":"` + base + `/release.tar.gz","sha256":"` + sum + `","version":"153.4.0esr-bb10","within":"betterbird"}]},
|
|
"resources":[{"id":"installation","type":"archive","path":"/opt/betterbird","artifact":"archive"}]}`
|
|
}
|
|
|
|
func TestAnArchivePinnedByURLIsFetchedCheckedAndPackedWithoutItsOwnDirectory(t *testing.T) {
|
|
body := release(t)
|
|
v := &vendor{files: map[string][]byte{"/release.tar.gz": body}}
|
|
base := serveVendor(t, v)
|
|
r, workspace := aRepository(t, pinnedManifest(base, sumHex(body)), nil)
|
|
got, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
|
Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest, _ := got.Manifest.Resources[0]["digest"].(string)
|
|
if !strings.HasPrefix(digest, "sha256:") || r.archives["betterbird/archive"] != digest {
|
|
t.Fatalf("the release was not published and pinned as the module's archive: %v, published %v",
|
|
got.Manifest.Resources[0], r.archives)
|
|
}
|
|
|
|
packed, err := upstreamArchive(context.Background(), workspace, catalogue.Artifact{Name: "archive", Kind: "archive",
|
|
URL: base + "/release.tar.gz", SHA256: sumHex(body), Version: "153.4.0esr-bb10", Within: "betterbird"},
|
|
func(string, string, ...any) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if "sha256:"+sumHex(packed) != digest {
|
|
t.Fatalf("a second packing of one pin gave another digest: %s and %s", digest, sumHex(packed))
|
|
}
|
|
entries := entriesOf(t, packed)
|
|
want := map[string]string{
|
|
"application.ini": "[App]\nVersion=153.4.0\n -rw-r--r--",
|
|
"betterbird": "#!/bin/sh\n -rwxr-xr-x",
|
|
"defaults/pref/channel-prefs.js": "pref(\"app.update.channel\", \"default\");\n -rw-r--r--",
|
|
}
|
|
if len(entries) != len(want) {
|
|
t.Fatalf("the archive holds %v, want %v", entries, want)
|
|
}
|
|
for name, w := range want {
|
|
if entries[name] != w {
|
|
t.Errorf("%s is %q in the archive, want %q", name, entries[name], w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnUpstreamFileIsFetchedOnceAndKeptByItsDigest(t *testing.T) {
|
|
// The vendor of an add-on serves only its newest version at one address: a pin that was fetched once
|
|
// must still build after the address moved on.
|
|
body := release(t)
|
|
v := &vendor{files: map[string][]byte{"/release.tar.gz": body}}
|
|
base := serveVendor(t, v)
|
|
r, workspace := aRepository(t, pinnedManifest(base, sumHex(body)), nil)
|
|
first, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
|
Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v.files["/release.tar.gz"] = []byte("the vendor's next release")
|
|
second, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
|
Npmrc{}, GitCredential{}, nil)
|
|
if err != nil {
|
|
t.Fatalf("a pin fetched before failed to build once the address served something else: %v", err)
|
|
}
|
|
if v.asked != 1 {
|
|
t.Fatalf("the vendor was asked %d times for one pin, want once", v.asked)
|
|
}
|
|
if first.Manifest.Resources[0]["digest"] != second.Manifest.Resources[0]["digest"] {
|
|
t.Fatal("two builds of one pin gave two digests")
|
|
}
|
|
}
|
|
|
|
func TestAnUpstreamFileWhoseDownloadDoesNotMatchItsPinFailsAndNamesBothDigests(t *testing.T) {
|
|
pinned := release(t)
|
|
served := gzipped(t, tarOf(t, []tarEntry{{name: "betterbird/betterbird", body: "not what was reviewed", mode: 0o755}}))
|
|
v := &vendor{files: map[string][]byte{"/release.tar.gz": served}}
|
|
base := serveVendor(t, v)
|
|
r, workspace := aRepository(t, pinnedManifest(base, sumHex(pinned)), nil)
|
|
_, err := Build(context.Background(), r.run, r, "https://forge.invalid/catalogue.git", "", "", workspace, nil,
|
|
Npmrc{}, GitCredential{}, nil)
|
|
if err == nil {
|
|
t.Fatal("a download that is not the pinned file was built")
|
|
}
|
|
for _, digest := range []string{sumHex(pinned), sumHex(served)} {
|
|
if !strings.Contains(err.Error(), "sha256:"+digest) {
|
|
t.Errorf("the refusal does not name sha256:%s: %v", digest, err)
|
|
}
|
|
}
|
|
if len(r.archives) != 0 {
|
|
t.Fatalf("something was published: %v", r.archives)
|
|
}
|
|
kept, _ := filepath.Glob(filepath.Join(upstreamDir(workspace), "*"))
|
|
if len(kept) != 0 {
|
|
t.Fatalf("the refused download was kept: %v", kept)
|
|
}
|
|
}
|
|
|
|
func TestAnUpstreamFileThatIsNotAnArchiveIsKeptWholeUnderItsName(t *testing.T) {
|
|
xpi := []byte("PK\x03\x04 an add-on")
|
|
v := &vendor{files: map[string][]byte{"/owl.xpi": xpi}}
|
|
base := serveVendor(t, v)
|
|
packed, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "owl", Kind: "archive",
|
|
URL: base + "/owl.xpi", SHA256: sumHex(xpi), Version: "1.5.6.1", File: "owl.xpi"}, func(string, string, ...any) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
entries := entriesOf(t, packed)
|
|
if len(entries) != 1 || entries["owl.xpi"] != string(xpi)+" -rw-r--r--" {
|
|
t.Fatalf("the add-on was not kept whole as owl.xpi: %v", entries)
|
|
}
|
|
}
|
|
|
|
func TestAnUpstreamArchiveHoldingALinkOrAnEntryOutsideItsDirectoryIsRefused(t *testing.T) {
|
|
cases := map[string][]tarEntry{
|
|
// The community package links the dictionaries; a release carrying a link is not one a machine unpacks.
|
|
"is a 2": {{name: "betterbird/betterbird", body: "x", mode: 0o755},
|
|
{name: "betterbird/dictionaries", kind: tar.TypeSymlink}},
|
|
"outside betterbird": {{name: "betterbird/betterbird", body: "x", mode: 0o755},
|
|
{name: "README", body: "x", mode: 0o644}},
|
|
"outside the archive": {{name: "../etc/passwd", body: "x", mode: 0o644}},
|
|
"no file within": {{name: "betterbird/", kind: tar.TypeDir, mode: 0o755}},
|
|
}
|
|
for want, entries := range cases {
|
|
body := gzipped(t, tarOf(t, entries))
|
|
v := &vendor{files: map[string][]byte{"/r.tar.gz": body}}
|
|
base := serveVendor(t, v)
|
|
_, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "archive", Kind: "archive",
|
|
URL: base + "/r.tar.gz", SHA256: sumHex(body), Version: "1", Within: "betterbird"}, func(string, string, ...any) {})
|
|
if err == nil || !strings.Contains(err.Error(), want) {
|
|
t.Errorf("an archive whose entry is %q was not refused for it: %v", want, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnUpstreamArchiveCompressedWithXzIsRead(t *testing.T) {
|
|
// Betterbird publishes its Linux release as .tar.xz; the build seat's image carries xz for it.
|
|
if _, err := exec.LookPath("xz"); err != nil {
|
|
t.Skip("NOT TESTED here: no xz on this machine (the build seat's image carries it)")
|
|
}
|
|
plain := tarOf(t, []tarEntry{{name: "betterbird/betterbird", body: "#!/bin/sh\n", mode: 0o755}})
|
|
cmd := exec.Command("xz", "-zc")
|
|
cmd.Stdin = bytes.NewReader(plain)
|
|
compressed, err := cmd.Output()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := &vendor{files: map[string][]byte{"/r.tar.xz": compressed}}
|
|
base := serveVendor(t, v)
|
|
packed, err := upstreamArchive(context.Background(), t.TempDir(), catalogue.Artifact{Name: "archive", Kind: "archive",
|
|
URL: base + "/r.tar.xz", SHA256: sumHex(compressed), Version: "1", Within: "betterbird"}, func(string, string, ...any) {})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var names []string
|
|
for n := range entriesOf(t, packed) {
|
|
names = append(names, n)
|
|
}
|
|
sort.Strings(names)
|
|
if strings.Join(names, ",") != "betterbird" {
|
|
t.Fatalf("the xz release unpacked to %v", names)
|
|
}
|
|
}
|