7f77666709
## Why The alpine provider only supported remote (proxy) repositories, so there was no way to publish first-party `.apk` packages the way `rpm-local` and `deb-local` already allow. This extends the existing alpine provider into a real apk repository: uploaded `.apk` files are parsed in pure Go and a per-arch `APKINDEX.tar.gz` is generated on demand, at parity with rpm repodata and deb Packages generation. (The metadata-only `github_alpine` type is a separate follow-up and is not part of this PR.) ## How - Implements `LocalUploader` / `LocalIndexer` / `PostUploadHook` / `PostDeleteHook` on the existing `alpine` provider, leaving the remote proxy methods (`UpstreamURL`/`ContentType`/`AuthHeaders`/`RewriteResponse`/`Classify`) intact. - Parses the `.apk` (up to three concatenated, independently gzipped tar streams) in pure Go: locates the control stream by its `.PKGINFO` member, reads the `key = value` fields, and computes the apk pull checksum `C:` = `Q1` + base64(sha1(**control gzip stream bytes**)) — the sha1 of the second gzip member, not of the whole file. - Derives arch from `.PKGINFO` and records download size (`S:` blob size) and installed size (`I:` from `.PKGINFO size`). - Generates an **unsigned** per-arch `APKINDEX.tar.gz` = gzip(tar(`APKINDEX`)) filtered by requested arch (clients use `--allow-untrusted`, matching rpm `gpgcheck=0` / deb `[trusted=yes]`), applying the same dot-segment normalization as deb so `./<arch>/APKINDEX.tar.gz` resolves. Non-index / `.apk` paths return `false` so the generic file streamer serves the stored blob. - Adds `AlpineMetadata` plus **separate** `AlpineMetadataStore` / `AlpineMetadataReader` / `AlpineMetadataDeleter` interfaces (type-asserted from the generic hooks) so the shared rpm/deb metadata interfaces and their test doubles are untouched. - Adds the `alpine_metadata` table (keyed by `repo_name` + `file_path`, per-arch index) and its `Insert`/`Delete`/`List` DB methods. - Adds `testsupport.MinimalApk`, unit tests (`.PKGINFO` parse, Q1 checksum over the control stream, per-arch filtering, empty-field omission, `./` dot-segment handling, ValidateUpload accept/reject), and a `dockere2e` `TestLocalAlpineIndex`. ## Consumption `/etc/apk/repositories` line = `<url>/api/v1/local/<name>` (apk appends `/<arch>/APKINDEX.tar.gz`); `apk update --allow-untrusted && apk add --allow-untrusted <pkg>`. Packages live at `/api/v1/local/<name>/<arch>/<file>.apk`. ## Verification `go build ./...`, `go vet ./...` (incl. `-tags dockere2e`), `go mod tidy` (no change), `make test` (`-race`), and `pre-commit run --all-files` all pass. Reviewed-on: #114 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
39 lines
1.1 KiB
Go
39 lines
1.1 KiB
Go
package testsupport
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
)
|
|
|
|
// MinimalApk builds a valid-enough Alpine package in pure Go (no committed
|
|
// binary fixture, no abuild): two concatenated, independently gzipped tar
|
|
// streams -- a control stream carrying .PKGINFO and a data stream carrying a
|
|
// single payload file. It mirrors MinimalDeb/MinimalRPM and is parseable by the
|
|
// alpine provider (which derives arch/name/version and the Q1 pull checksum from
|
|
// the control stream).
|
|
func MinimalApk(name, version, arch string) []byte {
|
|
pkginfo := fmt.Sprintf(
|
|
"# generated by testsupport\n"+
|
|
"pkgname = %s\n"+
|
|
"pkgver = %s\n"+
|
|
"arch = %s\n"+
|
|
"pkgdesc = minimal test package\n"+
|
|
"url = https://example.com/%s\n"+
|
|
"license = MIT\n"+
|
|
"origin = %s\n"+
|
|
"maintainer = e2e <e2e@example.com>\n"+
|
|
"builddate = 1700000000\n"+
|
|
"size = 4\n"+
|
|
"depend = so:libc.musl-x86_64.so.1\n"+
|
|
"provides = cmd:%s=%s\n",
|
|
name, version, arch, name, name, name, version)
|
|
|
|
control := gzipBytes(tarSingle(".PKGINFO", []byte(pkginfo)))
|
|
data := gzipBytes(tarSingle("usr/bin/"+name, []byte("body")))
|
|
|
|
var buf bytes.Buffer
|
|
buf.Write(control)
|
|
buf.Write(data)
|
|
return buf.Bytes()
|
|
}
|