Skip to content

Instantly share code, notes, and snippets.

@jxsl13
Created November 7, 2022 11:53
Show Gist options
  • Save jxsl13/14aa6c340b2c11d0be01ee099fc21375 to your computer and use it in GitHub Desktop.
Save jxsl13/14aa6c340b2c11d0be01ee099fc21375 to your computer and use it in GitHub Desktop.
urfave/cli and knadh/koanf integration v1
package config
import (
"errors"
"fmt"
"log"
"net/url"
"os"
"strings"
"sync"
"github.com/go-playground/validator/v10"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/dotenv"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/providers/posflag"
"github.com/knadh/koanf/providers/structs"
"github.com/spf13/pflag"
"github.com/urfave/cli/v2"
)
var (
cfg *config
mu sync.Mutex
)
type config struct {
Username string `koanf:"username"`
Password string `koanf:"password"`
ClientId string `koanf:"client.id"`
ClientSecret string `koanf:"client.secret"`
TokenUrl string `koanf:"token.url" validate:"required,url"`
Insecure bool `koanf:"insecure"`
}
func Parse(app *cli.App) error {
c := &config{
ClientId: "public",
TokenUrl: "https://example.com/auth/realms/REALM/protocol/openid-connect/token",
}
mu.Lock()
defer mu.Unlock()
const (
prefix = "PREFIX_"
delimiter = "."
tag = "koanf"
flatStruct = true
helpFlag = "help"
configFlag = "config"
)
k := koanf.New(delimiter)
transform := func(s string) string {
return strings.ToLower(strings.ReplaceAll(s, "_", delimiter))
}
f := func(s string) string {
return transform(strings.TrimPrefix(s, prefix))
}
err := k.Load(structs.ProviderWithDelim(c, tag, delimiter), nil)
if err != nil {
return err
}
err = k.Load(env.Provider(prefix, delimiter, f), nil)
if err != nil {
return err
}
fs := pflag.NewFlagSet("", pflag.ContinueOnError)
fs.BoolP(helpFlag, "h", false, "in order to print this screen")
fs.StringP(configFlag, "c", "", ".env configuration file path")
for k, v := range k.All() {
k = strings.ReplaceAll(k, delimiter, "-")
switch x := v.(type) {
case bool:
fs.Bool(k, x, "")
// prevent duplicate flag definitions
if !containsFlag(k, app.Flags) {
// make flags known to cli framework
app.Flags = append(app.Flags, &cli.BoolFlag{
Name: k,
Value: x,
})
}
default:
value := fmt.Sprintf("%v", v)
fs.String(k, value, "")
// prevent duplicate flag definitions
if !containsFlag(k, app.Flags) {
// make flags known to cli framework
app.Flags = append(app.Flags, &cli.StringFlag{
Name: k,
Value: value,
})
}
}
}
passAppFlagsDown(app)
err = fs.Parse(os.Args)
if err != nil {
return fmt.Errorf("failed to parse config flags: %w", err)
}
flagK := koanf.New(delimiter)
err = flagK.Load(
posflag.ProviderWithValue(
fs,
delimiter,
nil,
func(key, value string) (string, interface{}) {
return transform(key), value
},
), nil)
if err != nil {
return err
}
if flagK.Bool(helpFlag) {
return nil
}
configPath := flagK.String(configFlag)
if configPath != "" {
err = k.Load(file.Provider(configPath), dotenv.ParserEnv(prefix, delimiter, f))
if err != nil {
return err
}
}
err = k.Merge(flagK)
if err != nil {
return err
}
err = k.UnmarshalWithConf("", &c, koanf.UnmarshalConf{
FlatPaths: flatStruct,
})
if err != nil {
return err
}
err = c.Validate()
if err != nil {
return err
}
cfg = c
return nil
}
func (c *config) Validate() error {
if c.Username == "" && c.Password == "" && c.ClientSecret == "" {
return errors.New("missing credentials, either username and password or client id and client secret")
}
return validator.New().Struct(c)
}
func (c *config) Form() form.Form {
if c.Username == "" && c.Password == "" {
return form.ClientCredentialsAccessToken(c.ClientId, c.ClientSecret)
} else if c.ClientId != "" {
return form.PasswordAccessToken(c.Username, c.Password, c.ClientId, c.ClientSecret)
} else {
return form.PasswordAccessToken(c.Username, c.Password, "public", "")
}
}
func (c *config) IsInsecure() bool {
return c.Insecure
}
func (c *config) KeycloakTokenUrl() *url.URL {
u, err := url.ParseRequestURI(c.TokenUrl)
if err != nil {
log.Fatalf("invalid keycloak token url: %s: %v\n", c.TokenUrl, err)
}
return u
}
func containsFlag(name string, flags []cli.Flag) bool {
for _, f := range flags {
for _, knownName := range f.Names() {
if name == knownName {
return true
}
}
}
return false
}
func passAppFlagsDown(app *cli.App) {
trickleDownFlags(app.Flags, app.Commands)
}
func trickleDownFlags(parentFlags []cli.Flag, children []*cli.Command) {
if len(children) == 0 {
// parent is a leaf
return
}
for _, cmd := range children {
if len(parentFlags) > 0 {
cmd.Flags = append(cmd.Flags, parentFlags...)
}
trickleDownFlags(cmd.Flags, cmd.Subcommands)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment