mirror of
https://github.com/imjasonh/terraform-playground
synced 2026-07-18 15:08:24 +00:00
cache: make caching opt-out (default on) with --no-cache
Caching (layer blobs, downloaded wheels, base interpreter detection) is now enabled by default at the per-user cache dir (or --cache-dir / $PYMAGE_CACHE_DIR). --no-cache disables it: layer/meta caches off and wheels downloaded into an ephemeral temp dir that's cleaned up after the build. - setupCaches centralizes cache wiring (one cache.Cache serves layers + meta); --no-cache uses a temp wheel dir with cleanup - DefaultCacheDir honors $PYMAGE_CACHE_DIR; cli tests point it at a temp dir so the on-by-default cache doesn't touch the user's cache - tests: setupCaches (default + --no-cache cleanup); README/config updated Co-authored-by: Jason Hall <imjasonh@users.noreply.github.com>
This commit is contained in:
parent
4100e12b20
commit
1f0ade9788
5 changed files with 95 additions and 29 deletions
|
|
@ -72,6 +72,7 @@ the config value, which overrides the built-in default.
|
|||
| `max-layers` | `--max-layers` | `127` |
|
||||
| `max-wheel-layers` | `--max-wheel-layers` | *(derived from `max-layers`)* |
|
||||
| `push-concurrency` | `--push-concurrency` | auto (≥ 4, scales with CPUs) |
|
||||
| `no-cache` | `--no-cache` | `false` (caching is on by default) |
|
||||
| `python` | `--python` | auto-detected from the base |
|
||||
| `prefix` | `--prefix` | `/app/.venv` |
|
||||
| `workdir` | `--workdir` | `/app` |
|
||||
|
|
@ -178,7 +179,8 @@ base that advertises its version.
|
|||
| `--push-concurrency` | Max concurrent layer uploads when pushing (0 = auto). |
|
||||
| `--platform` | Target platform(s); selects compatible wheels and base. Repeatable / comma-separated (e.g. `linux/amd64,linux/arm64`) builds a multi-arch image index. Defaults to the platforms the base image supports. |
|
||||
| `--python` | Interpreter version, e.g. `python3.12`. Optional — **auto-detected from the base** when omitted; if set, must match the base. Drives wheel selection and the site-packages layout. |
|
||||
| `--cache-dir` | Content-addressed layer cache; reuses compressed layers and downloaded wheels across rebuilds. |
|
||||
| `--cache-dir` | Cache root (default: `$PYMAGE_CACHE_DIR` or the per-user cache dir). Caches compressed layers, downloaded wheels, and base interpreter detection. |
|
||||
| `--no-cache` | Disable all caching (layers, downloaded wheels, interpreter detection). |
|
||||
| `--prefix` | install prefix / venv root (default `/app/.venv`). |
|
||||
| `--workdir` | image working dir and source destination (default `/app`). |
|
||||
| `--user` | image user, e.g. `65532`. |
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ type buildFlags struct {
|
|||
requireHash bool
|
||||
insecure bool
|
||||
cacheDir string
|
||||
noCache bool
|
||||
}
|
||||
|
||||
func buildCmd() *cobra.Command {
|
||||
|
|
@ -112,7 +113,8 @@ func buildCmd() *cobra.Command {
|
|||
fs.StringVar(&f.sbomOut, "sbom", "", "write a CycloneDX SBOM to this path")
|
||||
fs.BoolVar(&f.requireHash, "require-hashes", true, "require every requirement to carry --hash entries")
|
||||
fs.BoolVar(&f.insecure, "insecure", false, "use plain HTTP for the registry")
|
||||
fs.StringVar(&f.cacheDir, "cache-dir", "", "directory for the content-addressed layer cache (speeds up rebuilds)")
|
||||
fs.StringVar(&f.cacheDir, "cache-dir", "", "cache root directory (default: per-user cache dir)")
|
||||
fs.BoolVar(&f.noCache, "no-cache", false, "disable all build caches (layers, downloaded wheels, interpreter detection)")
|
||||
return cmd
|
||||
}
|
||||
|
||||
|
|
@ -170,24 +172,13 @@ func runBuild(cmd *cobra.Command, f *buildFlags) error {
|
|||
platforms = baseSet.Platforms()
|
||||
}
|
||||
|
||||
var layerCache *cache.Cache
|
||||
wheelCache, err := wheelCacheDir(f)
|
||||
// Caching is on by default (layers, downloaded wheels, and interpreter
|
||||
// detection); --no-cache disables it.
|
||||
layerCache, metaCache, wheelCache, cleanup, err := setupCaches(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.cacheDir != "" {
|
||||
layerCache, err = cache.New(f.cacheDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// A small metadata cache (default per-user) remembers per-base interpreter
|
||||
// detection so repeat builds don't re-download the base layer holding
|
||||
// apko.json. Best-effort: a failure here just disables the cache.
|
||||
var metaCache *cache.Cache
|
||||
if dir, derr := metaCacheDir(f); derr == nil {
|
||||
metaCache, _ = cache.New(dir)
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
// Build one image per target platform. With more than one platform we
|
||||
// assemble them into a multi-arch OCI image index; with zero or one we push
|
||||
|
|
|
|||
|
|
@ -24,6 +24,19 @@ import (
|
|||
"github.com/imjasonh/terraform-playground/pymage/internal/testwheel"
|
||||
)
|
||||
|
||||
// TestMain points the default cache dir at a throwaway location so the CLI
|
||||
// tests (which use the on-by-default cache) don't write to the user's cache.
|
||||
func TestMain(m *testing.M) {
|
||||
tmp, err := os.MkdirTemp("", "pymage-test-cache-")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
_ = os.Setenv("PYMAGE_CACHE_DIR", tmp)
|
||||
code := m.Run()
|
||||
_ = os.RemoveAll(tmp)
|
||||
os.Exit(code)
|
||||
}
|
||||
|
||||
// TestBuildMultiArchIndex drives the CLI to build a linux/amd64 + linux/arm64
|
||||
// image index from a single host, and verifies the pushed artifact is an index
|
||||
// covering both platforms and is reproducible.
|
||||
|
|
@ -561,6 +574,44 @@ func TestCachedInterpreter(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestSetupCaches(t *testing.T) {
|
||||
// Default: caching on; layer and meta share one cache; wheels under it.
|
||||
root := t.TempDir()
|
||||
layer, meta, wheelDir, cleanup, err := setupCaches(&buildFlags{cacheDir: root})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer cleanup()
|
||||
if layer == nil || meta == nil {
|
||||
t.Fatal("expected caches to be enabled by default")
|
||||
}
|
||||
if layer != meta {
|
||||
t.Error("layer and meta cache should share one instance")
|
||||
}
|
||||
if wheelDir != filepath.Join(root, "wheels") {
|
||||
t.Errorf("wheel dir = %q, want %q", wheelDir, filepath.Join(root, "wheels"))
|
||||
}
|
||||
|
||||
// --no-cache: no persistent caches, ephemeral wheel dir removed by cleanup.
|
||||
layer, meta, wheelDir, cleanup, err = setupCaches(&buildFlags{noCache: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if layer != nil || meta != nil {
|
||||
t.Error("--no-cache should disable layer/meta caches")
|
||||
}
|
||||
if wheelDir == "" {
|
||||
t.Fatal("expected an ephemeral wheel dir with --no-cache")
|
||||
}
|
||||
if _, err := os.Stat(wheelDir); err != nil {
|
||||
t.Fatalf("ephemeral wheel dir should exist: %v", err)
|
||||
}
|
||||
cleanup()
|
||||
if _, err := os.Stat(wheelDir); !os.IsNotExist(err) {
|
||||
t.Errorf("cleanup should remove the ephemeral wheel dir, stat err = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushConcurrency(t *testing.T) {
|
||||
if got := pushConcurrency(7); got != 7 {
|
||||
t.Errorf("explicit value not honored: got %d, want 7", got)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cli
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
|
@ -10,6 +11,7 @@ import (
|
|||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/imjasonh/terraform-playground/pymage/internal/build"
|
||||
"github.com/imjasonh/terraform-playground/pymage/internal/cache"
|
||||
"github.com/imjasonh/terraform-playground/pymage/internal/project"
|
||||
)
|
||||
|
||||
|
|
@ -64,6 +66,9 @@ func applyDefaults(cmd *cobra.Command, f *buildFlags) error {
|
|||
if !changed("push-concurrency") && cfg.PushJobs != 0 {
|
||||
f.pushJobs = cfg.PushJobs
|
||||
}
|
||||
if !changed("no-cache") && cfg.NoCache {
|
||||
f.noCache = true
|
||||
}
|
||||
if !changed("python") && cfg.Python != "" {
|
||||
f.pythonTag = cfg.Python
|
||||
}
|
||||
|
|
@ -119,20 +124,32 @@ func applyDefaults(cmd *cobra.Command, f *buildFlags) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func wheelCacheDir(f *buildFlags) (string, error) {
|
||||
if f.cacheDir != "" {
|
||||
return filepath.Join(f.cacheDir, "wheels"), nil
|
||||
// setupCaches resolves the build caches. Caching is on by default; --no-cache
|
||||
// disables persistent caching (downloads still work, via an ephemeral temp dir
|
||||
// that the returned cleanup removes). The layer cache also serves the small
|
||||
// metadata cache (interpreter detection), so they share one directory.
|
||||
func setupCaches(f *buildFlags) (layer, meta *cache.Cache, wheelDir string, cleanup func(), err error) {
|
||||
noop := func() {}
|
||||
if f.noCache {
|
||||
tmp, err := os.MkdirTemp("", "pymage-wheels-")
|
||||
if err != nil {
|
||||
return nil, nil, "", noop, err
|
||||
}
|
||||
return nil, nil, tmp, func() { _ = os.RemoveAll(tmp) }, nil
|
||||
}
|
||||
return project.DefaultWheelCacheDir()
|
||||
}
|
||||
|
||||
// metaCacheDir is where small build metadata (e.g. detected base interpreter
|
||||
// versions) is cached.
|
||||
func metaCacheDir(f *buildFlags) (string, error) {
|
||||
if f.cacheDir != "" {
|
||||
return f.cacheDir, nil
|
||||
root := f.cacheDir
|
||||
if root == "" {
|
||||
root, err = project.DefaultCacheDir()
|
||||
if err != nil {
|
||||
return nil, nil, "", noop, err
|
||||
}
|
||||
}
|
||||
return project.DefaultCacheDir()
|
||||
c, err := cache.New(root)
|
||||
if err != nil {
|
||||
return nil, nil, "", noop, err
|
||||
}
|
||||
return c, c, filepath.Join(root, "wheels"), noop, nil
|
||||
}
|
||||
|
||||
func validateBuildFlags(f *buildFlags) error {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ type Config struct {
|
|||
Env []string `toml:"env"`
|
||||
Labels map[string]string `toml:"labels"`
|
||||
FindLinks []string `toml:"find-links"`
|
||||
NoCache bool `toml:"no-cache"`
|
||||
}
|
||||
|
||||
const defaultBase = "cgr.dev/chainguard/python:latest"
|
||||
|
|
@ -182,8 +183,12 @@ func srcModuleDir(root, pkg string) bool {
|
|||
return err == nil && st.IsDir()
|
||||
}
|
||||
|
||||
// DefaultCacheDir returns pymage's per-user cache root.
|
||||
// DefaultCacheDir returns pymage's cache root: $PYMAGE_CACHE_DIR if set,
|
||||
// otherwise a "pymage" directory under the per-user cache dir.
|
||||
func DefaultCacheDir() (string, error) {
|
||||
if v := os.Getenv("PYMAGE_CACHE_DIR"); v != "" {
|
||||
return v, nil
|
||||
}
|
||||
dir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue