Files
go-app/pkg/config/config.go

132 lines
3.3 KiB
Go

// Package config provides types and methods for managing
// app config. A go-app configuration can be extended by any type
// that embeds AppConfig, and has helpers to generate json
// schema merged with the custom type's fields
package config
import (
"context"
"errors"
"flag"
"fmt"
"os"
"regexp"
"runtime/debug"
"time"
"github.com/caarlos0/env/v11"
"gopkg.in/yaml.v3"
)
// Version is to be set by ldflags in go build command or
// retrieved from build meta below.
var Version = "(devel)"
// LoadConfig will try to load from config if -config is
// provided as a file, and will apply any environment overrides
// on-top of configuration defaults.
// Config is stored in returned context, and can be retrieved
// using config.FromCtx(ctx).
func LoadConfig(ctx context.Context) (context.Context, error) {
configPath := flag.String("config", "", "Path to the configuration file")
flag.Parse()
// Start with defaults
// Load from config if provided
// Layer on environment
cfg, err := loadConfig(*configPath)
if err != nil {
return ctx, err
}
// Add config to context, and return
// an updated context
ctx = cfg.AddToCtx(ctx)
return ctx, nil
}
// loadConfig loads the application configuration from the specified path,
// applying environment variable overrides.
func loadConfig(configPath string) (*AppConfig, error) {
cfg := *DefaultConfig
if configPath != "" {
file, err := os.Open(configPath)
if err != nil {
return nil, fmt.Errorf("could not open config file: %w", err)
}
defer file.Close()
decoder := yaml.NewDecoder(file)
if err := decoder.Decode(&cfg); err != nil {
return nil, fmt.Errorf("could not decode config file: %w", err)
}
}
if err := env.Parse(&cfg); err != nil {
return nil, fmt.Errorf("could not parse environment variables: %w", err)
}
// Perform updates / enrichments
err := prepareConfig(&cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}
// prepareConfig enriches and validates the AppConfig, parsing duration strings
// for HTTP timeouts.
func prepareConfig(cfg *AppConfig) error {
var errs error
// Set timeouts
if cfg.HTTP.ReadTimeout != "" {
if rT, err := time.ParseDuration(cfg.HTTP.ReadTimeout); err == nil {
cfg.HTTP.rT = &rT
} else {
errs = errors.Join(errs, err)
}
}
if cfg.HTTP.ReadTimeout != "" {
if wT, err := time.ParseDuration(cfg.HTTP.WriteTimeout); err == nil {
cfg.HTTP.wT = &wT
} else {
errs = errors.Join(errs, err)
}
}
if cfg.HTTP.IdleTimeout != "" {
if iT, err := time.ParseDuration(cfg.HTTP.IdleTimeout); err == nil {
cfg.HTTP.iT = &iT
} else {
errs = errors.Join(errs, err)
}
}
// Prepare user-provided expressions, panic up-front if invalid
cfg.HTTP.excludeRegexps = make([]*regexp.Regexp, len(cfg.HTTP.LogExcludePathRegexps))
for i, re := range cfg.HTTP.LogExcludePathRegexps {
cfg.HTTP.excludeRegexps[i] = regexp.MustCompile(re)
}
return errs
}
// Timeouts returns read timeout, write timeout, and idle timeout, in that order.
// Returns nil if unset.
func (h *HTTPConfig) Timeouts() (*time.Duration, *time.Duration, *time.Duration) {
return h.rT, h.wT, h.iT
}
// getVersion returns the application version, preferring the build info version
// over the compile-time set Version variable.
func getVersion() string {
if info, ok := debug.ReadBuildInfo(); ok && info.Main.Version != "(devel)" {
return info.Main.Version
}
return Version
}