imagecat: catalog + fetch for banger image bundles
New package mirroring `kernelcat`: catalog + SHA256-verified HTTP fetch of `.tar.zst` bundles that contain rootfs.ext4 + manifest.json. Mounted empty (version:1, entries:[]) so nothing is pullable via the bundle path yet; wiring into `banger image pull` lands in a later phase. - catalog.go: Catalog/CatEntry, LoadEmbedded, ParseCatalog, Lookup, ValidateName. - fetch.go: Fetch(ctx, client, destDir, entry) downloads the bundle, verifies sha256, extracts exactly rootfs.ext4 and manifest.json into destDir, returns the parsed manifest. Rejects unexpected tar entries, unsafe paths, non-regular files, and cleans up partial writes on failure. - Thirteen unit tests (happy path + every failure mode). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da471b0640
commit
3d9ae624b1
5 changed files with 597 additions and 0 deletions
88
internal/imagecat/catalog.go
Normal file
88
internal/imagecat/catalog.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// Package imagecat is the published catalog of banger image bundles
|
||||
// (rootfs.ext4 + manifest.json, packaged as a .tar.zst). It ships
|
||||
// embedded in the banger binary. Downloading a bundle is the fast
|
||||
// path for pulling a curated banger image — the rootfs is already
|
||||
// flattened, ownership-fixed, and has banger's guest agents injected
|
||||
// at build time.
|
||||
//
|
||||
// This package is the metadata + fetch layer. Writing to the banger
|
||||
// image store is done by higher layers (the daemon's PullImage
|
||||
// orchestrator), so imagecat has no local-storage concept of its own.
|
||||
package imagecat
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed catalog.json
|
||||
var embeddedCatalog []byte
|
||||
|
||||
// Catalog is the list of pullable image bundles compiled into this
|
||||
// banger binary.
|
||||
type Catalog struct {
|
||||
Version int `json:"version"`
|
||||
Entries []CatEntry `json:"entries"`
|
||||
}
|
||||
|
||||
// CatEntry describes one downloadable bundle. TarballURL points at a
|
||||
// .tar.zst containing rootfs.ext4 and manifest.json.
|
||||
type CatEntry struct {
|
||||
Name string `json:"name"`
|
||||
Distro string `json:"distro,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
KernelRef string `json:"kernel_ref,omitempty"` // kernelcat entry name to pair with
|
||||
TarballURL string `json:"tarball_url"`
|
||||
TarballSHA256 string `json:"tarball_sha256"`
|
||||
SizeBytes int64 `json:"size_bytes,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
// LoadEmbedded returns the catalog compiled into this banger binary.
|
||||
func LoadEmbedded() (Catalog, error) {
|
||||
return ParseCatalog(embeddedCatalog)
|
||||
}
|
||||
|
||||
// ParseCatalog decodes a catalog.json payload. An empty payload is
|
||||
// valid and yields a zero Catalog.
|
||||
func ParseCatalog(data []byte) (Catalog, error) {
|
||||
var cat Catalog
|
||||
if len(data) == 0 {
|
||||
return cat, nil
|
||||
}
|
||||
if err := json.Unmarshal(data, &cat); err != nil {
|
||||
return Catalog{}, fmt.Errorf("parse catalog: %w", err)
|
||||
}
|
||||
return cat, nil
|
||||
}
|
||||
|
||||
// Lookup returns the entry matching name, or os.ErrNotExist.
|
||||
func (c Catalog) Lookup(name string) (CatEntry, error) {
|
||||
for _, e := range c.Entries {
|
||||
if e.Name == name {
|
||||
return e, nil
|
||||
}
|
||||
}
|
||||
return CatEntry{}, os.ErrNotExist
|
||||
}
|
||||
|
||||
// namePattern accepts short filesystem-safe identifiers. Same rule as
|
||||
// kernelcat so `--kernel-ref` and bundle-name refs share syntax.
|
||||
var namePattern = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$`)
|
||||
|
||||
// ValidateName returns an error unless name is a non-empty identifier
|
||||
// of alphanumerics, dots, hyphens, and underscores, starting with an
|
||||
// alphanumeric and at most 64 characters long.
|
||||
func ValidateName(name string) error {
|
||||
if strings.TrimSpace(name) == "" {
|
||||
return fmt.Errorf("image name is required")
|
||||
}
|
||||
if !namePattern.MatchString(name) {
|
||||
return fmt.Errorf("invalid image name %q: use alphanumerics, dots, hyphens, underscores (<=64 chars, starts with alphanumeric)", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue