From ed0d28981287440465b8448fb7199600205480cd Mon Sep 17 00:00:00 2001 From: Eric Wollesen Date: Thu, 28 Mar 2024 16:40:01 -0600 Subject: [PATCH] adds a Kafka message consumer using the Sarama library These are a handful of data structures and behaviors that can be combined to provide flexible processing of Kafka messages. SaramaMessageConsumer is tightly focused on the consumption of Kafka messages. **This is the interface that other packages will want to implement in order to process Kafka messages.** Being a simple interface, it can easily be extended to provide additional behavior, such as retries. One such example is implemented, the NTimesRetryingConsumer. SaramaConsumerGroupHandler implements sarama.ConsumerGroupHandler. The handler is controlled by the consumer group, and is responsible for managing the lifecycle of a SaramaMessageConsumer. It can be extended if the need for more lifecycle support is required. The SaramaEventsConsumer combines a SaramaMessageConsumer and a SaramaConsumerGroupHandler providing them with a simple lifecycle API (the Run() method). This allows other systems (namely platform) to use a very thin adapter layer to control the lifecycle of the consumer. BACK-2554 --- asyncevents/sarama.go | 224 ++++++++++++++++++++++ asyncevents/sarama_test.go | 370 +++++++++++++++++++++++++++++++++++++ events/config.go | 1 + go.sum | 61 ++++++ 4 files changed, 656 insertions(+) create mode 100644 asyncevents/sarama.go create mode 100644 asyncevents/sarama_test.go diff --git a/asyncevents/sarama.go b/asyncevents/sarama.go new file mode 100644 index 00000000..83ad151e --- /dev/null +++ b/asyncevents/sarama.go @@ -0,0 +1,224 @@ +package asyncevents + +import ( + "context" + "errors" + "fmt" + "log/slog" + "math" + "time" + + "github.com/IBM/sarama" +) + +// SaramaEventsConsumer consumes Kafka messages for asynchronous event +// handling. +type SaramaEventsConsumer struct { + Handler sarama.ConsumerGroupHandler + ConsumerGroup sarama.ConsumerGroup + Topics []string +} + +func NewSaramaEventsConsumer(consumerGroup sarama.ConsumerGroup, + handler sarama.ConsumerGroupHandler, topics ...string) *SaramaEventsConsumer { + + return &SaramaEventsConsumer{ + ConsumerGroup: consumerGroup, + Handler: handler, + Topics: topics, + } +} + +// Run the consumer, to begin consuming Kafka messages. +// +// Run is stopped by its context being canceled. When its context is canceled, +// it returns nil. +func (p *SaramaEventsConsumer) Run(ctx context.Context) (err error) { + for { + err := p.ConsumerGroup.Consume(ctx, p.Topics, p.Handler) + if err != nil { + return err + } + if ctxErr := ctx.Err(); ctxErr != nil { + return nil + } + } +} + +// SaramaConsumerGroupHandler implements sarama.ConsumerGroupHandler. +type SaramaConsumerGroupHandler struct { + Consumer SaramaMessageConsumer + ConsumerTimeout time.Duration + Logger Logger +} + +// NewSaramaConsumerGroupHandler builds a consumer group handler. +// +// A timeout of 0 will use DefaultMessageConsumptionTimeout. +func NewSaramaConsumerGroupHandler(logger Logger, consumer SaramaMessageConsumer, + timeout time.Duration) *SaramaConsumerGroupHandler { + + if timeout == 0 { + timeout = DefaultMessageConsumptionTimeout + } + if logger == nil { + logger = slog.Default() + } + return &SaramaConsumerGroupHandler{ + Consumer: consumer, + ConsumerTimeout: timeout, + Logger: logger, + } +} + +const ( + // DefaultMessageConsumptionTimeout is the default time to allow + // SaramaMessageConsumer.Consume to work before canceling. + DefaultMessageConsumptionTimeout = 30 * time.Second +) + +// Setup implements sarama.ConsumerGroupHandler. +func (h *SaramaConsumerGroupHandler) Setup(_ sarama.ConsumerGroupSession) error { return nil } + +// Cleanup implements sarama.ConsumerGroupHandler. +func (h *SaramaConsumerGroupHandler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil } + +// ConsumeClaim implements sarama.ConsumerGroupHandler. +func (h *SaramaConsumerGroupHandler) ConsumeClaim(session sarama.ConsumerGroupSession, + claim sarama.ConsumerGroupClaim) error { + + done := session.Context().Done() + for { + select { + case <-done: + return nil + case message, more := <-claim.Messages(): + if !more { + return nil + } + err := func() error { + ctx, cancel := context.WithTimeout(session.Context(), h.ConsumerTimeout) + defer cancel() + return h.Consumer.Consume(ctx, session, message) + }() + switch { + case errors.Is(err, context.DeadlineExceeded): + h.Logger.Log(session.Context(), slog.LevelDebug, err.Error()) + case !errors.Is(err, nil): + return err + } + } + } +} + +// Close implements sarama.ConsumerGroupHandler. +func (h *SaramaConsumerGroupHandler) Close() error { return nil } + +// SaramaMessageConsumer processes Kafka messages. +type SaramaMessageConsumer interface { + // Consume should process a message. + // + // Consume is responsible for marking the message consumed, unless the + // context is canceled, in which case the caller should retry, or mark the + // message as appropriate. + Consume(ctx context.Context, session sarama.ConsumerGroupSession, msg *sarama.ConsumerMessage) error +} + +var ErrRetriesLimitExceeded = errors.New("retry limit exceeded") + +// NTimesRetryingConsumer enhances a SaramaMessageConsumer with a finite +// number of immediate retries. +// +// The delay between each retry can be controlled via the Delay property. If +// no Delay property is specified, a delay based on the Fibonacci sequence is +// used. +// +// Logger is intentionally minimal. The slog.Log function is used by default. +type NTimesRetryingConsumer struct { + Times int + Consumer SaramaMessageConsumer + Delay func(tries int) time.Duration + Logger Logger +} + +// Logger is an intentionally minimal interface for basic logging. +// +// It matches the signature of slog.Log. +type Logger interface { + Log(ctx context.Context, level slog.Level, msg string, args ...any) +} + +func (c *NTimesRetryingConsumer) Consume(ctx context.Context, + session sarama.ConsumerGroupSession, message *sarama.ConsumerMessage) (err error) { + + var joinedErrors error + var tries int = 0 + var delay time.Duration = 0 + if c.Delay == nil { + c.Delay = DelayFibonacci + } + if c.Logger == nil { + c.Logger = slog.Default() + } + done := ctx.Done() + for tries < c.Times { + select { + case <-done: + return nil + case <-time.After(delay): + err := c.Consumer.Consume(ctx, session, message) + if errors.Is(err, nil) || errors.Is(err, context.Canceled) { + return nil + } + delay = c.Delay(tries) + c.Logger.Log(ctx, slog.LevelInfo, "failure consuming Kafka message, will retry", + slog.Attr{Key: "tries", Value: slog.IntValue(tries)}, + slog.Attr{Key: "times", Value: slog.IntValue(c.Times)}, + slog.Attr{Key: "delay", Value: slog.DurationValue(delay)}, + slog.Attr{Key: "err", Value: slog.AnyValue(err)}, + ) + joinedErrors = errors.Join(joinedErrors, err) + tries++ + } + } + + return errors.Join(joinedErrors, c.retryLimitError()) +} + +func (c *NTimesRetryingConsumer) retryLimitError() error { + return fmt.Errorf("%w (%d)", ErrRetriesLimitExceeded, c.Times) +} + +// DelayNone is a function returning a constant "no delay" of 0 seconds. +var DelayNone = func(_ int) time.Duration { return DelayConstant(0) } + +// DelayConstant is a function returning a constant number of seconds. +func DelayConstant(n int) time.Duration { return time.Duration(n) * time.Second } + +// DelayExponentialBinary returns a binary exponential delay. +// +// The delay is 2**tries seconds. +func DelayExponentialBinary(tries int) time.Duration { + return time.Second * time.Duration(math.Pow(2, float64(tries))) +} + +// DelayFibonacci returns a delay based on the Fibonacci sequence. +func DelayFibonacci(tries int) time.Duration { + return time.Second * time.Duration(Fib(tries)) +} + +// Fib returns the nth number in the Fibonacci sequence. +func Fib(n int) int { + if n == 0 { + return 0 + } else if n < 3 { + return 1 + } + + n1, n2 := 1, 1 + for i := 3; i <= n; i++ { + n1, n2 = n1+n2, n1 + } + + return n1 +} diff --git a/asyncevents/sarama_test.go b/asyncevents/sarama_test.go new file mode 100644 index 00000000..707aafd3 --- /dev/null +++ b/asyncevents/sarama_test.go @@ -0,0 +1,370 @@ +package asyncevents + +import ( + "context" + "errors" + "fmt" + "log/slog" + "testing" + "time" + + "github.com/IBM/sarama" +) + +var errTest error = errors.New("test error") + +func TestSaramaAsyncEventsConsumerLifecycle(s *testing.T) { + consumerGroup := &nullSaramaConsumerGroup{} + topics := []string{"test"} + + s.Run("successful start and stop", func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + handler := &nullSaramaConsumerGroupHandler{} + eventsConsumer := NewSaramaEventsConsumer(consumerGroup, handler, topics...) + err := launchStart(ctx, t, eventsConsumer) + if !errors.Is(err, nil) { + t.Errorf("expected nil error, got %v", err) + } + }) + + s.Run("reports errors (that aren't context.Canceled)", func(t *testing.T) { + consumerGroup := &erroringSaramaConsumerGroup{err: errTest} + handler := &nullSaramaConsumerGroupHandler{} + eventsConsumer := NewSaramaEventsConsumer(consumerGroup, handler, topics...) + err := launchStart(context.Background(), t, eventsConsumer) + if !errors.Is(err, errTest) { + t.Errorf("expected %s, got %v", errTest, err) + } + }) +} + +func TestSaramaConsumerGroupHandler(s *testing.T) { + s.Run("works as expected", func(t *testing.T) { + testConsumer := &sleepingSaramaMessageConsumer{time.Nanosecond, nil} + testSession := &nullSaramaConsumerGroupSession{} + messages := make(chan *sarama.ConsumerMessage) + testClaim := newTestSaramaConsumerGroupClaim(messages) + go func() { messages <- &sarama.ConsumerMessage{}; close(messages) }() + handler := NewSaramaConsumerGroupHandler(slog.Default(), testConsumer, 0) + + err := handler.ConsumeClaim(testSession, testClaim) + if !errors.Is(err, nil) { + t.Errorf("expected ConsumeClaim to return nil, got %v", err) + } + }) + + s.Run("returns errors", func(t *testing.T) { + testConsumer := newCountingSaramaMessageConsumer(errTest) + testSession := &nullSaramaConsumerGroupSession{} + messages := make(chan *sarama.ConsumerMessage) + testClaim := newTestSaramaConsumerGroupClaim(messages) + go func() { messages <- &sarama.ConsumerMessage{}; close(messages) }() + handler := NewSaramaConsumerGroupHandler(slog.Default(), testConsumer, time.Nanosecond) + + err := handler.ConsumeClaim(testSession, testClaim) + if !errors.Is(err, errTest) { + t.Errorf("expected ConsumeClaim to return %s, got %v", errTest, err) + } + }) + + s.Run("enforces a deadline", func(t *testing.T) { + testConsumer := &sleepingSaramaMessageConsumer{time.Second, nil} + testSession := &nullSaramaConsumerGroupSession{} + messages := make(chan *sarama.ConsumerMessage) + testClaim := newTestSaramaConsumerGroupClaim(messages) + go func() { messages <- &sarama.ConsumerMessage{}; close(messages) }() + handler := NewSaramaConsumerGroupHandler(slog.Default(), testConsumer, time.Nanosecond) + + err := handler.ConsumeClaim(testSession, testClaim) + if !errors.Is(testConsumer.consumeError, context.DeadlineExceeded) { + t.Errorf("expected ConsumeClaim to return %s, got %v", context.DeadlineExceeded, err) + } + }) +} + +func TestNTimesRetryingConsumer(s *testing.T) { + var testTimes = 3 + + s.Run("retries N times", func(t *testing.T) { + testConsumer := newCountingSaramaMessageConsumer(errTest) + c := &NTimesRetryingConsumer{ + Times: testTimes, + Consumer: testConsumer, + Delay: DelayNone, + } + ctx := context.Background() + err := c.Consume(ctx, nil, nil) + if !errors.Is(err, ErrRetriesLimitExceeded) { + t.Errorf("expected %s, got %v", ErrRetriesLimitExceeded, err) + } + if testConsumer.Count != testTimes { + t.Errorf("expected %d tries, got %d", testTimes, testConsumer.Count) + } + }) + + s.Run("retries when the context deadline is exceeded", func(t *testing.T) { + testConsumer := newCountingSaramaMessageConsumer(context.DeadlineExceeded) + c := &NTimesRetryingConsumer{ + Times: testTimes, + Consumer: testConsumer, + Delay: DelayNone, + } + ctx := context.Background() + err := c.Consume(ctx, nil, nil) + if !errors.Is(err, context.DeadlineExceeded) { + t.Errorf("expected %s, got %v", context.DeadlineExceeded, err) + } + if testConsumer.Count != testTimes { + t.Errorf("expected %d tries, got %d", testTimes, testConsumer.Count) + } + }) + + s.Run("returns nil when the context is canceled", func(t *testing.T) { + testConsumer := newCountingSaramaMessageConsumer(context.Canceled) + c := &NTimesRetryingConsumer{ + Times: testTimes, + Consumer: testConsumer, + Delay: DelayNone, + } + ctx := context.Background() + err := c.Consume(ctx, nil, nil) + if !errors.Is(err, nil) { + t.Errorf("expected nil error, got %s", err) + } + if testConsumer.Count >= testTimes { + t.Errorf("expected < %d tries, got %d", testTimes, testConsumer.Count) + } + }) + + s.Run("doesn't retry on successful consumption", func(t *testing.T) { + testConsumer := newCountingSaramaMessageConsumer(nil) + c := &NTimesRetryingConsumer{ + Times: testTimes, + Consumer: testConsumer, + Delay: DelayNone, + } + ctx := context.Background() + err := c.Consume(ctx, nil, nil) + if !errors.Is(err, nil) { + t.Errorf("expected nil error, got %v", err) + } + if testConsumer.Count != 1 { + t.Errorf("expected 1 try, got %d", testConsumer.Count) + } + }) +} + +func ExampleDelayNone() { + fmt.Println( + DelayNone(0), + DelayNone(10), + DelayNone(20), + DelayNone(100), + DelayNone(1024), + ) + // Output: 0s 0s 0s 0s 0s +} + +func ExampleDelayExponentialBinary() { + fmt.Println( + DelayExponentialBinary(0), + DelayExponentialBinary(1), + DelayExponentialBinary(2), + DelayExponentialBinary(3), + DelayExponentialBinary(4), + DelayExponentialBinary(5), + DelayExponentialBinary(6), + DelayExponentialBinary(7), + DelayExponentialBinary(8), + ) + // Output: 1s 2s 4s 8s 16s 32s 1m4s 2m8s 4m16s +} + +func ExampleFib() { + fmt.Println( + Fib(0), + Fib(1), + Fib(2), + Fib(10), + Fib(20), + ) + // Output: 0 1 1 55 6765 +} + +// launchStart is a helper for calling Start in a goroutine. +// +// It uses a channel to know that the goroutine has seen some amount of CPU +// time, which isn't guaranteed to alleviate the race of calling Start, but in +// practice seems to be sufficient. Running with -count 10000 had 0 failures. +func launchStart(ctx context.Context, t testing.TB, ec *SaramaEventsConsumer) (err error) { + t.Helper() + runReturned := make(chan error) + go func() { + defer close(runReturned) + runReturned <- ec.Run(ctx) + }() + return <-runReturned +} + +// nullSaramaConsumerGroup is a null/no-op base from which to mock test behavior. +type nullSaramaConsumerGroup struct{} + +func (g *nullSaramaConsumerGroup) Errors() <-chan error { + return nil +} + +func (g *nullSaramaConsumerGroup) Close() error { + return nil +} + +func (g *nullSaramaConsumerGroup) Pause(partitions map[string][]int32) {} + +func (g *nullSaramaConsumerGroup) Resume(partitions map[string][]int32) {} + +func (g *nullSaramaConsumerGroup) PauseAll() {} + +func (g *nullSaramaConsumerGroup) ResumeAll() {} + +func (g *nullSaramaConsumerGroup) Consume(ctx context.Context, topics []string, handler sarama.ConsumerGroupHandler) error { + return nil +} + +type erroringSaramaConsumerGroup struct { + nullSaramaConsumerGroup + err error +} + +func (g *erroringSaramaConsumerGroup) Consume(_ context.Context, _ []string, _ sarama.ConsumerGroupHandler) error { + return g.err +} + +// nullSaramaConsumerGroupHandler is a no-op base from which to mock test +// behavior. +type nullSaramaConsumerGroupHandler struct { + t testing.TB +} + +func (h *nullSaramaConsumerGroupHandler) Setup(_ sarama.ConsumerGroupSession) error { return nil } +func (h *nullSaramaConsumerGroupHandler) Cleanup(_ sarama.ConsumerGroupSession) error { return nil } +func (h *nullSaramaConsumerGroupHandler) ConsumeClaim(_ sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { + for msg := range claim.Messages() { + if h.t != nil { + h.t.Logf("consuming message: %+v", msg) + } + } + return nil +} + +type testSaramaConsumerGroupHandler struct { + *nullSaramaConsumerGroupHandler + consumed []*sarama.ConsumerMessage + err error +} + +func (h *testSaramaConsumerGroupHandler) ConsumeClaim(s sarama.ConsumerGroupSession, c sarama.ConsumerGroupClaim) error { + for msg := range c.Messages() { + h.consumed = append(h.consumed, msg) + } + return nil +} + +type nullSaramaConsumerGroupSession struct { + context context.Context +} + +func (s *nullSaramaConsumerGroupSession) Claims() map[string][]int32 { + return map[string][]int32{} +} + +func (s *nullSaramaConsumerGroupSession) MemberID() string { + return "" +} + +func (s *nullSaramaConsumerGroupSession) GenerationID() int32 { + return 0 +} + +func (s *nullSaramaConsumerGroupSession) MarkOffset(topic string, partition int32, offset int64, metadata string) { +} + +func (s *nullSaramaConsumerGroupSession) Commit() {} + +func (s *nullSaramaConsumerGroupSession) ResetOffset(topic string, partition int32, offset int64, metadata string) { +} + +func (s *nullSaramaConsumerGroupSession) MarkMessage(msg *sarama.ConsumerMessage, metadata string) {} + +func (s *nullSaramaConsumerGroupSession) Context() context.Context { + if s.context == nil { + s.context = context.Background() + } + return s.context +} + +func (c *testSaramaConsumerGroupClaim) Topic() string { + return "" +} + +func (c *testSaramaConsumerGroupClaim) Partition() int32 { + return 0 +} + +func (c *testSaramaConsumerGroupClaim) InitialOffset() int64 { + return 0 +} + +func (c *testSaramaConsumerGroupClaim) HighWaterMarkOffset() int64 { + return 0 +} + +type testSaramaConsumerGroupClaim struct { + messages <-chan *sarama.ConsumerMessage +} + +func newTestSaramaConsumerGroupClaim(messages <-chan *sarama.ConsumerMessage) *testSaramaConsumerGroupClaim { + return &testSaramaConsumerGroupClaim{ + messages: messages, + } +} + +func (c *testSaramaConsumerGroupClaim) Messages() <-chan *sarama.ConsumerMessage { + return c.messages +} + +type sleepingSaramaMessageConsumer struct { + sleepDuration time.Duration + consumeError error +} + +func (c *sleepingSaramaMessageConsumer) Consume(ctx context.Context, + session sarama.ConsumerGroupSession, message *sarama.ConsumerMessage) (err error) { + defer func(err *error) { + if err != nil { + c.consumeError = *err + } + }(&err) + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(c.sleepDuration): + return nil + } +} + +type countingSaramaMessageConsumer struct { + err error + Count int +} + +func newCountingSaramaMessageConsumer(err error) *countingSaramaMessageConsumer { + return &countingSaramaMessageConsumer{ + err: err, + } +} + +func (c *countingSaramaMessageConsumer) Consume(ctx context.Context, + _ sarama.ConsumerGroupSession, _ *sarama.ConsumerMessage) error { + c.Count++ + return c.err +} diff --git a/events/config.go b/events/config.go index a07d70ed..5deff14d 100644 --- a/events/config.go +++ b/events/config.go @@ -2,6 +2,7 @@ package events import ( "errors" + "github.com/IBM/sarama" "github.com/kelseyhightower/envconfig" ) diff --git a/go.sum b/go.sum index cefb9dd1..c5ab4c5a 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,21 @@ +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= github.com/IBM/sarama v1.43.2 h1:HABeEqRUh32z8yzY2hGB/j8mHSzC/HA9zlEjqFNCzSw= github.com/IBM/sarama v1.43.2/go.mod h1:Kyo4WkF24Z+1nz7xeVUFWIuKVV8RS3wM8mkvPKMdXFQ= +github.com/Joker/jade v1.1.3/go.mod h1:T+2WLyt7VH6Lp0TRxQrUYEs64nRc83wkMQrfeIQKduM= github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk= +github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06/go.mod h1:7erjKLwalezA0k99cWs5L11HWOAPNjdUZ6RxH1BXbbM= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w= +github.com/bytedance/sonic v1.10.0-rc3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= +github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= +github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2 v2.15.2 h1:dl2xbFLV2FGd3OBNC6ncSN9l+gPNEP0DYE+1yKVV5DQ= github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2 v2.15.2/go.mod h1:jXfl9I1Q78+4zdYGTjHNQcrbNtJL63jpzSgVE2rE79U= github.com/cloudevents/sdk-go/v2 v2.15.2 h1:54+I5xQEnI73RBhWHxbI1XJcqOFOVJN85vb41+8mHUc= @@ -20,17 +30,28 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4A github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/flosch/pongo2/v4 v4.0.2/go.mod h1:B5ObFANs/36VwxxlgKpdchIJHMvHB562PW+BWPhwZD8= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomarkdown/markdown v0.0.0-20230922112808-5421fefb8386/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -41,6 +62,7 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/iris-contrib/schema v0.0.6/go.mod h1:iYszG0IOsuIsfzjymw1kMzTL8YQcCWlm65f3wX8J5iA= github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= @@ -53,27 +75,46 @@ github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh6 github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= +github.com/kataras/blocks v0.0.7/go.mod h1:UJIU97CluDo0f+zEjbnbkeMRlvYORtmc1304EeyXf4I= +github.com/kataras/golog v0.1.9/go.mod h1:jlpk/bOaYCyqDqH18pgDHdaJab72yBE6i0O3s30hpWY= +github.com/kataras/iris/v12 v12.2.6-0.20230908161203-24ba4e8933b9/go.mod h1:ldkoR3iXABBeqlTibQ3MYaviA1oSlPvim6f55biwBh4= +github.com/kataras/pio v0.0.12/go.mod h1:ODK/8XBhhQ5WqrAhKy+9lTPS7sBf6O3KcLhc9klfRcY= +github.com/kataras/sitemap v0.0.6/go.mod h1:dW4dOCNs896OR1HmG+dMLdT7JjDk7mYBzoIRwuj5jA4= +github.com/kataras/tunnel v0.0.4/go.mod h1:9FkU4LaeifdMWqZu7o20ojmW4B7hdhv2CMLwfnHGpYw= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/oapi-codegen/runtime v1.1.1 h1:EXLHh0DXIJnWhdRPN2w4MXAzFyE4CskzhNLUmtpMYro= github.com/oapi-codegen/runtime v1.1.1/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= @@ -81,6 +122,9 @@ github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqn github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/schollz/closestmatch v2.1.0+incompatible/go.mod h1:RtP1ddjLong6gTkbtmuhtR2uUrrJOpYzYRvbcPAid+g= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -92,19 +136,29 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 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/tdewolff/minify/v2 v2.12.9/go.mod h1:qOqdlDfL+7v0/fyymB+OP497nIxJYSvX4MQWA8OoiXU= +github.com/tdewolff/parse/v2 v2.6.8/go.mod h1:XHDhaU6IBgsryfdnpzUXBlT6leW/l25yrFBTEb4eIyM= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xdg/scram v1.0.5 h1:TuS0RFmt5Is5qm9Tm2SoD89OPqe4IRiFtyFY4iwWXsw= github.com/xdg/scram v1.0.5/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.3 h1:cmL5Enob4W83ti/ZHuZLuKD/xqJfus4fVPwE+/BDm+4= github.com/xdg/stringprep v1.0.3/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= +github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/arch v0.4.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= @@ -113,6 +167,7 @@ golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5D golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -133,9 +188,11 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 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/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -149,10 +206,14 @@ golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=