diff --git a/cmd/cachewd/main.go b/cmd/cachewd/main.go index 8a1f5c8..7247a32 100644 --- a/cmd/cachewd/main.go +++ b/cmd/cachewd/main.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "os" + "reflect" "slices" "strings" "time" @@ -46,8 +47,30 @@ var cli struct { } func main() { + kctx, providersConfig := parseConfig() + + ctx := context.Background() + logger, ctx := logging.Configure(ctx, cli.LoggingConfig) + + startServer(ctx, logger, kctx, providersConfig) +} + +func parseConfig() (*kong.Context, *hcl.AST) { + // 1. Get defaults + defaults := struct{ GlobalConfig }{} + _, err := kong.New(&defaults, kong.Exit(func(int) {})) + if err != nil { + fmt.Fprintf(os.Stderr, "Error getting defaults: %v\n", err) + os.Exit(1) + } + + // 2. Parse CLI/env kctx := kong.Parse(&cli, kong.DefaultEnvars("CACHEW")) + // 3. Save CLI/env values that differ from defaults (these take precedence) + saved := saveNonDefaultValues(&cli.GlobalConfig, &defaults.GlobalConfig) + + // 4. Parse and unmarshal HCL (this overwrites cli.GlobalConfig) ast, err := hcl.Parse(cli.Config) kctx.FatalIfErrorf(err) @@ -56,9 +79,13 @@ func main() { err = hcl.UnmarshalAST(globalConfig, &cli.GlobalConfig) kctx.FatalIfErrorf(err) - ctx := context.Background() - logger, ctx := logging.Configure(ctx, cli.LoggingConfig) + // 5. Restore CLI/env values (precedence: defaults < HCL < env < CLI) + restoreValues(&cli.GlobalConfig, saved) + return kctx, providersConfig +} + +func startServer(ctx context.Context, logger *slog.Logger, kctx *kong.Context, providersConfig *hcl.AST) { scheduler := jobscheduler.New(ctx, cli.SchedulerConfig) cr := cache.NewRegistry() @@ -107,11 +134,11 @@ func main() { _, _ = w.Write([]byte("OK")) //nolint:errcheck }) - err = config.Load(ctx, cr, sr, providersConfig, mux, parseEnvars()) + 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") + metricsClient, metricsErr := metrics.New(ctx, cli.MetricsConfig) + kctx.FatalIfErrorf(metricsErr, "failed to create metrics client") defer func() { if err := metricsClient.Close(); err != nil { logger.ErrorContext(ctx, "failed to close metrics client", "error", err) @@ -160,3 +187,79 @@ func parseEnvars() map[string]string { } return envars } + +// buildFieldPath constructs a dot-separated field path. +func buildFieldPath(path, fieldName string) string { + if path != "" { + return path + "." + fieldName + } + return fieldName +} + +// saveNonDefaultValues recursively saves field values that differ from defaults. +// Returns a map of field paths to their values. +func saveNonDefaultValues(target, defaults *GlobalConfig) map[string]any { + saved := make(map[string]any) + saveFieldValues(reflect.ValueOf(target).Elem(), reflect.ValueOf(defaults).Elem(), "", saved) + return saved +} + +func saveFieldValues(targetVal, defaultsVal reflect.Value, path string, saved map[string]any) { + targetType := targetVal.Type() + + for i := range targetVal.NumField() { + field := targetType.Field(i) + targetField := targetVal.Field(i) + defaultField := defaultsVal.Field(i) + + // Skip unexported fields + if !targetField.CanSet() { + continue + } + + fieldPath := buildFieldPath(path, field.Name) + + // If the field is a struct, recurse into it + if targetField.Kind() == reflect.Struct { + saveFieldValues(targetField, defaultField, fieldPath, saved) + continue + } + + // If the field differs from default, save it + if !reflect.DeepEqual(targetField.Interface(), defaultField.Interface()) { + saved[fieldPath] = targetField.Interface() + } + } +} + +// restoreValues recursively restores saved values back into the target struct. +func restoreValues(target *GlobalConfig, saved map[string]any) { + restoreFieldValues(reflect.ValueOf(target).Elem(), "", saved) +} + +func restoreFieldValues(targetVal reflect.Value, path string, saved map[string]any) { + targetType := targetVal.Type() + + for i := range targetVal.NumField() { + field := targetType.Field(i) + targetField := targetVal.Field(i) + + // Skip unexported fields + if !targetField.CanSet() { + continue + } + + fieldPath := buildFieldPath(path, field.Name) + + // If the field is a struct, recurse into it + if targetField.Kind() == reflect.Struct { + restoreFieldValues(targetField, fieldPath, saved) + continue + } + + // If we have a saved value for this field, restore it + if savedValue, ok := saved[fieldPath]; ok { + targetField.Set(reflect.ValueOf(savedValue)) + } + } +}