Files
artifactapi/internal/testsupport/apk.go
T
unkin-agent 7f77666709 Add Alpine/apk local repository support (#114)
## 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>
2026-08-12 20:39:27 +10:00

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()
}