diff --git a/cmd/hetty/hetty.go b/cmd/hetty/hetty.go index 7b74f69..8129d7d 100644 --- a/cmd/hetty/hetty.go +++ b/cmd/hetty/hetty.go @@ -16,16 +16,15 @@ import ( "strings" "github.com/chromedp/chromedp" - badgerdb "github.com/dgraph-io/badger/v3" "github.com/gorilla/mux" "github.com/mitchellh/go-homedir" "github.com/peterbourgon/ff/v3/ffcli" + "go.etcd.io/bbolt" "go.uber.org/zap" - "go.uber.org/zap/zapcore" "github.com/dstotijn/hetty/pkg/api" "github.com/dstotijn/hetty/pkg/chrome" - "github.com/dstotijn/hetty/pkg/db/badger" + "github.com/dstotijn/hetty/pkg/db/bolt" "github.com/dstotijn/hetty/pkg/proj" "github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/proxy/intercept" @@ -89,7 +88,7 @@ func NewHettyCommand() (*ffcli.Command, *Config) { "Path to root CA certificate. Creates a new certificate if file doesn't exist.") fs.StringVar(&cmd.key, "key", "~/.hetty/hetty_key.pem", "Path to root CA private key. Creates a new private key if file doesn't exist.") - fs.StringVar(&cmd.db, "db", "~/.hetty/db", "Database directory path.") + fs.StringVar(&cmd.db, "db", "~/.hetty/hetty.db", "Database file path. Creates file if it doesn't exist.") fs.StringVar(&cmd.addr, "addr", ":8080", "TCP address to listen on, in the form \"host:port\".") fs.BoolVar(&cmd.chrome, "chrome", false, "Launch Chrome with proxy settings applied and certificate errors ignored.") fs.BoolVar(&cmd.version, "version", false, "Output version.") @@ -154,25 +153,21 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error { cmd.config.logger.Fatal("Failed to load or create CA key pair.", zap.Error(err)) } - // BadgerDB logs some verbose entries with `INFO` level, so unless - // we're running in debug mode, bump the minimal level to `WARN`. - dbLogger := cmd.config.logger.Named("badgerdb").WithOptions(zap.IncreaseLevel(zapcore.WarnLevel)) + dbLogger := cmd.config.logger.Named("boltdb").Sugar() + boltOpts := *bbolt.DefaultOptions + boltOpts.Logger = &bolt.Logger{SugaredLogger: dbLogger} - dbSugaredLogger := dbLogger.Sugar() - - badger, err := badger.OpenDatabase( - badgerdb.DefaultOptions(dbPath).WithLogger(badger.NewLogger(dbSugaredLogger)), - ) + boltDB, err := bolt.OpenDatabase(dbPath, &boltOpts) if err != nil { cmd.config.logger.Fatal("Failed to open database.", zap.Error(err)) } - defer badger.Close() + defer boltDB.Close() scope := &scope.Scope{} reqLogService := reqlog.NewService(reqlog.Config{ Scope: scope, - Repository: badger, + Repository: boltDB, Logger: cmd.config.logger.Named("reqlog").Sugar(), }) @@ -181,12 +176,12 @@ func (cmd *HettyCommand) Exec(ctx context.Context, _ []string) error { }) senderService := sender.NewService(sender.Config{ - Repository: badger, + Repository: boltDB, ReqLogService: reqLogService, }) projService, err := proj.NewService(proj.Config{ - Repository: badger, + Repository: boltDB, InterceptService: interceptService, ReqLogService: reqLogService, SenderService: senderService, diff --git a/go.mod b/go.mod index 79af44c..435b273 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,12 @@ module github.com/dstotijn/hetty -go 1.17 +go 1.23 + +toolchain go1.23.4 require ( github.com/99designs/gqlgen v0.14.0 github.com/chromedp/chromedp v0.7.8 - github.com/dgraph-io/badger/v3 v3.2103.2 github.com/google/go-cmp v0.5.6 github.com/gorilla/mux v1.7.4 github.com/mitchellh/go-homedir v1.1.0 @@ -13,31 +14,21 @@ require ( github.com/peterbourgon/ff/v3 v3.1.2 github.com/smallstep/truststore v0.11.0 github.com/vektah/gqlparser/v2 v2.2.0 + go.etcd.io/bbolt v1.4.0-beta.0 go.uber.org/zap v1.21.0 ) require ( github.com/agnivade/levenshtein v1.1.0 // indirect - github.com/cespare/xxhash v1.1.0 // indirect - github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf // indirect github.com/chromedp/sysutil v1.0.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d // indirect - github.com/dgraph-io/ristretto v0.1.0 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.1.0 // indirect - github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect - github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect - github.com/golang/protobuf v1.3.1 // indirect - github.com/golang/snappy v0.0.3 // indirect - github.com/google/flatbuffers v1.12.1 // indirect github.com/gorilla/websocket v1.4.2 // indirect github.com/hashicorp/golang-lru v0.5.1 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/klauspost/compress v1.12.3 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matryer/moq v0.2.5 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect @@ -46,12 +37,10 @@ require ( github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/urfave/cli/v2 v2.1.1 // indirect github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e // indirect - go.opencensus.io v0.22.5 // indirect go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect golang.org/x/mod v0.4.2 // indirect - golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect - golang.org/x/sys v0.0.0-20220209214540-3681064d5158 // indirect + golang.org/x/sys v0.26.0 // indirect golang.org/x/tools v0.1.5 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect gopkg.in/yaml.v2 v2.2.8 // indirect diff --git a/go.sum b/go.sum index 4c90842..9d698cd 100644 --- a/go.sum +++ b/go.sum @@ -1,9 +1,6 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/99designs/gqlgen v0.14.0 h1:Wg8aNYQUjMR/4v+W3xD+7SizOy6lSvVeQ06AobNQAXI= github.com/99designs/gqlgen v0.14.0/go.mod h1:S7z4boV+Nx4VvzMUpVrY/YuHjFX4n7rDyuTqvAkuoRE= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/agnivade/levenshtein v1.0.1/go.mod h1:CURSv5d9Uaml+FovSIICkLbAUZ9S4RqaHDIsdSBg7lM= github.com/agnivade/levenshtein v1.1.0 h1:n6qGwyHG61v3ABce1rPVZklEYRT8NFpCMrpZdBUbYGM= github.com/agnivade/levenshtein v1.1.0/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVbJomOvKkmgYbo= @@ -11,63 +8,28 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= -github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf h1:1omDWNUsWxn2HpiMiMuyRmzjl9uG7RP3IE6GTlpgJWU= github.com/chromedp/cdproto v0.0.0-20220217222649-d8c14a5c6edf/go.mod h1:At5TxYYdxkbQL0TSefRjhLE3Q0lgvqKKMSFUglJ7i1U= github.com/chromedp/chromedp v0.7.8 h1:JFPIFb28LPjcx6l6mUUzLOTD/TgswcTtg7KrDn8S/2I= github.com/chromedp/chromedp v0.7.8/go.mod h1:HcIUFBa5vA+u2QI3+xljiU59llUQ8lgGoLzYSCBfmUA= github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= -github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= -github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk= -github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgraph-io/badger/v3 v3.2103.2 h1:dpyM5eCJAtQCBcMCZcT4UBZchuTJgCywerHHgmxfxM8= -github.com/dgraph-io/badger/v3 v3.2103.2/go.mod h1:RHo4/GmYcKKh5Lxu63wLEMHJ70Pac2JqZRYGhlyAo2M= -github.com/dgraph-io/ristretto v0.1.0 h1:Jv3CGQHp9OjuMBSne1485aDpUkTKEcUqF+jm/LuerPI= -github.com/dgraph-io/ristretto v0.1.0/go.mod h1:fux0lOrBhrVCJd3lcTHsIJhq1T2rokOu6v9Vcb3Q9ug= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= -github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g= github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/snappy v0.0.3 h1:fHPg5GQYlCeLIPB9BZqMVR5nR9A+IM5zcgeTdjMYmLA= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= -github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= @@ -79,22 +41,17 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.12.3 h1:G5AfA94pHPysR56qqrkO2pxEexdDzrpFJ6yt/VqWxVU= -github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= -github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/matryer/moq v0.0.0-20200106131100-75d0ddfc0007/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ= @@ -114,7 +71,6 @@ github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKw github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5 h1:1SoBaSPudixRecmlHXb/GxmaD3fLMtHIDN13QujwQuc= github.com/orisano/pixelmatch v0.0.0-20210112091706-4fa4c7ba91d5/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= -github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= github.com/peterbourgon/ff/v3 v3.1.2 h1:0GNhbRhO9yHA4CC27ymskOsuRpmX0YQxwxM9UPiP6JM= github.com/peterbourgon/ff/v3 v3.1.2/go.mod h1:XNJLY8EIl6MjMVjBS4F0+G0LYoAqs0DTa4rmHHukKDE= @@ -124,8 +80,6 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= -github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo= -github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= @@ -136,36 +90,25 @@ github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeV github.com/shurcooL/vfsgen v0.0.0-20180121065927-ffb13db8def0/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw= github.com/smallstep/truststore v0.11.0 h1:JUTkQ4oHr40jHTS/A2t0usEhteMWG+45CDD2iJA/dIk= github.com/smallstep/truststore v0.11.0/go.mod h1:HwHKRcBi0RUxxw1LYDpTRhYC4jZUuxPpkHdVonlkoDM= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= -github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= -github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= -github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e h1:+w0Zm/9gaWpEAyDlU1eKOuk5twTjAjuevXqcJJw8hrg= github.com/vektah/dataloaden v0.2.1-0.20190515034641-a19b9a6e7c9e/go.mod h1:/HUdMve7rvxZma+2ZELQeNh88+003LL7Pf/CZ089j8U= github.com/vektah/gqlparser/v2 v2.2.0 h1:bAc3slekAAJW6sZTi07aGq0OrfaCjj4jxARAaC7g2EM= github.com/vektah/gqlparser/v2 v2.2.0/go.mod h1:i3mQIGIrbK2PD1RrCeMTlVbkF2FJ6WkU1KJlJlC+3F4= -github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -go.opencensus.io v0.22.5 h1:dntmOdLpSpHlVqbW5Eay97DelsZHe+55D+xC6i0dDS0= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.etcd.io/bbolt v1.4.0-beta.0 h1:U7Y9yH6ZojEo5/BDFMXDXD1RNx9L7iKxudzqR68jLaM= +go.etcd.io/bbolt v1.4.0-beta.0/go.mod h1:Qv5yHB6jkQESXT/uVfxJgUPMqgAyhL0GLxcQaz9bSec= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= @@ -174,62 +117,46 @@ go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= -golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190515012406-7d7faa4812bd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -243,12 +170,6 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= @@ -259,9 +180,9 @@ gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M= howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0= sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/pkg/db/badger/badger.go b/pkg/db/badger/badger.go deleted file mode 100644 index 696eeac..0000000 --- a/pkg/db/badger/badger.go +++ /dev/null @@ -1,57 +0,0 @@ -package badger - -import ( - "fmt" - - "github.com/dgraph-io/badger/v3" -) - -const ( - // Key prefixes. Each prefix value should be unique. - projectPrefix = 0x00 - reqLogPrefix = 0x01 - resLogPrefix = 0x02 - senderReqPrefix = 0x03 - - // Request log indices. - reqLogProjectIDIndex = 0x00 - - // Sender request indices. - senderReqProjectIDIndex = 0x00 -) - -// Database is used to store and retrieve data from an underlying Badger database. -type Database struct { - badger *badger.DB -} - -// OpenDatabase opens a new Badger database. -func OpenDatabase(opts badger.Options) (*Database, error) { - db, err := badger.Open(opts) - if err != nil { - return nil, fmt.Errorf("badger: failed to open database: %w", err) - } - - return &Database{badger: db}, nil -} - -// Close closes the underlying Badger database. -func (db *Database) Close() error { - return db.badger.Close() -} - -// DatabaseFromBadgerDB returns a Database with `db` set as the underlying -// Badger database. -func DatabaseFromBadgerDB(db *badger.DB) *Database { - return &Database{badger: db} -} - -func entryKey(prefix, index byte, value []byte) []byte { - // Key consists of: | prefix (byte) | index (byte) | value - key := make([]byte, 2+len(value)) - key[0] = prefix - key[1] = index - copy(key[2:len(value)+2], value) - - return key -} diff --git a/pkg/db/badger/logger.go b/pkg/db/badger/logger.go deleted file mode 100644 index 46b78ef..0000000 --- a/pkg/db/badger/logger.go +++ /dev/null @@ -1,21 +0,0 @@ -package badger - -import ( - "github.com/dgraph-io/badger/v3" - "go.uber.org/zap" -) - -// Interface guard. -var _ badger.Logger = (*Logger)(nil) - -type Logger struct { - *zap.SugaredLogger -} - -func NewLogger(l *zap.SugaredLogger) *Logger { - return &Logger{l} -} - -func (l *Logger) Warningf(template string, args ...interface{}) { - l.Warnf(template, args) -} diff --git a/pkg/db/badger/proj.go b/pkg/db/badger/proj.go deleted file mode 100644 index 699767d..0000000 --- a/pkg/db/badger/proj.go +++ /dev/null @@ -1,115 +0,0 @@ -package badger - -import ( - "bytes" - "context" - "encoding/gob" - "errors" - "fmt" - - "github.com/dgraph-io/badger/v3" - "github.com/oklog/ulid" - - "github.com/dstotijn/hetty/pkg/proj" -) - -func (db *Database) UpsertProject(ctx context.Context, project proj.Project) error { - buf := bytes.Buffer{} - - err := gob.NewEncoder(&buf).Encode(project) - if err != nil { - return fmt.Errorf("badger: failed to encode project: %w", err) - } - - err = db.badger.Update(func(txn *badger.Txn) error { - return txn.Set(entryKey(projectPrefix, 0, project.ID[:]), buf.Bytes()) - }) - if err != nil { - return fmt.Errorf("badger: failed to commit transaction: %w", err) - } - - return nil -} - -func (db *Database) FindProjectByID(ctx context.Context, projectID ulid.ULID) (project proj.Project, err error) { - err = db.badger.View(func(txn *badger.Txn) error { - item, err := txn.Get(entryKey(projectPrefix, 0, projectID[:])) - if err != nil { - return err - } - - err = item.Value(func(rawProject []byte) error { - return gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project) - }) - if err != nil { - return fmt.Errorf("failed to retrieve or parse project: %w", err) - } - - return nil - }) - - if errors.Is(err, badger.ErrKeyNotFound) { - return proj.Project{}, proj.ErrProjectNotFound - } - - if err != nil { - return proj.Project{}, fmt.Errorf("badger: failed to commit transaction: %w", err) - } - - return project, nil -} - -func (db *Database) DeleteProject(ctx context.Context, projectID ulid.ULID) error { - err := db.ClearRequestLogs(ctx, projectID) - if err != nil { - return fmt.Errorf("badger: failed to delete project request logs: %w", err) - } - - err = db.DeleteSenderRequests(ctx, projectID) - if err != nil { - return fmt.Errorf("badger: failed to delete project sender requests: %w", err) - } - - err = db.badger.Update(func(txn *badger.Txn) error { - return txn.Delete(entryKey(projectPrefix, 0, projectID[:])) - }) - if err != nil { - return fmt.Errorf("badger: failed to delete project item: %w", err) - } - - return nil -} - -func (db *Database) Projects(ctx context.Context) ([]proj.Project, error) { - projects := make([]proj.Project, 0) - - err := db.badger.View(func(txn *badger.Txn) error { - var rawProject []byte - prefix := entryKey(projectPrefix, 0, nil) - - iterator := txn.NewIterator(badger.DefaultIteratorOptions) - defer iterator.Close() - - for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() { - rawProject, err := iterator.Item().ValueCopy(rawProject) - if err != nil { - return fmt.Errorf("failed to copy value: %w", err) - } - - var project proj.Project - err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project) - if err != nil { - return fmt.Errorf("failed to decode project: %w", err) - } - - projects = append(projects, project) - } - - return nil - }) - if err != nil { - return nil, fmt.Errorf("badger: failed to commit transaction: %w", err) - } - - return projects, nil -} diff --git a/pkg/db/badger/proj_test.go b/pkg/db/badger/proj_test.go deleted file mode 100644 index 7918723..0000000 --- a/pkg/db/badger/proj_test.go +++ /dev/null @@ -1,328 +0,0 @@ -package badger - -import ( - "bytes" - "context" - "encoding/gob" - "errors" - "math/rand" - "regexp" - "testing" - "time" - - badgerdb "github.com/dgraph-io/badger/v3" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" - "github.com/oklog/ulid" - - "github.com/dstotijn/hetty/pkg/filter" - "github.com/dstotijn/hetty/pkg/proj" - "github.com/dstotijn/hetty/pkg/scope" -) - -//nolint:gosec -var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano())) - -var regexpCompareOpt = cmp.Comparer(func(x, y *regexp.Regexp) bool { - switch { - case x == nil && y == nil: - return true - case x == nil || y == nil: - return false - default: - return x.String() == y.String() - } -}) - -func TestUpsertProject(t *testing.T) { - t.Parallel() - - badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true)) - if err != nil { - t.Fatalf("failed to open badger database: %v", err) - } - - database := DatabaseFromBadgerDB(badgerDB) - defer database.Close() - - searchExpr, err := filter.ParseQuery("foo AND bar OR NOT baz") - if err != nil { - t.Fatalf("unexpected error (expected: nil, got: %v)", err) - } - - exp := proj.Project{ - ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), - Name: "foobar", - Settings: proj.Settings{ - ReqLogBypassOutOfScope: true, - ReqLogOnlyFindInScope: true, - ReqLogSearchExpr: searchExpr, - ScopeRules: []scope.Rule{ - { - URL: regexp.MustCompile("^https://(.*)example.com(.*)$"), - Header: scope.Header{ - Key: regexp.MustCompile("^X-Foo(.*)$"), - Value: regexp.MustCompile("^foo(.*)$"), - }, - Body: regexp.MustCompile("^foo(.*)"), - }, - }, - }, - } - - err = database.UpsertProject(context.Background(), exp) - if err != nil { - t.Fatalf("unexpected error storing project: %v", err) - } - - var rawProject []byte - - err = badgerDB.View(func(txn *badgerdb.Txn) error { - item, err := txn.Get(entryKey(projectPrefix, 0, exp.ID[:])) - if err != nil { - return err - } - - rawProject, err = item.ValueCopy(nil) - return err - }) - if err != nil { - t.Fatalf("unexpected error retrieving project from database: %v", err) - } - - got := proj.Project{} - - err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&got) - if err != nil { - t.Fatalf("unexpected error decoding project: %v", err) - } - - if diff := cmp.Diff(exp, got, regexpCompareOpt, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" { - t.Fatalf("project not equal (-exp, +got):\n%v", diff) - } -} - -func TestFindProjectByID(t *testing.T) { - t.Parallel() - - t.Run("existing project", func(t *testing.T) { - t.Parallel() - - badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true)) - if err != nil { - t.Fatalf("failed to open badger database: %v", err) - } - - database := DatabaseFromBadgerDB(badgerDB) - defer database.Close() - - exp := proj.Project{ - ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), - Name: "foobar", - Settings: proj.Settings{}, - } - - buf := bytes.Buffer{} - - err = gob.NewEncoder(&buf).Encode(exp) - if err != nil { - t.Fatalf("unexpected error encoding project: %v", err) - } - - err = badgerDB.Update(func(txn *badgerdb.Txn) error { - return txn.Set(entryKey(projectPrefix, 0, exp.ID[:]), buf.Bytes()) - }) - if err != nil { - t.Fatalf("unexpected error setting project: %v", err) - } - - got, err := database.FindProjectByID(context.Background(), exp.ID) - if err != nil { - t.Fatalf("unexpected error finding project: %v", err) - } - - if diff := cmp.Diff(exp, got, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" { - t.Fatalf("project not equal (-exp, +got):\n%v", diff) - } - }) - - t.Run("project not found", func(t *testing.T) { - t.Parallel() - - database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) - if err != nil { - t.Fatalf("failed to open badger database: %v", err) - } - defer database.Close() - - projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) - - _, err = database.FindProjectByID(context.Background(), projectID) - if !errors.Is(err, proj.ErrProjectNotFound) { - t.Fatalf("expected `proj.ErrProjectNotFound`, got: %v", err) - } - }) -} - -func TestDeleteProject(t *testing.T) { - t.Parallel() - - badgerDB, err := badgerdb.Open(badgerdb.DefaultOptions("").WithInMemory(true)) - if err != nil { - t.Fatalf("failed to open badger database: %v", err) - } - - database := DatabaseFromBadgerDB(badgerDB) - defer database.Close() - - // Store fixtures. - projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) - reqLogID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) - senderReqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) - - err = badgerDB.Update(func(txn *badgerdb.Txn) error { - // Project item. - if err := txn.Set(entryKey(projectPrefix, 0, projectID[:]), nil); err != nil { - return err - } - - // Sender request items. - if err := txn.Set(entryKey(senderReqPrefix, 0, senderReqID[:]), nil); err != nil { - return err - } - if err := txn.Set(entryKey(resLogPrefix, 0, senderReqID[:]), nil); err != nil { - return err - } - err := txn.Set(entryKey(senderReqPrefix, senderReqProjectIDIndex, append(projectID[:], senderReqID[:]...)), nil) - if err != nil { - return err - } - - // Request log items. - if err := txn.Set(entryKey(reqLogPrefix, 0, reqLogID[:]), nil); err != nil { - return err - } - if err := txn.Set(entryKey(resLogPrefix, 0, reqLogID[:]), nil); err != nil { - return err - } - err = txn.Set(entryKey(reqLogPrefix, reqLogProjectIDIndex, append(projectID[:], reqLogID[:]...)), nil) - if err != nil { - return err - } - - return nil - }) - if err != nil { - t.Fatalf("unexpected error creating fixtures: %v", err) - } - - err = database.DeleteProject(context.Background(), projectID) - if err != nil { - t.Fatalf("unexpected error deleting project: %v", err) - } - - // Assert project key was deleted. - err = badgerDB.View(func(txn *badgerdb.Txn) error { - _, err := txn.Get(entryKey(projectPrefix, 0, projectID[:])) - return err - }) - if !errors.Is(err, badgerdb.ErrKeyNotFound) { - t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err) - } - - // Assert request log item was deleted. - err = badgerDB.View(func(txn *badgerdb.Txn) error { - _, err := txn.Get(entryKey(reqLogPrefix, 0, reqLogID[:])) - return err - }) - if !errors.Is(err, badgerdb.ErrKeyNotFound) { - t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err) - } - - // Assert response log item related to request log was deleted. - err = badgerDB.View(func(txn *badgerdb.Txn) error { - _, err := txn.Get(entryKey(resLogPrefix, 0, reqLogID[:])) - return err - }) - if !errors.Is(err, badgerdb.ErrKeyNotFound) { - t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err) - } - - // Assert request log project ID index key was deleted. - err = badgerDB.View(func(txn *badgerdb.Txn) error { - _, err := txn.Get(entryKey(reqLogPrefix, reqLogProjectIDIndex, append(projectID[:], reqLogID[:]...))) - return err - }) - if !errors.Is(err, badgerdb.ErrKeyNotFound) { - t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err) - } - - // Assert sender request item was deleted. - err = badgerDB.View(func(txn *badgerdb.Txn) error { - _, err := txn.Get(entryKey(senderReqPrefix, 0, senderReqID[:])) - return err - }) - if !errors.Is(err, badgerdb.ErrKeyNotFound) { - t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err) - } - - // Assert response log item related to sender request was deleted. - err = badgerDB.View(func(txn *badgerdb.Txn) error { - _, err := txn.Get(entryKey(resLogPrefix, 0, senderReqID[:])) - return err - }) - if !errors.Is(err, badgerdb.ErrKeyNotFound) { - t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err) - } - - // Assert sender request project ID index key was deleted. - err = badgerDB.View(func(txn *badgerdb.Txn) error { - _, err := txn.Get(entryKey(senderReqPrefix, senderReqProjectIDIndex, append(projectID[:], senderReqID[:]...))) - return err - }) - if !errors.Is(err, badgerdb.ErrKeyNotFound) { - t.Fatalf("expected `badger.ErrKeyNotFound`, got: %v", err) - } -} - -func TestProjects(t *testing.T) { - t.Parallel() - - database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) - if err != nil { - t.Fatalf("failed to open badger database: %v", err) - } - defer database.Close() - - exp := []proj.Project{ - { - ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), - Name: "one", - }, - { - ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy), - Name: "two", - }, - } - - // Store fixtures. - for _, project := range exp { - err = database.UpsertProject(context.Background(), project) - if err != nil { - t.Fatalf("unexpected error creating project fixture: %v", err) - } - } - - got, err := database.Projects(context.Background()) - if err != nil { - t.Fatalf("unexpected error finding projects: %v", err) - } - - if len(exp) != len(got) { - t.Fatalf("expected %v projects, got: %v", len(exp), len(got)) - } - - if diff := cmp.Diff(exp, got, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" { - t.Fatalf("projects not equal (-exp, +got):\n%v", diff) - } -} diff --git a/pkg/db/badger/reqlog.go b/pkg/db/badger/reqlog.go deleted file mode 100644 index b925f90..0000000 --- a/pkg/db/badger/reqlog.go +++ /dev/null @@ -1,260 +0,0 @@ -package badger - -import ( - "bytes" - "context" - "encoding/gob" - "errors" - "fmt" - - "github.com/dgraph-io/badger/v3" - "github.com/oklog/ulid" - - "github.com/dstotijn/hetty/pkg/reqlog" - "github.com/dstotijn/hetty/pkg/scope" -) - -func (db *Database) FindRequestLogs( - ctx context.Context, - filter reqlog.FindRequestsFilter, - scope *scope.Scope) ([]reqlog.RequestLog, error, -) { - if filter.ProjectID.Compare(ulid.ULID{}) == 0 { - return nil, reqlog.ErrProjectIDMustBeSet - } - - txn := db.badger.NewTransaction(false) - defer txn.Discard() - - reqLogIDs, err := findRequestLogIDsByProjectID(txn, filter.ProjectID) - if err != nil { - return nil, fmt.Errorf("badger: failed to find request log IDs: %w", err) - } - - reqLogs := make([]reqlog.RequestLog, 0, len(reqLogIDs)) - - for _, reqLogID := range reqLogIDs { - reqLog, err := getRequestLogWithResponse(txn, reqLogID) - if err != nil { - return nil, fmt.Errorf("badger: failed to get request log (id: %v): %w", reqLogID.String(), err) - } - - if filter.OnlyInScope { - if !reqLog.MatchScope(scope) { - continue - } - } - - // Filter by search expression. - // TODO: Once pagination is introduced, this filter logic should be done - // as items are retrieved (e.g. when using a `badger.Iterator`). - if filter.SearchExpr != nil { - match, err := reqLog.Matches(filter.SearchExpr) - if err != nil { - return nil, fmt.Errorf( - "badger: failed to match search expression for request log (id: %v): %w", - reqLogID.String(), err, - ) - } - - if !match { - continue - } - } - - reqLogs = append(reqLogs, reqLog) - } - - return reqLogs, nil -} - -func getRequestLogWithResponse(txn *badger.Txn, reqLogID ulid.ULID) (reqlog.RequestLog, error) { - item, err := txn.Get(entryKey(reqLogPrefix, 0, reqLogID[:])) - - switch { - case errors.Is(err, badger.ErrKeyNotFound): - return reqlog.RequestLog{}, reqlog.ErrRequestNotFound - case err != nil: - return reqlog.RequestLog{}, fmt.Errorf("failed to lookup request log item: %w", err) - } - - reqLog := reqlog.RequestLog{ - ID: reqLogID, - } - - err = item.Value(func(rawReqLog []byte) error { - err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog) - if err != nil { - return fmt.Errorf("failed to decode request log: %w", err) - } - - return nil - }) - if err != nil { - return reqlog.RequestLog{}, fmt.Errorf("failed to retrieve or parse request log value: %w", err) - } - - item, err = txn.Get(entryKey(resLogPrefix, 0, reqLogID[:])) - - if errors.Is(err, badger.ErrKeyNotFound) { - return reqLog, nil - } - - if err != nil { - return reqlog.RequestLog{}, fmt.Errorf("failed to get response log: %w", err) - } - - err = item.Value(func(rawReslog []byte) error { - var resLog reqlog.ResponseLog - err = gob.NewDecoder(bytes.NewReader(rawReslog)).Decode(&resLog) - if err != nil { - return fmt.Errorf("failed to decode response log: %w", err) - } - - reqLog.Response = &resLog - - return nil - }) - if err != nil { - return reqlog.RequestLog{}, fmt.Errorf("failed to retrieve or parse response log value: %w", err) - } - - return reqLog, nil -} - -func (db *Database) FindRequestLogByID(ctx context.Context, reqLogID ulid.ULID) (reqLog reqlog.RequestLog, err error) { - txn := db.badger.NewTransaction(false) - defer txn.Discard() - - reqLog, err = getRequestLogWithResponse(txn, reqLogID) - if err != nil { - return reqlog.RequestLog{}, fmt.Errorf("badger: failed to get request log: %w", err) - } - - return reqLog, nil -} - -func (db *Database) StoreRequestLog(ctx context.Context, reqLog reqlog.RequestLog) error { - buf := bytes.Buffer{} - - err := gob.NewEncoder(&buf).Encode(reqLog) - if err != nil { - return fmt.Errorf("badger: failed to encode request log: %w", err) - } - - entries := []*badger.Entry{ - // Request log itself. - { - Key: entryKey(reqLogPrefix, 0, reqLog.ID[:]), - Value: buf.Bytes(), - }, - // Index by project ID. - { - Key: entryKey(reqLogPrefix, reqLogProjectIDIndex, append(reqLog.ProjectID[:], reqLog.ID[:]...)), - }, - } - - err = db.badger.Update(func(txn *badger.Txn) error { - for i := range entries { - err := txn.SetEntry(entries[i]) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return fmt.Errorf("badger: failed to commit transaction: %w", err) - } - - return nil -} - -func (db *Database) StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error { - buf := bytes.Buffer{} - - err := gob.NewEncoder(&buf).Encode(resLog) - if err != nil { - return fmt.Errorf("badger: failed to encode response log: %w", err) - } - - err = db.badger.Update(func(txn *badger.Txn) error { - return txn.SetEntry(&badger.Entry{ - Key: entryKey(resLogPrefix, 0, reqLogID[:]), - Value: buf.Bytes(), - }) - }) - if err != nil { - return fmt.Errorf("badger: failed to commit transaction: %w", err) - } - - return nil -} - -func (db *Database) ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error { - // Note: this transaction is used just for reading; we use the `badger.WriteBatch` - // API to bulk delete items. - txn := db.badger.NewTransaction(false) - defer txn.Discard() - - reqLogIDs, err := findRequestLogIDsByProjectID(txn, projectID) - if err != nil { - return fmt.Errorf("badger: failed to find request log IDs: %w", err) - } - - writeBatch := db.badger.NewWriteBatch() - defer writeBatch.Cancel() - - for _, reqLogID := range reqLogIDs { - // Delete request logs. - err := writeBatch.Delete(entryKey(reqLogPrefix, 0, reqLogID[:])) - if err != nil { - return fmt.Errorf("badger: failed to delete request log: %w", err) - } - - // Delete related response log. - err = writeBatch.Delete(entryKey(resLogPrefix, 0, reqLogID[:])) - if err != nil { - return fmt.Errorf("badger: failed to delete request log: %w", err) - } - } - - if err := writeBatch.Flush(); err != nil { - return fmt.Errorf("badger: failed to commit batch write: %w", err) - } - - err = db.badger.DropPrefix(entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:])) - if err != nil { - return fmt.Errorf("badger: failed to drop request log project ID index items: %w", err) - } - - return nil -} - -func findRequestLogIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.ULID, error) { - reqLogIDs := make([]ulid.ULID, 0) - opts := badger.DefaultIteratorOptions - opts.PrefetchValues = false - opts.Reverse = true - iterator := txn.NewIterator(opts) - defer iterator.Close() - - var projectIndexKey []byte - - prefix := entryKey(reqLogPrefix, reqLogProjectIDIndex, projectID[:]) - - for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() { - projectIndexKey = iterator.Item().KeyCopy(projectIndexKey) - - var id ulid.ULID - // The request log ID starts *after* the first 2 prefix and index bytes - // and the 16 byte project ID. - if err := id.UnmarshalBinary(projectIndexKey[18:]); err != nil { - return nil, fmt.Errorf("failed to parse request log ID: %w", err) - } - - reqLogIDs = append(reqLogIDs, id) - } - - return reqLogIDs, nil -} diff --git a/pkg/db/badger/sender.go b/pkg/db/badger/sender.go deleted file mode 100644 index 6e2c2b8..0000000 --- a/pkg/db/badger/sender.go +++ /dev/null @@ -1,240 +0,0 @@ -package badger - -import ( - "bytes" - "context" - "encoding/gob" - "errors" - "fmt" - - "github.com/dgraph-io/badger/v3" - "github.com/oklog/ulid" - - "github.com/dstotijn/hetty/pkg/reqlog" - "github.com/dstotijn/hetty/pkg/scope" - "github.com/dstotijn/hetty/pkg/sender" -) - -func (db *Database) StoreSenderRequest(ctx context.Context, req sender.Request) error { - buf := bytes.Buffer{} - - err := gob.NewEncoder(&buf).Encode(req) - if err != nil { - return fmt.Errorf("badger: failed to encode sender request: %w", err) - } - - entries := []*badger.Entry{ - // Sender request itself. - { - Key: entryKey(senderReqPrefix, 0, req.ID[:]), - Value: buf.Bytes(), - }, - // Index by project ID. - { - Key: entryKey(senderReqPrefix, senderReqProjectIDIndex, append(req.ProjectID[:], req.ID[:]...)), - }, - } - - err = db.badger.Update(func(txn *badger.Txn) error { - for i := range entries { - err := txn.SetEntry(entries[i]) - if err != nil { - return err - } - } - return nil - }) - if err != nil { - return fmt.Errorf("badger: failed to commit transaction: %w", err) - } - - return nil -} - -func (db *Database) FindSenderRequestByID(ctx context.Context, senderReqID ulid.ULID) (sender.Request, error) { - txn := db.badger.NewTransaction(false) - defer txn.Discard() - - req, err := getSenderRequestWithResponseLog(txn, senderReqID) - if err != nil { - return sender.Request{}, fmt.Errorf("badger: failed to get sender request: %w", err) - } - - return req, nil -} - -func (db *Database) FindSenderRequests( - ctx context.Context, - filter sender.FindRequestsFilter, - scope *scope.Scope, -) ([]sender.Request, error) { - if filter.ProjectID.Compare(ulid.ULID{}) == 0 { - return nil, sender.ErrProjectIDMustBeSet - } - - txn := db.badger.NewTransaction(false) - defer txn.Discard() - - senderReqIDs, err := findSenderRequestIDsByProjectID(txn, filter.ProjectID) - if err != nil { - return nil, fmt.Errorf("badger: failed to find sender request IDs: %w", err) - } - - senderReqs := make([]sender.Request, 0, len(senderReqIDs)) - - for _, id := range senderReqIDs { - senderReq, err := getSenderRequestWithResponseLog(txn, id) - if err != nil { - return nil, fmt.Errorf("badger: failed to get sender request (id: %v): %w", id.String(), err) - } - - if filter.OnlyInScope { - if !senderReq.MatchScope(scope) { - continue - } - } - - // Filter by search expression. - // TODO: Once pagination is introduced, this filter logic should be done - // as items are retrieved (e.g. when using a `badger.Iterator`). - if filter.SearchExpr != nil { - match, err := senderReq.Matches(filter.SearchExpr) - if err != nil { - return nil, fmt.Errorf( - "badger: failed to match search expression for sender request (id: %v): %w", - id.String(), err, - ) - } - - if !match { - continue - } - } - - senderReqs = append(senderReqs, senderReq) - } - - return senderReqs, nil -} - -func (db *Database) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error { - // Note: this transaction is used just for reading; we use the `badger.WriteBatch` - // API to bulk delete items. - txn := db.badger.NewTransaction(false) - defer txn.Discard() - - senderReqIDs, err := findSenderRequestIDsByProjectID(txn, projectID) - if err != nil { - return fmt.Errorf("badger: failed to find sender request IDs: %w", err) - } - - writeBatch := db.badger.NewWriteBatch() - defer writeBatch.Cancel() - - for _, senderReqID := range senderReqIDs { - // Delete sender requests. - err := writeBatch.Delete(entryKey(senderReqPrefix, 0, senderReqID[:])) - if err != nil { - return fmt.Errorf("badger: failed to delete sender requests: %w", err) - } - - // Delete related response log. - err = writeBatch.Delete(entryKey(resLogPrefix, 0, senderReqID[:])) - if err != nil { - return fmt.Errorf("badger: failed to delete request log: %w", err) - } - } - - if err := writeBatch.Flush(); err != nil { - return fmt.Errorf("badger: failed to commit batch write: %w", err) - } - - err = db.badger.DropPrefix(entryKey(senderReqPrefix, senderReqProjectIDIndex, projectID[:])) - if err != nil { - return fmt.Errorf("badger: failed to drop sender request project ID index items: %w", err) - } - - return nil -} - -func getSenderRequestWithResponseLog(txn *badger.Txn, senderReqID ulid.ULID) (sender.Request, error) { - item, err := txn.Get(entryKey(senderReqPrefix, 0, senderReqID[:])) - - switch { - case errors.Is(err, badger.ErrKeyNotFound): - return sender.Request{}, sender.ErrRequestNotFound - case err != nil: - return sender.Request{}, fmt.Errorf("failed to lookup sender request item: %w", err) - } - - req := sender.Request{ - ID: senderReqID, - } - - err = item.Value(func(rawSenderReq []byte) error { - err = gob.NewDecoder(bytes.NewReader(rawSenderReq)).Decode(&req) - if err != nil { - return fmt.Errorf("failed to decode sender request: %w", err) - } - - return nil - }) - if err != nil { - return sender.Request{}, fmt.Errorf("failed to retrieve or parse sender request value: %w", err) - } - - item, err = txn.Get(entryKey(resLogPrefix, 0, senderReqID[:])) - - if errors.Is(err, badger.ErrKeyNotFound) { - return req, nil - } - - if err != nil { - return sender.Request{}, fmt.Errorf("failed to get response log: %w", err) - } - - err = item.Value(func(rawReslog []byte) error { - var resLog reqlog.ResponseLog - err = gob.NewDecoder(bytes.NewReader(rawReslog)).Decode(&resLog) - if err != nil { - return fmt.Errorf("failed to decode response log: %w", err) - } - - req.Response = &resLog - - return nil - }) - if err != nil { - return sender.Request{}, fmt.Errorf("failed to retrieve or parse response log value: %w", err) - } - - return req, nil -} - -func findSenderRequestIDsByProjectID(txn *badger.Txn, projectID ulid.ULID) ([]ulid.ULID, error) { - senderReqIDs := make([]ulid.ULID, 0) - opts := badger.DefaultIteratorOptions - opts.PrefetchValues = false - opts.Reverse = true - iterator := txn.NewIterator(opts) - defer iterator.Close() - - var projectIndexKey []byte - - prefix := entryKey(senderReqPrefix, senderReqProjectIDIndex, projectID[:]) - - for iterator.Seek(append(prefix, 255)); iterator.ValidForPrefix(prefix); iterator.Next() { - projectIndexKey = iterator.Item().KeyCopy(projectIndexKey) - - var id ulid.ULID - // The request log ID starts *after* the first 2 prefix and index bytes - // and the 16 byte project ID. - if err := id.UnmarshalBinary(projectIndexKey[18:]); err != nil { - return nil, fmt.Errorf("failed to parse sender request ID: %w", err) - } - - senderReqIDs = append(senderReqIDs, id) - } - - return senderReqIDs, nil -} diff --git a/pkg/db/bolt/bolt.go b/pkg/db/bolt/bolt.go new file mode 100644 index 0000000..0dca209 --- /dev/null +++ b/pkg/db/bolt/bolt.go @@ -0,0 +1,60 @@ +package bolt + +import ( + "fmt" + + bolt "go.etcd.io/bbolt" +) + +// Database is used to store and retrieve data from an underlying Bolt database. +type Database struct { + bolt *bolt.DB +} + +// OpenDatabase opens a new Bolt database. +func OpenDatabase(path string, opts *bolt.Options) (*Database, error) { + db, err := bolt.Open(path, 0o600, opts) + if err != nil { + return nil, fmt.Errorf("bolt: failed to open database: %w", err) + } + + return DatabaseFromBoltDB(db) +} + +// Close closes the underlying Bolt database. +func (db *Database) Close() error { + return db.bolt.Close() +} + +// DatabaseFromBoltDB returns a Database with `db` set as the underlying Bolt +// database. +func DatabaseFromBoltDB(db *bolt.DB) (*Database, error) { + err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(projectsBucketName) + if err != nil { + return err + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("bolt: failed to create projects bucket: %w", err) + } + + return &Database{bolt: db}, nil +} + +func createNestedBucket(tx *bolt.Tx, names ...[]byte) (b *bolt.Bucket, err error) { + for i, name := range names { + if b == nil { + b, err = tx.CreateBucketIfNotExists(name) + } else { + b, err = b.CreateBucketIfNotExists(name) + } + if err != nil { + return nil, fmt.Errorf("bolt: failed to create nested bucket %q: %w", names[:i+1], err) + } + } + + return b, nil +} diff --git a/pkg/db/bolt/logger.go b/pkg/db/bolt/logger.go new file mode 100644 index 0000000..2be083d --- /dev/null +++ b/pkg/db/bolt/logger.go @@ -0,0 +1,23 @@ +package bolt + +import ( + bolt "go.etcd.io/bbolt" + "go.uber.org/zap" +) + +// Interface guard. +var _ bolt.Logger = (*Logger)(nil) + +type Logger struct { + *zap.SugaredLogger +} + +// Warning implements bbolt.Logger. +func (l *Logger) Warning(v ...interface{}) { + l.Warn(v...) +} + +// Warningf implements bbolt.Logger. +func (l *Logger) Warningf(format string, v ...interface{}) { + l.Warnf(format, v...) +} diff --git a/pkg/db/bolt/proj.go b/pkg/db/bolt/proj.go new file mode 100644 index 0000000..4ac7c05 --- /dev/null +++ b/pkg/db/bolt/proj.go @@ -0,0 +1,176 @@ +package bolt + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + + "github.com/oklog/ulid" + bolt "go.etcd.io/bbolt" + + "github.com/dstotijn/hetty/pkg/proj" +) + +var ( + ErrProjectsBucketNotFound = errors.New("bolt: projects bucket not found") + ErrProjectBucketNotFound = errors.New("bolt: project bucket not found") +) + +var ( + projectsBucketName = []byte("projects") + projectKey = []byte("project") +) + +func projectsBucket(tx *bolt.Tx) (*bolt.Bucket, error) { + b := tx.Bucket(projectsBucketName) + if b == nil { + return nil, ErrProjectsBucketNotFound + } + + return b, nil +} + +func projectBucket(tx *bolt.Tx, projectID []byte) (*bolt.Bucket, error) { + pb, err := projectsBucket(tx) + if err != nil { + return nil, err + } + + b := pb.Bucket(projectID[:]) + if b == nil { + return nil, ErrProjectBucketNotFound + } + + return b, nil +} + +func (db *Database) UpsertProject(ctx context.Context, project proj.Project) error { + buf := bytes.Buffer{} + + err := gob.NewEncoder(&buf).Encode(project) + if err != nil { + return fmt.Errorf("bolt: failed to encode project: %w", err) + } + + err = db.bolt.Update(func(tx *bolt.Tx) error { + b, err := createNestedBucket(tx, projectsBucketName, project.ID[:]) + if err != nil { + return fmt.Errorf("bolt: failed to create project bucket: %w", err) + } + + err = b.Put(projectKey, buf.Bytes()) + if err != nil { + return fmt.Errorf("bolt: failed to upsert project: %w", err) + } + + _, err = b.CreateBucketIfNotExists(reqLogsBucketName) + if err != nil { + return fmt.Errorf("bolt: failed to create request logs bucket: %w", err) + } + + _, err = b.CreateBucketIfNotExists(senderReqsBucketName) + if err != nil { + return fmt.Errorf("bolt: failed to create sender requests bucket: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return nil +} + +func (db *Database) FindProjectByID(ctx context.Context, projectID ulid.ULID) (project proj.Project, err error) { + err = db.bolt.View(func(tx *bolt.Tx) error { + bucket, err := projectBucket(tx, projectID[:]) + if errors.Is(err, ErrProjectsBucketNotFound) || errors.Is(err, ErrProjectBucketNotFound) { + return proj.ErrProjectNotFound + } + if err != nil { + return err + } + + rawProject := bucket.Get(projectKey) + if rawProject == nil { + return proj.ErrProjectNotFound + } + + err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project) + if err != nil { + return fmt.Errorf("failed to decode project: %w", err) + } + + return nil + }) + if err != nil { + return proj.Project{}, fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return project, nil +} + +func (db *Database) DeleteProject(ctx context.Context, projectID ulid.ULID) error { + err := db.bolt.Update(func(tx *bolt.Tx) error { + pb, err := projectsBucket(tx) + if err != nil { + return err + } + + err = pb.DeleteBucket(projectID[:]) + if err != nil { + return fmt.Errorf("failed to delete project bucket: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return nil +} + +func (db *Database) Projects(ctx context.Context) ([]proj.Project, error) { + projects := make([]proj.Project, 0) + + err := db.bolt.View(func(tx *bolt.Tx) error { + pb, err := projectsBucket(tx) + if err != nil { + return err + } + + err = pb.ForEachBucket(func(projectID []byte) error { + bucket, err := projectBucket(tx, projectID) + if err != nil { + return err + } + + rawProject := bucket.Get(projectKey) + if rawProject == nil { + return proj.ErrProjectNotFound + } + + var project proj.Project + err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&project) + if err != nil { + return fmt.Errorf("bolt: failed to decode project: %w", err) + } + projects = append(projects, project) + return nil + }) + if err != nil { + return fmt.Errorf("bolt: failed to iterate over projects: %w", err) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return projects, nil +} diff --git a/pkg/db/bolt/proj_test.go b/pkg/db/bolt/proj_test.go new file mode 100644 index 0000000..2026238 --- /dev/null +++ b/pkg/db/bolt/proj_test.go @@ -0,0 +1,267 @@ +package bolt_test + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "math/rand" + "regexp" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + "github.com/oklog/ulid" + "go.etcd.io/bbolt" + + "github.com/dstotijn/hetty/pkg/db/bolt" + "github.com/dstotijn/hetty/pkg/filter" + "github.com/dstotijn/hetty/pkg/proj" + "github.com/dstotijn/hetty/pkg/scope" +) + +//nolint:gosec +var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano())) + +var regexpCompareOpt = cmp.Comparer(func(x, y *regexp.Regexp) bool { + switch { + case x == nil && y == nil: + return true + case x == nil || y == nil: + return false + default: + return x.String() == y.String() + } +}) + +func TestUpsertProject(t *testing.T) { + t.Parallel() + + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) + if err != nil { + t.Fatalf("failed to open bolt database: %v", err) + } + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + searchExpr, err := filter.ParseQuery("foo AND bar OR NOT baz") + if err != nil { + t.Fatalf("unexpected error (expected: nil, got: %v)", err) + } + + exp := proj.Project{ + ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), + Name: "foobar", + Settings: proj.Settings{ + ReqLogBypassOutOfScope: true, + ReqLogOnlyFindInScope: true, + ReqLogSearchExpr: searchExpr, + ScopeRules: []scope.Rule{ + { + URL: regexp.MustCompile("^https://(.*)example.com(.*)$"), + Header: scope.Header{ + Key: regexp.MustCompile("^X-Foo(.*)$"), + Value: regexp.MustCompile("^foo(.*)$"), + }, + Body: regexp.MustCompile("^foo(.*)"), + }, + }, + }, + } + + err = db.UpsertProject(context.Background(), exp) + if err != nil { + t.Fatalf("unexpected error storing project: %v", err) + } + + var rawProject []byte + + err = boltDB.View(func(tx *bbolt.Tx) error { + rawProject = tx.Bucket([]byte("projects")).Bucket(exp.ID[:]).Get([]byte("project")) + return nil + }) + if err != nil { + t.Fatalf("unexpected error retrieving project from database: %v", err) + } + if rawProject == nil { + t.Fatalf("expected raw project to be retrieved, got: nil") + } + + got := proj.Project{} + + err = gob.NewDecoder(bytes.NewReader(rawProject)).Decode(&got) + if err != nil { + t.Fatalf("unexpected error decoding project: %v", err) + } + + if diff := cmp.Diff(exp, got, regexpCompareOpt, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" { + t.Fatalf("project not equal (-exp, +got):\n%v", diff) + } +} + +func TestFindProjectByID(t *testing.T) { + t.Parallel() + + t.Run("existing project", func(t *testing.T) { + t.Parallel() + + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) + if err != nil { + t.Fatalf("failed to open bolt database: %v", err) + } + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + exp := proj.Project{ + ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), + } + + buf := bytes.Buffer{} + + err = gob.NewEncoder(&buf).Encode(exp) + if err != nil { + t.Fatalf("unexpected error encoding project: %v", err) + } + + err = boltDB.Update(func(tx *bbolt.Tx) error { + b, err := tx.Bucket([]byte("projects")).CreateBucket(exp.ID[:]) + if err != nil { + return err + } + return b.Put([]byte("project"), buf.Bytes()) + }) + if err != nil { + t.Fatalf("unexpected error setting project: %v", err) + } + + got, err := db.FindProjectByID(context.Background(), exp.ID) + if err != nil { + t.Fatalf("unexpected error finding project: %v", err) + } + + if diff := cmp.Diff(exp, got, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" { + t.Fatalf("project not equal (-exp, +got):\n%v", diff) + } + }) + + t.Run("project not found", func(t *testing.T) { + t.Parallel() + + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) + if err != nil { + t.Fatalf("failed to open bolt database: %v", err) + } + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + + _, err = db.FindProjectByID(context.Background(), projectID) + if !errors.Is(err, proj.ErrProjectNotFound) { + t.Fatalf("expected `proj.ErrProjectNotFound`, got: %v", err) + } + }) +} + +func TestDeleteProject(t *testing.T) { + t.Parallel() + + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) + if err != nil { + t.Fatalf("failed to open bolt database: %v", err) + } + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + // Insert test fixture. + projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + }) + if err != nil { + t.Fatalf("unexpected error storing project: %v", err) + } + + err = db.DeleteProject(context.Background(), projectID) + if err != nil { + t.Fatalf("unexpected error deleting project: %v", err) + } + + var got *bbolt.Bucket + err = boltDB.View(func(tx *bbolt.Tx) error { + got = tx.Bucket([]byte("projects")).Bucket(projectID[:]) + return nil + }) + if got != nil { + t.Fatalf("expected bucket to be nil, got: %v", got) + } +} + +func TestProjects(t *testing.T) { + t.Parallel() + + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) + if err != nil { + t.Fatalf("failed to open bolt database: %v", err) + } + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + exp := []proj.Project{ + { + ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), + Name: "one", + }, + { + ID: ulid.MustNew(ulid.Timestamp(time.Now())+100, ulidEntropy), + Name: "two", + }, + } + + // Store fixtures. + for _, project := range exp { + err = db.UpsertProject(context.Background(), project) + if err != nil { + t.Fatalf("unexpected error creating project fixture: %v", err) + } + } + + got, err := db.Projects(context.Background()) + if err != nil { + t.Fatalf("unexpected error finding projects: %v", err) + } + + if len(exp) != len(got) { + t.Fatalf("expected %v projects, got: %v", len(exp), len(got)) + } + + if diff := cmp.Diff(exp, got, cmpopts.IgnoreUnexported(proj.Project{})); diff != "" { + t.Fatalf("projects not equal (-exp, +got):\n%v", diff) + } +} diff --git a/pkg/db/bolt/reqlog.go b/pkg/db/bolt/reqlog.go new file mode 100644 index 0000000..ccff14e --- /dev/null +++ b/pkg/db/bolt/reqlog.go @@ -0,0 +1,204 @@ +package bolt + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + + "github.com/oklog/ulid" + bolt "go.etcd.io/bbolt" + + "github.com/dstotijn/hetty/pkg/reqlog" + "github.com/dstotijn/hetty/pkg/scope" +) + +var ErrRequestLogsBucketNotFound = errors.New("bolt: request logs bucket not found") + +var reqLogsBucketName = []byte("request_logs") + +func requestLogsBucket(tx *bolt.Tx, projectID ulid.ULID) (*bolt.Bucket, error) { + pb, err := projectBucket(tx, projectID[:]) + if err != nil { + return nil, err + } + + b := pb.Bucket(reqLogsBucketName) + if b == nil { + return nil, ErrRequestLogsBucketNotFound + } + + return b, nil +} + +func (db *Database) FindRequestLogs(ctx context.Context, filter reqlog.FindRequestsFilter, scope *scope.Scope) (reqLogs []reqlog.RequestLog, err error) { + if filter.ProjectID.Compare(ulid.ULID{}) == 0 { + return nil, reqlog.ErrProjectIDMustBeSet + } + + tx, err := db.bolt.Begin(false) + if err != nil { + return nil, fmt.Errorf("bolt: failed to begin transaction: %w", err) + } + defer tx.Rollback() + + b, err := requestLogsBucket(tx, filter.ProjectID) + if err != nil { + return nil, fmt.Errorf("bolt: failed to get request logs bucket: %w", err) + } + + err = b.ForEach(func(reqLogID, rawReqLog []byte) error { + var reqLog reqlog.RequestLog + err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog) + if err != nil { + return fmt.Errorf("failed to decode request log: %w", err) + } + + if filter.OnlyInScope && !reqLog.MatchScope(scope) { + return nil + } + + // Filter by search expression. TODO: Once pagination is introduced, + // this filter logic should be done as items are retrieved. + if filter.SearchExpr != nil { + match, err := reqLog.Matches(filter.SearchExpr) + if err != nil { + return fmt.Errorf("failed to match search expression for request log (id: %v): %w", reqLogID, err) + } + + if !match { + return nil + } + } + + reqLogs = append(reqLogs, reqLog) + return nil + }) + if err != nil { + return nil, fmt.Errorf("bolt: failed to iterate over request logs: %w", err) + } + + // Reverse items, so newest requests appear first. + for i, j := 0, len(reqLogs)-1; i < j; i, j = i+1, j-1 { + reqLogs[i], reqLogs[j] = reqLogs[j], reqLogs[i] + } + + return reqLogs, nil +} + +func (db *Database) FindRequestLogByID(ctx context.Context, projectID, reqLogID ulid.ULID) (reqLog reqlog.RequestLog, err error) { + err = db.bolt.View(func(tx *bolt.Tx) error { + b, err := requestLogsBucket(tx, projectID) + if err != nil { + return fmt.Errorf("bolt: failed to get request logs bucket: %w", err) + } + rawReqLog := b.Get(reqLogID[:]) + if rawReqLog == nil { + return reqlog.ErrRequestNotFound + } + + err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog) + if err != nil { + return fmt.Errorf("failed to decode request log: %w", err) + } + + return nil + }) + if err != nil { + return reqlog.RequestLog{}, fmt.Errorf("bolt: failed to find request log by ID: %w", err) + } + + return reqLog, nil +} + +func (db *Database) StoreRequestLog(ctx context.Context, reqLog reqlog.RequestLog) error { + buf := bytes.Buffer{} + + err := gob.NewEncoder(&buf).Encode(reqLog) + if err != nil { + return fmt.Errorf("bolt: failed to encode request log: %w", err) + } + + err = db.bolt.Update(func(txn *bolt.Tx) error { + b, err := requestLogsBucket(txn, reqLog.ProjectID) + if err != nil { + return fmt.Errorf("failed to get request logs bucket: %w", err) + } + + err = b.Put(reqLog.ID[:], buf.Bytes()) + if err != nil { + return fmt.Errorf("failed to put request log: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return nil +} + +func (db *Database) StoreResponseLog(ctx context.Context, projectID, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error { + buf := bytes.Buffer{} + + err := gob.NewEncoder(&buf).Encode(resLog) + if err != nil { + return fmt.Errorf("bolt: failed to encode response log: %w", err) + } + + err = db.bolt.Update(func(txn *bolt.Tx) error { + b, err := requestLogsBucket(txn, projectID) + if err != nil { + return fmt.Errorf("failed to get request logs bucket: %w", err) + } + + rawReqLog := b.Get(reqLogID[:]) + if rawReqLog == nil { + return reqlog.ErrRequestNotFound + } + + var reqLog reqlog.RequestLog + err = gob.NewDecoder(bytes.NewReader(rawReqLog)).Decode(&reqLog) + if err != nil { + return fmt.Errorf("failed to decode request log: %w", err) + } + + reqLog.Response = &resLog + + buf := bytes.Buffer{} + err = gob.NewEncoder(&buf).Encode(reqLog) + if err != nil { + return fmt.Errorf("failed to encode request log: %w", err) + } + + err = b.Put(reqLog.ID[:], buf.Bytes()) + if err != nil { + return fmt.Errorf("failed to put request log: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return nil +} + +func (db *Database) ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error { + err := db.bolt.Update(func(txn *bolt.Tx) error { + pb, err := projectBucket(txn, projectID[:]) + if err != nil { + return fmt.Errorf("failed to get project bucket: %w", err) + } + + return pb.DeleteBucket(reqLogsBucketName) + }) + if err != nil { + return fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return nil +} diff --git a/pkg/db/badger/reqlog_test.go b/pkg/db/bolt/reqlog_test.go similarity index 69% rename from pkg/db/badger/reqlog_test.go rename to pkg/db/bolt/reqlog_test.go index fca7a1c..16df508 100644 --- a/pkg/db/badger/reqlog_test.go +++ b/pkg/db/bolt/reqlog_test.go @@ -1,4 +1,4 @@ -package badger +package bolt_test import ( "context" @@ -8,10 +8,12 @@ import ( "testing" "time" - badgerdb "github.com/dgraph-io/badger/v3" "github.com/google/go-cmp/cmp" "github.com/oklog/ulid" + "go.etcd.io/bbolt" + "github.com/dstotijn/hetty/pkg/db/bolt" + "github.com/dstotijn/hetty/pkg/proj" "github.com/dstotijn/hetty/pkg/reqlog" ) @@ -21,15 +23,21 @@ func TestFindRequestLogs(t *testing.T) { t.Run("without project ID in filter", func(t *testing.T) { t.Parallel() - database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open badger database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) } - defer database.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() filter := reqlog.FindRequestsFilter{} - _, err = database.FindRequestLogs(context.Background(), filter, nil) + _, err = db.FindRequestLogs(context.Background(), filter, nil) if !errors.Is(err, reqlog.ErrProjectIDMustBeSet) { t.Fatalf("expected `reqlog.ErrProjectIDMustBeSet`, got: %v", err) } @@ -38,14 +46,27 @@ func TestFindRequestLogs(t *testing.T) { t.Run("returns request logs and related response logs", func(t *testing.T) { t.Parallel() - database, err := OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open badger database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) } - defer database.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + }) + if err != nil { + t.Fatalf("unexpected error upserting project: %v", err) + } + fixtures := []reqlog.RequestLog{ { ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), @@ -81,24 +102,17 @@ func TestFindRequestLogs(t *testing.T) { // Store fixtures. for _, reqLog := range fixtures { - err = database.StoreRequestLog(context.Background(), reqLog) + err = db.StoreRequestLog(context.Background(), reqLog) if err != nil { t.Fatalf("unexpected error creating request log fixture: %v", err) } - - if reqLog.Response != nil { - err = database.StoreResponseLog(context.Background(), reqLog.ID, *reqLog.Response) - if err != nil { - t.Fatalf("unexpected error creating response log fixture: %v", err) - } - } } filter := reqlog.FindRequestsFilter{ ProjectID: projectID, } - got, err := database.FindRequestLogs(context.Background(), filter, nil) + got, err := db.FindRequestLogs(context.Background(), filter, nil) if err != nil { t.Fatalf("unexpected error finding request logs: %v", err) } diff --git a/pkg/db/bolt/sender.go b/pkg/db/bolt/sender.go new file mode 100644 index 0000000..a777801 --- /dev/null +++ b/pkg/db/bolt/sender.go @@ -0,0 +1,172 @@ +package bolt + +import ( + "bytes" + "context" + "encoding/gob" + "errors" + "fmt" + + "github.com/oklog/ulid" + bolt "go.etcd.io/bbolt" + + "github.com/dstotijn/hetty/pkg/scope" + "github.com/dstotijn/hetty/pkg/sender" +) + +var ErrSenderRequestsBucketNotFound = errors.New("bolt: sender requests bucket not found") + +var senderReqsBucketName = []byte("sender_requests") + +func senderReqsBucket(tx *bolt.Tx, projectID ulid.ULID) (*bolt.Bucket, error) { + pb, err := projectBucket(tx, projectID[:]) + if err != nil { + return nil, err + } + + b := pb.Bucket(senderReqsBucketName) + if b == nil { + return nil, ErrSenderRequestsBucketNotFound + } + + return b, nil +} + +func (db *Database) StoreSenderRequest(ctx context.Context, req sender.Request) error { + buf := bytes.Buffer{} + + err := gob.NewEncoder(&buf).Encode(req) + if err != nil { + return fmt.Errorf("bolt: failed to encode sender request: %w", err) + } + + err = db.bolt.Update(func(tx *bolt.Tx) error { + senderReqsBucket, err := senderReqsBucket(tx, req.ProjectID) + if err != nil { + return fmt.Errorf("failed to get sender requests bucket: %w", err) + } + + err = senderReqsBucket.Put(req.ID[:], buf.Bytes()) + if err != nil { + return fmt.Errorf("failed to put sender request: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return nil +} + +func (db *Database) FindSenderRequestByID(ctx context.Context, projectID, senderReqID ulid.ULID) (req sender.Request, err error) { + if projectID.Compare(ulid.ULID{}) == 0 { + return sender.Request{}, sender.ErrProjectIDMustBeSet + } + + err = db.bolt.View(func(tx *bolt.Tx) error { + senderReqsBucket, err := senderReqsBucket(tx, projectID) + if err != nil { + return fmt.Errorf("failed to get sender requests bucket: %w", err) + } + + rawSenderReq := senderReqsBucket.Get(senderReqID[:]) + if rawSenderReq == nil { + return sender.ErrRequestNotFound + } + + err = gob.NewDecoder(bytes.NewReader(rawSenderReq)).Decode(&req) + if err != nil { + return fmt.Errorf("failed to decode sender request: %w", err) + } + + return nil + }) + if err != nil { + return sender.Request{}, fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return req, nil +} + +func (db *Database) FindSenderRequests(ctx context.Context, filter sender.FindRequestsFilter, scope *scope.Scope) (reqs []sender.Request, err error) { + if filter.ProjectID.Compare(ulid.ULID{}) == 0 { + return nil, sender.ErrProjectIDMustBeSet + } + + tx, err := db.bolt.Begin(false) + if err != nil { + return nil, fmt.Errorf("bolt: failed to begin transaction: %w", err) + } + defer tx.Rollback() + + b, err := senderReqsBucket(tx, filter.ProjectID) + if err != nil { + return nil, fmt.Errorf("failed to get sender requests bucket: %w", err) + } + + err = b.ForEach(func(senderReqID, rawSenderReq []byte) error { + var req sender.Request + err = gob.NewDecoder(bytes.NewReader(rawSenderReq)).Decode(&req) + if err != nil { + return fmt.Errorf("failed to decode sender request: %w", err) + } + + if filter.OnlyInScope { + if !req.MatchScope(scope) { + return nil + } + } + + // Filter by search expression. TODO: Once pagination is introduced, + // this filter logic should be done as items are retrieved. + if filter.SearchExpr != nil { + match, err := req.Matches(filter.SearchExpr) + if err != nil { + return fmt.Errorf( + "bolt: failed to match search expression for sender request (id: %v): %w", + senderReqID, err, + ) + } + + if !match { + return nil + } + } + + reqs = append(reqs, req) + return nil + }) + if err != nil { + return nil, fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + // Reverse items, so newest requests appear first. + for i, j := 0, len(reqs)-1; i < j; i, j = i+1, j-1 { + reqs[i], reqs[j] = reqs[j], reqs[i] + } + + return reqs, nil +} + +func (db *Database) DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error { + err := db.bolt.Update(func(tx *bolt.Tx) error { + senderReqsBucket, err := senderReqsBucket(tx, projectID) + if err != nil { + return fmt.Errorf("failed to get sender requests bucket: %w", err) + } + + err = senderReqsBucket.DeleteBucket(senderReqsBucketName) + if err != nil { + return fmt.Errorf("failed to delete sender requests bucket: %w", err) + } + + return nil + }) + if err != nil { + return fmt.Errorf("bolt: failed to commit transaction: %w", err) + } + + return nil +} diff --git a/pkg/db/badger/sender_test.go b/pkg/db/bolt/sender_test.go similarity index 63% rename from pkg/db/badger/sender_test.go rename to pkg/db/bolt/sender_test.go index c6477b1..68af0ee 100644 --- a/pkg/db/badger/sender_test.go +++ b/pkg/db/bolt/sender_test.go @@ -1,26 +1,23 @@ -package badger_test +package bolt_test import ( "context" "errors" - "math/rand" "net/http" "net/url" "testing" "time" - badgerdb "github.com/dgraph-io/badger/v3" "github.com/google/go-cmp/cmp" "github.com/oklog/ulid" + "go.etcd.io/bbolt" - "github.com/dstotijn/hetty/pkg/db/badger" + "github.com/dstotijn/hetty/pkg/db/bolt" + "github.com/dstotijn/hetty/pkg/proj" "github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/sender" ) -//nolint:gosec -var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano())) - var exampleURL = func() *url.URL { u, err := url.Parse("https://example.com/foobar") if err != nil { @@ -33,18 +30,35 @@ var exampleURL = func() *url.URL { func TestFindRequestByID(t *testing.T) { t.Parallel() - database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open badger database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) + } + defer boltDB.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + }) + if err != nil { + t.Fatalf("unexpected error upserting project: %v", err) } - defer database.Close() // See: https://go.dev/blog/subtests#cleaning-up-after-a-group-of-parallel-tests t.Run("group", func(t *testing.T) { t.Run("sender request not found", func(t *testing.T) { t.Parallel() - _, err := database.FindSenderRequestByID(context.Background(), ulid.ULID{}) + _, err := db.FindSenderRequestByID(context.Background(), projectID, reqID) if !errors.Is(err, sender.ErrRequestNotFound) { t.Fatalf("expected `sender.ErrRequestNotFound`, got: %v", err) } @@ -55,7 +69,7 @@ func TestFindRequestByID(t *testing.T) { exp := sender.Request{ ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), - ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), + ProjectID: projectID, SourceRequestLogID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), URL: exampleURL, @@ -65,31 +79,23 @@ func TestFindRequestByID(t *testing.T) { "X-Foo": []string{"bar"}, }, Body: []byte("foo"), - } - - err := database.StoreSenderRequest(context.Background(), exp) - if err != nil { - t.Fatalf("unexpected error (expected: nil, got: %v)", err) - } - - resLog := reqlog.ResponseLog{ - Proto: "HTTP/2.0", - Status: "200 OK", - StatusCode: 200, - Header: http.Header{ - "X-Yolo": []string{"swag"}, + Response: &reqlog.ResponseLog{ + Proto: "HTTP/2.0", + Status: "200 OK", + StatusCode: 200, + Header: http.Header{ + "X-Yolo": []string{"swag"}, + }, + Body: []byte("bar"), }, - Body: []byte("bar"), } - err = database.StoreResponseLog(context.Background(), exp.ID, resLog) + err := db.StoreSenderRequest(context.Background(), exp) if err != nil { t.Fatalf("unexpected error (expected: nil, got: %v)", err) } - exp.Response = &resLog - - got, err := database.FindSenderRequestByID(context.Background(), exp.ID) + got, err := db.FindSenderRequestByID(context.Background(), exp.ProjectID, exp.ID) if err != nil { t.Fatalf("unexpected error (expected: nil, got: %v)", err) } @@ -107,15 +113,22 @@ func TestFindSenderRequests(t *testing.T) { t.Run("without project ID in filter", func(t *testing.T) { t.Parallel() - database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open badger database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) } - defer database.Close() + defer boltDB.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() filter := sender.FindRequestsFilter{} - _, err = database.FindSenderRequests(context.Background(), filter, nil) + _, err = db.FindSenderRequests(context.Background(), filter, nil) if !errors.Is(err, sender.ErrProjectIDMustBeSet) { t.Fatalf("expected `sender.ErrProjectIDMustBeSet`, got: %v", err) } @@ -124,14 +137,30 @@ func TestFindSenderRequests(t *testing.T) { t.Run("returns sender requests and related response logs", func(t *testing.T) { t.Parallel() - database, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open badger database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) } - defer database.Close() + defer boltDB.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + Name: "foobar", + Settings: proj.Settings{}, + }) + if err != nil { + t.Fatalf("unexpected error creating project (expected: nil, got: %v)", err) + } + fixtures := []sender.Request{ { ID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), @@ -169,24 +198,17 @@ func TestFindSenderRequests(t *testing.T) { // Store fixtures. for _, senderReq := range fixtures { - err = database.StoreSenderRequest(context.Background(), senderReq) + err = db.StoreSenderRequest(context.Background(), senderReq) if err != nil { t.Fatalf("unexpected error creating request log fixture: %v", err) } - - if senderReq.Response != nil { - err = database.StoreResponseLog(context.Background(), senderReq.ID, *senderReq.Response) - if err != nil { - t.Fatalf("unexpected error creating response log fixture: %v", err) - } - } } filter := sender.FindRequestsFilter{ ProjectID: projectID, } - got, err := database.FindSenderRequests(context.Background(), filter, nil) + got, err := db.FindSenderRequests(context.Background(), filter, nil) if err != nil { t.Fatalf("unexpected error finding sender requests: %v", err) } diff --git a/pkg/reqlog/repo.go b/pkg/reqlog/repo.go index f991393..282e0c0 100644 --- a/pkg/reqlog/repo.go +++ b/pkg/reqlog/repo.go @@ -10,8 +10,8 @@ import ( type Repository interface { FindRequestLogs(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]RequestLog, error) - FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) + FindRequestLogByID(ctx context.Context, projectID, id ulid.ULID) (RequestLog, error) StoreRequestLog(ctx context.Context, reqLog RequestLog) error - StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog ResponseLog) error + StoreResponseLog(ctx context.Context, projectID, reqLogID ulid.ULID, resLog ResponseLog) error ClearRequestLogs(ctx context.Context, projectID ulid.ULID) error } diff --git a/pkg/reqlog/reqlog.go b/pkg/reqlog/reqlog.go index a489be6..c6ba8f7 100644 --- a/pkg/reqlog/reqlog.go +++ b/pkg/reqlog/reqlog.go @@ -67,16 +67,18 @@ type FindRequestsFilter struct { } type Config struct { - Scope *scope.Scope - Repository Repository - Logger log.Logger + ActiveProjectID ulid.ULID + Scope *scope.Scope + Repository Repository + Logger log.Logger } func NewService(cfg Config) *Service { s := &Service{ - repo: cfg.Repository, - scope: cfg.Scope, - logger: cfg.Logger, + activeProjectID: cfg.ActiveProjectID, + repo: cfg.Repository, + scope: cfg.Scope, + logger: cfg.Logger, } if s.logger == nil { @@ -91,7 +93,7 @@ func (svc *Service) FindRequests(ctx context.Context) ([]RequestLog, error) { } func (svc *Service) FindRequestLogByID(ctx context.Context, id ulid.ULID) (RequestLog, error) { - return svc.repo.FindRequestLogByID(ctx, id) + return svc.repo.FindRequestLogByID(ctx, svc.activeProjectID, id) } func (svc *Service) ClearRequests(ctx context.Context, projectID ulid.ULID) error { @@ -104,7 +106,7 @@ func (svc *Service) storeResponse(ctx context.Context, reqLogID ulid.ULID, res * return err } - return svc.repo.StoreResponseLog(ctx, reqLogID, resLog) + return svc.repo.StoreResponseLog(ctx, svc.activeProjectID, reqLogID, resLog) } func (svc *Service) RequestModifier(next proxy.RequestModifyFunc) proxy.RequestModifyFunc { diff --git a/pkg/reqlog/reqlog_test.go b/pkg/reqlog/reqlog_test.go index a90706d..acee01b 100644 --- a/pkg/reqlog/reqlog_test.go +++ b/pkg/reqlog/reqlog_test.go @@ -12,9 +12,10 @@ import ( "github.com/google/go-cmp/cmp" "github.com/oklog/ulid" + "go.etcd.io/bbolt" - badgerdb "github.com/dgraph-io/badger/v3" - "github.com/dstotijn/hetty/pkg/db/badger" + "github.com/dstotijn/hetty/pkg/db/bolt" + "github.com/dstotijn/hetty/pkg/proj" "github.com/dstotijn/hetty/pkg/proxy" "github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/scope" @@ -25,16 +26,32 @@ var ulidEntropy = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint:paralleltest func TestRequestModifier(t *testing.T) { - db, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) + } + defer boltDB.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + }) + if err != nil { + t.Fatalf("unexpected error upserting project: %v", err) } svc := reqlog.NewService(reqlog.Config{ Repository: db, Scope: &scope.Scope{}, }) - svc.SetActiveProjectID(ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)) + svc.SetActiveProjectID(projectID) next := func(req *http.Request) { req.Body = io.NopCloser(strings.NewReader("modified body")) @@ -70,15 +87,31 @@ func TestRequestModifier(t *testing.T) { //nolint:paralleltest func TestResponseModifier(t *testing.T) { - db, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) + } + defer boltDB.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() + + projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + }) + if err != nil { + t.Fatalf("unexpected error upserting project: %v", err) } svc := reqlog.NewService(reqlog.Config{ Repository: db, }) - svc.SetActiveProjectID(ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy)) + svc.SetActiveProjectID(projectID) next := func(res *http.Response) error { res.Body = io.NopCloser(strings.NewReader("modified body")) @@ -91,7 +124,8 @@ func TestResponseModifier(t *testing.T) { req = req.WithContext(context.WithValue(req.Context(), reqlog.ReqLogIDKey, reqLogID)) err = db.StoreRequestLog(context.Background(), reqlog.RequestLog{ - ID: reqLogID, + ID: reqLogID, + ProjectID: projectID, }) if err != nil { t.Fatalf("failed to store request log: %v", err) diff --git a/pkg/sender/repo.go b/pkg/sender/repo.go index f41a7c5..32cbcad 100644 --- a/pkg/sender/repo.go +++ b/pkg/sender/repo.go @@ -5,14 +5,12 @@ import ( "github.com/oklog/ulid" - "github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/scope" ) type Repository interface { - FindSenderRequestByID(ctx context.Context, id ulid.ULID) (Request, error) + FindSenderRequestByID(ctx context.Context, projectID, id ulid.ULID) (Request, error) FindSenderRequests(ctx context.Context, filter FindRequestsFilter, scope *scope.Scope) ([]Request, error) StoreSenderRequest(ctx context.Context, req Request) error - StoreResponseLog(ctx context.Context, reqLogID ulid.ULID, resLog reqlog.ResponseLog) error DeleteSenderRequests(ctx context.Context, projectID ulid.ULID) error } diff --git a/pkg/sender/sender.go b/pkg/sender/sender.go index 9a5197a..2d5377e 100644 --- a/pkg/sender/sender.go +++ b/pkg/sender/sender.go @@ -86,7 +86,7 @@ type Request struct { } func (svc *Service) FindRequestByID(ctx context.Context, id ulid.ULID) (Request, error) { - req, err := svc.repo.FindSenderRequestByID(ctx, id) + req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, id) if err != nil { return Request{}, fmt.Errorf("sender: failed to find request: %w", err) } @@ -167,7 +167,7 @@ func (svc *Service) FindReqsFilter() FindRequestsFilter { } func (svc *Service) SendRequest(ctx context.Context, id ulid.ULID) (Request, error) { - req, err := svc.repo.FindSenderRequestByID(ctx, id) + req, err := svc.repo.FindSenderRequestByID(ctx, svc.activeProjectID, id) if err != nil { return Request{}, fmt.Errorf("sender: failed to find request: %w", err) } @@ -182,7 +182,9 @@ func (svc *Service) SendRequest(ctx context.Context, id ulid.ULID) (Request, err return Request{}, fmt.Errorf("sender: could not send HTTP request: %w", err) } - err = svc.repo.StoreResponseLog(ctx, id, resLog) + req.Response = &resLog + + err = svc.repo.StoreSenderRequest(ctx, req) if err != nil { return Request{}, fmt.Errorf("sender: failed to store sender response log: %w", err) } diff --git a/pkg/sender/sender_test.go b/pkg/sender/sender_test.go index 8ca5481..5caa404 100644 --- a/pkg/sender/sender_test.go +++ b/pkg/sender/sender_test.go @@ -11,12 +11,13 @@ import ( "testing" "time" - badgerdb "github.com/dgraph-io/badger/v3" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "github.com/oklog/ulid" + "go.etcd.io/bbolt" - "github.com/dstotijn/hetty/pkg/db/badger" + "github.com/dstotijn/hetty/pkg/db/bolt" + "github.com/dstotijn/hetty/pkg/proj" "github.com/dstotijn/hetty/pkg/reqlog" "github.com/dstotijn/hetty/pkg/sender" ) @@ -54,16 +55,34 @@ func TestStoreRequest(t *testing.T) { t.Run("with active project", func(t *testing.T) { t.Parallel() - db, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) } + defer boltDB.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() svc := sender.NewService(sender.Config{ Repository: db, }) projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + Name: "foobar", + Settings: proj.Settings{}, + }) + if err != nil { + t.Fatalf("unexpected error upserting project: %v", err) + } + + svc.SetActiveProjectID(projectID) svc.SetActiveProjectID(projectID) exp := sender.Request{ @@ -99,7 +118,7 @@ func TestStoreRequest(t *testing.T) { t.Fatalf("request not equal (-exp, +got):\n%v", diff) } - got, err = db.FindSenderRequestByID(context.Background(), got.ID) + got, err = db.FindSenderRequestByID(context.Background(), projectID, got.ID) if err != nil { t.Fatalf("failed to find request by ID: %v", err) } @@ -130,16 +149,30 @@ func TestCloneFromRequestLog(t *testing.T) { t.Run("with active project", func(t *testing.T) { t.Parallel() - db, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) } + defer boltDB.Close() + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } defer db.Close() + projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + }) + if err != nil { + t.Fatalf("unexpected error upserting project: %v", err) + } + reqLog := reqlog.RequestLog{ ID: reqLogID, - ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), + ProjectID: projectID, URL: exampleURL, Method: http.MethodPost, Proto: "HTTP/1.1", @@ -155,12 +188,12 @@ func TestCloneFromRequestLog(t *testing.T) { svc := sender.NewService(sender.Config{ ReqLogService: reqlog.NewService(reqlog.Config{ - Repository: db, + ActiveProjectID: projectID, + Repository: db, }), Repository: db, }) - projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) svc.SetActiveProjectID(projectID) exp := sender.Request{ @@ -190,10 +223,18 @@ func TestCloneFromRequestLog(t *testing.T) { func TestSendRequest(t *testing.T) { t.Parallel() - db, err := badger.OpenDatabase(badgerdb.DefaultOptions("").WithInMemory(true)) + path := t.TempDir() + "bolt.db" + boltDB, err := bbolt.Open(path, 0o600, nil) if err != nil { - t.Fatalf("failed to open database: %v", err) + t.Fatalf("failed to open bolt database: %v", err) } + defer boltDB.Close() + + db, err := bolt.DatabaseFromBoltDB(boltDB) + if err != nil { + t.Fatalf("failed to create database: %v", err) + } + defer db.Close() date := time.Now().Format(http.TimeFormat) @@ -206,10 +247,19 @@ func TestSendRequest(t *testing.T) { tsURL, _ := url.Parse(ts.URL) + projectID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) + err = db.UpsertProject(context.Background(), proj.Project{ + ID: projectID, + Settings: proj.Settings{}, + }) + if err != nil { + t.Fatalf("unexpected error upserting project: %v", err) + } + reqID := ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy) req := sender.Request{ ID: reqID, - ProjectID: ulid.MustNew(ulid.Timestamp(time.Now()), ulidEntropy), + ProjectID: projectID, URL: tsURL, Method: http.MethodPost, Proto: "HTTP/1.1", @@ -229,6 +279,7 @@ func TestSendRequest(t *testing.T) { }), Repository: db, }) + svc.SetActiveProjectID(projectID) exp := &reqlog.ResponseLog{ Proto: "HTTP/1.1",