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
2 changes: 1 addition & 1 deletion cachew.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
# }

url = "http://127.0.0.1:8080"
logging {
log {
level = "debug"
}

Expand Down
105 changes: 63 additions & 42 deletions cmd/cachewd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,68 @@ 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-"`
LoggingConfig logging.Config `embed:"" hcl:"log,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"`
Config kong.ConfigFlag `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"))
kctx := kong.Parse(&cli, kong.DefaultEnvars("CACHEW"), kong.Configuration(config.KongLoader[GlobalConfig], "cachew.hcl"))

ast, err := hcl.Parse(cli.Config)
configReader, err := os.Open(string(cli.Config))
kctx.FatalIfErrorf(err)
defer configReader.Close()

globalConfig, providersConfig := config.Split[GlobalConfig](ast)

err = hcl.UnmarshalAST(globalConfig, &cli.GlobalConfig)
ast, err := hcl.Parse(configReader)
kctx.FatalIfErrorf(err)

_, providersConfig := config.Split[GlobalConfig](ast)

ctx := context.Background()
logger, ctx := logging.Configure(ctx, cli.LoggingConfig)

scheduler := jobscheduler.New(ctx, cli.SchedulerConfig)

cr, sr := newRegistries(scheduler)

// Commands
switch { //nolint:gocritic
case cli.Schema:
printSchema(kctx, cr, sr)
return
}

mux, err := newMux(ctx, cr, sr, providersConfig)
kctx.FatalIfErrorf(err)

metricsClient, err := metrics.New(ctx, cli.MetricsConfig)
kctx.FatalIfErrorf(err, "failed to create metrics client")
defer func() {
if err := metricsClient.Close(); err != nil {
logger.ErrorContext(ctx, "failed to close metrics client", "error", err)
}
}()

if err := metricsClient.ServeMetrics(ctx); err != nil {
kctx.FatalIfErrorf(err, "failed to start metrics server")
}

logger.InfoContext(ctx, "Starting cachewd", slog.String("bind", cli.Bind))

server := newServer(ctx, logger, mux)
err = server.ListenAndServe()
kctx.FatalIfErrorf(err)
}

func newRegistries(scheduler jobscheduler.Scheduler) (*cache.Registry, *strategy.Registry) {
cr := cache.NewRegistry()
cache.RegisterMemory(cr)
cache.RegisterDisk(cr)
Expand All @@ -75,28 +108,28 @@ func main() {
git.Register(sr, scheduler)
gomod.Register(sr)

// Commands
switch { //nolint:gocritic
case cli.Schema:
schema := config.Schema[GlobalConfig](cr, sr)
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)
return cr, sr
}

if fileInfo, err := os.Stdout.Stat(); err == nil && (fileInfo.Mode()&os.ModeCharDevice) != 0 {
err = quick.Highlight(os.Stdout, string(text), "terraform", "terminal256", "solarized")
kctx.FatalIfErrorf(err)
} else {
fmt.Printf("%s\n", text) //nolint:forbidigo
}
return
func printSchema(kctx *kong.Context, cr *cache.Registry, sr *strategy.Registry) {
schema := config.Schema[GlobalConfig](cr, sr)
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)

if fileInfo, err := os.Stdout.Stat(); err == nil && (fileInfo.Mode()&os.ModeCharDevice) != 0 {
err = quick.Highlight(os.Stdout, string(text), "terraform", "terminal256", "solarized")
kctx.FatalIfErrorf(err)
} else {
fmt.Printf("%s\n", text) //nolint:forbidigo
}
}

func newMux(ctx context.Context, cr *cache.Registry, sr *strategy.Registry, providersConfig *hcl.AST) (*http.ServeMux, error) {
mux := http.NewServeMux()

// Health check endpoints
mux.HandleFunc("GET /_liveness", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("OK")) //nolint:errcheck
Expand All @@ -107,23 +140,14 @@ func main() {
_, _ = w.Write([]byte("OK")) //nolint:errcheck
})

err = config.Load(ctx, cr, sr, providersConfig, mux, parseEnvars())
kctx.FatalIfErrorf(err)

metricsClient, err := metrics.New(ctx, cli.MetricsConfig)
kctx.FatalIfErrorf(err, "failed to create metrics client")
defer func() {
if err := metricsClient.Close(); err != nil {
logger.ErrorContext(ctx, "failed to close metrics client", "error", err)
}
}()

if err := metricsClient.ServeMetrics(ctx); err != nil {
kctx.FatalIfErrorf(err, "failed to start metrics server")
if err := config.Load(ctx, cr, sr, providersConfig, mux, parseEnvars()); err != nil {
return nil, fmt.Errorf("load config: %w", err)
}

logger.InfoContext(ctx, "Starting cachewd", slog.String("bind", cli.Bind))
return mux, nil
}

func newServer(ctx context.Context, logger *slog.Logger, mux *http.ServeMux) *http.Server {
var handler http.Handler = mux

handler = otelhttp.NewMiddleware(cli.MetricsConfig.ServiceName,
Expand All @@ -133,7 +157,7 @@ func main() {

handler = httputil.LoggingMiddleware(handler)

server := &http.Server{
return &http.Server{
Addr: cli.Bind,
Handler: handler,
ReadTimeout: 30 * time.Minute,
Expand All @@ -146,9 +170,6 @@ func main() {
return logging.ContextWithLogger(ctx, logger.With("client", c.RemoteAddr().String()))
},
}

err = server.ListenAndServe()
kctx.FatalIfErrorf(err)
}

func parseEnvars() map[string]string {
Expand Down
119 changes: 119 additions & 0 deletions internal/config/kong.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package config

import (
"fmt"
"io"
"strings"

"github.com/alecthomas/hcl/v2"
"github.com/alecthomas/kong"
)

func KongLoader[GlobalConfig any](r io.Reader) (kong.Resolver, error) {
ast, err := hcl.Parse(r)
if err != nil {
return nil, fmt.Errorf("failed to parse HCL: %w", err)
}
return &kongResolver{flattenHCL(ast)}, nil
}

type kongResolver struct {
values map[string]any
}

var _ kong.Resolver = (*kongResolver)(nil)

func (k *kongResolver) Resolve(_ *kong.Context, _ *kong.Path, flag *kong.Flag) (any, error) {
name := strings.ReplaceAll(flag.Name, "-", "_")
if v, ok := k.values[name]; ok {
return v, nil
}
if v, ok := k.values[flag.Name]; ok {
return v, nil
}
return nil, nil //nolint:nilnil
}

func (k *kongResolver) Validate(_ *kong.Application) error { return nil }

// Convert HCL AST to a flattened map of key to value. Each hierarchy in the HCL joined by "-".
//
// eg.
//
// block {
// value = "foo"
// }
//
// block-with-label label {
// value = "foo"
// }
//
// Would flatten to:
//
// block-value = "foo"
// block-with-label-label = "foo"
func flattenHCL(node hcl.Node) map[string]any {
out := map[string]any{}
flattenNode(out, "", node)
return out
}

func flattenNode(out map[string]any, prefix string, node hcl.Node) {
switch node := node.(type) {
case *hcl.AST:
for _, entry := range node.Entries {
flattenNode(out, prefix, entry)
}

case *hcl.Block:
parts := make([]string, 0, 1+len(node.Labels))
parts = append(parts, node.Name)
parts = append(parts, node.Labels...)
key := strings.Join(parts, "-")
if prefix != "" {
key = prefix + "-" + key
}
for _, entry := range node.Body {
flattenNode(out, key, entry)
}

case *hcl.Attribute:
key := node.Key
if prefix != "" {
key = prefix + "-" + key
}
out[key] = hclValue(node.Value)
}
}

func hclValue(v hcl.Value) any {
switch v := v.(type) {
case *hcl.String:
return v.Str
case *hcl.Number:
if v.Float.IsInt() {
i, _ := v.Float.Int64()
return i
}
f, _ := v.Float.Float64()
return f
case *hcl.Bool:
return v.Bool
case *hcl.List:
out := make([]any, len(v.List))
for i, item := range v.List {
out[i] = hclValue(item)
}
return out
case *hcl.Map:
out := map[string]any{}
for _, entry := range v.Entries {
out[fmt.Sprintf("%v", hclValue(entry.Key))] = hclValue(entry.Value)
}
return out
case *hcl.Heredoc:
return v.GetHeredoc()
default:
return nil
}
}
Loading