diff --git a/demo/handlers.go b/demo/handlers.go new file mode 100644 index 0000000..949fbc6 --- /dev/null +++ b/demo/handlers.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "net/http" + "text/tabwriter" + "time" + + "github.com/cilium/statedb" + v1 "k8s.io/api/core/v1" +) + +func registerStateDBHTTPHandler(mux *http.ServeMux, db *statedb.DB) { + mux.Handle("/statedb", db) +} + +func registerPodHTTPHandler(mux *http.ServeMux, db *statedb.DB, pods statedb.Table[*Pod]) { + mux.HandleFunc("/pods/running", func(w http.ResponseWriter, req *http.Request) { + txn := db.ReadTxn() + iter, _ := pods.Get(txn, PodPhaseIndex.Query(v1.PodRunning)) + t := tabwriter.NewWriter(w, 10, 4, 2, ' ', 0) + fmt.Fprintf(t, "NAME\tSTARTED\tSTATUS\n") + for pod, _, ok := iter.Next(); ok; pod, _, ok = iter.Next() { + fmt.Fprintf(t, "%s/%s\t%s ago\t\t%s\n", pod.Namespace, pod.Name, time.Since(pod.StartTime), pod.ReconciliationStatus()) + } + t.Flush() + }) +} diff --git a/demo/main.go b/demo/main.go new file mode 100644 index 0000000..ac809ce --- /dev/null +++ b/demo/main.go @@ -0,0 +1,118 @@ +package main + +import ( + "log/slog" + "net/http" + "path" + "time" + + "github.com/cilium/hive" + "github.com/cilium/hive/cell" + "github.com/cilium/hive/job" + "github.com/cilium/statedb" + "github.com/cilium/statedb/reconciler" + "github.com/cilium/statedb/reflector" + "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/util/homedir" +) + +var Hive = hive.New( + job.Cell, + statedb.Cell, + cell.SimpleHealthCell, + + // Kubernetes client + cell.Provide( + newClientset, + ), + + // HTTP server + cell.Provide( + http.NewServeMux, + ), + cell.Invoke( + registerHTTPServer, + registerStateDBHTTPHandler, + ), + + // Pod tables and the reconciler + cell.Provide( + NewPodTable, + statedb.RWTable[*Pod].ToTable, + podReflectorConfig, + podReconcilerConfig, + ), + + reflector.KubernetesCell[*Pod](), + + cell.Invoke( + statedb.RegisterTable[*Pod], + + reconciler.Register[*Pod], + + registerPodHTTPHandler, + ), +) + +var cmd = &cobra.Command{ + Use: "example", + RunE: func(_ *cobra.Command, args []string) error { + return Hive.Run() + }, +} + +func main() { + // Register all configuration flags in the hive to the command + Hive.RegisterFlags(cmd.Flags()) + + // Add the "hive" sub-command for inspecting the hive + cmd.AddCommand(Hive.Command()) + + // And finally execute the command to parse the command-line flags and + // run the hive + cmd.Execute() +} + +func podReflectorConfig(client *kubernetes.Clientset, pods statedb.RWTable[*Pod]) reflector.KubernetesConfig[*Pod] { + lw := ListerWatcherFromTyped(client.CoreV1().Pods("")) + return reflector.KubernetesConfig[*Pod]{ + BufferSize: 100, + BufferWaitTime: 100 * time.Millisecond, + ListerWatcher: lw, + Table: pods, + Transform: func(obj any) (*Pod, bool) { + pod, ok := obj.(*v1.Pod) + if ok { + return fromV1Pod(pod), true + } + return nil, false + }, + } +} + +func newClientset() (*kubernetes.Clientset, error) { + cfg, err := clientcmd.BuildConfigFromFlags("", path.Join(homedir.HomeDir(), ".kube", "config")) + if err != nil { + panic(err.Error()) + } + + return kubernetes.NewForConfig(cfg) +} + +func registerHTTPServer(log *slog.Logger, mux *http.ServeMux, lc cell.Lifecycle) { + s := &http.Server{Addr: ":8080", Handler: mux} + lc.Append(cell.Hook{ + OnStart: func(cell.HookContext) error { + log.Info("Serving HTTP", "addr", s.Addr) + go s.ListenAndServe() + return nil + }, + OnStop: func(ctx cell.HookContext) error { + return s.Shutdown(ctx) + }, + }) + +} diff --git a/demo/reconciler.go b/demo/reconciler.go new file mode 100644 index 0000000..e08c0ed --- /dev/null +++ b/demo/reconciler.go @@ -0,0 +1,45 @@ +package main + +import ( + "context" + "log/slog" + "time" + + "github.com/cilium/statedb" + "github.com/cilium/statedb/reconciler" +) + +type podOps struct { + log *slog.Logger +} + +// Delete implements reconciler.Operations. +func (o *podOps) Delete(_ context.Context, _ statedb.ReadTxn, pod *Pod) error { + o.log.Info("Pod deleted", "name", pod.Namespace+"/"+pod.Name) + return nil +} + +// Prune implements reconciler.Operations. +func (o *podOps) Prune(context.Context, statedb.ReadTxn, statedb.Iterator[*Pod]) error { + return nil +} + +// Update implements reconciler.Operations. +func (o *podOps) Update(ctx context.Context, txn statedb.ReadTxn, pod *Pod, changed *bool) error { + o.log.Info("Pod updated", "name", pod.Namespace+"/"+pod.Name, "phase", pod.Phase) + return nil +} + +var _ reconciler.Operations[*Pod] = &podOps{} + +func podReconcilerConfig(log *slog.Logger) reconciler.Config[*Pod] { + return reconciler.Config[*Pod]{ + FullReconcilationInterval: time.Minute, + RetryBackoffMinDuration: 100 * time.Millisecond, + RetryBackoffMaxDuration: time.Minute, + IncrementalRoundSize: 1000, + GetObjectStatus: (*Pod).ReconciliationStatus, + WithObjectStatus: (*Pod).WithReconciliationStatus, + Operations: &podOps{log}, + } +} diff --git a/demo/tables.go b/demo/tables.go new file mode 100644 index 0000000..4b39dbe --- /dev/null +++ b/demo/tables.go @@ -0,0 +1,71 @@ +package main + +import ( + "time" + + v1 "k8s.io/api/core/v1" + + "github.com/cilium/statedb" + "github.com/cilium/statedb/index" + "github.com/cilium/statedb/reconciler" +) + +type Pod struct { + Name, Namespace string + Phase v1.PodPhase + StartTime time.Time + + reconciliationStatus reconciler.Status +} + +func fromV1Pod(p *v1.Pod) *Pod { + return &Pod{ + Name: p.Name, + Namespace: p.Namespace, + Phase: p.Status.Phase, + StartTime: p.Status.StartTime.Time, + reconciliationStatus: reconciler.StatusPending(), + } +} + +func (p *Pod) ReconciliationStatus() reconciler.Status { + return p.reconciliationStatus +} + +func (p *Pod) WithReconciliationStatus(s reconciler.Status) *Pod { + p2 := *p + p2.reconciliationStatus = s + return &p2 +} + +const PodTableName = "pods" + +var ( + PodNameIndex = statedb.Index[*Pod, string]{ + Name: "name", + FromObject: func(pod *Pod) index.KeySet { + return index.NewKeySet(index.String(pod.Namespace + "/" + pod.Name)) + }, + FromKey: index.String, + Unique: true, + } + PodPhaseIndex = statedb.Index[*Pod, v1.PodPhase]{ + Name: "phase", + FromObject: func(pod *Pod) index.KeySet { + return index.NewKeySet(index.String(string(pod.Phase))) + }, + FromKey: func(key v1.PodPhase) index.Key { + return index.String(string(key)) + }, + Unique: false, + } +) + +func NewPodTable(db *statedb.DB) (statedb.RWTable[*Pod], error) { + return statedb.NewTable[*Pod]( + PodTableName, + + PodNameIndex, + PodPhaseIndex, + ) +} diff --git a/demo/utils.go b/demo/utils.go new file mode 100644 index 0000000..7bffbbd --- /dev/null +++ b/demo/utils.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/client-go/tools/cache" +) + +// typedListWatcher is a generic interface that all the typed k8s clients match. +type typedListWatcher[T runtime.Object] interface { + List(ctx context.Context, opts metav1.ListOptions) (T, error) + Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error) +} + +// genListWatcher takes a typed list watcher and implements cache.ListWatch +// using it. +type genListWatcher[T runtime.Object] struct { + lw typedListWatcher[T] +} + +func (g *genListWatcher[T]) List(opts metav1.ListOptions) (runtime.Object, error) { + return g.lw.List(context.Background(), opts) +} + +func (g *genListWatcher[T]) Watch(opts metav1.ListOptions) (watch.Interface, error) { + return g.lw.Watch(context.Background(), opts) +} + +// ListerWatcherFromTyped adapts a typed k8s client to cache.ListerWatcher so it can be used +// with an informer. With this construction we can use fake clients for testing, +// which would not be possible if we used NewListWatchFromClient and RESTClient(). +func ListerWatcherFromTyped[T runtime.Object](lw typedListWatcher[T]) cache.ListerWatcher { + return &genListWatcher[T]{lw: lw} +} diff --git a/go.mod b/go.mod index dfd7580..b0ab41d 100644 --- a/go.mod +++ b/go.mod @@ -19,23 +19,30 @@ require ( require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.4 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/magiconair/properties v1.8.7 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -59,6 +66,7 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/kube-openapi v0.0.0-20231010175941-2dd684a91f00 // indirect k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect diff --git a/go.sum b/go.sum index d102e5f..ce3aa5c 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,7 @@ github.com/cilium/hive v0.0.0-20240209163124-bd6ebb4ec11d/go.mod h1:6tW1eCwSq8Wz github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d h1:p6MgATaKEB9o7iAsk9rlzXNDMNCeKPAkx4Y8f+Zq8X8= github.com/cilium/stream v0.0.0-20240209152734-a0792b51812d/go.mod h1:3VLiLgs8wfjirkuYqos4t0IBPQ+sXtf3tFkChLm6ARM= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -15,12 +16,16 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -36,6 +41,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= @@ -46,6 +53,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -54,8 +63,11 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -71,6 +83,10 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.13.0 h1:0jY9lJquiL8fcf3M4LAXN5aMlS/b2BV86HFFPCPMgE4= +github.com/onsi/ginkgo/v2 v2.13.0/go.mod h1:TE309ZR8s5FsKKpuB1YAQYBzCaAfUgatB/xlT/ETL/o= +github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= +github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -101,6 +117,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -163,6 +180,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= +golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=