diff --git a/go.mod b/go.mod index d69a7f7..5fce658 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,10 @@ go 1.15 require ( github.com/GeertJohan/go.rice v1.0.0 github.com/didip/tollbooth/v6 v6.0.2 + github.com/knadh/koanf v0.14.0 github.com/labstack/echo/v4 v4.1.17 github.com/lightningnetwork/lnd v0.11.1-beta + github.com/paked/configure v0.0.0-20190218140148-28f9c3f21a44 google.golang.org/grpc v1.33.0 gopkg.in/macaroon.v2 v2.1.0 ) diff --git a/go.sum b/go.sum index e5daf80..a5b26ed 100644 --- a/go.sum +++ b/go.sum @@ -84,9 +84,12 @@ github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25Kn github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= @@ -123,12 +126,14 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.14.3 h1:OCJlWkOUoTnl0neNGlf4fUm3TmbEtguw7vR+nGtnDjY= github.com/grpc-ecosystem/grpc-gateway v1.14.3/go.mod h1:6CwZWGDSPRJidgKAtJVvND6soZe6fT7iteq8wDPdhb0= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= @@ -146,6 +151,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs= github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/knadh/koanf v0.14.0 h1:h9XeG4wEiEuxdxqv/SbY7TEK+7vzrg/dOaGB+S6+mPo= +github.com/knadh/koanf v0.14.0/go.mod h1:H5mEFsTeWizwFXHKtsITL5ipsLTuAMQoGuQpp+1JL9U= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -188,6 +195,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws= github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= +github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -198,6 +207,9 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/paked/configure v0.0.0-20190218140148-28f9c3f21a44/go.mod h1:y9MA8YrqgIDBMhU+Dgzu5okImVGccMdjHnWv6md5rfs= +github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -213,12 +225,14 @@ github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rhnvrm/simples3 v0.5.0/go.mod h1:Y+3vYm2V7Y4VijFoJHHTrja6OgPrJ2cBti8dPGkC3sA= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= @@ -294,10 +308,12 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8= golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -350,6 +366,7 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/ln/lnd.go b/ln/lnd.go index dc2b54e..25793c5 100644 --- a/ln/lnd.go +++ b/ln/lnd.go @@ -1,11 +1,13 @@ package ln import ( + "fmt" "context" "encoding/hex" "io/ioutil" "log" "os" + "crypto/x509" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/macaroons" @@ -29,7 +31,9 @@ type Invoice struct { type LNDoptions struct { Address string CertFile string + CertHex string MacaroonFile string + MacaroonHex string } type LNDclient struct { @@ -103,23 +107,52 @@ func (c LNDclient) GetInvoice(paymentHashStr string) (Invoice, error) { func NewLNDclient(lndOptions LNDoptions) (LNDclient, error) { result := LNDclient{} - creds, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "") - if err != nil { - return result, err - } + // Get credentials either from a hex string or a file + var creds credentials.TransportCredentials + // if a hex string is provided + if lndOptions.CertHex != "" { + cp := x509.NewCertPool() + cert, err := hex.DecodeString(lndOptions.CertHex) + if err != nil { + return result, err + } + cp.AppendCertsFromPEM(cert) + creds = credentials.NewClientTLSFromCert(cp, "") + // if a path to a cert file is provided + } else if lndOptions.CertFile != "" { + credsFromFile, err := credentials.NewClientTLSFromFile(lndOptions.CertFile, "") + if err != nil { + return result, err + } + creds = credsFromFile // make it available outside of the else if block + } else { + return result, fmt.Errorf("LND credential is missing") + } opts := []grpc.DialOption{ grpc.WithTransportCredentials(creds), } - macaroonData, err := ioutil.ReadFile(lndOptions.MacaroonFile) - if err != nil { - return result, err - } - mac := &macaroon.Macaroon{} - if err = mac.UnmarshalBinary(macaroonData); err != nil { - return result, err - } + var macaroonData []byte + if lndOptions.MacaroonHex != "" { + macBytes, err := hex.DecodeString(lndOptions.MacaroonHex) + if err != nil { + return result, err + } + macaroonData = macBytes + } else if lndOptions.MacaroonFile != "" { + macBytes, err := ioutil.ReadFile(lndOptions.MacaroonFile) + if err != nil { + return result, err + } + macaroonData = macBytes // make it available outside of the else if block + } else { + return result, fmt.Errorf("LND macaroon is missing") + } + mac := &macaroon.Macaroon{} + if err := mac.UnmarshalBinary(macaroonData); err != nil { + return result, err + } macCred := macaroons.NewMacaroonCredential(mac) opts = append(opts, grpc.WithPerRPCCredentials(macCred)) diff --git a/lnme.go b/lnme.go index 969d4fb..eb3f46a 100644 --- a/lnme.go +++ b/lnme.go @@ -1,16 +1,22 @@ package main import ( - "flag" - "github.com/GeertJohan/go.rice" + "strings" + "flag" + "log" + "net/http" + "os" "github.com/bumi/lnme/ln" + "github.com/GeertJohan/go.rice" "github.com/didip/tollbooth/v6" "github.com/didip/tollbooth/v6/limiter" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" - "log" - "net/http" - "os" + "github.com/knadh/koanf" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/providers/env" + "github.com/knadh/koanf/providers/basicflag" ) // Middleware for request limited to prevent too many requests @@ -35,24 +41,15 @@ type Invoice struct { } func main() { - address := flag.String("address", "localhost:10009", "The host and port of the lnd gRPC server.") - certFile := flag.String("cert", "~/.lnd/tls.cert", "Path to the lnd tls.cert file.") - macaroonFile := flag.String("macaroon", "~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon", "Path to the lnd macaroon file.") - bind := flag.String("bind", ":1323", "Host and port to bind on.") - staticPath := flag.String("static-path", "", "Path to a static assets directory. Leave blank to not serve any static files.") - disableWebsite := flag.Bool("disable-website", false, "Disable default embedded website.") - disableCors := flag.Bool("disable-cors", false, "Disable CORS headers.") - requestLimit := flag.Float64("request-limit", 5, "Request limit per second.") + cfg := LoadConfig() - flag.Parse() - - e := echo.New() + e := echo.New() // Serve static files if configured - if *staticPath != "" { - e.Static("/", *staticPath) + if cfg.String("static_path") != "" { + e.Static("/", cfg.String("static_path")) // Serve default page - } else if !*disableWebsite { + } else if !cfg.Bool("disable_website") { rootBox := rice.MustFindBox("files/root") indexPage, err := rootBox.String("index.html") if err == nil { @@ -69,7 +66,7 @@ func main() { e.GET("/lnme/*", echo.WrapHandler(http.StripPrefix("/lnme/", assetHandler))) // CORS settings - if !*disableCors { + if !cfg.Bool("disable_cors") { e.Use(middleware.CORS()) } @@ -77,17 +74,19 @@ func main() { e.Use(middleware.Recover()) // Request limit per second. DoS protection - if *requestLimit > 0 { - limiter := tollbooth.NewLimiter(*requestLimit, nil) + if cfg.Int("request_limit") > 0 { + limiter := tollbooth.NewLimiter(cfg.Float64("request_limit"), nil) e.Use(LimitMiddleware(limiter)) } // Setup lightning client - stdOutLogger.Printf("Connection to %s using macaroon %s and cert %s", *address, *macaroonFile, *certFile) + stdOutLogger.Printf("Connecting to %s", cfg.String("lnd.address")) lndOptions := ln.LNDoptions{ - Address: *address, - CertFile: *certFile, - MacaroonFile: *macaroonFile, + Address: cfg.String("lnd.address"), + CertFile: cfg.String("lnd.cert_path"), + CertHex: cfg.String("lnd.cert"), + MacaroonFile: cfg.String("lnd.macaroon_path"), + MacaroonHex: cfg.String("lnd.macaroon"), } lnClient, err := ln.NewLNDclient(lndOptions) if err != nil { @@ -142,5 +141,44 @@ func main() { return c.JSON(http.StatusOK, "pong") }) - e.Logger.Fatal(e.Start(*bind)) + e.Logger.Fatal(e.Start(":" + cfg.String("port"))) +} + +func LoadConfig() *koanf.Koanf { + k := koanf.New(".") + + f := flag.NewFlagSet("LnMe", flag.ExitOnError) + f.String("lnd.address", "localhost:10009", "The host and port of the LND gRPC server.") + f.String("lnd.macaroon_path", "~/.lnd/data/chain/bitcoin/mainnet/invoice.macaroon", "Path to the LND macaroon file.") + f.String("lnd.cert_path", "~/.lnd/tls.cert", "Path to the LND tls.cert file.") + f.Bool("disable_website", false, "Disable default embedded website.") + f.Bool("disable_cors", false, "Disable CORS headers.") + f.Float64("request_limit", 5, "Request limit per second.") + f.String("static_path", "", "Path to a static assets directory.") + f.String("port", "1323", "Port to bind on.") + var configPath string + f.StringVar(&configPath, "config", "config.toml", "Path to a .toml config file.") + f.Parse(os.Args[1:]) + + // Load config from flags, including defaults + if err := k.Load(basicflag.Provider(f, "."), nil); err != nil { + log.Fatalf("Error loading config: %v", err) + } + + // Load config from environment variables + k.Load(env.Provider("LNME_", ".", func(s string) string { + return strings.Replace(strings.ToLower( + strings.TrimPrefix(s, "LNME_")), "_", ".", -1) + }), nil) + + // Load config from file if available + if configPath != "" { + if _, err := os.Stat(configPath); err == nil { + if err := k.Load(file.Provider(configPath), toml.Parser()); err != nil { + log.Fatalf("Error loading config file: %v", err) + } + } + } + + return k }