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.