From accd7e75d81acd569992f48eaf5fab6e0053eba8 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Tue, 28 Oct 2025 19:23:39 +0700 Subject: [PATCH] Handle config options --- cmd/repeater/main.go | 9 ++++- internal/config/cli.go | 24 ++++++++++++ internal/config/config.go | 79 +++++++++++++++++++++++++++++++++++++++ internal/config/custom.go | 27 +++++++++++++ internal/config/env.go | 21 +++++++++++ internal/config/file.go | 33 ++++++++++++++++ 6 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 internal/config/cli.go create mode 100644 internal/config/config.go create mode 100644 internal/config/custom.go create mode 100644 internal/config/env.go create mode 100644 internal/config/file.go diff --git a/cmd/repeater/main.go b/cmd/repeater/main.go index 20752c8..aee995e 100644 --- a/cmd/repeater/main.go +++ b/cmd/repeater/main.go @@ -8,12 +8,19 @@ import ( "syscall" "github.com/nemunaire/repeater/internal/app" + "github.com/nemunaire/repeater/internal/config" ) //go:embed all:static var assets embed.FS func main() { + // Load and parse options + cfg, err := config.ConsolidateConfig() + if err != nil { + log.Fatal(err) + } + // Create application instance application := app.New(assets) @@ -34,7 +41,7 @@ func main() { }() // Start the server - if err := application.Run(":8080"); err != nil { + if err := application.Run(cfg.Bind); err != nil { log.Fatalf("Failed to start server: %v", err) } } diff --git a/internal/config/cli.go b/internal/config/cli.go new file mode 100644 index 0000000..0cb90c6 --- /dev/null +++ b/internal/config/cli.go @@ -0,0 +1,24 @@ +package config + +import ( + "flag" +) + +// declareFlags registers flags for the structure Options. +func declareFlags(o *Config) { + flag.StringVar(&o.Bind, "bind", ":8081", "Bind port/socket") +} + +// parseCLI parse the flags and treats extra args as configuration filename. +func parseCLI(o *Config) error { + flag.Parse() + + for _, conf := range flag.Args() { + err := parseFile(o, conf) + if err != nil { + return err + } + } + + return nil +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..aed3693 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,79 @@ +package config + +import ( + "flag" + "log" + "os" + "path" + "strings" +) + +type Config struct { + Bind string +} + +// ConsolidateConfig fills an Options struct by reading configuration from +// config files, environment, then command line. +// +// Should be called only one time. +func ConsolidateConfig() (opts *Config, err error) { + // Define defaults options + opts = &Config{ + Bind: ":8080", + } + + declareFlags(opts) + + // Establish a list of possible configuration file locations + configLocations := []string{ + "repeater.conf", + } + + if home, err := os.UserConfigDir(); err == nil { + configLocations = append(configLocations, path.Join(home, "repeater", "repeater.conf")) + } + + configLocations = append(configLocations, path.Join("etc", "repeater.conf")) + + // If config file exists, read configuration from it + for _, filename := range configLocations { + if _, e := os.Stat(filename); !os.IsNotExist(e) { + log.Printf("Loading configuration from %s\n", filename) + err = parseFile(opts, filename) + if err != nil { + return + } + break + } + } + + // Then, overwrite that by what is present in the environment + err = parseEnvironmentVariables(opts) + if err != nil { + return + } + + // Finaly, command line takes precedence + err = parseCLI(opts) + if err != nil { + return + } + + return +} + +// parseLine treats a config line and place the read value in the variable +// declared to the corresponding flag. +func parseLine(o *Config, line string) (err error) { + fields := strings.SplitN(line, "=", 2) + orig_key := strings.TrimSpace(fields[0]) + value := strings.TrimSpace(fields[1]) + + key := strings.TrimPrefix(orig_key, "REPEATER_") + key = strings.Replace(key, "_", "-", -1) + key = strings.ToLower(key) + + err = flag.Set(key, value) + + return +} diff --git a/internal/config/custom.go b/internal/config/custom.go new file mode 100644 index 0000000..71428fc --- /dev/null +++ b/internal/config/custom.go @@ -0,0 +1,27 @@ +package config + +import ( + "net/url" +) + +type URL struct { + URL *url.URL +} + +func (i *URL) String() string { + if i.URL != nil { + return i.URL.String() + } else { + return "" + } +} + +func (i *URL) Set(value string) error { + u, err := url.Parse(value) + if err != nil { + return err + } + + *i.URL = *u + return nil +} diff --git a/internal/config/env.go b/internal/config/env.go new file mode 100644 index 0000000..8888746 --- /dev/null +++ b/internal/config/env.go @@ -0,0 +1,21 @@ +package config + +import ( + "fmt" + "os" + "strings" +) + +// parseEnvironmentVariables analyzes all the environment variables to find +// each one starting by REPEATER_ +func parseEnvironmentVariables(o *Config) (err error) { + for _, line := range os.Environ() { + if strings.HasPrefix(line, "REPEATER_") { + err := parseLine(o, line) + if err != nil { + return fmt.Errorf("error in environment (%q): %w", line, err) + } + } + } + return +} diff --git a/internal/config/file.go b/internal/config/file.go new file mode 100644 index 0000000..8ba6a10 --- /dev/null +++ b/internal/config/file.go @@ -0,0 +1,33 @@ +package config + +import ( + "bufio" + "fmt" + "os" + "strings" +) + +// parseFile opens the file at the given filename path, then treat each line +// not starting with '#' as a configuration statement. +func parseFile(o *Config, filename string) error { + fp, err := os.Open(filename) + if err != nil { + return err + } + defer fp.Close() + + scanner := bufio.NewScanner(fp) + n := 0 + for scanner.Scan() { + n += 1 + line := strings.TrimSpace(scanner.Text()) + if len(line) > 0 && !strings.HasPrefix(line, "#") && strings.Index(line, "=") > 0 { + err := parseLine(o, line) + if err != nil { + return fmt.Errorf("%v:%d: error in configuration: %w", filename, n, err) + } + } + } + + return nil +}