peterbourgon / ff

Flags-first package for configuration

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

How to read from a yaml file?

jonleopard opened this issue · comments

No bugs or any issues to report here, just a usage question :-)

After reading a couple of your blog posts, I’m sold on your stance on flags being the best way to configure your app. I’m working on a personal project that requires some API credentials. Using flags for simple parameters like a port is very straight forward. For running in a dev environment, I think it would make sense to have API secrets be kept in a file. I’ve created a .yaml file in my root, config.dev.yaml, and would like ff to read this file and inject it where necessary (for example, see the HandleTopGames method below). I will have an example sample.dev.yaml file where developers can fill in their own API credentials.

Where I’m lost is how to actually get ff to read the config file. How does it know the path (or do I need to specify where it is?). Suffice it to say I’m still pretty new to go, and I’m trying to learn some best practices. Apologies in advanced if my question is trivial! Any input is greatly appreciated :-)

Cheers!

...


type Server struct {
	mux    *chi.Mux
	log    *zap.SugaredLogger
	server *http.Server
	// db       *db.Conn
}

func main() {
	// TODO: Implement Zap logger
	if err := run(); err != nil {
		fmt.Fprintf(os.Stderr, "%s\n", err)
		os.Exit(1)
	}
}


func run() error {
	// ========================================================== Flags
	fs := flag.NewFlagSet("DANKSTATS-API", flag.ExitOnError)

	var (
		listenAddr = fs.String("listen-addr", "localhost:4000", "listen address")
		_          = fs.String("config", "", "config file")
		//dsn        = fs.String("dsn", "", "Postgres DSN")
	)

	ff.Parse(fs, os.Args[1:],
		ff.WithEnvVarPrefix("DANKSTATS-API"),
		ff.WithConfigFileFlag("config"),
		ff.WithConfigFileParser(ff.PlainParser),
	)
	
	// ========================================================== API SETUP
	mux := chi.NewRouter()

	srv := &Server{
		mux: mux,
		server: &http.Server{
			Addr:              *listenAddr,
			ReadTimeout:       5 * time.Second,
			ReadHeaderTimeout: 5 * time.Second,
			WriteTimeout:      5 * time.Second,
			IdleTimeout:       5 * time.Second,
		},
	}
	
	mux.HandleFunc("/top-games", srv.HandleTopGames)
	
	// ========================================================== BOOT
	log.Println("Starting web server on", *listenAddr)
	http.ListenAndServe(*listenAddr, mux)
	log.Println("Stopping...")
}

// HandleTopGames responds with the top twitch games at the moment.
func (s *Server) HandleTopGames(w http.ResponseWriter, r *http.Request) {
	client, err := helix.NewClient(&helix.Options{
		ClientID:       "CLIENT_API_KEY",
		AppAccessToken: "APP_ACCESS_TOKEN",
	})
	if err != nil {
		panic(err)
	}

	resp, err := client.GetTopGames(&helix.TopGamesParams{
		First: 20,
	})
	if err != nil {
		panic(err)
	}
	json.NewEncoder(w).Encode(resp)
}
...
	fs := flag.NewFlagSet("DANKSTATS-API", flag.ExitOnError)

	var (
		listenAddr = fs.String("listen-addr", "localhost:4000", "listen address")
		_          = fs.String("config", "", "config file")
	)

	ff.Parse(fs, os.Args[1:],
		ff.WithEnvVarPrefix("DANKSTATS-API"),
		ff.WithConfigFileFlag("config"),
		ff.WithConfigFileParser(ff.PlainParser),
	)

So ff.WithConfigFileFlag("config") tells ff to use the value of the -config flag as the filename to read, and ff.WithConfigFileParser(ff.PlainParser) tells ff to parse that file with the PlainParser. You probably want to use the ffyaml.Parser instead, there.

Also, I'm not sure that env var prefixes can contain - — you probably want DANKSTATS_API.

Does that work?

You’re right, I had to change it to DANKSTATS_API. I think I’ll skip env vars and just go with setting with flags or a file. What would be the cleanest way to give my handler methods access to these keys? My handlers are already hanging off of the server, so I guess I could add an additional field for the config?

type Server struct {
	mux    *chi.Mux
	log    *zap.SugaredLogger
	server *http.Server
	config ???
}

...
	srv := &Server{
		mux: mux,
		config: ???
		server: &http.Server{
			Addr:              *listenAddr,
			ReadTimeout:       5 * time.Second,
			ReadHeaderTimeout: 5 * time.Second,
			WriteTimeout:      5 * time.Second,
			IdleTimeout:       5 * time.Second,
		},
	}
...
	
// HandleTopGames responds with the top twitch games
func (s *Server) HandleTopGames(w http.ResponseWriter, r *http.Request) {
	client, err := helix.NewClient(&helix.Options{
		ClientID:       *clientID,    <--- value read from flag || file
		AppAccessToken: *appAccessToken,
	})
	if err != nil {
		panic(err)
	}

	resp, err := client.GetTopGames(&helix.TopGamesParams{
		First: 20,
	})
	if err != nil {
		panic(err)
	}
	json.NewEncoder(w).Encode(resp)
}

Thanks again for your input on this!

Components don't take the whole app config, they take the specific bits of it which are relevant to them, by default just as individual parameters.

Glad this helped!