diff --git a/.docker/mongo-test.yml b/.docker/mongo-test.yml index 1f6c0830..eb729f28 100644 --- a/.docker/mongo-test.yml +++ b/.docker/mongo-test.yml @@ -45,6 +45,7 @@ services: dockerfile: .docker/tag-test.Dockerfile args: TAGS: mongo + TEST_PATH: ./backend/mongo/... environment: - MONGOSTORE_URL=mongodb://mongostore:27017 - MONGOREPLSTORE_URL=mongodb://mongostore_replicaset:27017 diff --git a/.docker/tag-test.Dockerfile b/.docker/tag-test.Dockerfile index e028bc17..900a403a 100644 --- a/.docker/tag-test.Dockerfile +++ b/.docker/tag-test.Dockerfile @@ -1,8 +1,10 @@ FROM golang:1.18 ARG TAGS +ARG TEST_PATH=./... ENV TAGS $TAGS +ENV TEST_PATH $TEST_PATH WORKDIR /test COPY go.mod go.sum /test/ RUN go mod download COPY . . -CMD go test -v -tags=$TAGS ./... +CMD go test -v -tags=$TAGS $TEST_PATH diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 89f3e7c0..f857afe5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,7 +8,7 @@ on: jobs: test: - runs-on: self-hosted + runs-on: ubuntu-latest timeout-minutes: 10 if: | !startsWith(github.event.head_commit.message, 'docs') && diff --git a/Makefile b/Makefile index bfff79d9..5ca2b8b8 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ .PHONY: docs docs: - @./scripts/clear.sh @./scripts/docs.sh .PHONY: generate diff --git a/backend/mongo/mongotest/test.go b/backend/mongo/mongotest/test.go index 1c078649..f1f3e839 100644 --- a/backend/mongo/mongotest/test.go +++ b/backend/mongo/mongotest/test.go @@ -13,14 +13,21 @@ import ( // Option that ensures a unique database name for every call to NewEventStore // during the current process. func NewEventStore(enc codec.Encoding, opts ...mongo.EventStoreOption) *mongo.EventStore { + return mongo.NewEventStore(enc, append( + []mongo.EventStoreOption{mongo.Database(UniqueName("event_"))}, + opts..., + )...) +} + +// UniqueName generates a unique string by appending a random hexadecimal +// string to the provided prefix. The generated string is intended for use as a +// unique identifier in database names. If there's an error in reading from the +// random source, the function will panic. +func UniqueName(prefix string) string { b := make([]byte, 8) if _, err := rand.Read(b); err != nil { panic(err) } id := hex.EncodeToString(b) - - return mongo.NewEventStore(enc, append( - []mongo.EventStoreOption{mongo.Database(fmt.Sprintf("event_%s", id))}, - opts..., - )...) + return fmt.Sprintf("%s%s", prefix, id) } diff --git a/backend/mongo/store.go b/backend/mongo/store.go index 45f65e0e..d1fe61f2 100644 --- a/backend/mongo/store.go +++ b/backend/mongo/store.go @@ -10,19 +10,44 @@ import ( stdtime "time" "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "go.mongodb.org/mongo-driver/x/mongo/driver" + "github.com/modernice/goes/backend/mongo/indices" "github.com/modernice/goes/codec" "github.com/modernice/goes/event" "github.com/modernice/goes/event/query/time" "github.com/modernice/goes/event/query/version" "github.com/modernice/goes/helper/pick" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" - "go.mongodb.org/mongo-driver/mongo/options" - "go.mongodb.org/mongo-driver/x/mongo/driver" ) -// EventStore is a MongoDB event store. +var ( + // PreInsert represents a hook that is executed before inserting events into the + // EventStore. This allows for additional operations to be performed within the + // same transaction, such as validation or transformation of data. The hook + // function should return an error if anything goes wrong, causing the + // transaction to abort. + PreInsert = TransactionHook("pre:insert") + + // PostInsert represents a hook that is executed after inserting events into the + // EventStore. This allows for additional operations to be performed within the + // same transaction, such as cleanup or logging operations. The hook function + // should return an error if anything goes wrong, causing the transaction to + // abort. + PostInsert = TransactionHook("post:insert") +) + +// EventStore is a type that provides an interface to store, retrieve, and +// manage events in a MongoDB database. It supports insertion and deletion of +// events, querying for specific events, and consistency checks through version +// validation. EventStore also allows for the use of hooks that can be executed +// before or after inserting events into the store. It provides transactional +// operations to ensure atomicity and consistency of the stored data. The +// EventStore can be configured with various options such as MongoDB connection +// details, collections for storing events and aggregate states, and the use of +// transactions. type EventStore struct { enc codec.Encoding url string @@ -33,41 +58,50 @@ type EventStore struct { transactions bool validateVersions bool additionalIndices []mongo.IndexModel + preInsertHooks []func(TransactionContext) error + postInsertHooks []func(TransactionContext) error client *mongo.Client db *mongo.Database entries *mongo.Collection states *mongo.Collection + isTransactionStore bool + tx *transaction + root *EventStore + onceConnect sync.Once } -// EventStoreOption is an eventStore option. +// EventStoreOption is a function that modifies an EventStore. These options are +// used to configure various aspects of the EventStore, such as the MongoDB +// instance connection details, the collections where events and aggregate +// states are stored, the use of transactions when inserting events, and more. +// It also supports the use of hooks that are executed before or after inserting +// events into the store. type EventStoreOption func(*EventStore) -// A VersionError means the insertion of events failed because at least one of -// the events has an invalid/inconsistent version. +// VersionError represents an error that occurs when an event has an incorrect +// version. This usually happens when the event's version does not match the +// expected version based on the current state of its corresponding aggregate. +// This error is used within the EventStore to ensure consistency of events and +// aggregates. The fields in VersionError provide additional information about +// the error, including the name and ID of the aggregate, the current version of +// the aggregate, and the event that caused the error. Methods are provided to +// return a string representation of the error and to check if it is a +// consistency error. type VersionError struct { - // AggregateName is the name of the aggregate. - AggregateName string - - // AggregateID is the UUID of the aggregate. - AggregateID uuid.UUID - - // CurrentVersion is the current version of the aggregate. + AggregateName string + AggregateID uuid.UUID CurrentVersion int - - // Event is the event with the invalid version. - Event event.Event - - err error + Event event.Event + err error } -// Error returns a string representation of a VersionError. A VersionError -// indicates that the insertion of events failed because at least one of the -// events has an invalid/inconsistent version. The returned string describes the -// validation error(s) and includes details about the aggregate's name, UUID, -// and current version; and the event with the invalid version. +// Error returns a string representation of the VersionError. If an underlying +// error is present, it prepends "version error: " to the error message. +// Otherwise, it generates a message indicating that an event has an incorrect +// version compared to what was expected. func (err VersionError) Error() string { if err.err != nil { return fmt.Sprintf("version error: %s", err.err) @@ -80,7 +114,11 @@ func (err VersionError) Error() string { ) } -// IsConsistencyError reports whether a VersionError is a consistency error. +// IsConsistencyError checks if a VersionError is a consistency error. It always +// returns true as all VersionErrors are considered consistency errors. This +// method is useful for handling errors where consistency needs to be ensured, +// such as when the version of an event does not match the expected version +// based on the current state of its corresponding aggregate. func (err VersionError) IsConsistencyError() bool { return true } @@ -88,7 +126,8 @@ func (err VersionError) IsConsistencyError() bool { // CommandError is a mongo.CommandError that satisfies aggregate.IsConsistencyError(err). type CommandError mongo.CommandError -// CommandError returns the error as a mongo.CommandError. +// CommandError CommandError returns a mongo.CommandError from the receiver. It +// is used for type conversion from CommandError to mongo.CommandError. func (err CommandError) CommandError() mongo.CommandError { return mongo.CommandError(err) } @@ -133,8 +172,10 @@ func URL(url string) EventStoreOption { } } -// Client returns an Option that specifies the underlying mongo.Client to be -// used by the Store. +// Client returns an EventStoreOption that sets the provided mongo.Client to be +// used by the EventStore. This option is useful when you already have a +// mongo.Client and want to reuse it, instead of letting the EventStore create +// its own client. func Client(c *mongo.Client) EventStoreOption { return func(s *EventStore) { s.client = c @@ -171,6 +212,9 @@ func StateCollection(name string) EventStoreOption { // https://docs.mongodb.com/manual/core/transactions/ func Transactions(tx bool) EventStoreOption { return func(s *EventStore) { + if !tx && s.transactions && (len(s.preInsertHooks) > 0 || len(s.postInsertHooks) > 0) { + panic(fmt.Errorf("transactions must be enabled for transaction hooks")) + } s.transactions = tx } } @@ -203,7 +247,80 @@ func WithIndices(models ...mongo.IndexModel) EventStoreOption { } } -// NewEventStore returns a MongoDB event.Store. +// TransactionHook represents a hook that can be executed before or after +// inserting events into the EventStore. The hook function should return an +// error if anything goes wrong, causing the transaction to abort. +type TransactionHook string + +// Transaction represents a set of operations that are executed within the same +// session in an EventStore. It encapsulates the session of the MongoDB driver +// and provides a reference to the associated EventStore. It also keeps track of +// all events that have been inserted into the EventStore within its session. +// Transactional operations ensure atomicity and consistency of data in the +// EventStore, making sure that either all operations are successfully +// completed, or none are in case of an error. +type Transaction interface { + // Session retrieves the active MongoDB session associated with the current + // transaction. This function allows for direct interaction with the MongoDB + // session for operations not directly supported by the Transaction interface. + // It returns a [mongo.Session] instance that represents the current session. + // The Session method is only available within a Transaction and should not be + // used outside of it to avoid unexpected behaviour or errors. + Session() mongo.Session + + // EventStore is a method of the Transaction interface. It returns an instance + // of the EventStore type that is associated with the transaction. This can be + // used to perform operations on the event store within the context of the + // transaction, ensuring consistency and atomicity of operations. + EventStore() *EventStore + + // InsertedEvents retrieves all events that have been inserted into the store + // within the current transaction. The function is part of the [Transaction] + // interface and returns a slice of [event.Event]. This method is useful for + // tracking changes made during a transaction, allowing for operations such as + // rollbacks or additional processing based on the inserted events. + InsertedEvents() []event.Event +} + +// TransactionContext is an interface that embeds the standard context.Context +// and Transaction interfaces. It provides a way to pass transaction-specific +// data, such as the session information and inserted events, along with the +// usual context data. This is particularly useful when using hooks in the +// EventStore, allowing hook functions to access additional information about +// the ongoing transaction. +type TransactionContext interface { + context.Context + Transaction +} + +// WithTransactionHook configures a transaction hook for the EventStore. The +// hook function will be called either before or after inserting events into the +// EventStore, depending on the specified TransactionHook. If the +// TransactionHook is "pre:insert", the hook function will be called before +// insertion. If it's "post:insert", the function will be called after +// insertion. The hook function should return an error if anything goes wrong, +// causing the transaction to abort. +func WithTransactionHook(hook TransactionHook, fn func(TransactionContext) error) EventStoreOption { + return func(s *EventStore) { + s.transactions = true + switch hook { + case PreInsert: + s.preInsertHooks = append(s.preInsertHooks, fn) + case PostInsert: + s.postInsertHooks = append(s.postInsertHooks, fn) + } + } +} + +// NewEventStore creates a new instance of an EventStore. This function accepts +// a codec.Encoding and any number of EventStoreOption functions. The EventStore +// instance created by this function will use the provided codec.Encoding for +// event serialization and deserialization. The EventStoreOptions are used to +// configure the behavior and characteristics of the EventStore, such as the +// underlying MongoDB client to use, the database and collections to store +// events in, and whether or not to validate event versions. If no database or +// collection names are provided via EventStoreOptions, default names will be +// used. By default, version validation is enabled. func NewEventStore(enc codec.Encoding, opts ...EventStoreOption) *EventStore { s := EventStore{ enc: enc, @@ -257,61 +374,124 @@ func (s *EventStore) StateCollection() *mongo.Collection { } // Insert saves the given events into the database. -func (s *EventStore) Insert(ctx context.Context, events ...event.Event) error { +func (s *EventStore) Insert(ctx context.Context, events ...event.Event) (out error) { + defer func() { + if out == nil { + return + } + + var cmdError mongo.CommandError + if errors.As(out, &cmdError) && (cmdError.HasErrorLabel(driver.TransientTransactionError) || + cmdError.HasErrorLabel(driver.UnknownTransactionCommitResult)) { + out = CommandError(cmdError) + } + }() + + if s.isTransactionStore { + return s.txInsert(ctx, events) + } + if err := s.connectOnce(ctx); err != nil { return fmt.Errorf("connect: %w", err) } - return s.client.UseSession(ctx, func(ctx mongo.SessionContext) (out error) { - defer func() { - if out == nil { - return - } + tx, err := s.createTransaction(ctx) + if err != nil { + return err + } + defer tx.Session().EndSession(ctx) - var cmdError mongo.CommandError - if errors.As(out, &cmdError) && (cmdError.HasErrorLabel(driver.TransientTransactionError) || - cmdError.HasErrorLabel(driver.UnknownTransactionCommitResult)) { - out = CommandError(cmdError) - } - }() + sessionCtx := mongo.NewSessionContext(ctx, tx.Session()) - if s.transactions { - if err := ctx.StartTransaction(); err != nil { - return fmt.Errorf("start transaction: %w", err) - } + if s.transactions { + if err := sessionCtx.StartTransaction(); err != nil { + return fmt.Errorf("start transaction: %w", err) } + } - st, err := s.validateEventVersions(ctx, events) - if err != nil { - return fmt.Errorf("validate version: %w", err) + txCtx := newTransactionContext(sessionCtx, tx) + for _, hook := range s.preInsertHooks { + if err := hook(txCtx); err != nil { + return s.abortTransaction(sessionCtx, fmt.Errorf("pre-insert hook: %w", err)) } + } - if err := s.insert(ctx, events); err != nil { - if s.transactions { - if abortError := ctx.AbortTransaction(ctx); abortError != nil { - return fmt.Errorf("abort transaction: %w", abortError) - } - } - return err - } + if err := s.insertInSession(sessionCtx, events); err != nil { + return err + } - if err := s.updateState(ctx, st, events); err != nil { - if s.transactions { - if abortError := ctx.AbortTransaction(ctx); abortError != nil { - return fmt.Errorf("abort transaction: %w", abortError) - } - } - return fmt.Errorf("update state: %w", err) + tx.appendEvents(events) + + for _, hook := range s.postInsertHooks { + if err := hook(txCtx); err != nil { + return s.abortTransaction(sessionCtx, fmt.Errorf("post-insert hook: %w", err)) } + } - if s.transactions { - if err := ctx.CommitTransaction(ctx); err != nil { - return fmt.Errorf("commit transaction: %w", err) - } + if s.transactions { + if err := sessionCtx.CommitTransaction(sessionCtx); err != nil { + return fmt.Errorf("commit transaction: %w", err) } + } - return nil - }) + return nil +} + +func (s *EventStore) txInsert(ctx context.Context, events []event.Event) error { + if err := s.root.connectOnce(ctx); err != nil { + return fmt.Errorf("connect: %w", err) + } + + sessionCtx := mongo.NewSessionContext(ctx, s.tx.Session()) + + if err := s.root.insertInSession(sessionCtx, events); err != nil { + return err + } + + s.tx.appendEvents(events) + + return nil +} + +func (s *EventStore) createTransaction(ctx context.Context) (tx *transaction, err error) { + session, err := s.client.StartSession() + if err != nil { + return nil, fmt.Errorf("start session: %w", err) + } + return newTransaction(session, s), nil +} + +func (s *EventStore) abortTransaction(ctx mongo.SessionContext, err error) error { + if s.isTransactionStore { + return s.root.abortTransaction(ctx, err) + } + + if !s.transactions { + return err + } + + if abortError := ctx.AbortTransaction(ctx); abortError != nil { + return fmt.Errorf("abort transaction with error %q: %w", err, abortError) + } + + return err +} + +func (s *EventStore) insertInSession(ctx mongo.SessionContext, events []event.Event) (out error) { + st, err := s.validateEventVersions(ctx, events) + if err != nil { + return s.abortTransaction(ctx, fmt.Errorf("validate versions: %w", err)) + } + + if err := s.insert(ctx, events); err != nil { + return s.abortTransaction(ctx, err) + } + + if err := s.updateState(ctx, st, events); err != nil { + return s.abortTransaction(ctx, fmt.Errorf("update aggregate state: %w", err)) + } + + return nil } func (s *EventStore) validateEventVersions(ctx mongo.SessionContext, events []event.Event) (state, error) { @@ -408,97 +588,138 @@ func (s *EventStore) insert(ctx context.Context, events []event.Event) error { // Find returns the event with the specified UUID from the database if it exists. func (s *EventStore) Find(ctx context.Context, id uuid.UUID) (event.Event, error) { + if s.isTransactionStore { + return s.root.Find(ctx, id) + } + if err := s.connectOnce(ctx); err != nil { return nil, fmt.Errorf("connect: %w", err) } + res := s.entries.FindOne(ctx, bson.M{"id": id}) + var e entry if err := res.Decode(&e); err != nil { return nil, fmt.Errorf("decode document: %w", err) } + return e.event(s.enc) } // Delete deletes the given event from the database. func (s *EventStore) Delete(ctx context.Context, events ...event.Event) error { - if len(events) == 0 { - return nil + if s.root != nil { + return s.txDelete(ctx, events) } - ids := make([]uuid.UUID, len(events)) - for i, evt := range events { - ids[i] = evt.ID() + if len(events) == 0 { + return nil } if err := s.connectOnce(ctx); err != nil { return fmt.Errorf("connect: %w", err) } - return s.client.UseSession(ctx, func(ctx mongo.SessionContext) error { - if s.transactions { - if err := ctx.StartTransaction(); err != nil { - return fmt.Errorf("start transaction: %w", err) - } - } + tx, err := s.createTransaction(ctx) + if err != nil { + return err + } + defer tx.Session().EndSession(ctx) - abort := func(err error) error { - if s.transactions { - if abortError := ctx.AbortTransaction(ctx); abortError != nil { - return fmt.Errorf("abort transaction: %w", abortError) - } - } - return err + sessionCtx := mongo.NewSessionContext(ctx, tx.Session()) + + if s.transactions { + if err := sessionCtx.StartTransaction(); err != nil { + return fmt.Errorf("start transaction: %w", err) } + } - commit := func() error { - if s.transactions { - if err := ctx.CommitTransaction(ctx); err != nil { - return fmt.Errorf("commit transaction: %w", err) - } + commit := func() error { + if s.transactions { + if err := sessionCtx.CommitTransaction(ctx); err != nil { + return fmt.Errorf("commit transaction: %w", err) } - return nil } + return nil + } - if _, err := s.entries.DeleteMany(ctx, bson.D{ - {Key: "id", Value: bson.D{{Key: "$in", Value: ids}}}, - }); err != nil { - return abort(err) - } + ids := make([]uuid.UUID, len(events)) + for i, evt := range events { + ids[i] = evt.ID() + } - aggregateName := pick.AggregateName(events[0]) - aggregateID := pick.AggregateID(events[0]) - aggregateVersion := pick.AggregateVersion(events[0]) + if err := s.deleteInSession(sessionCtx, ids); err != nil { + return err + } - if aggregateName == "" || aggregateID == uuid.Nil || aggregateVersion != 1 { - return commit() - } + name, id, version, hasAggregateData := checkDeletion(events) + if !hasAggregateData { + return commit() + } - for _, evt := range events[1:] { - id, name, v := evt.Aggregate() - if name != aggregateName || id != aggregateID { - return commit() - } + if _, err := s.states.DeleteOne(ctx, bson.D{ + {Key: "aggregateName", Value: name}, + {Key: "aggregateId", Value: id}, + {Key: "aggregateVersion", Value: version}, + }); err != nil { + return s.abortTransaction(sessionCtx, fmt.Errorf("delete aggregate state: %w", err)) + } - if v > aggregateVersion { - aggregateVersion = v - } - } + return commit() +} - if _, err := s.states.DeleteOne(ctx, bson.D{ - {Key: "aggregateName", Value: aggregateName}, - {Key: "aggregateId", Value: aggregateID}, - {Key: "aggregateVersion", Value: aggregateVersion}, - }); err != nil { - return abort(fmt.Errorf("delete aggregate state: %w", err)) - } +func (s *EventStore) deleteInSession(ctx mongo.SessionContext, ids []uuid.UUID) error { + if _, err := s.entries.DeleteMany(ctx, bson.D{ + {Key: "id", Value: bson.D{{Key: "$in", Value: ids}}}, + }); err != nil { + return s.abortTransaction(ctx, err) + } + return nil +} - return commit() - }) +func (s *EventStore) txDelete(ctx context.Context, events []event.Event) error { + if len(events) == 0 { + return nil + } + + if err := s.root.connectOnce(ctx); err != nil { + return fmt.Errorf("connect: %w", err) + } + + sessionCtx := mongo.NewSessionContext(ctx, s.tx.Session()) + + ids := make([]uuid.UUID, len(events)) + for i, evt := range events { + ids[i] = evt.ID() + } + + if err := s.root.deleteInSession(sessionCtx, ids); err != nil { + return err + } + + name, id, version, hasAggregateData := checkDeletion(events) + if !hasAggregateData { + return nil + } + + if _, err := s.root.states.DeleteOne(ctx, bson.D{ + {Key: "aggregateName", Value: name}, + {Key: "aggregateId", Value: id}, + {Key: "aggregateVersion", Value: version}, + }); err != nil { + return s.root.abortTransaction(sessionCtx, fmt.Errorf("delete aggregate state: %w", err)) + } + + return nil } // Query queries the database for events filtered by Query q and returns an // streams.New for those events. func (s *EventStore) Query(ctx context.Context, q event.Query) (<-chan event.Event, <-chan error, error) { + if s.isTransactionStore { + return s.root.Query(ctx, q) + } + if err := s.connectOnce(ctx); err != nil { return nil, nil, fmt.Errorf("connect: %w", err) } @@ -562,9 +783,14 @@ func (s *EventStore) Query(ctx context.Context, q event.Query) (<-chan event.Eve // automatically on the first call to s.Insert, s.Find, s.Delete or s.Query. Use // Connect if you want to explicitly control when to connect to MongoDB. func (s *EventStore) Connect(ctx context.Context, opts ...*options.ClientOptions) (*mongo.Client, error) { + if s.isTransactionStore { + return s.root.Connect(ctx, opts...) + } + if err := s.connectOnce(ctx, opts...); err != nil { return nil, err } + return s.client, nil } @@ -842,3 +1068,25 @@ func applySortings(opts *options.FindOptions, sortings ...event.SortOptions) *op } return opts.SetSort(sorts) } + +func checkDeletion(events []event.Event) (string, uuid.UUID, int, bool) { + head := events[0] + tail := events[1:] + + aggregateName := pick.AggregateName(head) + aggregateID := pick.AggregateID(head) + aggregateVersion := pick.AggregateVersion(head) + + for _, evt := range tail { + id, name, v := evt.Aggregate() + if name != aggregateName || id != aggregateID { + return aggregateName, aggregateID, aggregateVersion, false + } + + if v > aggregateVersion { + aggregateVersion = v + } + } + + return aggregateName, aggregateID, aggregateVersion, aggregateName != "" || aggregateID != uuid.Nil || aggregateVersion > 0 +} diff --git a/backend/mongo/store_test.go b/backend/mongo/store_test.go index 7a57685f..15810bda 100644 --- a/backend/mongo/store_test.go +++ b/backend/mongo/store_test.go @@ -10,7 +10,13 @@ import ( "sync/atomic" "testing" + "github.com/google/go-cmp/cmp" "github.com/google/uuid" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + mongodb "go.mongodb.org/mongo-driver/mongo" + "go.mongodb.org/mongo-driver/mongo/options" + "github.com/modernice/goes/aggregate" "github.com/modernice/goes/backend/mongo" "github.com/modernice/goes/backend/mongo/mongotest" @@ -18,7 +24,6 @@ import ( "github.com/modernice/goes/codec" "github.com/modernice/goes/event" etest "github.com/modernice/goes/event/test" - "go.mongodb.org/mongo-driver/bson" ) func TestEventStore(t *testing.T) { @@ -40,13 +45,6 @@ func TestEventStore(t *testing.T) { }) } -var evtDBID uint64 - -func nextEventDatabase() string { - id := atomic.AddUint64(&evtDBID, 1) - return fmt.Sprintf("events_%d", id) -} - func TestEventStore_Insert_versionError(t *testing.T) { enc := etest.NewEncoder() s := mongo.NewEventStore(enc, mongo.URL(os.Getenv("MONGOSTORE_URL")), mongo.Database(nextEventDatabase())) @@ -67,9 +65,9 @@ func TestEventStore_Insert_versionError(t *testing.T) { } events := []event.Event{ - event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), a.AggregateVersion()+5)), - event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), a.AggregateVersion()+6)), - event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), a.AggregateVersion()+7)), + event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 5)), + event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 6)), + event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 7)), } err := s.Insert(context.Background(), events...) @@ -95,3 +93,339 @@ func TestEventStore_Insert_versionError(t *testing.T) { t.Errorf("VersionError should have Event %v; got %v", events[0], versionError.Event) } } + +// TestEventStore_Insert_preAndPostHooks tests the following scenario +// Given: [0: "insert:pre", 1: "insert:pre", 2: "insert:post", 3: "insert:post"] hooks, then +// +// for hook 0: InsertedEvents() returns empty slice +// for hook 1: InsertedEvents() returns events that were inserted by hook 0 +// for hook 2: InsertedEvents() returns events that were inserted by hook 0 and 1, and the "main" events +// for hook 3: InsertedEvents() returns events that were inserted by hook 0 and 1, the "main" events, and the events inserted by hook 2 +func TestEventStore_Insert_withPreAndPostHooks(t *testing.T) { + enc := etest.NewEncoder() + + a := aggregate.New("foo", uuid.New()) + expectedEvent0 := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 1)) + expectedEvent1 := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 2)) + expectedEvent2 := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 3)) + expectedEvent3 := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 4)) + expectedEvent4 := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 5)) + allEvents := []event.Event{expectedEvent0, expectedEvent1, expectedEvent2, expectedEvent3, expectedEvent4} + + hookObserver0 := aHookObserver(func(ctx mongo.TransactionContext) error { return ctx.EventStore().Insert(ctx, expectedEvent0) }) + hookObserver1 := aHookObserver(func(ctx mongo.TransactionContext) error { return ctx.EventStore().Insert(ctx, expectedEvent1) }) + hookObserver2 := aHookObserver(func(ctx mongo.TransactionContext) error { return ctx.EventStore().Insert(ctx, expectedEvent3) }) + hookObserver3 := aHookObserver(func(ctx mongo.TransactionContext) error { return ctx.EventStore().Insert(ctx, expectedEvent4) }) + + s := mongotest.NewEventStore( + enc, + mongo.URL(os.Getenv("MONGOREPLSTORE_URL")), + mongo.Database(nextEventDatabase()), + mongo.WithTransactionHook(mongo.PreInsert, hookObserver0.hook), + mongo.WithTransactionHook(mongo.PreInsert, hookObserver1.hook), + mongo.WithTransactionHook(mongo.PostInsert, hookObserver2.hook), + mongo.WithTransactionHook(mongo.PostInsert, hookObserver3.hook), + ) + + if _, err := s.Connect(context.Background()); err != nil { + t.Fatalf("failed to connect to mongodb: %v", err) + } + + err := s.Insert(context.Background(), expectedEvent2) + if err != nil { + t.Errorf("failed to insert event: %s", err) + } + + type testData struct { + name string + observer *hookObserver + events []event.Event + } + + tests := []testData{ + { + "observer0", + hookObserver0, + nil, + }, + { + "observer1", + hookObserver1, + []event.Event{expectedEvent0}, + }, + { + "observer2", + hookObserver2, + []event.Event{expectedEvent0, expectedEvent1, expectedEvent2}, + }, + { + "observer3", + hookObserver3, + []event.Event{expectedEvent0, expectedEvent1, expectedEvent2, expectedEvent3}, + }, + } + + for _, tt := range tests { + if tt.observer.called != 1 { + t.Errorf("hook %q was not called once", tt.name) + } + if !cmp.Equal(tt.observer.insertedEvents, tt.events) { + t.Errorf("hook inserted events dont match expected\nhook: %#v\n\ninserted: %#v\n\ndiff: %s", tt.observer.insertedEvents, tt.events, cmp.Diff( + tt.observer.insertedEvents, tt.events, + )) + } + } + for _, expected := range allEvents { + found, err := s.Find(context.Background(), expected.ID()) + if err != nil { + t.Fatalf("expected store.Find not to return error; got %#v", err) + } + + if !event.Equal(found, expected) { + t.Errorf("found event doesn't match inserted event\ninserted: %#v\n\nfound: %#v\n\ndiff: %s", expected, found, cmp.Diff( + expected, found, cmp.AllowUnexported(expected), + )) + } + } +} + +func TestEventStore_Insert_preHookInsertingIntoCollection(t *testing.T) { + enc := etest.NewEncoder() + + dbName := nextEventDatabase() + client, testCollection, err := aCollection(dbName) + if err != nil { + t.Fatalf("failed to connect to mongodb: %v", err) + } + defer client.Disconnect(context.TODO()) + + a := aggregate.New("foo", uuid.New()) + expectedEvent0 := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 1)) + expectedEvent1 := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 2)) + allEvents := []event.Event{expectedEvent0, expectedEvent1} + expectedEntity := testEntity{Name: "Test"} + + hookObserver0 := aHookObserver(func(ctx mongo.TransactionContext) error { + result, err := testCollection.InsertOne(ctx, expectedEntity) + if err != nil { + return err + } + id, _ := result.InsertedID.(primitive.ObjectID) + expectedEntity.ID = id + return nil + }) + hookObserver1 := aHookObserver(func(ctx mongo.TransactionContext) error { return ctx.EventStore().Insert(ctx, expectedEvent1) }) + + s := mongotest.NewEventStore( + enc, + mongo.URL(os.Getenv("MONGOREPLSTORE_URL")), + mongo.Client(client), + mongo.Database(dbName), + mongo.WithTransactionHook(mongo.PreInsert, hookObserver0.hook), + mongo.WithTransactionHook(mongo.PostInsert, hookObserver1.hook), + ) + + if _, err = s.Connect(context.Background()); err != nil { + t.Fatalf("failed to connect to mongodb: %v", err) + } + + err = s.Insert(context.Background(), expectedEvent0) + if err != nil { + t.Errorf("error inserting %s", err) + } + + if hookObserver0.called != 1 { + t.Errorf("hook 1 was not called once") + } + if hookObserver1.called != 1 { + t.Errorf("hook 2 was not called once") + } + for _, expected := range allEvents { + found, err := s.Find(context.Background(), expected.ID()) + if err != nil { + t.Fatalf("expected store.Find not to return error; got %#v", err) + } + if !event.Equal(found, expected) { + t.Errorf("found event doesn't match inserted event\ninserted: %#v\n\nfound: %#v\n\ndiff: %s", expected, found, cmp.Diff( + expected, found, cmp.AllowUnexported(expected), + )) + } + } + + var actual testEntity + err = testCollection.FindOne(context.Background(), bson.M{"_id": expectedEntity.ID}).Decode(&actual) + if err != nil { + t.Errorf("expected to find entity, got err: %s", err) + } + if actual != expectedEntity { + t.Errorf("actual entity doesnt match the expected entity\nactual: %#v\n\nexpected: %#v\n\ndiff: %s", actual, expectedEntity, cmp.Diff( + actual, expectedEntity, cmp.AllowUnexported(expectedEntity), + )) + } + + if !cmp.Equal(hookObserver1.insertedEvents, []event.Event{expectedEvent0}) { + t.Errorf("hook inserted events dont match expected\nhook: %#v\n\ninserted: %#v\n\ndiff: %s", hookObserver1.insertedEvents, []event.Event{expectedEvent0}, cmp.Diff( + hookObserver1.insertedEvents, []event.Event{expectedEvent0}, + )) + } +} + +func TestEventStore_Insert_with3HooksLastFailing(t *testing.T) { + enc := etest.NewEncoder() + + dbName := nextEventDatabase() + client, testCollection, err := aCollection(dbName) + if err != nil { + t.Fatalf("failed to connect to mongodb: %v", err) + } + defer client.Disconnect(context.TODO()) + + a := aggregate.New("foo", uuid.New()) + mainEvent := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 2)) + preEvent := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 1)) + postEvent := event.New[any]("foo", etest.FooEventData{}, event.Aggregate(a.AggregateID(), a.AggregateName(), 3)) + allEvents := []event.Event{preEvent, mainEvent, postEvent} + expectedEntity := testEntity{Name: "Test"} + expectedErr := errors.New("some error in the handler") + + preObserver := aHookObserver(func(ctx mongo.TransactionContext) error { + return ctx.EventStore().Insert(ctx, preEvent) + }) + postObserver := aHookObserver(func(ctx mongo.TransactionContext) error { + return ctx.EventStore().Insert(ctx, postEvent) + }) + failingObserver := aHookObserver(func(ctx mongo.TransactionContext) error { + result, err := testCollection.InsertOne(ctx, expectedEntity) + if err != nil { + return err + } + id, _ := result.InsertedID.(primitive.ObjectID) + expectedEntity.ID = id + return expectedErr + }) + + s := mongotest.NewEventStore( + enc, + mongo.URL(os.Getenv("MONGOREPLSTORE_URL")), + mongo.Client(client), + mongo.Database(dbName), + mongo.WithTransactionHook(mongo.PreInsert, preObserver.hook), + mongo.WithTransactionHook(mongo.PostInsert, postObserver.hook), + mongo.WithTransactionHook(mongo.PostInsert, failingObserver.hook), + ) + + if _, err = s.Connect(context.Background()); err != nil { + t.Fatalf("failed to connect to mongodb: %v", err) + } + + err = s.Insert(context.Background(), mainEvent) + if !errors.Is(err, expectedErr) { + t.Errorf("not expected error: %s", err) + } + + if preObserver.called != 1 { + t.Errorf("pre-insert hook was not called once") + } + if postObserver.called != 1 { + t.Errorf("post-insert hook was not called once") + } + + for _, expected := range allEvents { + found, err := s.Find(context.Background(), expected.ID()) + if err == nil { + t.Errorf("expected store.Find() to return error after a failed transaction") + } + if found != nil { + t.Errorf("found event, expected nil: %v", found) + } + } + + var actual *testEntity + result := testCollection.FindOne(context.Background(), bson.M{"_id": expectedEntity.ID}) + + if result.Err() == nil { + t.Errorf("expected error to find entity %q, got nil", expectedEntity.ID) + } + if !errors.Is(result.Err(), mongodb.ErrNoDocuments) { + t.Errorf("expected: '%s', got err: '%s'", mongodb.ErrNoDocuments, result.Err()) + } + + if err := result.Decode(&actual); err == nil { + t.Errorf("expected error to decode entity %q, got nil", expectedEntity.ID) + } + + if actual != nil { + t.Errorf("expected nil document, got: %v", actual) + } +} + +func TestEventStore_WithTxHook_failsWithoutTransactionsEnabled(t *testing.T) { + enc := etest.NewEncoder() + hook := func(ctx mongo.TransactionContext) error { + return nil + } + + defer func() { + if r := recover(); r == nil { + t.Errorf("the code did not panic") + } + }() + + _ = mongotest.NewEventStore( + enc, + mongo.URL(os.Getenv("MONGOREPLSTORE_URL")), + mongo.Database(nextEventDatabase()), + mongo.WithTransactionHook(mongo.PostInsert, hook), + mongo.Transactions(false), + ) +} + +var evtDBID uint64 + +func nextEventDatabase() string { + id := atomic.AddUint64(&evtDBID, 1) + return fmt.Sprintf("events_%d", id) +} + +type hookObserver struct { + hook func(hook mongo.TransactionContext) error + called int + insertedEvents []event.Event +} + +func aHookObserver(fns ...func(mongo.TransactionContext) error) *hookObserver { + obs := &hookObserver{} + hook := func(ctx mongo.TransactionContext) error { + obs.called++ + obs.insertedEvents = ctx.InsertedEvents() + for _, fn := range fns { + err := fn(ctx) + if err != nil { + return err + } + } + return nil + } + obs.hook = hook + return obs +} + +type testEntity struct { + ID primitive.ObjectID `bson:"_id,omitempty"` + Name string `bson:"name,omitempty"` +} + +func aCollection(dbName string) (*mongodb.Client, *mongodb.Collection, error) { + client, err := mongodb.Connect(context.TODO(), + options.Client().ApplyURI(os.Getenv("MONGOREPLSTORE_URL"))) + if err != nil { + return nil, nil, err + } + + colname := mongotest.UniqueName("hooks_") + db := client.Database(dbName) + db.RunCommand(context.Background(), bson.D{{Key: "create", Value: colname}}) + testCollection := db.Collection(colname) + + return client, testCollection, nil +} diff --git a/backend/mongo/transaction.go b/backend/mongo/transaction.go new file mode 100644 index 00000000..359f877b --- /dev/null +++ b/backend/mongo/transaction.go @@ -0,0 +1,91 @@ +package mongo + +import ( + "context" + "sync" + + "go.mongodb.org/mongo-driver/mongo" + + "github.com/modernice/goes/event" +) + +var _ Transaction = (*transaction)(nil) + +type transactionKey struct{} + +type transaction struct { + mux sync.RWMutex + session mongo.Session + store *EventStore + insertedEvents []event.Event +} + +func newTransaction(session mongo.Session, store *EventStore) *transaction { + tx := &transaction{session: session} + + tx.store = &EventStore{ + isTransactionStore: true, + root: store, + tx: tx, + } + + return tx +} + +// Session retrieves the mongo.Session associated with the current transaction. +// This is the session used for executing MongoDB operations within the +// transaction. +func (tx *transaction) Session() mongo.Session { + return tx.session +} + +// EventStore returns the associated event store of the transaction. The +// returned event store is a transactional store, which means that changes to +// the store are only visible within the scope of the transaction until it's +// committed. +func (tx *transaction) EventStore() *EventStore { + return tx.store +} + +// InsertedEvents returns a slice of all the events that have been inserted in +// the current transaction. The events are returned in the order they were +// inserted. This method is safe for concurrent use. +func (tx *transaction) InsertedEvents() []event.Event { + tx.mux.RLock() + defer tx.mux.RUnlock() + return tx.insertedEvents +} + +func (tx *transaction) appendEvents(events []event.Event) { + tx.mux.Lock() + defer tx.mux.Unlock() + tx.insertedEvents = append(tx.insertedEvents, events...) +} + +type transactionContext struct { + context.Context + Transaction +} + +func newTransactionContext(ctx mongo.SessionContext, tx Transaction) *transactionContext { + return &transactionContext{ + Context: context.WithValue(ctx, transactionKey{}, tx), + Transaction: tx, + } +} + +// TransactionFromContext retrieves the Transaction from the provided context. +// If no Transaction exists in the context, it returns nil. +func TransactionFromContext(ctx context.Context) Transaction { + val := ctx.Value(transactionKey{}) + if val == nil { + return nil + } + + tx, ok := val.(Transaction) + if !ok { + return nil + } + + return tx +} diff --git a/go.work.sum b/go.work.sum index 48cbf768..15bb7293 100644 --- a/go.work.sum +++ b/go.work.sum @@ -79,9 +79,11 @@ cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARy cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= +cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= @@ -290,8 +292,10 @@ cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNp cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= @@ -299,8 +303,10 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= @@ -309,13 +315,18 @@ github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6D github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe h1:QQ3GSy+MqSHxm/d8nCtnAiZdYFd45cYZPs8vOOIYKfk= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -323,16 +334,21 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.m github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= +github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f h1:7T++XKzy4xg7PKy+bM+Sa9/oe1OC88yz2hXQUISoXfA= github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= +github.com/envoyproxy/protoc-gen-validate v0.10.1 h1:c0g45+xCJhdgFGw7a5QAfdS4byAbud7miNWJ1WwEVf8= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/log v0.1.0 h1:DGJh0Sm43HbOeYDNnVZFl8BvcYVvjD5bqYJvp0REbwQ= +github.com/go-logfmt/logfmt v0.5.0 h1:TrB8swr/68K7m9CcGut2g3UOihhbcbiMAYiuTXdEih4= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY= github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg= @@ -360,6 +376,7 @@ github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY9 github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= +github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -408,6 +425,7 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= @@ -442,19 +460,26 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4= github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= github.com/klauspost/compress v1.13.4/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.14.2/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.4/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.16.3/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/minio/highwayhash v1.0.1/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= @@ -492,16 +517,25 @@ github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.3.0 h1:RR9dF3JtopPvtkroDZuVD7qquD0bnHlKSqaQhgwt8yk= +github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc= +github.com/rs/zerolog v1.15.0 h1:uPRuwkWF4J6fGsJ2R0Gn2jB1EQiav9k3S6CSdygQJXY= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 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.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/zenazn/goji v0.9.0 h1:RSQQAbXGArQ0dIDEq+PI6WqN6if+5KHu6x2Cx/GXLTQ= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw= go.mongodb.org/mongo-driver v1.8.3/go.mod h1:0sQWfOeY63QTntERDJJ/0SuKK0T1uVSgKCuAROlKEPY= @@ -521,6 +555,10 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= +go.uber.org/atomic v1.6.0 h1:Ezj3JGmsOnG1MoRWQkPBsKLe9DwWD9QeXzTRzzldNVk= +go.uber.org/multierr v1.5.0 h1:KCa4XfM8CWFCpxXRGok+Q0SS/0XBhMDbHHGABQLvD2A= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= +go.uber.org/zap v1.13.0 h1:nR6NoDBgAf67s68NhaXbsojM+2gxp3S1hWkHDl27pVU= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -552,6 +590,7 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= @@ -606,6 +645,7 @@ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= +golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= 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= @@ -655,6 +695,7 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.7.0 h1:BEvjmm5fURWqcfbSKTdpkDXYBrUS1c0m8agp14W48vQ= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -712,6 +753,7 @@ golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= @@ -738,6 +780,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -782,6 +825,7 @@ google.golang.org/genproto v0.0.0-20230327215041-6ac7f18bb9d5/go.mod h1:UUQDJDOl google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54 h1:9NWlQfY2ePejTmfwUH1OWwmznFa+0kKcHGPDvcPza9M= google.golang.org/genproto v0.0.0-20230526161137-0005af68ea54/go.mod h1:zqTuNwFlFRsw5zIts5VnzLQxSRqh+CGOTVMlYbY0Eyk= +google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9 h1:m8v1xLLLzMe1m5P+gCTF8nJB9epwZQUBERm20Oy1poQ= google.golang.org/genproto/googleapis/api v0.0.0-20230525234035-dd9d682886f9/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= @@ -821,12 +865,16 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/errgo.v2 v2.1.0 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8= +gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec h1:RlWgLqCMMIYYEVcAR5MDsuHlVkaIPDAF+5Dehzg8L5A= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 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= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=