From 73c99fb6ceb434bd54603049781a13c9181f656e Mon Sep 17 00:00:00 2001 From: Alec Thomas Date: Fri, 6 Feb 2026 14:45:11 +1100 Subject: [PATCH] feat: allow global configuration to be defined in HCL Previously this had to always be passed by flag or envar, or duplicated in every provider. This allows us to push shared configuration out of the providers, such as eg. the cloner configuration. --- .golangci.yml | 2 +- cachew.hcl | 1 + cmd/cachewd/main.go | 30 +++++++++++++++------ go.mod | 2 +- go.sum | 4 +-- internal/cache/api.go | 5 ++++ internal/config/config.go | 56 ++++++++++++++++++++++++++++++++------- internal/strategy/api.go | 5 ++++ 8 files changed, 83 insertions(+), 22 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index b64d8d0..aff0fb5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -58,7 +58,7 @@ linters: - depguard # checks if package imports are in a list of acceptable packages - dupl # tool for code clone detection - durationcheck # checks for two durations multiplied together - - embeddedstructfieldcheck # checks embedded types in structs + # - embeddedstructfieldcheck # checks embedded types in structs - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases - errname # checks that sentinel errors are prefixed with the Err and error types are suffixed with the Error - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 diff --git a/cachew.hcl b/cachew.hcl index 35bd5b9..f7b1c9f 100644 --- a/cachew.hcl +++ b/cachew.hcl @@ -7,6 +7,7 @@ # target = "https://example.jfrog.io" # } +url = "http://127.0.0.1:8080" git { mirror-root = "./state/git-mirrors" diff --git a/cmd/cachewd/main.go b/cmd/cachewd/main.go index 076bcfa..8a1f5c8 100644 --- a/cmd/cachewd/main.go +++ b/cmd/cachewd/main.go @@ -28,20 +28,34 @@ import ( "github.com/block/cachew/internal/strategy/gomod" ) +type GlobalConfig struct { + Bind string `hcl:"bind" default:"127.0.0.1:8080" help:"Bind address for the server."` + URL string `hcl:"url" default:"http://127.0.0.1:8080/" help:"Base URL for cachewd."` + SchedulerConfig jobscheduler.Config `embed:"" hcl:"scheduler,block" prefix:"scheduler-"` + LoggingConfig logging.Config `embed:"" hcl:"logging,block" prefix:"log-"` + MetricsConfig metrics.Config `embed:"" hcl:"metrics,block" prefix:"metrics-"` +} + 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."` - URL string `hcl:"url" default:"http://127.0.0.1:8080/" help:"Base URL for cachewd."` - SchedulerConfig jobscheduler.Config `embed:"" prefix:"scheduler-"` - LoggingConfig logging.Config `embed:"" prefix:"log-"` - MetricsConfig metrics.Config `embed:"" prefix:"metrics-"` + Config *os.File `hcl:"-" help:"Configuration file path." placeholder:"PATH" required:"" default:"cachew.hcl"` + + // GlobalConfig accepts command-line, but can also be parsed from HCL. + GlobalConfig } func main() { kctx := kong.Parse(&cli, kong.DefaultEnvars("CACHEW")) + ast, err := hcl.Parse(cli.Config) + kctx.FatalIfErrorf(err) + + globalConfig, providersConfig := config.Split[GlobalConfig](ast) + + err = hcl.UnmarshalAST(globalConfig, &cli.GlobalConfig) + kctx.FatalIfErrorf(err) + ctx := context.Background() logger, ctx := logging.Configure(ctx, cli.LoggingConfig) @@ -64,7 +78,7 @@ func main() { // Commands switch { //nolint:gocritic case cli.Schema: - schema := config.Schema(cr, sr) + schema := config.Schema[GlobalConfig](cr, sr) slices.SortStableFunc(schema.Entries, func(a, b hcl.Entry) int { return strings.Compare(a.EntryKey(), b.EntryKey()) }) @@ -93,7 +107,7 @@ func main() { _, _ = w.Write([]byte("OK")) //nolint:errcheck }) - err := config.Load(ctx, cr, sr, cli.Config, mux, parseEnvars()) + err = config.Load(ctx, cr, sr, providersConfig, mux, parseEnvars()) kctx.FatalIfErrorf(err) metricsClient, err := metrics.New(ctx, cli.MetricsConfig) diff --git a/go.mod b/go.mod index 00d68a5..e1a978a 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/block/cachew go 1.25.5 require ( - github.com/alecthomas/hcl/v2 v2.4.0 + github.com/alecthomas/hcl/v2 v2.5.0 github.com/alecthomas/kong v1.13.0 github.com/goproxy/goproxy v0.25.0 github.com/lmittmann/tint v1.1.2 diff --git a/go.sum b/go.sum index 2dd5ffe..4fb62b8 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= github.com/alecthomas/errors v0.9.1 h1:JNXtU30rtMNARCkW41OTZ4yL6Lyocq20xIJgIw2raqI= github.com/alecthomas/errors v0.9.1/go.mod h1:l8mjMEHMGUdIWPMNtvDyRYPVS1fQFXHFXc/iVCCLGkI= -github.com/alecthomas/hcl/v2 v2.4.0 h1:j7sPnff/f6FLAPTZmpFzHS2ENwE/dHj6K40bRb9nk4g= -github.com/alecthomas/hcl/v2 v2.4.0/go.mod h1:4UUp66q8ony5j8tm2bANErujUpZ3GgHBLgaKxTUQlQI= +github.com/alecthomas/hcl/v2 v2.5.0 h1:0L0oGrZPHokiXaKtsEcLa3hBjfVrRLUUK3u5vXQSybg= +github.com/alecthomas/hcl/v2 v2.5.0/go.mod h1:4UUp66q8ony5j8tm2bANErujUpZ3GgHBLgaKxTUQlQI= github.com/alecthomas/kong v1.13.0 h1:5e/7XC3ugvhP1DQBmTS+WuHtCbcv44hsohMgcvVxSrA= github.com/alecthomas/kong v1.13.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I= github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U= diff --git a/internal/cache/api.go b/internal/cache/api.go index a676053..1ad68ad 100644 --- a/internal/cache/api.go +++ b/internal/cache/api.go @@ -67,6 +67,11 @@ func (r *Registry) Schema() *hcl.AST { return ast } +func (r *Registry) Exists(name string) bool { + _, ok := r.registry[name] + return ok +} + // Create a new cache instance from the given name and configuration. // // Will return "ErrNotFound" if the cache backend is not found. diff --git a/internal/config/config.go b/internal/config/config.go index 68893af..935b389 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,7 +3,6 @@ package config import ( "context" - "io" "log/slog" "net/http" "os" @@ -36,27 +35,64 @@ func (l *loggingMux) HandleFunc(pattern string, handler func(http.ResponseWriter var _ strategy.Mux = (*loggingMux)(nil) // Schema returns the configuration file schema. -func Schema(cr *cache.Registry, sr *strategy.Registry) *hcl.AST { +func Schema[GlobalConfig any](cr *cache.Registry, sr *strategy.Registry) *hcl.AST { + globalSchema, err := hcl.Schema(new(GlobalConfig)) + if err != nil { + panic(err) + } return &hcl.AST{ - Entries: append(sr.Schema().Entries, cr.Schema().Entries...), + Entries: append(globalSchema.Entries, append(sr.Schema().Entries, cr.Schema().Entries...)...), + } +} + +// Split configuration into global config and provider-specific config. +// +// At this point we don't know what config the providers require, so we just pull out the global config and assume +// everything else is for the providers. +func Split[GlobalConfig any](ast *hcl.AST) (global, providers *hcl.AST) { + globalSchema, err := hcl.Schema(new(GlobalConfig)) + if err != nil { + panic(err) + } + + globals := map[string]bool{} + for _, entry := range globalSchema.Entries { + switch entry.(type) { + case *hcl.Attribute, *hcl.Block: + globals[entry.EntryKey()] = true + } + } + + global = &hcl.AST{Pos: ast.Pos} + providers = &hcl.AST{Pos: ast.Pos} + + for _, node := range ast.Entries { + switch node := node.(type) { + case *hcl.Block: + if globals[node.Name] { + global.Entries = append(global.Entries, node.Body...) + } else { + providers.Entries = append(providers.Entries, node) + } + + case *hcl.Attribute: // Attributes are always for the global config + global.Entries = append(global.Entries, node) + } } + + return global, providers } -// Load HCL configuration and uses that to construct the cache backend, and proxy strategies. +// Load HCL configuration and use that to construct the cache backend, and proxy strategies. func Load( ctx context.Context, cr *cache.Registry, sr *strategy.Registry, - r io.Reader, + ast *hcl.AST, mux *http.ServeMux, vars map[string]string, ) error { logger := logging.FromContext(ctx) - ast, err := hcl.Parse(r) - if err != nil { - return errors.WithStack(err) - } - expandVars(ast, vars) strategyCandidates := []*hcl.Block{ diff --git a/internal/strategy/api.go b/internal/strategy/api.go index 90ecb31..c2ef959 100644 --- a/internal/strategy/api.go +++ b/internal/strategy/api.go @@ -70,6 +70,11 @@ func (r *Registry) Schema() *hcl.AST { return ast } +func (r *Registry) Exists(name string) bool { + _, ok := r.registry[name] + return ok +} + // Create a new proxy strategy. // // Will return "ErrNotFound" if the strategy is not found.