mirror of
https://github.com/wagoodman/dive
synced 2026-03-14 22:35:50 +01:00
feat(mcp): implement cache TTL and finalize Phase 3 roadmap
This commit is contained in:
parent
eb4ca70fc2
commit
e05e3bce68
3 changed files with 55 additions and 11 deletions
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
|
|
@ -18,9 +19,14 @@ import (
|
|||
"github.com/wagoodman/dive/internal/log"
|
||||
)
|
||||
|
||||
type cachedAnalysis struct {
|
||||
Analysis *image.Analysis
|
||||
Timestamp time.Time
|
||||
}
|
||||
|
||||
type toolHandlers struct {
|
||||
opts options.MCP
|
||||
analyses *lru.Cache[string, *image.Analysis]
|
||||
analyses *lru.Cache[string, cachedAnalysis]
|
||||
}
|
||||
|
||||
func newToolHandlers(opts options.MCP) *toolHandlers {
|
||||
|
|
@ -28,7 +34,7 @@ func newToolHandlers(opts options.MCP) *toolHandlers {
|
|||
if cacheSize <= 0 {
|
||||
cacheSize = 10
|
||||
}
|
||||
cache, _ := lru.New[string, *image.Analysis](cacheSize)
|
||||
cache, _ := lru.New[string, cachedAnalysis](cacheSize)
|
||||
return &toolHandlers{
|
||||
opts: opts,
|
||||
analyses: cache,
|
||||
|
|
@ -139,11 +145,15 @@ func (h *toolHandlers) getAnalysis(ctx context.Context, imageName string, source
|
|||
|
||||
cacheKey := fmt.Sprintf("%s:%s", sourceStr, imageName)
|
||||
|
||||
if analysis, ok := h.analyses.Get(cacheKey); ok {
|
||||
return analysis, nil
|
||||
if cached, ok := h.analyses.Get(cacheKey); ok {
|
||||
ttl, err := time.ParseDuration(h.opts.CacheTTL)
|
||||
if err == nil && time.Since(cached.Timestamp) < ttl {
|
||||
return cached.Analysis, nil
|
||||
}
|
||||
h.analyses.Remove(cacheKey)
|
||||
}
|
||||
|
||||
log.Infof("Image %s not in cache, analyzing...", imageName)
|
||||
log.Infof("Image %s not in cache or expired, analyzing...", imageName)
|
||||
resolver, err := dive.GetImageResolver(source)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get image resolver: %v", err)
|
||||
|
|
@ -159,7 +169,10 @@ func (h *toolHandlers) getAnalysis(ctx context.Context, imageName string, source
|
|||
return nil, fmt.Errorf("cannot analyze image: %v", err)
|
||||
}
|
||||
|
||||
h.analyses.Add(cacheKey, analysis)
|
||||
h.analyses.Add(cacheKey, cachedAnalysis{
|
||||
Analysis: analysis,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
return analysis, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mark3labs/mcp-go/mcp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
|
@ -77,11 +78,14 @@ func TestHandlers_SandboxViolation(t *testing.T) {
|
|||
|
||||
func TestHandlers_ResourceSummary(t *testing.T) {
|
||||
h := newToolHandlers(options.DefaultMCP())
|
||||
h.analyses.Add("docker:ubuntu:latest", &image.Analysis{
|
||||
Image: "ubuntu:latest",
|
||||
WastedBytes: 0,
|
||||
SizeBytes: 100,
|
||||
Efficiency: 1.0,
|
||||
h.analyses.Add("docker:ubuntu:latest", cachedAnalysis{
|
||||
Analysis: &image.Analysis{
|
||||
Image: "ubuntu:latest",
|
||||
WastedBytes: 0,
|
||||
SizeBytes: 100,
|
||||
Efficiency: 1.0,
|
||||
},
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
|
|
@ -100,6 +104,29 @@ func TestHandlers_ResourceSummary(t *testing.T) {
|
|||
assert.Equal(t, "ubuntu:latest", summary.Image)
|
||||
}
|
||||
|
||||
func TestHandlers_CacheTTL(t *testing.T) {
|
||||
opts := options.DefaultMCP()
|
||||
opts.CacheTTL = "1ms" // Very short TTL
|
||||
h := newToolHandlers(opts)
|
||||
|
||||
h.analyses.Add("docker-archive:invalid.tar", cachedAnalysis{
|
||||
Analysis: &image.Analysis{
|
||||
Image: "invalid.tar",
|
||||
},
|
||||
Timestamp: time.Now().Add(-1 * time.Hour), // Way in the past
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
// This should trigger a real analysis attempts because cache is expired
|
||||
imageName := "invalid.tar"
|
||||
sourceStr := "docker-archive"
|
||||
|
||||
_, err := h.getAnalysis(ctx, imageName, sourceStr)
|
||||
// It should fail to find the file, proving it didn't use the cache
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no such file or directory")
|
||||
}
|
||||
|
||||
func TestHandlers_DiffLayers(t *testing.T) {
|
||||
wd, _ := os.Getwd()
|
||||
root := wd
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ type MCP struct {
|
|||
Sandbox string `yaml:"sandbox" json:"sandbox" mapstructure:"sandbox"`
|
||||
// CacheSize is the maximum number of analysis results to cache
|
||||
CacheSize int `yaml:"cache-size" json:"cache-size" mapstructure:"cache-size"`
|
||||
// CacheTTL is the time analysis results stay in cache before being considered stale
|
||||
CacheTTL string `yaml:"cache-ttl" json:"cache-ttl" mapstructure:"cache-ttl"`
|
||||
}
|
||||
|
||||
func DefaultMCP() MCP {
|
||||
|
|
@ -28,6 +30,7 @@ func DefaultMCP() MCP {
|
|||
Host: "localhost",
|
||||
Port: 8080,
|
||||
CacheSize: 10,
|
||||
CacheTTL: "24h",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -37,4 +40,5 @@ func (o *MCP) AddFlags(flags clio.FlagSet) {
|
|||
flags.IntVarP(&o.Port, "port", "", "The port to listen on for the MCP HTTP/SSE server.")
|
||||
flags.StringVarP(&o.Sandbox, "mcp-sandbox", "", "A directory to restrict docker-archive lookups to.")
|
||||
flags.IntVarP(&o.CacheSize, "mcp-cache-size", "", "The maximum number of analysis results to cache.")
|
||||
flags.StringVarP(&o.CacheTTL, "mcp-cache-ttl", "", "The duration to keep analysis results in cache (e.g. 1h, 30m).")
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue