From b6c6e22857448d25888b439b6e5bc039bc7e5f15 Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Wed, 21 Jan 2026 13:31:37 +1100 Subject: [PATCH] feat: add a --schema flag to dump the HCL schema Current schema: ```hcl // The stable API of the cache server. apiv1 {} // Caches artifacts from an Artifactory server. artifactory target { // List of hostnames to accept for host-based routing. If empty, uses path-based routing only. hosts = [string](optional) } // Caches objects on local disk, with a maximum size limit and LRU eviction disk { // Root directory for the disk storage. root = string // Maximum size of the disk cache in megabytes (defaults to 10GB). limit-mb = number(optional default(10240)) // Maximum time-to-live for entries in the disk cache (defaults to 1 hour). max-ttl = string(optional default("1h")) // Interval at which to check files for eviction (defaults to 1 minute). evict-interval = string(optional default("1m")) } // Caches Git repositories, including bundle and tarball snapshots. git { // Directory to store git clones. mirror-root = string // How often to fetch from upstream in minutes. fetch-interval = string(optional default("15m")) // How long to cache ref checks. ref-check-interval = string(optional default("10s")) // How often to generate bundles. 0 disables bundling. bundle-interval = string(optional default("0")) // Depth for shallow clones. 0 means full clone. clone-depth = number(optional default(0)) } // Caches public and authenticated GitHub releases. github-releases { // GitHub token for authentication. token = string // List of private GitHub organisations. private-orgs = [string] } // A generic host-based proxying strategy. host target {} // Caches objects in memory, with a maximum size limit and LRU eviction memory { // Maximum size of the disk cache in megabytes (defaults to 1GB). limit-mb = number(optional default(1024)) // Maximum time-to-live for entries in the disk cache (defaults to 1 hour). max-ttl = string(optional default("1h")) } // Caches objects in S3 s3 { // S3 bucket name. bucket = string // S3 endpoint URL (e.g., s3.amazonaws.com or localhost:9000). endpoint = string(optional default("s3.amazonaws.com")) // S3 region (defaults to us-west-2). region = string(optional default("us-west-2")) // Use SSL for S3 connections (defaults to true). use-ssl = boolean(optional default(true)) // Skip SSL certificate verification (defaults to false). skip-ssl-verify = boolean(optional default(false)) // Maximum time-to-live for entries in the S3 cache (defaults to 1 hour). max-ttl = string(optional default("1h")) // Number of concurrent workers for multi-part uploads (0 = use all CPU cores, defaults to 1). upload-concurrency = number(optional default(1)) // Size of each part for multi-part uploads in megabytes (defaults to 16MB, minimum 5MB). upload-part-size-mb = number(optional default(16)) } ``` --- cmd/cachewd/main.go | 17 +++++++++++ internal/cache/api.go | 44 +++++++++++++++++++++------- internal/cache/disk.go | 6 +++- internal/cache/memory.go | 6 +++- internal/cache/s3.go | 6 +++- internal/config/config.go | 7 +++++ internal/strategy/api.go | 44 +++++++++++++++++++++------- internal/strategy/apiv1.go | 2 +- internal/strategy/artifactory.go | 2 +- internal/strategy/git/git.go | 2 +- internal/strategy/github_releases.go | 2 +- internal/strategy/host.go | 2 +- 12 files changed, 112 insertions(+), 28 deletions(-) diff --git a/cmd/cachewd/main.go b/cmd/cachewd/main.go index a5baa5f..0eb3eb3 100644 --- a/cmd/cachewd/main.go +++ b/cmd/cachewd/main.go @@ -2,13 +2,16 @@ package main import ( "context" + "fmt" "log/slog" "net" "net/http" "os" + "slices" "strings" "time" + "github.com/alecthomas/hcl/v2" "github.com/alecthomas/kong" "github.com/block/cachew/internal/config" @@ -18,6 +21,8 @@ import ( ) var cli struct { + Schema bool `help:"Print the configuration file schema." xor:"command"` + Config *os.File `hcl:"-" help:"Configuration file path." placeholder:"PATH" required:"" default:"cachew.hcl"` Bind string `hcl:"bind" default:"127.0.0.1:8080" help:"Bind address for the server."` SchedulerConfig jobscheduler.Config `embed:"" prefix:"scheduler-"` @@ -30,6 +35,18 @@ func main() { ctx := context.Background() logger, ctx := logging.Configure(ctx, cli.LoggingConfig) + switch { + case cli.Schema: + schema := config.Schema() + slices.SortStableFunc(schema.Entries, func(a, b hcl.Entry) int { + return strings.Compare(a.EntryKey(), b.EntryKey()) + }) + text, err := hcl.MarshalAST(schema) + kctx.FatalIfErrorf(err) + fmt.Printf("%s\n", text) + return + } + mux := http.NewServeMux() scheduler := jobscheduler.New(ctx, cli.SchedulerConfig) diff --git a/internal/cache/api.go b/internal/cache/api.go index e246471..d69179c 100644 --- a/internal/cache/api.go +++ b/internal/cache/api.go @@ -16,28 +16,52 @@ import ( // ErrNotFound is returned when a cache backend is not found. var ErrNotFound = errors.New("cache backend not found") -var registry = map[string]func(ctx context.Context, config *hcl.Block) (Cache, error){} +type registryEntry struct { + schema *hcl.Block + factory func(ctx context.Context, config *hcl.Block) (Cache, error) +} + +var registry = map[string]registryEntry{} // Factory is a function that creates a new cache instance from the given hcl-tagged configuration struct. type Factory[Config any, C Cache] func(ctx context.Context, config Config) (C, error) // Register a cache factory function. -func Register[Config any, C Cache](id string, factory Factory[Config, C]) { - registry[id] = func(ctx context.Context, config *hcl.Block) (Cache, error) { - var cfg Config - if err := hcl.UnmarshalBlock(config, &cfg); err != nil { - return nil, errors.WithStack(err) - } - return factory(ctx, cfg) +func Register[Config any, C Cache](id, description string, factory Factory[Config, C]) { + var c Config + schema, err := hcl.BlockSchema(id, &c) + if err != nil { + panic(err) + } + block := schema.Entries[0].(*hcl.Block) + block.Comments = hcl.CommentList{description} + registry[id] = registryEntry{ + schema: block, + factory: func(ctx context.Context, config *hcl.Block) (Cache, error) { + var cfg Config + if err := hcl.UnmarshalBlock(config, &cfg); err != nil { + return nil, errors.WithStack(err) + } + return factory(ctx, cfg) + }, + } +} + +// Schema returns the schema for all registered cache backends. +func Schema() *hcl.AST { + ast := &hcl.AST{} + for _, entry := range registry { + ast.Entries = append(ast.Entries, entry.schema) } + return ast } // Create a new cache instance from the given name and configuration. // // Will return "ErrNotFound" if the cache backend is not found. func Create(ctx context.Context, name string, config *hcl.Block) (Cache, error) { - if factory, ok := registry[name]; ok { - return errors.WithStack2(factory(ctx, config)) + if entry, ok := registry[name]; ok { + return errors.WithStack2(entry.factory(ctx, config)) } return nil, errors.Errorf("%s: %w", name, ErrNotFound) } diff --git a/internal/cache/disk.go b/internal/cache/disk.go index cef70f2..f97dd1e 100644 --- a/internal/cache/disk.go +++ b/internal/cache/disk.go @@ -19,7 +19,11 @@ import ( ) func init() { - Register("disk", NewDisk) + Register( + "disk", + "Caches objects on local disk, with a maximum size limit and LRU eviction", + NewDisk, + ) } type DiskConfig struct { diff --git a/internal/cache/memory.go b/internal/cache/memory.go index 6151986..703183c 100644 --- a/internal/cache/memory.go +++ b/internal/cache/memory.go @@ -16,7 +16,11 @@ import ( ) func init() { - Register("memory", NewMemory) + Register( + "memory", + "Caches objects in memory, with a maximum size limit and LRU eviction", + NewMemory, + ) } type MemoryConfig struct { diff --git a/internal/cache/s3.go b/internal/cache/s3.go index c2e38c1..03d6ef8 100644 --- a/internal/cache/s3.go +++ b/internal/cache/s3.go @@ -21,7 +21,11 @@ import ( ) func init() { - Register("s3", NewS3) + Register( + "s3", + "Caches objects in S3", + NewS3, + ) } type S3Config struct { diff --git a/internal/config/config.go b/internal/config/config.go index fdadd31..ed540c2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -35,6 +35,13 @@ func (l *loggingMux) HandleFunc(pattern string, handler func(http.ResponseWriter var _ strategy.Mux = (*loggingMux)(nil) +// Schema returns the configuration file schema. +func Schema() *hcl.AST { + return &hcl.AST{ + Entries: append(strategy.Schema().Entries, cache.Schema().Entries...), + } +} + // Load HCL configuration and uses that to construct the cache backend, and proxy strategies. func Load(ctx context.Context, r io.Reader, scheduler jobscheduler.Scheduler, mux *http.ServeMux, vars map[string]string) error { logger := logging.FromContext(ctx) diff --git a/internal/strategy/api.go b/internal/strategy/api.go index 8dc9afc..2c8c899 100644 --- a/internal/strategy/api.go +++ b/internal/strategy/api.go @@ -20,19 +20,43 @@ type Mux interface { HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) } -var registry = map[string]func(ctx context.Context, config *hcl.Block, scheduler jobscheduler.Scheduler, cache cache.Cache, mux Mux) (Strategy, error){} +type registryEntry struct { + schema *hcl.Block + factory func(ctx context.Context, config *hcl.Block, scheduler jobscheduler.Scheduler, cache cache.Cache, mux Mux) (Strategy, error) +} + +var registry = map[string]registryEntry{} type Factory[Config any, S Strategy] func(ctx context.Context, config Config, scheduler jobscheduler.Scheduler, cache cache.Cache, mux Mux) (S, error) // Register a new proxy strategy. -func Register[Config any, S Strategy](id string, factory Factory[Config, S]) { - registry[id] = func(ctx context.Context, config *hcl.Block, scheduler jobscheduler.Scheduler, cache cache.Cache, mux Mux) (Strategy, error) { - var cfg Config - if err := hcl.UnmarshalBlock(config, &cfg, hcl.AllowExtra(false)); err != nil { - return nil, errors.WithStack(err) - } - return factory(ctx, cfg, scheduler, cache, mux) +func Register[Config any, S Strategy](id, description string, factory Factory[Config, S]) { + var c Config + schema, err := hcl.BlockSchema(id, &c) + if err != nil { + panic(err) + } + block := schema.Entries[0].(*hcl.Block) + block.Comments = hcl.CommentList{description} + registry[id] = registryEntry{ + schema: block, + factory: func(ctx context.Context, config *hcl.Block, scheduler jobscheduler.Scheduler, cache cache.Cache, mux Mux) (Strategy, error) { + var cfg Config + if err := hcl.UnmarshalBlock(config, &cfg, hcl.AllowExtra(false)); err != nil { + return nil, errors.WithStack(err) + } + return factory(ctx, cfg, scheduler, cache, mux) + }, + } +} + +// Schema returns the schema for all registered strategies. +func Schema() *hcl.AST { + ast := &hcl.AST{} + for _, entry := range registry { + ast.Entries = append(ast.Entries, entry.schema) } + return ast } // Create a new proxy strategy. @@ -46,8 +70,8 @@ func Create( cache cache.Cache, mux Mux, ) (Strategy, error) { - if factory, ok := registry[name]; ok { - return errors.WithStack2(factory(ctx, config, scheduler.WithQueuePrefix(name), cache, mux)) + if entry, ok := registry[name]; ok { + return errors.WithStack2(entry.factory(ctx, config, scheduler.WithQueuePrefix(name), cache, mux)) } return nil, errors.Errorf("%s: %w", name, ErrNotFound) } diff --git a/internal/strategy/apiv1.go b/internal/strategy/apiv1.go index c5f5725..215a08b 100644 --- a/internal/strategy/apiv1.go +++ b/internal/strategy/apiv1.go @@ -17,7 +17,7 @@ import ( ) func init() { - Register("apiv1", NewAPIV1) + Register("apiv1", "The stable API of the cache server.", NewAPIV1) } var _ Strategy = (*APIV1)(nil) diff --git a/internal/strategy/artifactory.go b/internal/strategy/artifactory.go index b9fcf8b..a764774 100644 --- a/internal/strategy/artifactory.go +++ b/internal/strategy/artifactory.go @@ -16,7 +16,7 @@ import ( ) func init() { - Register("artifactory", NewArtifactory) + Register("artifactory", "Caches artifacts from an Artifactory server.", NewArtifactory) } // ArtifactoryConfig represents the configuration for the Artifactory strategy. diff --git a/internal/strategy/git/git.go b/internal/strategy/git/git.go index 30c43ae..e2a1f3c 100644 --- a/internal/strategy/git/git.go +++ b/internal/strategy/git/git.go @@ -23,7 +23,7 @@ import ( ) func init() { - strategy.Register("git", New) + strategy.Register("git", "Caches Git repositories, including bundle and tarball snapshots.", New) } type Config struct { diff --git a/internal/strategy/github_releases.go b/internal/strategy/github_releases.go index 16ed815..5a4866f 100644 --- a/internal/strategy/github_releases.go +++ b/internal/strategy/github_releases.go @@ -18,7 +18,7 @@ import ( ) func init() { - Register("github-releases", NewGitHubReleases) + Register("github-releases", "Caches public and authenticated GitHub releases.", NewGitHubReleases) } type GitHubReleasesConfig struct { diff --git a/internal/strategy/host.go b/internal/strategy/host.go index a263151..2d55cae 100644 --- a/internal/strategy/host.go +++ b/internal/strategy/host.go @@ -14,7 +14,7 @@ import ( ) func init() { - Register("host", NewHost) + Register("host", "A generic host-based proxying strategy.", NewHost) } // HostConfig represents the configuration for the Host strategy.