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 }