peterbourgon / ff

Flags-first package for configuration
Apache License 2.0
1.37k stars 59 forks source link

How to read from a yaml file? #85

Closed jonleopard closed 3 years ago

jonleopard commented 3 years ago

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)
}
...
peterbourgon commented 3 years ago
    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?

jonleopard commented 3 years ago

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!

peterbourgon commented 3 years ago

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!