Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion internal/strategy/hermit.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import (
"log/slog"
"net/http"
"net/url"
"regexp"
"strings"
"time"

"github.com/alecthomas/errors"

Expand All @@ -23,8 +25,14 @@ func RegisterHermit(r *Registry) {

const defaultGitHubBaseURL = "http://127.0.0.1:8080/github.com"

// hermitBinaryPattern matches the Hermit self-update binary (e.g. hermit-linux-amd64.gz).
// Unlike versioned package downloads, this URL is mutable — the same path serves
// different versions over time — so it needs a short cache TTL.
var hermitBinaryPattern = regexp.MustCompile(`/hermit-[a-z]+-[a-z0-9]+\.gz$`)

type HermitConfig struct {
GitHubBaseURL string `hcl:"github-base-url" help:"Base URL for GitHub release redirects" default:"http://127.0.0.1:8080/github.com"`
GitHubBaseURL string `hcl:"github-base-url" help:"Base URL for GitHub release redirects" default:"http://127.0.0.1:8080/github.com"`
BinaryTTL time.Duration `hcl:"binary-ttl,optional" help:"Cache TTL for the mutable Hermit self-update binary" default:"1h"`
}

// Hermit caches Hermit package downloads.
Expand Down Expand Up @@ -76,6 +84,12 @@ func (s *Hermit) createDirectHandler(c cache.Cache) http.Handler {
CacheKey(func(r *http.Request) string {
return s.buildOriginalURL(r)
}).
TTL(func(r *http.Request) time.Duration {
if hermitBinaryPattern.MatchString(r.URL.Path) {
return s.config.BinaryTTL
}
return 0
}).
Transform(func(r *http.Request) (*http.Request, error) {
return s.buildDirectRequest(r)
})
Expand Down
53 changes: 53 additions & 0 deletions internal/strategy/hermit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,59 @@ func setupHermitTest(t *testing.T) (*http.ServeMux, context.Context, cache.Cache
return mux, ctx, memCache
}

func TestHermitBinaryShortTTL(t *testing.T) {
httpTransportMutexHermit.Lock()
defer httpTransportMutexHermit.Unlock()

callCount := 0
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
callCount++
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("hermit-binary"))
}))
defer backend.Close()

originalTransport := http.DefaultTransport
defer func() { http.DefaultTransport = originalTransport }() //nolint:reassign
http.DefaultTransport = &mockTransport{backend: backend, originalTransport: originalTransport} //nolint:reassign

_, ctx := logging.Configure(context.Background(), logging.Config{Level: slog.LevelError})
memCache, err := cache.NewMemory(ctx, cache.MemoryConfig{MaxTTL: 100 * time.Millisecond})
assert.NoError(t, err)
t.Cleanup(func() { memCache.Close() })

mux := http.NewServeMux()
_, err = strategy.NewHermit(ctx, strategy.HermitConfig{
GitHubBaseURL: "http://localhost:8080",
BinaryTTL: 100 * time.Millisecond,
}, nil, memCache, mux)
assert.NoError(t, err)

// First request: cache miss
req1 := httptest.NewRequestWithContext(ctx, http.MethodGet, "/hermit/example.com/square/hermit-linux-amd64.gz", nil)
w1 := httptest.NewRecorder()
mux.ServeHTTP(w1, req1)
assert.Equal(t, http.StatusOK, w1.Code)
assert.Equal(t, 1, callCount)

// Second request: cache hit
req2 := httptest.NewRequestWithContext(ctx, http.MethodGet, "/hermit/example.com/square/hermit-linux-amd64.gz", nil)
w2 := httptest.NewRecorder()
mux.ServeHTTP(w2, req2)
assert.Equal(t, http.StatusOK, w2.Code)
assert.Equal(t, 1, callCount, "should be served from cache")

// Wait for TTL to expire
time.Sleep(150 * time.Millisecond)

// Third request: cache miss after TTL expiry
req3 := httptest.NewRequestWithContext(ctx, http.MethodGet, "/hermit/example.com/square/hermit-linux-amd64.gz", nil)
w3 := httptest.NewRecorder()
mux.ServeHTTP(w3, req3)
assert.Equal(t, http.StatusOK, w3.Code)
assert.Equal(t, 2, callCount, "should re-fetch after TTL expiry")
}

func TestHermitNonGitHubCaching(t *testing.T) {
httpTransportMutexHermit.Lock()
defer httpTransportMutexHermit.Unlock()
Expand Down