package envpool import ( "context" "crypto/rand" "encoding/hex" "errors" "fmt" "os/exec" "strings" "sync" "time" ) type CommandRunner interface { CombinedOutput(ctx context.Context, name string, args ...string) ([]byte, error) } type execRunner struct{} func (r execRunner) CombinedOutput( ctx context.Context, name string, args ...string, ) ([]byte, error) { cmd := exec.CommandContext(ctx, name, args...) return cmd.CombinedOutput() } type Pool struct { runner CommandRunner cache map[string]cacheEntry imagePrefix string cacheTTL time.Duration cacheMu sync.Mutex } type cacheEntry struct { expires time.Time exists bool } func New(imagePrefix string) *Pool { prefix := strings.TrimSpace(imagePrefix) if prefix == "" { prefix = "fetchml-prewarm" } return &Pool{ runner: execRunner{}, imagePrefix: prefix, cache: make(map[string]cacheEntry), cacheTTL: 30 * time.Second, } } func (p *Pool) WithRunner(r CommandRunner) *Pool { if r != nil { p.runner = r } return p } func (p *Pool) WithCacheTTL(ttl time.Duration) *Pool { if ttl > 0 { p.cacheTTL = ttl } return p } func (p *Pool) WarmImageTag(depsManifestSHA256 string) (string, error) { sha := strings.TrimSpace(depsManifestSHA256) if sha == "" { return "", fmt.Errorf("missing deps sha256") } if !isLowerHexLen(sha, 64) { return "", fmt.Errorf("invalid deps sha256") } return fmt.Sprintf("%s:%s", p.imagePrefix, sha[:12]), nil } func (p *Pool) ImageExists(ctx context.Context, imageRef string) (bool, error) { ref := strings.TrimSpace(imageRef) if ref == "" { return false, fmt.Errorf("missing image ref") } p.cacheMu.Lock() if ent, ok := p.cache[ref]; ok && time.Now().Before(ent.expires) { exists := ent.exists p.cacheMu.Unlock() return exists, nil } p.cacheMu.Unlock() out, err := p.runner.CombinedOutput(ctx, "podman", "image", "inspect", ref) if err == nil { p.setCache(ref, true) return true, nil } if looksLikeImageNotFound(out) { p.setCache(ref, false) return false, nil } var ee *exec.ExitError if errors.As(err, &ee) { p.setCache(ref, false) return false, nil } return false, err } func looksLikeImageNotFound(out []byte) bool { s := strings.ToLower(strings.TrimSpace(string(out))) if s == "" { return false } return strings.Contains(s, "no such") || strings.Contains(s, "not found") || strings.Contains(s, "does not exist") } func (p *Pool) setCache(imageRef string, exists bool) { p.cacheMu.Lock() p.cache[imageRef] = cacheEntry{exists: exists, expires: time.Now().Add(p.cacheTTL)} p.cacheMu.Unlock() } type PrepareRequest struct { BaseImage string TargetImage string HostWorkspace string ContainerWorkspace string DepsPathInContainer string } func (p *Pool) PruneImages(ctx context.Context, olderThan time.Duration) error { if olderThan <= 0 { return fmt.Errorf("invalid olderThan") } h := int(olderThan.Round(time.Hour).Hours()) if h < 1 { h = 1 } until := fmt.Sprintf("%dh", h) _, err := p.runner.CombinedOutput( ctx, "podman", "image", "prune", "-a", "-f", "--filter", "label=fetchml.prewarm=true", "--filter", "until="+until, ) return err } func (p *Pool) Prepare(ctx context.Context, req PrepareRequest) error { baseImage := strings.TrimSpace(req.BaseImage) targetImage := strings.TrimSpace(req.TargetImage) hostWS := strings.TrimSpace(req.HostWorkspace) containerWS := strings.TrimSpace(req.ContainerWorkspace) depsInContainer := strings.TrimSpace(req.DepsPathInContainer) if baseImage == "" { return fmt.Errorf("missing base image") } if targetImage == "" { return fmt.Errorf("missing target image") } if hostWS == "" { return fmt.Errorf("missing host workspace") } if containerWS == "" { return fmt.Errorf("missing container workspace") } if depsInContainer == "" { return fmt.Errorf("missing deps path") } if !strings.HasPrefix(depsInContainer, containerWS) { return fmt.Errorf("deps path must be under container workspace") } exists, err := p.ImageExists(ctx, targetImage) if err != nil { return err } if exists { return nil } containerName, err := randomContainerName("fetchml-prewarm") if err != nil { return err } // Do not use --rm since we need a container to commit. runArgs := []string{ "run", "--name", containerName, "--security-opt", "no-new-privileges", "--cap-drop", "ALL", "--userns", "keep-id", "-v", fmt.Sprintf("%s:%s:rw", hostWS, containerWS), baseImage, "--workspace", containerWS, "--deps", depsInContainer, "--prepare-only", } if out, err := p.runner.CombinedOutput(ctx, "podman", runArgs...); err != nil { _ = p.cleanupContainer(context.Background(), containerName) return fmt.Errorf("podman run prewarm failed: %w", scrubOutput(out, err)) } if out, err := p.runner.CombinedOutput( ctx, "podman", "commit", containerName, targetImage, ); err != nil { _ = p.cleanupContainer(context.Background(), containerName) return fmt.Errorf("podman commit prewarm failed: %w", scrubOutput(out, err)) } _, _ = p.runner.CombinedOutput( ctx, "podman", "image", "label", targetImage, "fetchml.prewarm=true", ) _ = p.cleanupContainer(context.Background(), containerName) p.setCache(targetImage, true) return nil } func (p *Pool) cleanupContainer(ctx context.Context, name string) error { n := strings.TrimSpace(name) if n == "" { return nil } _, err := p.runner.CombinedOutput(ctx, "podman", "rm", n) return err } func randomContainerName(prefix string) (string, error) { p := strings.TrimSpace(prefix) if p == "" { p = "fetchml-prewarm" } b := make([]byte, 6) if _, err := rand.Read(b); err != nil { return "", err } return fmt.Sprintf("%s-%s", p, hex.EncodeToString(b)), nil } func isLowerHexLen(s string, want int) bool { if len(s) != want { return false } for i := 0; i < len(s); i++ { c := s[i] if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') { continue } return false } return true } func scrubOutput(out []byte, err error) error { if len(out) == 0 { return err } s := strings.TrimSpace(string(out)) if len(s) > 400 { s = s[:400] } return fmt.Errorf("%w (output=%q)", err, s) }