friendica-exporter/internal/config/config.go
2024-10-27 19:51:26 +01:00

230 lines
5.6 KiB
Go

package config
import (
"errors"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/spf13/pflag"
"gopkg.in/yaml.v3"
)
const (
envPrefix = "FRIENDICA_"
envListenAddress = envPrefix + "LISTEN_ADDRESS"
envTimeout = envPrefix + "TIMEOUT"
envServerURL = envPrefix + "SERVER"
envStatsToken = envPrefix + "STATS_TOKEN"
envTLSSkipVerify = envPrefix + "TLS_SKIP_VERIFY"
)
// RunMode signals what the main application should do after parsing the options.
type RunMode int
const (
// RunModeExporter is the normal operation as an exporter serving metrics via HTTP.
RunModeExporter RunMode = iota
// RunModeHelp shows information about available options.
RunModeHelp
// RunModeVersion shows version information.
RunModeVersion
)
func (m RunMode) String() string {
switch m {
case RunModeExporter:
return "exporter"
case RunModeHelp:
return "help"
case RunModeVersion:
return "version"
default:
return "error"
}
}
// Config contains the configuration options for friendica-exporter.
type Config struct {
ListenAddr string `yaml:"listenAddress"`
Timeout time.Duration `yaml:"timeout"`
ServerURL string `yaml:"server"`
StatsToken string `yaml:"statsToken"`
TLSSkipVerify bool `yaml:"tlsSkipVerify"`
RunMode RunMode
}
var (
errValidateNoServerURL = errors.New("need to set a server URL")
errValidateNoStatsToken = errors.New("need to provide a stats token")
)
// Validate checks if the configuration contains all necessary parameters.
func (c Config) Validate() error {
if len(c.ServerURL) == 0 {
return errValidateNoServerURL
}
if len(c.StatsToken) == 0 {
return errValidateNoStatsToken
}
return nil
}
// Get loads the configuration. Flags, environment variables and configuration file are considered.
func Get() (Config, error) {
return parseConfig(os.Args, os.Getenv)
}
func parseConfig(args []string, envFunc func(string) string) (Config, error) {
result, configFile, err := loadConfigFromFlags(args)
if err != nil {
return Config{}, fmt.Errorf("error parsing flags: %w", err)
}
if configFile != "" {
rawFile, err := loadConfigFromFile(configFile)
if err != nil {
return Config{}, fmt.Errorf("error reading configuration file: %w", err)
}
result = mergeConfig(result, rawFile)
}
env, err := loadConfigFromEnv(envFunc)
if err != nil {
return Config{}, fmt.Errorf("error reading environment variables: %w", err)
}
result = mergeConfig(result, env)
if strings.HasPrefix(result.StatsToken, "@") {
fileName := strings.TrimPrefix(result.StatsToken, "@")
statsToken, err := readStatsTokenFile(fileName)
if err != nil {
return Config{}, fmt.Errorf("can not read stats token file: %w", err)
}
result.StatsToken = statsToken
}
return result, nil
}
func defaultConfig() Config {
return Config{
ListenAddr: ":9205",
Timeout: 5 * time.Second,
}
}
func loadConfigFromFlags(args []string) (result Config, configFile string, err error) {
defaults := defaultConfig()
flags := pflag.NewFlagSet(args[0], pflag.ContinueOnError)
flags.StringVarP(&configFile, "config-file", "c", "", "Path to YAML configuration file.")
flags.StringVarP(&result.ListenAddr, "addr", "a", defaults.ListenAddr, "Address to listen on for connections.")
flags.DurationVarP(&result.Timeout, "timeout", "t", defaults.Timeout, "Timeout for getting server info document.")
flags.StringVarP(&result.ServerURL, "server", "s", "", "URL to Friendica server.")
flags.StringVarP(&result.StatsToken, "stats-token", "u", defaults.StatsToken, "Token for statistics Endpoint of Friendica.")
flags.BoolVar(&result.TLSSkipVerify, "tls-skip-verify", defaults.TLSSkipVerify, "Skip certificate verification of Friendica server.")
modeVersion := flags.BoolP("version", "V", false, "Show version information and exit.")
if err := flags.Parse(args[1:]); err != nil {
if err == pflag.ErrHelp {
return Config{
RunMode: RunModeHelp,
}, "", nil
}
return Config{}, "", err
}
if *modeVersion {
return Config{
RunMode: RunModeVersion,
}, "", nil
}
return result, configFile, nil
}
func loadConfigFromFile(fileName string) (Config, error) {
file, err := os.Open(fileName)
if err != nil {
return Config{}, err
}
var result Config
if err := yaml.NewDecoder(file).Decode(&result); err != nil {
return Config{}, err
}
return result, nil
}
func loadConfigFromEnv(getEnv func(string) string) (Config, error) {
tlsSkipVerify := false
if rawValue := getEnv(envTLSSkipVerify); rawValue != "" {
value, err := strconv.ParseBool(rawValue)
if err != nil {
return Config{}, fmt.Errorf("can not parse value for %q: %s", envTLSSkipVerify, rawValue)
}
tlsSkipVerify = value
}
result := Config{
ListenAddr: getEnv(envListenAddress),
ServerURL: getEnv(envServerURL),
StatsToken: getEnv(envStatsToken),
TLSSkipVerify: tlsSkipVerify,
}
if raw := getEnv(envTimeout); raw != "" {
value, err := time.ParseDuration(raw)
if err != nil {
return Config{}, err
}
result.Timeout = value
}
return result, nil
}
func mergeConfig(base, override Config) Config {
result := base
if override.ListenAddr != "" {
result.ListenAddr = override.ListenAddr
}
if override.ServerURL != "" {
result.ServerURL = override.ServerURL
}
if override.StatsToken != "" {
result.StatsToken = override.StatsToken
}
if override.Timeout != 0 {
result.Timeout = override.Timeout
}
if override.TLSSkipVerify {
result.TLSSkipVerify = override.TLSSkipVerify
}
return result
}
func readStatsTokenFile(fileName string) (string, error) {
bytes, err := os.ReadFile(fileName)
if err != nil {
return "", err
}
return strings.TrimSuffix(string(bytes), "\n"), nil
}