New internal/imagepull/ subpackage. Three concerns, each independently testable: Pull (imagepull.go): - github.com/google/go-containerregistry's remote.Image with the linux/amd64 platform pinned. Anonymous pulls only for v1. - Layer blobs cached on disk via cache.NewFilesystemCache under <cacheDir>/blobs/sha256/<hex> — OCI-standard layout so skopeo/crane could co-exist later. - Eagerly touches every layer once so network errors surface at Pull time, not deep in Flatten. Flatten (flatten.go): - Replays layers oldest-first into destDir. - Whiteout-aware: .wh.<name> deletes the named entry, .wh..wh..opq wipes the parent directory's contents from prior layers. - Path-traversal hardening mirrored from kernelcat extractTar: reject .., absolute paths, and symlinks/hardlinks whose resolved target escapes destDir. - Handles tar.TypeReg, TypeDir, TypeSymlink, TypeLink. Skips device/fifo nodes silently (need privilege; udev/devtmpfs handles them in the guest). BuildExt4 (ext4.go): - Truncates outFile to sizeBytes, then runs `mkfs.ext4 -F -d <srcDir> -E root_owner=0:0`. No mount, no sudo, no loopback. - 64 MiB floor; callers handle real sizing with content-aware headroom. - File ownership in the resulting ext4 reflects srcDir's on-disk ownership — runner's uid/gid since extraction was unprivileged. Documented in package doc as a Phase A v1 limitation; Phase B will add a debugfs- or tar2ext4-based ownership fixup. paths.Layout gains OCICacheDir at $XDG_CACHE_HOME/banger/oci/, ensured at startup alongside the other dirs. Tests use go-containerregistry's in-process registry to push and pull synthetic multi-layer images. Cover: layer caching round-trip, whiteout + opaque-marker handling, path-traversal rejection, unsafe symlink rejection, real mkfs.ext4 round-trip (skipped if mkfs.ext4 absent), and tiny-size rejection. go-containerregistry v0.21.5 added as a direct dep, plus its transitive closure (containerd/stargz, opencontainers/go-digest, docker/cli config helpers, etc). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
70 lines
1.9 KiB
Go
70 lines
1.9 KiB
Go
package imagepull
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
|
|
"banger/internal/system"
|
|
)
|
|
|
|
// MinExt4Size is the smallest ext4 image we'll create. mkfs.ext4 needs a
|
|
// few megabytes for its bookkeeping; for a real rootfs the staging tree
|
|
// will dominate anyway.
|
|
const MinExt4Size int64 = 1 << 20 * 64 // 64 MiB
|
|
|
|
// BuildExt4 creates outFile as a sparse ext4 image of sizeBytes and
|
|
// populates it from srcDir using `mkfs.ext4 -F -d`. No mount, no sudo.
|
|
//
|
|
// sizeBytes must be at least MinExt4Size. Callers are expected to size
|
|
// the file with headroom over the staged tree (the daemon orchestrator
|
|
// does this; this function only enforces a sanity floor).
|
|
//
|
|
// The resulting image's file ownership reflects srcDir's on-disk
|
|
// ownership — see the package doc for the implications.
|
|
func BuildExt4(ctx context.Context, runner system.CommandRunner, srcDir, outFile string, sizeBytes int64) error {
|
|
if sizeBytes < MinExt4Size {
|
|
return fmt.Errorf("ext4 size %d below minimum %d", sizeBytes, MinExt4Size)
|
|
}
|
|
info, err := os.Stat(srcDir)
|
|
if err != nil {
|
|
return fmt.Errorf("stat source: %w", err)
|
|
}
|
|
if !info.IsDir() {
|
|
return fmt.Errorf("%s is not a directory", srcDir)
|
|
}
|
|
|
|
if err := os.Remove(outFile); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return err
|
|
}
|
|
f, err := os.OpenFile(outFile, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0o644)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := f.Truncate(sizeBytes); err != nil {
|
|
_ = f.Close()
|
|
_ = os.Remove(outFile)
|
|
return err
|
|
}
|
|
if err := f.Close(); err != nil {
|
|
_ = os.Remove(outFile)
|
|
return err
|
|
}
|
|
|
|
out, runErr := runner.Run(ctx, "mkfs.ext4",
|
|
"-F",
|
|
"-q",
|
|
"-d", srcDir,
|
|
"-L", "banger-rootfs",
|
|
"-E", "root_owner=0:0",
|
|
outFile,
|
|
strconv.FormatInt(sizeBytes/4096, 10), // size in 4 KiB blocks
|
|
)
|
|
if runErr != nil {
|
|
_ = os.Remove(outFile)
|
|
return fmt.Errorf("mkfs.ext4 -d: %w: %s", runErr, string(out))
|
|
}
|
|
return nil
|
|
}
|