├── .gitignore ├── README.md ├── configuration_generator.go ├── configuration_generator_test.go ├── consumer.go ├── consumer_test.go ├── event_handler.go ├── event_handler_builder.go ├── execution_behaviour ├── behavioral │ ├── behavioral_test.go │ ├── error_execute_behavioral.go │ ├── error_execute_behavioral_test.go │ ├── logic_executor.go │ ├── normal_execute_behavioral.go │ ├── normal_execute_behavioral_test.go │ ├── retry_execute_behavioral.go │ ├── retry_execute_behavioral_test.go │ └── type.go └── behavioral_selector.go ├── go.mod ├── go.sum ├── internal └── testcontainers │ └── kafka │ └── container.go ├── kafka_suite_test.go ├── mocks ├── behavior_executor_mock.go ├── header_mock.go └── logic_operator_mock.go ├── params ├── connection_parameters.go ├── topic_parameters.go └── topic_parameters_test.go ├── producer.go ├── producer_test.go ├── remote_consumer.go ├── remote_consumer_test.go ├── test_utils ├── fake_event_handler.go └── utils.go └── utils ├── header.go └── header_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kafka-wrapper 2 | Golang Sarama Kafka wrapper with retry support -------------------------------------------------------------------------------- /configuration_generator.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "time" 6 | ) 7 | 8 | func GenerateKafkaConfiguration(version, clientId string, shouldWaitAckFromAll bool) *sarama.Config { 9 | v, err := sarama.ParseKafkaVersion(version) 10 | if err != nil { 11 | panic(err) 12 | } 13 | //consumer 14 | config := sarama.NewConfig() 15 | config.Version = v 16 | config.Consumer.Return.Errors = true 17 | config.ClientID = clientId 18 | 19 | config.Consumer.Offsets.Initial = sarama.OffsetNewest 20 | config.Metadata.Retry.Max = 3 21 | config.Metadata.Retry.Backoff = 10 * time.Second 22 | config.Metadata.Full = false 23 | 24 | config.Consumer.Group.Session.Timeout = 30 * time.Second 25 | config.Consumer.Group.Heartbeat.Interval = 6 * time.Second 26 | config.Consumer.MaxProcessingTime = 3 * time.Second 27 | config.Consumer.Fetch.Default = 2048 * 1024 28 | config.Consumer.Group.Rebalance.GroupStrategies = []sarama.BalanceStrategy{sarama.BalanceStrategyRoundRobin} 29 | config.Consumer.Group.Rebalance.Timeout = 3 * time.Minute 30 | 31 | //producer 32 | config.Producer.Retry.Max = 3 33 | config.Producer.Retry.Backoff = 10 * time.Second 34 | config.Producer.Return.Successes = true 35 | config.Producer.Return.Errors = true 36 | config.Producer.Timeout = 3 * time.Minute 37 | config.Net.ReadTimeout = 3 * time.Minute 38 | config.Net.DialTimeout = 3 * time.Minute 39 | config.Net.WriteTimeout = 3 * time.Minute 40 | config.Producer.MaxMessageBytes = 2000000 41 | 42 | if shouldWaitAckFromAll { 43 | config.Producer.RequiredAcks = sarama.WaitForAll 44 | } else { 45 | config.Producer.RequiredAcks = sarama.WaitForLocal 46 | } 47 | 48 | return config 49 | } 50 | -------------------------------------------------------------------------------- /configuration_generator_test.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper_test 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | kafka_wrapper "github.com/Trendyol/kafka-wrapper" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_it_should_reset_full_data_retrieval_parameter(t *testing.T) { 11 | // When 12 | config := kafka_wrapper.GenerateKafkaConfiguration("2.2.0", "clientId", false) 13 | 14 | // Then 15 | assert.False(t, config.Metadata.Full) 16 | } 17 | 18 | func Test_it_should_generate_configuration_to_wait_for_all_brokers_when_publishing(t *testing.T) { 19 | // When 20 | config := kafka_wrapper.GenerateKafkaConfiguration("2.2.0", "clientId", true) 21 | 22 | // Then 23 | assert.Equal(t, sarama.WaitForAll, config.Producer.RequiredAcks) 24 | } 25 | 26 | func Test_it_should_generate_configuration_to_wait_for_local_broker_when_publishing(t *testing.T) { 27 | // When 28 | config := kafka_wrapper.GenerateKafkaConfiguration("2.2.0", "clientId", false) 29 | 30 | // Then 31 | assert.Equal(t, sarama.WaitForLocal, config.Producer.RequiredAcks) 32 | } 33 | -------------------------------------------------------------------------------- /consumer.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/IBM/sarama" 8 | "github.com/Trendyol/kafka-wrapper/params" 9 | "strings" 10 | ) 11 | 12 | type Consumer interface { 13 | Subscribe(topicParams []params.TopicsParameters, handler EventHandler) 14 | SubscribeToTopic(topicParams params.TopicsParameters, handler EventHandler) 15 | Unsubscribe() 16 | } 17 | 18 | type kafkaConsumer struct { 19 | consumerGroup sarama.ConsumerGroup 20 | ctx context.Context 21 | cancelFunc context.CancelFunc 22 | } 23 | 24 | func NewConsumer(connectionParams params.ConnectionParameters) (Consumer, error) { 25 | cg, err := sarama.NewConsumerGroup(strings.Split(connectionParams.Brokers, ","), connectionParams.ConsumerGroupID, connectionParams.Conf) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | ctx, cancel := context.WithCancel(context.Background()) 31 | return &kafkaConsumer{ 32 | consumerGroup: cg, 33 | ctx: ctx, 34 | cancelFunc: cancel, 35 | }, nil 36 | } 37 | 38 | func (c *kafkaConsumer) SubscribeToTopic(topicParams params.TopicsParameters, handler EventHandler) { 39 | c.Subscribe([]params.TopicsParameters{topicParams}, handler) 40 | } 41 | 42 | func (c *kafkaConsumer) Subscribe(topicParams []params.TopicsParameters, handler EventHandler) { 43 | go func() { 44 | for { 45 | if c.ctx.Err() != nil { 46 | fmt.Printf("Consumer context canceled, exiting\n") 47 | return 48 | } 49 | 50 | err := c.consumerGroup.Consume(c.ctx, params.JoinAllTopics(topicParams), handler) 51 | if err != nil { 52 | if errors.Is(err, sarama.ErrClosedConsumerGroup) { 53 | return 54 | } 55 | fmt.Printf("Error from consumer: %v\n", err) 56 | } 57 | } 58 | }() 59 | 60 | go func() { 61 | for err := range c.consumerGroup.Errors() { 62 | fmt.Printf("Error from consumer group: %v\n", err) 63 | } 64 | }() 65 | 66 | fmt.Printf("Kafka consumer listens topic : %+v \n", topicParams) 67 | } 68 | 69 | func (c *kafkaConsumer) Unsubscribe() { 70 | c.cancelFunc() 71 | if err := c.consumerGroup.Close(); err != nil { 72 | fmt.Printf("Client wasn't closed: %v\n", err) 73 | } 74 | fmt.Println("Kafka consumer closed") 75 | } 76 | -------------------------------------------------------------------------------- /consumer_test.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper_test 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "github.com/Trendyol/kafka-wrapper" 6 | "github.com/Trendyol/kafka-wrapper/params" 7 | "github.com/Trendyol/kafka-wrapper/test_utils" 8 | testifyAssert "github.com/stretchr/testify/assert" 9 | "time" 10 | ) 11 | 12 | func (s *testKafkaSuite) Test_consume_when_broker_is_reachable() { 13 | // Given 14 | var ( 15 | assert = testifyAssert.New(s.T()) 16 | 17 | connectionParams = params.ConnectionParameters{ 18 | ConsumerGroupID: "consumer-group", 19 | } 20 | topicParams = params.TopicsParameters{ 21 | Topic: "test-topic", 22 | RetryTopic: "test-topic_retry", 23 | ErrorTopic: "test-topic_error", 24 | } 25 | expectedMessage = "test" 26 | messageChn = make(chan string, 1) 27 | receivedMessage string 28 | ) 29 | 30 | connectionParams.Brokers = s.Wrapper.GetBrokerAddress() 31 | connectionParams.Conf = test_utils.CreateBasicConf() 32 | 33 | time.Sleep(5 * time.Second) 34 | 35 | testProducer, _ := kafka_wrapper.NewProducer(connectionParams) 36 | 37 | // When 38 | _, _, _ = testProducer.SendMessage(&sarama.ProducerMessage{ 39 | Value: sarama.StringEncoder(expectedMessage), 40 | Topic: topicParams.Topic, 41 | }) 42 | testConsumer, _ := kafka_wrapper.NewConsumer(connectionParams) 43 | testConsumer.SubscribeToTopic(topicParams, test_utils.NewEventHandler(messageChn)) 44 | receivedMessage = <-messageChn 45 | 46 | // Then 47 | assert.Equal(receivedMessage, expectedMessage) 48 | } 49 | 50 | func (s *testKafkaSuite) Test_consume_multiple_topic_when_broker_is_reachable() { 51 | // Given 52 | var ( 53 | assert = testifyAssert.New(s.T()) 54 | 55 | connectionParams = params.ConnectionParameters{ 56 | ConsumerGroupID: "consumer-group", 57 | } 58 | testTopic1Params = params.TopicsParameters{ 59 | Topic: "test-topic1", 60 | RetryTopic: "test-topic1_retry", 61 | ErrorTopic: "test-topic1_error", 62 | } 63 | testTopic2Params = params.TopicsParameters{ 64 | Topic: "test-topic2", 65 | RetryTopic: "test-topic2_retry", 66 | ErrorTopic: "test-topic2_error", 67 | } 68 | 69 | expectedMessage1 = "test1" 70 | expectedMessage2 = "test2" 71 | receivedMessage1 string 72 | receivedMessage2 string 73 | 74 | messageChn = make(chan string, 1) 75 | ) 76 | 77 | connectionParams.Brokers = s.Wrapper.GetBrokerAddress() 78 | connectionParams.Conf = test_utils.CreateBasicConf() 79 | 80 | topics := params.FromTopics(testTopic1Params, testTopic2Params) 81 | 82 | time.Sleep(5 * time.Second) 83 | testProducer, _ := kafka_wrapper.NewProducer(connectionParams) 84 | 85 | // When && Then 86 | testConsumer, _ := kafka_wrapper.NewConsumer(connectionParams) 87 | testConsumer.Subscribe(topics, test_utils.NewEventHandler(messageChn)) 88 | 89 | _, _, _ = testProducer.SendMessage(&sarama.ProducerMessage{ 90 | Value: sarama.StringEncoder(expectedMessage1), 91 | Topic: testTopic1Params.Topic, 92 | }) 93 | receivedMessage1 = <-messageChn 94 | assert.Equal(receivedMessage1, expectedMessage1) 95 | 96 | time.Sleep(5 * time.Second) 97 | 98 | _, _, _ = testProducer.SendMessage(&sarama.ProducerMessage{ 99 | Value: sarama.StringEncoder(expectedMessage2), 100 | Topic: testTopic2Params.Topic, 101 | }) 102 | receivedMessage2 = <-messageChn 103 | assert.Equal(receivedMessage2, expectedMessage2) 104 | } 105 | 106 | func (s *testKafkaSuite) Test_not_consume_when_broker_is_not_reachable() { 107 | // Given 108 | var ( 109 | assert = testifyAssert.New(s.T()) 110 | 111 | wrongConf = params.ConnectionParameters{ 112 | Conf: sarama.NewConfig(), 113 | Brokers: "localhost:9093", 114 | } 115 | expectedError error 116 | ) 117 | 118 | // When 119 | _, expectedError = kafka_wrapper.NewConsumer(wrongConf) 120 | 121 | // Then 122 | assert.NotNil(expectedError) 123 | } 124 | 125 | func (s *testKafkaSuite) Test_stop_consume_after_unsubscription() { 126 | // Given 127 | var ( 128 | assert = testifyAssert.New(s.T()) 129 | 130 | connectionParams = params.ConnectionParameters{ 131 | ConsumerGroupID: "consumer-group", 132 | } 133 | topicParams = params.TopicsParameters{ 134 | Topic: "test-topic", 135 | RetryTopic: "test-topic_retry", 136 | ErrorTopic: "test-topic_error", 137 | } 138 | messageChn = make(chan string, 1) 139 | ) 140 | 141 | connectionParams.Brokers = s.Wrapper.GetBrokerAddress() 142 | connectionParams.Conf = test_utils.CreateBasicConf() 143 | 144 | time.Sleep(5 * time.Second) 145 | 146 | // When 147 | testConsumer, err := kafka_wrapper.NewConsumer(connectionParams) 148 | assert.NoError(err, "NewConsumer should not error") 149 | testConsumer.SubscribeToTopic(topicParams, test_utils.NewEventHandler(messageChn)) 150 | 151 | testConsumer.Unsubscribe() 152 | 153 | testProducer, err := kafka_wrapper.NewProducer(connectionParams) 154 | assert.NoError(err, "NewProducer should not error") 155 | _, _, err = testProducer.SendMessage(&sarama.ProducerMessage{ 156 | Value: sarama.StringEncoder("should-not-be-received"), 157 | Topic: topicParams.Topic, 158 | }) 159 | assert.NoError(err, "SendMessage should not error") 160 | 161 | // Then 162 | select { 163 | case msg := <-messageChn: 164 | assert.FailNow("Received message after unsubscription: " + msg) 165 | case <-time.After(3 * time.Second): 166 | // No message received: Test passes. 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /event_handler.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | ) 6 | 7 | type EventHandler interface { 8 | Setup(sarama.ConsumerGroupSession) error 9 | Cleanup(sarama.ConsumerGroupSession) error 10 | ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error 11 | } 12 | -------------------------------------------------------------------------------- /event_handler_builder.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper 2 | 3 | import ( 4 | "github.com/Trendyol/kafka-wrapper/execution_behaviour" 5 | "github.com/Trendyol/kafka-wrapper/params" 6 | ) 7 | 8 | type EventHandlerBuilder interface { 9 | AddBehavioralSelectors(topics params.TopicsParameters, selector execution_behaviour.BehavioralSelector) EventHandlerBuilder 10 | Build() map[string]execution_behaviour.BehavioralSelector 11 | } 12 | 13 | type eventHandlerBuilder struct { 14 | behavioralSelectors map[string]execution_behaviour.BehavioralSelector 15 | } 16 | 17 | func NewEventHandlerBuilder() EventHandlerBuilder { 18 | return &eventHandlerBuilder{ 19 | behavioralSelectors: make(map[string]execution_behaviour.BehavioralSelector), 20 | } 21 | } 22 | 23 | func (e *eventHandlerBuilder) AddBehavioralSelectors(topics params.TopicsParameters, selector execution_behaviour.BehavioralSelector) EventHandlerBuilder { 24 | e.behavioralSelectors[topics.Topic] = selector 25 | e.behavioralSelectors[topics.RetryTopic] = selector 26 | e.behavioralSelectors[topics.ErrorTopic] = selector 27 | return e 28 | } 29 | 30 | func (e *eventHandlerBuilder) Build() map[string]execution_behaviour.BehavioralSelector { 31 | return e.behavioralSelectors 32 | } 33 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/behavioral_test.go: -------------------------------------------------------------------------------- 1 | package behavioral_test 2 | 3 | import ( 4 | "errors" 5 | "github.com/IBM/sarama" 6 | ) 7 | 8 | type mockSyncProducer struct { 9 | SendingCount int32 10 | Message *sarama.ProducerMessage 11 | shouldFail bool 12 | } 13 | 14 | func NewMockSyncProducer(shouldFail bool) *mockSyncProducer { 15 | return &mockSyncProducer{SendingCount: 0, Message: nil, shouldFail: shouldFail} 16 | } 17 | 18 | func (m *mockSyncProducer) SendMessage(msg *sarama.ProducerMessage) (partition int32, offset int64, err error) { 19 | m.SendingCount++ 20 | m.Message = msg 21 | err = nil 22 | if m.shouldFail { 23 | err = errors.New("testingError") 24 | } 25 | return 0, 0, err 26 | } 27 | 28 | func (m *mockSyncProducer) SendMessages(msgs []*sarama.ProducerMessage) error { 29 | m.SendingCount++ 30 | return nil 31 | } 32 | 33 | func (m *mockSyncProducer) Close() error { return nil } 34 | func (m *mockSyncProducer) TxnStatus() sarama.ProducerTxnStatusFlag { return 0 } 35 | func (m *mockSyncProducer) IsTransactional() bool { return false } 36 | func (m *mockSyncProducer) BeginTxn() error { return nil } 37 | func (m *mockSyncProducer) CommitTxn() error { return nil } 38 | func (m *mockSyncProducer) AbortTxn() error { return nil } 39 | func (m *mockSyncProducer) AddMessageToTxn(msg *sarama.ConsumerMessage, groupId string, metadata *string) error { 40 | return nil 41 | } 42 | func (m *mockSyncProducer) AddOffsetsToTxn(offsets map[string][]*sarama.PartitionOffsetMetadata, groupId string) error { 43 | return nil 44 | } 45 | 46 | func (m *mockSyncProducer) GetHeaderOfMessage(headerName string) (string, error) { 47 | for _, header := range m.Message.Headers { 48 | if string(header.Key) == headerName { 49 | return string(header.Value), nil 50 | } 51 | } 52 | return "", errors.New("Header not found") 53 | } 54 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/error_execute_behavioral.go: -------------------------------------------------------------------------------- 1 | package behavioral 2 | 3 | import ( 4 | "context" 5 | "github.com/IBM/sarama" 6 | ) 7 | 8 | type errorBehaviour struct { 9 | executor LogicOperator 10 | } 11 | 12 | func ErrorBehavioral(executor LogicOperator) BehaviourExecutor { 13 | return &errorBehaviour{ 14 | executor: executor, 15 | } 16 | } 17 | 18 | func (k *errorBehaviour) Process(ctx context.Context, message *sarama.ConsumerMessage) error { 19 | return k.executor.Operate(ctx, message) 20 | } 21 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/error_execute_behavioral_test.go: -------------------------------------------------------------------------------- 1 | package behavioral_test 2 | 3 | import ( 4 | "context" 5 | "github.com/IBM/sarama" 6 | "github.com/Trendyol/kafka-wrapper/execution_behaviour/behavioral" 7 | "github.com/Trendyol/kafka-wrapper/mocks" 8 | "github.com/golang/mock/gomock" 9 | "github.com/stretchr/testify/assert" 10 | "testing" 11 | ) 12 | 13 | func Test_error_executor_should_just_call_executor(t *testing.T) { 14 | // Given 15 | var ( 16 | ctrl = gomock.NewController(t) 17 | executorMock = mocks.NewMockLogicOperator(ctrl) 18 | normalExecutor = behavioral.ErrorBehavioral(executorMock) 19 | ) 20 | 21 | executorMock.EXPECT().Operate(gomock.Any(), gomock.Any()).Times(1) 22 | 23 | // When 24 | err := normalExecutor.Process(context.TODO(), &sarama.ConsumerMessage{Topic: "topic"}) 25 | 26 | // Then 27 | assert.Nil(t, err) 28 | } 29 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/logic_executor.go: -------------------------------------------------------------------------------- 1 | package behavioral 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/IBM/sarama" 7 | ) 8 | 9 | //go:generate mockgen -destination=../../mocks/logic_operator_mock.go -package=mocks -source=logic_executor.go 10 | 11 | type LogicOperator interface { 12 | Operate(ctx context.Context, message *sarama.ConsumerMessage) error 13 | } 14 | 15 | // LogicOperatorFunc functional implementation of the LogicOperator interface 16 | type LogicOperatorFunc func(ctx context.Context, message *sarama.ConsumerMessage) error 17 | 18 | func (fn LogicOperatorFunc) Operate(ctx context.Context, message *sarama.ConsumerMessage) error { 19 | return fn(ctx, message) 20 | } 21 | 22 | // type-check 23 | var _ LogicOperator = (LogicOperatorFunc)(nil) 24 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/normal_execute_behavioral.go: -------------------------------------------------------------------------------- 1 | package behavioral 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/IBM/sarama" 7 | ) 8 | 9 | type normalBehaviour struct { 10 | producer sarama.SyncProducer 11 | executor LogicOperator 12 | retryTopic string 13 | } 14 | 15 | func NormalBehavioral(producer sarama.SyncProducer, retryTopic string, executor LogicOperator) BehaviourExecutor { 16 | return &normalBehaviour{ 17 | producer: producer, 18 | executor: executor, 19 | retryTopic: retryTopic, 20 | } 21 | } 22 | 23 | func (k *normalBehaviour) Process(ctx context.Context, message *sarama.ConsumerMessage) (err error) { 24 | if err = k.executor.Operate(ctx, message); err != nil { 25 | fmt.Printf("Have an error occurred while executing the logic: %+v, err:%+v\n", message.Topic, err) 26 | err = k.sendToRetryTopic(message, err) 27 | if err != nil { 28 | fmt.Printf("Have an error occurred while publishing to retry topic: %+v , err:%+v\n", k.retryTopic, err) 29 | } 30 | } 31 | return err 32 | } 33 | 34 | func (k *normalBehaviour) sendToRetryTopic(message *sarama.ConsumerMessage, err error) error { 35 | headers := make([]sarama.RecordHeader, 0) 36 | for _, v := range message.Headers { 37 | headers = append(headers, *v) 38 | } 39 | 40 | headers = append(headers, sarama.RecordHeader{ 41 | Key: []byte(RetryErrorKey), 42 | Value: []byte(err.Error()), 43 | }) 44 | 45 | _, _, sendError := k.producer.SendMessage(&sarama.ProducerMessage{ 46 | Topic: k.retryTopic, 47 | Key: sarama.StringEncoder(message.Key), 48 | Value: sarama.StringEncoder(message.Value), 49 | Headers: headers, 50 | }) 51 | return sendError 52 | } 53 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/normal_execute_behavioral_test.go: -------------------------------------------------------------------------------- 1 | package behavioral_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/IBM/sarama" 7 | "github.com/Trendyol/kafka-wrapper/execution_behaviour/behavioral" 8 | "github.com/Trendyol/kafka-wrapper/mocks" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | "testing" 12 | ) 13 | 14 | func Test_normal_executor_should_call_executor_and_not_produce_message_when_executor_succeeds(t *testing.T) { 15 | // Given 16 | var ( 17 | producerMock = NewMockSyncProducer(false) 18 | retryTopic = "RetryTopicName" 19 | ctrl = gomock.NewController(t) 20 | executorMock = mocks.NewMockLogicOperator(ctrl) 21 | normalExecutor = behavioral.NormalBehavioral(producerMock, retryTopic, executorMock) 22 | ) 23 | 24 | executorMock.EXPECT().Operate(gomock.Any(), gomock.Any()).Times(1).Return(nil) 25 | 26 | // When 27 | err := normalExecutor.Process(context.TODO(), &sarama.ConsumerMessage{Topic: "topic"}) 28 | 29 | // Then 30 | assert.Nil(t, err) 31 | assert.Equal(t, int32(0), producerMock.SendingCount) 32 | } 33 | 34 | func Test_normal_executor_should_call_executor_and_produce_message_when_executor_fails(t *testing.T) { 35 | // Given 36 | var ( 37 | producerMock = NewMockSyncProducer(false) 38 | retryTopic = "RetryTopicName" 39 | ctrl = gomock.NewController(t) 40 | executorMock = mocks.NewMockLogicOperator(ctrl) 41 | normalExecutor = behavioral.NormalBehavioral(producerMock, retryTopic, executorMock) 42 | ) 43 | 44 | executorMock.EXPECT().Operate(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("testingError")) 45 | 46 | // When 47 | err := normalExecutor.Process(context.TODO(), &sarama.ConsumerMessage{Topic: "topic"}) 48 | 49 | // Then 50 | assert.Nil(t, err) 51 | assert.Equal(t, int32(1), producerMock.SendingCount) 52 | assert.Equal(t, retryTopic, producerMock.Message.Topic) 53 | } 54 | 55 | func Test_normal_executor_should_fail_when_message_producing_operations_fails(t *testing.T) { 56 | // Given 57 | var ( 58 | producerMock = NewMockSyncProducer(true) 59 | retryTopic = "RetryTopicName" 60 | ctrl = gomock.NewController(t) 61 | executorMock = mocks.NewMockLogicOperator(ctrl) 62 | normalExecutor = behavioral.NormalBehavioral(producerMock, retryTopic, executorMock) 63 | ) 64 | 65 | executorMock.EXPECT().Operate(gomock.Any(), gomock.Any()).Times(1).Return(errors.New("testingError")) 66 | 67 | // When 68 | err := normalExecutor.Process(context.TODO(), &sarama.ConsumerMessage{Topic: "topic"}) 69 | 70 | // Then 71 | assert.NotNil(t, err) 72 | } 73 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/retry_execute_behavioral.go: -------------------------------------------------------------------------------- 1 | package behavioral 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/IBM/sarama" 7 | "github.com/Trendyol/kafka-wrapper/utils" 8 | "time" 9 | ) 10 | 11 | const ErrorKey = "ErrorMessage" 12 | const RetryTopicKey = "RetryTopic" 13 | const RetryErrorKey = "RetryErrorMessage" 14 | 15 | type retryBehaviour struct { 16 | producer sarama.SyncProducer 17 | executor LogicOperator 18 | retryCount int 19 | errorTopic string 20 | headerOperator utils.HeaderOperation 21 | } 22 | 23 | func RetryBehavioral(producer sarama.SyncProducer, errorTopic string, executor LogicOperator, retryCount int, headerOperator utils.HeaderOperation) BehaviourExecutor { 24 | return &retryBehaviour{ 25 | producer: producer, 26 | executor: executor, 27 | retryCount: retryCount, 28 | errorTopic: errorTopic, 29 | headerOperator: headerOperator, 30 | } 31 | } 32 | 33 | func (k *retryBehaviour) Process(ctx context.Context, message *sarama.ConsumerMessage) (err error) { 34 | 35 | for i := 0; i < k.retryCount; i++ { 36 | if i == 0 { 37 | latestExecutableTime := time.Now().Add(time.Duration(-5) * time.Second) 38 | if latestExecutableTime.Before(message.Timestamp) { 39 | sleepTime := message.Timestamp.Sub(latestExecutableTime) 40 | fmt.Printf("System will sleep for %+v\n", sleepTime) 41 | time.Sleep(sleepTime) 42 | } else { 43 | fmt.Printf("No need to sleep for message time %+v\n", message.Timestamp) 44 | } 45 | } else { 46 | time.Sleep(250 * time.Millisecond) 47 | } 48 | 49 | err = k.executor.Operate(ctx, message) 50 | if err == nil { 51 | fmt.Printf("Message is executed successfully, message key: %+v\n", message.Key) 52 | break 53 | } 54 | } 55 | 56 | if err != nil { 57 | fmt.Printf("Message is not executed successfully: %+v so routing it to the error topic: %+v \n", message.Topic, k.errorTopic) 58 | err = k.sendToErrorTopic(message, err.Error()) 59 | if err != nil { 60 | fmt.Printf("Message is not published to the error topic: %+v\n", k.errorTopic) 61 | return err 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (k *retryBehaviour) sendToErrorTopic(message *sarama.ConsumerMessage, errorMessage string) error { 69 | 70 | headers := k.updateCurrentHeaders(message, errorMessage) 71 | 72 | _, _, err := k.producer.SendMessage(&sarama.ProducerMessage{ 73 | Headers: headers, 74 | Key: sarama.StringEncoder(message.Key), 75 | Topic: k.errorTopic, 76 | Value: sarama.StringEncoder(message.Value), 77 | }) 78 | return err 79 | } 80 | 81 | func (k *retryBehaviour) updateCurrentHeaders(message *sarama.ConsumerMessage, errorMessage string) []sarama.RecordHeader { 82 | 83 | headers := k.headerOperator.ExtractHeader(message) 84 | headers = k.headerOperator.AddIntoHeader(headers, ErrorKey, errorMessage) 85 | headers = k.headerOperator.AddIntoHeader(headers, RetryTopicKey, message.Topic) 86 | 87 | return headers 88 | } 89 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/retry_execute_behavioral_test.go: -------------------------------------------------------------------------------- 1 | package behavioral_test 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/IBM/sarama" 7 | "github.com/Trendyol/kafka-wrapper/execution_behaviour/behavioral" 8 | "github.com/Trendyol/kafka-wrapper/mocks" 9 | "github.com/golang/mock/gomock" 10 | "github.com/stretchr/testify/assert" 11 | "testing" 12 | ) 13 | 14 | func Test_retry_executor_should_call_executor_and_not_produce_message_when_executor_succeeds(t *testing.T) { 15 | // Given 16 | var ( 17 | producerMock = NewMockSyncProducer(false) 18 | errorTopic = "ErrorTopicName" 19 | ctrl = gomock.NewController(t) 20 | executorMock = mocks.NewMockLogicOperator(ctrl) 21 | retryCount = 3 22 | headerOperatorMock = mocks.NewMockheaderOperation(ctrl) 23 | retryTopicName = "TheRetryTopic" 24 | retryExecutor = behavioral.RetryBehavioral(producerMock, errorTopic, executorMock, retryCount, headerOperatorMock) 25 | ) 26 | 27 | executorMock.EXPECT().Operate(gomock.Any(), gomock.Any()).Times(1).Return(nil) 28 | headerOperatorMock.EXPECT().ExtractHeader(gomock.Any()).Times(0) 29 | headerOperatorMock.EXPECT().AddIntoHeader(gomock.Any(), gomock.Any(), gomock.Any()).Times(0) 30 | 31 | // When 32 | err := retryExecutor.Process(context.TODO(), &sarama.ConsumerMessage{Topic: retryTopicName}) 33 | 34 | // Then 35 | assert.Nil(t, err) 36 | assert.Equal(t, int32(0), producerMock.SendingCount) 37 | } 38 | 39 | func Test_retry_executor_should_call_executor_retry_limit_times_and_produce_message_with_header_manipulation_when_executor_fails(t *testing.T) { 40 | // Given 41 | var ( 42 | producerMock = NewMockSyncProducer(false) 43 | errorTopic = "ErrorTopicName" 44 | ctrl = gomock.NewController(t) 45 | executorMock = mocks.NewMockLogicOperator(ctrl) 46 | retryCount = 3 47 | retryTopicName = "theRetryTopic" 48 | headerOperatorMock = mocks.NewMockheaderOperation(ctrl) 49 | retryExecutor = behavioral.RetryBehavioral(producerMock, errorTopic, executorMock, retryCount, headerOperatorMock) 50 | ) 51 | 52 | executorMock.EXPECT().Operate(gomock.Any(), gomock.Any()).Times(3).Return(errors.New("testingError")) 53 | headerOperatorMock.EXPECT().ExtractHeader(gomock.Any()).Return(nil) 54 | headerOperatorMock.EXPECT().AddIntoHeader(nil, behavioral.RetryTopicKey, retryTopicName).Return(nil) 55 | headerOperatorMock.EXPECT().AddIntoHeader(nil, behavioral.ErrorKey, "testingError").Return(nil) 56 | 57 | // When 58 | err := retryExecutor.Process(context.TODO(), &sarama.ConsumerMessage{Topic: retryTopicName}) 59 | 60 | // Then 61 | assert.Nil(t, err) 62 | assert.Equal(t, int32(1), producerMock.SendingCount) 63 | assert.Equal(t, errorTopic, producerMock.Message.Topic) 64 | } 65 | 66 | func Test_retry_executor_should_fail_when_producing_operation_fails(t *testing.T) { 67 | // Given 68 | var ( 69 | producerMock = NewMockSyncProducer(true) 70 | errorTopic = "ErrorTopicName" 71 | ctrl = gomock.NewController(t) 72 | executorMock = mocks.NewMockLogicOperator(ctrl) 73 | retryCount = 3 74 | headerOperatorMock = mocks.NewMockheaderOperation(ctrl) 75 | retryExecutor = behavioral.RetryBehavioral(producerMock, errorTopic, executorMock, retryCount, headerOperatorMock) 76 | ) 77 | 78 | executorMock.EXPECT().Operate(gomock.Any(), gomock.Any()).Times(3).Return(errors.New("testingError")) 79 | headerOperatorMock.EXPECT().ExtractHeader(gomock.Any()).Return(nil) 80 | headerOperatorMock.EXPECT().AddIntoHeader(gomock.Any(), gomock.Any(), gomock.Any()).Times(2).Return(nil) 81 | 82 | // When 83 | err := retryExecutor.Process(context.TODO(), &sarama.ConsumerMessage{Topic: "topic"}) 84 | 85 | // Then 86 | assert.NotNil(t, err) 87 | } 88 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral/type.go: -------------------------------------------------------------------------------- 1 | package behavioral 2 | 3 | import ( 4 | "context" 5 | "github.com/IBM/sarama" 6 | ) 7 | 8 | //go:generate mockgen -destination=../../mocks/behavior_executor_mock.go -package=mocks -source=type.go 9 | 10 | type BehaviourExecutor interface { 11 | Process(ctx context.Context, message *sarama.ConsumerMessage) error 12 | } 13 | -------------------------------------------------------------------------------- /execution_behaviour/behavioral_selector.go: -------------------------------------------------------------------------------- 1 | package execution_behaviour 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "github.com/Trendyol/kafka-wrapper/execution_behaviour/behavioral" 6 | "github.com/Trendyol/kafka-wrapper/utils" 7 | ) 8 | 9 | type BehavioralSelector interface { 10 | GetBehavioral(claim sarama.ConsumerGroupClaim) behavioral.BehaviourExecutor 11 | } 12 | 13 | type behaviourSelector struct { 14 | normalOperator behavioral.LogicOperator 15 | errorOperator behavioral.LogicOperator 16 | producer sarama.SyncProducer 17 | retryTopic string 18 | errorTopic string 19 | retryCount int 20 | headerOperator utils.HeaderOperation 21 | } 22 | 23 | func NewBehaviourSelector(normalOperator behavioral.LogicOperator, errorOperator behavioral.LogicOperator, producer sarama.SyncProducer, 24 | retryCount int, retryTopic, errorTopic string) *behaviourSelector { 25 | return &behaviourSelector{ 26 | normalOperator: normalOperator, 27 | producer: producer, 28 | retryTopic: retryTopic, 29 | errorTopic: errorTopic, 30 | retryCount: retryCount, 31 | errorOperator: errorOperator, 32 | headerOperator: utils.NewHeaderOperator(), 33 | } 34 | } 35 | 36 | func NewRetryOnlyBehavioralSelector(normalOperator behavioral.LogicOperator, producer sarama.SyncProducer, 37 | retryCount int, retryTopic, errorTopic string) *behaviourSelector { 38 | return &behaviourSelector{ 39 | normalOperator: normalOperator, 40 | errorOperator: nil, 41 | producer: producer, 42 | retryTopic: retryTopic, 43 | errorTopic: errorTopic, 44 | retryCount: retryCount, 45 | headerOperator: utils.NewHeaderOperator(), 46 | } 47 | } 48 | 49 | func (r *behaviourSelector) GetBehavioral(claim sarama.ConsumerGroupClaim) behavioral.BehaviourExecutor { 50 | if claim.Topic() == r.retryTopic { 51 | return behavioral.RetryBehavioral(r.producer, r.errorTopic, r.normalOperator, r.retryCount, r.headerOperator) 52 | } else if r.errorOperator != nil && claim.Topic() == r.errorTopic { 53 | return behavioral.ErrorBehavioral(r.errorOperator) 54 | } else { 55 | return behavioral.NormalBehavioral(r.producer, r.retryTopic, r.normalOperator) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Trendyol/kafka-wrapper 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/IBM/sarama v1.43.2 7 | github.com/docker/go-connections v0.4.0 8 | github.com/golang/mock v1.6.0 9 | github.com/stretchr/testify v1.9.0 10 | github.com/testcontainers/testcontainers-go v0.19.0 11 | ) 12 | 13 | require ( 14 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 15 | github.com/Microsoft/go-winio v0.5.2 // indirect 16 | github.com/cenkalti/backoff/v4 v4.2.0 // indirect 17 | github.com/containerd/containerd v1.6.19 // indirect 18 | github.com/cpuguy83/dockercfg v0.3.1 // indirect 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/docker/distribution v2.8.1+incompatible // indirect 21 | github.com/docker/docker v23.0.1+incompatible // indirect 22 | github.com/docker/go-units v0.5.0 // indirect 23 | github.com/eapache/go-resiliency v1.6.0 // indirect 24 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect 25 | github.com/eapache/queue v1.1.0 // indirect 26 | github.com/gogo/protobuf v1.3.2 // indirect 27 | github.com/golang/protobuf v1.5.2 // indirect 28 | github.com/golang/snappy v0.0.4 // indirect 29 | github.com/google/uuid v1.3.0 // indirect 30 | github.com/hashicorp/errwrap v1.1.0 // indirect 31 | github.com/hashicorp/go-multierror v1.1.1 // indirect 32 | github.com/hashicorp/go-uuid v1.0.3 // indirect 33 | github.com/jcmturner/aescts/v2 v2.0.0 // indirect 34 | github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect 35 | github.com/jcmturner/gofork v1.7.6 // indirect 36 | github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect 37 | github.com/jcmturner/rpc/v2 v2.0.3 // indirect 38 | github.com/klauspost/compress v1.17.9 // indirect 39 | github.com/magiconair/properties v1.8.7 // indirect 40 | github.com/moby/patternmatcher v0.5.0 // indirect 41 | github.com/moby/sys/sequential v0.5.0 // indirect 42 | github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f // indirect 43 | github.com/morikuni/aec v1.0.0 // indirect 44 | github.com/opencontainers/go-digest v1.0.0 // indirect 45 | github.com/opencontainers/image-spec v1.1.0-rc2 // indirect 46 | github.com/opencontainers/runc v1.1.3 // indirect 47 | github.com/pierrec/lz4/v4 v4.1.21 // indirect 48 | github.com/pkg/errors v0.9.1 // indirect 49 | github.com/pmezard/go-difflib v1.0.0 // indirect 50 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect 51 | github.com/sirupsen/logrus v1.9.0 // indirect 52 | golang.org/x/crypto v0.25.0 // indirect 53 | golang.org/x/net v0.27.0 // indirect 54 | golang.org/x/sys v0.22.0 // indirect 55 | google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad // indirect 56 | google.golang.org/grpc v1.47.0 // indirect 57 | google.golang.org/protobuf v1.28.0 // indirect 58 | gopkg.in/yaml.v3 v3.0.1 // indirect 59 | ) 60 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= 4 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 6 | github.com/IBM/sarama v1.43.2 h1:HABeEqRUh32z8yzY2hGB/j8mHSzC/HA9zlEjqFNCzSw= 7 | github.com/IBM/sarama v1.43.2/go.mod h1:Kyo4WkF24Z+1nz7xeVUFWIuKVV8RS3wM8mkvPKMdXFQ= 8 | github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= 9 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 10 | github.com/Microsoft/hcsshim v0.9.7 h1:mKNHW/Xvv1aFH87Jb6ERDzXTJTLPlmzfZ28VBFD/bfg= 11 | github.com/Microsoft/hcsshim v0.9.7/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= 12 | github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= 13 | github.com/cenkalti/backoff/v4 v4.2.0 h1:HN5dHm3WBOgndBH6E8V0q2jIYIR3s9yglV8k/+MN3u4= 14 | github.com/cenkalti/backoff/v4 v4.2.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 15 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 16 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= 18 | github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= 19 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 20 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 21 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 22 | github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= 23 | github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 24 | github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 25 | github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= 26 | github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= 27 | github.com/containerd/containerd v1.6.19 h1:F0qgQPrG0P2JPgwpxWxYavrVeXAG0ezUIB9Z/4FTUAU= 28 | github.com/containerd/containerd v1.6.19/go.mod h1:HZCDMn4v/Xl2579/MvtOC2M206i+JJ6VxFWU/NetrGY= 29 | github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= 30 | github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= 31 | github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 32 | github.com/cpuguy83/dockercfg v0.3.1 h1:/FpZ+JaygUR/lZP2NlFI2DVfrOEMAIKP5wWEJdoYe9E= 33 | github.com/cpuguy83/dockercfg v0.3.1/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= 34 | github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 35 | github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= 36 | github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 37 | github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 38 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 39 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 40 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 41 | github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= 42 | github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 43 | github.com/docker/docker v23.0.1+incompatible h1:vjgvJZxprTTE1A37nm+CLNAdwu6xZekyoiVlUZEINcY= 44 | github.com/docker/docker v23.0.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 45 | github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= 46 | github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= 47 | github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 48 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 49 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 50 | github.com/eapache/go-resiliency v1.6.0 h1:CqGDTLtpwuWKn6Nj3uNUdflaq+/kIPsg0gfNzHton30= 51 | github.com/eapache/go-resiliency v1.6.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= 52 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4ALJ04o5Qqpdz8XLIpNA3WM/iSIXqxtqo7UGVws= 53 | github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= 54 | github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= 55 | github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= 56 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 57 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 58 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 59 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 60 | github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= 61 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 62 | github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= 63 | github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 64 | github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= 65 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 66 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 67 | github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 68 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 69 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 70 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 71 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 72 | github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 73 | github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= 74 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 75 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 76 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 77 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 78 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 79 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 80 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 81 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 82 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 83 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 84 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 85 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 86 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 87 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 88 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 89 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 90 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 91 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 92 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 93 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 94 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 95 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 96 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 97 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 98 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 99 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 100 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 101 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 102 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 103 | github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= 104 | github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= 105 | github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= 106 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 107 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 108 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 109 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 110 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 111 | github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 112 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 113 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 114 | github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= 115 | github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= 116 | github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= 117 | github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= 118 | github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= 119 | github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= 120 | github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= 121 | github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= 122 | github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= 123 | github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= 124 | github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= 125 | github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= 126 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 127 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 128 | github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= 129 | github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= 130 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 131 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 132 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 133 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 134 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 135 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 136 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 137 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 138 | github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo= 139 | github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= 140 | github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= 141 | github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= 142 | github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= 143 | github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f h1:J/7hjLaHLD7epG0m6TBMGmp4NQ+ibBYLfeyJWdAIFLA= 144 | github.com/moby/term v0.0.0-20221128092401-c43b287e0e0f/go.mod h1:15ce4BGCFxt7I5NQKT+HV0yEDxmf6fSysfEDiVo3zFM= 145 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 146 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 147 | github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= 148 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 149 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 150 | github.com/opencontainers/image-spec v1.1.0-rc2 h1:2zx/Stx4Wc5pIPDvIxHXvXtQFW/7XWJGmnM7r3wg034= 151 | github.com/opencontainers/image-spec v1.1.0-rc2/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= 152 | github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= 153 | github.com/opencontainers/runc v1.1.3/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= 154 | github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= 155 | github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= 156 | github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= 157 | github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 158 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 159 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 160 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 161 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 162 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 163 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= 164 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= 165 | github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 166 | github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= 167 | github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= 168 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 169 | github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= 170 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 171 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 172 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 173 | github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= 174 | github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 175 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 176 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 177 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 178 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 179 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 180 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 181 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 182 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 183 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 184 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 185 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 186 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 187 | github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= 188 | github.com/testcontainers/testcontainers-go v0.19.0 h1:3bmFPuQRgVIQwxZJERyzB8AogmJW3Qzh8iDyfJbPhi8= 189 | github.com/testcontainers/testcontainers-go v0.19.0/go.mod h1:3YsSoxK0rGEUzbGD4gUVt1Nm3GJpCIq94GX+2LSf3d4= 190 | github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 191 | github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= 192 | github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= 193 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 194 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 195 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 196 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 197 | go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= 198 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 199 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 200 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 201 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 202 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 203 | golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= 204 | golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= 205 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 206 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 207 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 208 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 209 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 210 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 211 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 212 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 213 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 214 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 215 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 216 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 217 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 218 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 219 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 220 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 221 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 222 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 223 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 224 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 225 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 226 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 227 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 228 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 229 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 230 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 231 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 232 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 233 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 234 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 235 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 236 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 237 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 238 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 239 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 240 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 241 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 243 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 244 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 245 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 246 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 247 | golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 248 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 249 | golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 250 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 251 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 252 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 257 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 258 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 259 | golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 260 | golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 261 | golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 262 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 263 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 264 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 265 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 266 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 267 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 268 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 269 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 270 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 271 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 272 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 273 | golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 274 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 275 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 276 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 277 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 278 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= 279 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 280 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 281 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 282 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 283 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 284 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 285 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 286 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 287 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 288 | golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 289 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 290 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 291 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 292 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 293 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 294 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 295 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 296 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 297 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 298 | google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 299 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 300 | google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad h1:kqrS+lhvaMHCxul6sKQvKJ8nAAhlVItmZV822hYFH/U= 301 | google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= 302 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 303 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 304 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 305 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 306 | google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= 307 | google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 308 | google.golang.org/grpc v1.47.0 h1:9n77onPX5F3qfFCqjy9dhn8PbNQsIKeVU04J9G7umt8= 309 | google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= 310 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 311 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 312 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 313 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 314 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 315 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 316 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 317 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 318 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 319 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 320 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 321 | google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 322 | google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= 323 | google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 324 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 325 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 326 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 327 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 328 | gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 329 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 330 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 331 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 332 | gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= 333 | gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= 334 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 335 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 336 | -------------------------------------------------------------------------------- /internal/testcontainers/kafka/container.go: -------------------------------------------------------------------------------- 1 | package kafka 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/docker/go-connections/nat" 7 | "github.com/testcontainers/testcontainers-go" 8 | "github.com/testcontainers/testcontainers-go/wait" 9 | "log" 10 | "runtime" 11 | "time" 12 | ) 13 | 14 | const ( 15 | RedpandaImage = "redpandadata/redpanda" 16 | RedpandaVersion = "v24.1.3" 17 | ) 18 | 19 | type TestContainerWrapper struct { 20 | container testcontainers.Container 21 | hostPort int 22 | } 23 | 24 | func (t *TestContainerWrapper) RunContainer(portInfo string) error { 25 | req := testcontainers.ContainerRequest{ 26 | Image: fmt.Sprintf("%s:%s", RedpandaImage, RedpandaVersion), 27 | ExposedPorts: []string{ 28 | portInfo + ":" + portInfo + "/tcp", 29 | }, 30 | Cmd: []string{ 31 | "redpanda", 32 | "start", 33 | "--overprovisioned --smp 1 --memory 1G --reserve-memory 0M --check=false", 34 | }, 35 | WaitingFor: wait.ForLog("Successfully started Redpanda!"), 36 | AutoRemove: true, 37 | } 38 | 39 | container, err := testcontainers.GenericContainer(context.Background(), testcontainers.GenericContainerRequest{ 40 | ContainerRequest: req, 41 | Started: true, 42 | Reuse: false, 43 | }) 44 | if err != nil { 45 | log.Fatalf("could not create container: %v", err) 46 | } 47 | 48 | // Container'ın loglarını almak için 49 | logs, err := container.Logs(context.Background()) 50 | if err != nil { 51 | log.Fatalf("could not get container logs: %v", err) 52 | } 53 | 54 | // Logları yazdır 55 | log.Println("Container logs:") 56 | _, err = fmt.Println(logs) 57 | if err != nil { 58 | log.Fatalf("could not write logs: %v", err) 59 | } 60 | 61 | mPort, err := container.MappedPort(context.Background(), nat.Port(portInfo)) 62 | if err != nil { 63 | return fmt.Errorf("could not get mapped port from the container: %w", err) 64 | } 65 | 66 | t.container = container 67 | t.hostPort = mPort.Int() 68 | 69 | log.Printf("Container %s is running on port: %d", "redpanda", t.hostPort) 70 | return nil 71 | } 72 | 73 | func (t *TestContainerWrapper) CleanUp() { 74 | ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second) 75 | defer cancelFunc() 76 | 77 | if err := t.container.Terminate(ctx); err != nil { 78 | log.Printf("could not terminate container: %s\n", err) 79 | } 80 | } 81 | 82 | func (t *TestContainerWrapper) GetBrokerAddress() string { 83 | ip := "localhost" 84 | if isRunningOnOSX() { 85 | ip = "127.0.0.1" 86 | } 87 | 88 | return fmt.Sprintf("%s:%d", ip, t.hostPort) 89 | } 90 | 91 | func isRunningOnOSX() bool { 92 | return runtime.GOOS == "darwin" 93 | } 94 | -------------------------------------------------------------------------------- /kafka_suite_test.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper_test 2 | 3 | import ( 4 | "github.com/Trendyol/kafka-wrapper/internal/testcontainers/kafka" 5 | "github.com/stretchr/testify/suite" 6 | "log" 7 | "testing" 8 | ) 9 | 10 | type testKafkaSuite struct { 11 | suite.Suite 12 | Wrapper kafka.TestContainerWrapper 13 | RemoteWrapper kafka.TestContainerWrapper 14 | } 15 | 16 | func TestKafka(t *testing.T) { 17 | suite.Run(t, new(testKafkaSuite)) 18 | } 19 | 20 | func (s *testKafkaSuite) SetupSuite() { 21 | 22 | log.Println("Starting local container") 23 | if err := s.Wrapper.RunContainer("9092"); err != nil { 24 | log.Fatalf("Error occurred while running local container, err: %+v", err) 25 | } 26 | 27 | log.Println("Starting remote container") 28 | if err := s.RemoteWrapper.RunContainer("9093"); err != nil { 29 | log.Fatalf("Error occurred while running remote container, err: %+v", err) 30 | } 31 | 32 | log.Printf("Local Container is running on address: %s", s.Wrapper.GetBrokerAddress()) 33 | log.Printf("Remote Container is running on address: %s", s.RemoteWrapper.GetBrokerAddress()) 34 | 35 | } 36 | 37 | func (s *testKafkaSuite) TearDownSuite() { 38 | s.Wrapper.CleanUp() 39 | s.RemoteWrapper.CleanUp() 40 | } 41 | -------------------------------------------------------------------------------- /mocks/behavior_executor_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: type.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | sarama "github.com/IBM/sarama" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockBehaviourExecutor is a mock of BehaviourExecutor interface. 16 | type MockBehaviourExecutor struct { 17 | ctrl *gomock.Controller 18 | recorder *MockBehaviourExecutorMockRecorder 19 | } 20 | 21 | // MockBehaviourExecutorMockRecorder is the mock recorder for MockBehaviourExecutor. 22 | type MockBehaviourExecutorMockRecorder struct { 23 | mock *MockBehaviourExecutor 24 | } 25 | 26 | // NewMockBehaviourExecutor creates a new mock instance. 27 | func NewMockBehaviourExecutor(ctrl *gomock.Controller) *MockBehaviourExecutor { 28 | mock := &MockBehaviourExecutor{ctrl: ctrl} 29 | mock.recorder = &MockBehaviourExecutorMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockBehaviourExecutor) EXPECT() *MockBehaviourExecutorMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Process mocks base method. 39 | func (m *MockBehaviourExecutor) Process(ctx context.Context, message *sarama.ConsumerMessage) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Process", ctx, message) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // Process indicates an expected call of Process. 47 | func (mr *MockBehaviourExecutorMockRecorder) Process(ctx, message interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Process", reflect.TypeOf((*MockBehaviourExecutor)(nil).Process), ctx, message) 50 | } 51 | -------------------------------------------------------------------------------- /mocks/header_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: header.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | sarama "github.com/IBM/sarama" 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockheaderOperation is a mock of headerOperation interface. 15 | type MockheaderOperation struct { 16 | ctrl *gomock.Controller 17 | recorder *MockheaderOperationMockRecorder 18 | } 19 | 20 | // MockheaderOperationMockRecorder is the mock recorder for MockheaderOperation. 21 | type MockheaderOperationMockRecorder struct { 22 | mock *MockheaderOperation 23 | } 24 | 25 | // NewMockheaderOperation creates a new mock instance. 26 | func NewMockheaderOperation(ctrl *gomock.Controller) *MockheaderOperation { 27 | mock := &MockheaderOperation{ctrl: ctrl} 28 | mock.recorder = &MockheaderOperationMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockheaderOperation) EXPECT() *MockheaderOperationMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // AddIntoHeader mocks base method. 38 | func (m *MockheaderOperation) AddIntoHeader(headers []sarama.RecordHeader, key, value string) []sarama.RecordHeader { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "AddIntoHeader", headers, key, value) 41 | ret0, _ := ret[0].([]sarama.RecordHeader) 42 | return ret0 43 | } 44 | 45 | // AddIntoHeader indicates an expected call of AddIntoHeader. 46 | func (mr *MockheaderOperationMockRecorder) AddIntoHeader(headers, key, value interface{}) *gomock.Call { 47 | mr.mock.ctrl.T.Helper() 48 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddIntoHeader", reflect.TypeOf((*MockheaderOperation)(nil).AddIntoHeader), headers, key, value) 49 | } 50 | 51 | // ExtractHeader mocks base method. 52 | func (m *MockheaderOperation) ExtractHeader(message *sarama.ConsumerMessage) []sarama.RecordHeader { 53 | m.ctrl.T.Helper() 54 | ret := m.ctrl.Call(m, "ExtractHeader", message) 55 | ret0, _ := ret[0].([]sarama.RecordHeader) 56 | return ret0 57 | } 58 | 59 | // ExtractHeader indicates an expected call of ExtractHeader. 60 | func (mr *MockheaderOperationMockRecorder) ExtractHeader(message interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExtractHeader", reflect.TypeOf((*MockheaderOperation)(nil).ExtractHeader), message) 63 | } 64 | -------------------------------------------------------------------------------- /mocks/logic_operator_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: logic_executor.go 3 | 4 | // Package mocks is a generated GoMock package. 5 | package mocks 6 | 7 | import ( 8 | context "context" 9 | reflect "reflect" 10 | 11 | sarama "github.com/IBM/sarama" 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockLogicOperator is a mock of LogicOperator interface. 16 | type MockLogicOperator struct { 17 | ctrl *gomock.Controller 18 | recorder *MockLogicOperatorMockRecorder 19 | } 20 | 21 | // MockLogicOperatorMockRecorder is the mock recorder for MockLogicOperator. 22 | type MockLogicOperatorMockRecorder struct { 23 | mock *MockLogicOperator 24 | } 25 | 26 | // NewMockLogicOperator creates a new mock instance. 27 | func NewMockLogicOperator(ctrl *gomock.Controller) *MockLogicOperator { 28 | mock := &MockLogicOperator{ctrl: ctrl} 29 | mock.recorder = &MockLogicOperatorMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockLogicOperator) EXPECT() *MockLogicOperatorMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Operate mocks base method. 39 | func (m *MockLogicOperator) Operate(ctx context.Context, message *sarama.ConsumerMessage) error { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Operate", ctx, message) 42 | ret0, _ := ret[0].(error) 43 | return ret0 44 | } 45 | 46 | // Operate indicates an expected call of Operate. 47 | func (mr *MockLogicOperatorMockRecorder) Operate(ctx, message interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Operate", reflect.TypeOf((*MockLogicOperator)(nil).Operate), ctx, message) 50 | } 51 | -------------------------------------------------------------------------------- /params/connection_parameters.go: -------------------------------------------------------------------------------- 1 | package params 2 | 3 | import "github.com/IBM/sarama" 4 | 5 | type ConnectionParameters struct { 6 | Conf *sarama.Config 7 | Brokers string 8 | ConsumerGroupID string 9 | } 10 | -------------------------------------------------------------------------------- /params/topic_parameters.go: -------------------------------------------------------------------------------- 1 | package params 2 | 3 | import "fmt" 4 | 5 | type TopicsParameters struct { 6 | Topic string 7 | ErrorTopic string 8 | RetryTopic string 9 | UsingSharedErrorTopic bool 10 | } 11 | 12 | func FromTopics(topics ...TopicsParameters) []TopicsParameters { 13 | allTopics := make([]TopicsParameters, 0) 14 | for _, topic := range topics { 15 | allTopics = append(allTopics, topic) 16 | } 17 | return allTopics 18 | } 19 | 20 | func JoinAllTopics(topics []TopicsParameters) []string { 21 | allTopics := make([]string, 0) 22 | for _, topic := range topics { 23 | allTopics = append(allTopics, topic.getAllTopics()...) 24 | } 25 | return allTopics 26 | } 27 | 28 | func JoinMainTopics(topics []TopicsParameters) []string { 29 | allTopics := make([]string, 0) 30 | for _, topic := range topics { 31 | allTopics = append(allTopics, topic.Topic) 32 | } 33 | fmt.Printf("JoinMainTopics : %+v \n", allTopics) 34 | 35 | return allTopics 36 | } 37 | 38 | func JoinSecondaryTopics(topics []TopicsParameters) []string { 39 | 40 | allTopics := make([]string, 0) 41 | for _, topic := range topics { 42 | allTopics = append(allTopics, topic.getSecondaryTopics()...) 43 | } 44 | fmt.Printf("JoinSecondaryTopics : %+v \n", allTopics) 45 | 46 | return allTopics 47 | } 48 | 49 | func (p TopicsParameters) getAllTopics() []string { 50 | topics := make([]string, 0) 51 | if p.ErrorTopic != "" && !p.UsingSharedErrorTopic { 52 | topics = append(topics, p.ErrorTopic) 53 | } 54 | if p.RetryTopic != "" { 55 | topics = append(topics, p.RetryTopic) 56 | } 57 | topics = append(topics, p.Topic) 58 | return topics 59 | } 60 | 61 | func (p TopicsParameters) getSecondaryTopics() []string { 62 | topics := make([]string, 0) 63 | if p.ErrorTopic != "" && !p.UsingSharedErrorTopic { 64 | topics = append(topics, p.ErrorTopic) 65 | } 66 | if p.RetryTopic != "" { 67 | topics = append(topics, p.RetryTopic) 68 | } 69 | return topics 70 | } 71 | -------------------------------------------------------------------------------- /params/topic_parameters_test.go: -------------------------------------------------------------------------------- 1 | package params_test 2 | 3 | import ( 4 | "github.com/Trendyol/kafka-wrapper/params" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_it_should_join_given_parameters(t *testing.T) { 10 | // Given 11 | topic_1 := params.TopicsParameters{ 12 | Topic: "topic_1", 13 | } 14 | topic_2 := params.TopicsParameters{ 15 | Topic: "topic_2", 16 | } 17 | topic_3 := params.TopicsParameters{ 18 | Topic: "topic_3", 19 | } 20 | 21 | // When 22 | aggregatedTopics := params.FromTopics(topic_1, topic_2, topic_3) 23 | 24 | // Then 25 | assert.Len(t, aggregatedTopics, 3) 26 | assert.Equal(t, topic_1.Topic, aggregatedTopics[0].Topic) 27 | assert.Equal(t, topic_2.Topic, aggregatedTopics[1].Topic) 28 | assert.Equal(t, topic_3.Topic, aggregatedTopics[2].Topic) 29 | } 30 | 31 | func Test_it_should_join_topic_names_except_the_shared_error_topic(t *testing.T) { 32 | // Given 33 | topic_1 := params.TopicsParameters{ 34 | Topic: "topic_1", 35 | ErrorTopic: "errtopic_1", 36 | RetryTopic: "retrytopic_1", 37 | UsingSharedErrorTopic: false, 38 | } 39 | topic_2 := params.TopicsParameters{ 40 | Topic: "topic_2", 41 | ErrorTopic: "errtopic_2", 42 | RetryTopic: "retrytopic_2", 43 | UsingSharedErrorTopic: true, 44 | } 45 | 46 | // When 47 | joinedTopicNames := params.JoinAllTopics([]params.TopicsParameters{topic_1, topic_2}) 48 | 49 | // Then 50 | assert.Len(t, joinedTopicNames, 5) 51 | assert.Equal(t, topic_1.ErrorTopic, joinedTopicNames[0]) 52 | assert.Equal(t, topic_1.RetryTopic, joinedTopicNames[1]) 53 | assert.Equal(t, topic_1.Topic, joinedTopicNames[2]) 54 | assert.Equal(t, topic_2.RetryTopic, joinedTopicNames[3]) 55 | assert.Equal(t, topic_2.Topic, joinedTopicNames[4]) 56 | } 57 | -------------------------------------------------------------------------------- /producer.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "github.com/Trendyol/kafka-wrapper/params" 6 | "log" 7 | "strings" 8 | ) 9 | 10 | func NewProducer(connectionParams params.ConnectionParameters) (sarama.SyncProducer, error) { 11 | syncProducer, err := sarama.NewSyncProducer(strings.Split(connectionParams.Brokers, ","), connectionParams.Conf) 12 | if err != nil { 13 | log.Printf("could not create producer: %s\n", err) 14 | return nil, err 15 | } 16 | 17 | return syncProducer, err 18 | } 19 | -------------------------------------------------------------------------------- /producer_test.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper_test 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "github.com/Trendyol/kafka-wrapper" 6 | "github.com/Trendyol/kafka-wrapper/params" 7 | "github.com/Trendyol/kafka-wrapper/test_utils" 8 | testifyAssert "github.com/stretchr/testify/assert" 9 | "time" 10 | ) 11 | 12 | func (s *testKafkaSuite) Test_produce_when_broker_is_reachable() { 13 | // Given 14 | var ( 15 | assert = testifyAssert.New(s.T()) 16 | 17 | message = "test" 18 | connectionParams = params.ConnectionParameters{ConsumerGroupID: "some-id"} 19 | topic = "some-topic" 20 | 21 | receivedPayload string 22 | err error 23 | producer sarama.SyncProducer 24 | ) 25 | 26 | connectionParams.Brokers = s.Wrapper.GetBrokerAddress() 27 | connectionParams.Conf = test_utils.CreateBasicConf() 28 | time.Sleep(5 * time.Second) 29 | 30 | producer, err = kafka_wrapper.NewProducer(connectionParams) 31 | 32 | // When 33 | _, _, err = producer.SendMessage(&sarama.ProducerMessage{ 34 | Value: sarama.StringEncoder(message), 35 | Topic: topic, 36 | }) 37 | time.Sleep(5 * time.Second) 38 | _, receivedPayload, _ = test_utils.Consume(connectionParams, topic) 39 | 40 | // Then 41 | assert.Nil(err) 42 | assert.Equal(receivedPayload, message) 43 | } 44 | 45 | func (s *testKafkaSuite) Test_not_produce_when_broker_is_not_reachable() { 46 | // Given 47 | var ( 48 | assert = testifyAssert.New(s.T()) 49 | 50 | wrongConf = params.ConnectionParameters{ConsumerGroupID: "some-id"} 51 | 52 | expectedError error 53 | ) 54 | 55 | wrongConf.Brokers = "localhost:9093" 56 | wrongConf.Conf = test_utils.CreateBasicConf() 57 | 58 | // When 59 | _, expectedError = kafka_wrapper.NewProducer(wrongConf) 60 | 61 | // Then 62 | assert.NotNil(expectedError) 63 | } 64 | -------------------------------------------------------------------------------- /remote_consumer.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/IBM/sarama" 7 | "github.com/Trendyol/kafka-wrapper/params" 8 | "strings" 9 | ) 10 | 11 | type remoteKafkaConsumer struct { 12 | remoteConsumer sarama.ConsumerGroup 13 | localConsumer sarama.ConsumerGroup 14 | ctx context.Context 15 | cancelFunc context.CancelFunc 16 | } 17 | 18 | func NewRemoteConsumer(remoteParams params.ConnectionParameters, localParams params.ConnectionParameters) (Consumer, error) { 19 | remote, err := sarama.NewConsumerGroup(strings.Split(remoteParams.Brokers, ","), remoteParams.ConsumerGroupID, remoteParams.Conf) 20 | if err != nil { 21 | return nil, err 22 | } 23 | local, err := sarama.NewConsumerGroup(strings.Split(localParams.Brokers, ","), localParams.ConsumerGroupID, localParams.Conf) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | return &remoteKafkaConsumer{ 30 | remoteConsumer: remote, 31 | localConsumer: local, 32 | ctx: ctx, 33 | cancelFunc: cancel, 34 | }, nil 35 | } 36 | func (c *remoteKafkaConsumer) SubscribeToTopic(topicParams params.TopicsParameters, handler EventHandler) { 37 | c.Subscribe([]params.TopicsParameters{topicParams}, handler) 38 | } 39 | 40 | func (c *remoteKafkaConsumer) Subscribe(topicParams []params.TopicsParameters, handler EventHandler) { 41 | go func() { 42 | for { 43 | if c.ctx.Err() != nil { 44 | return 45 | } 46 | err := c.remoteConsumer.Consume(c.ctx, params.JoinMainTopics(topicParams), handler) 47 | if err != nil { 48 | fmt.Printf("Error from remote consumer: %v\n", err) 49 | } 50 | } 51 | }() 52 | 53 | go func() { 54 | for { 55 | if c.ctx.Err() != nil { 56 | return 57 | } 58 | err := c.localConsumer.Consume(c.ctx, params.JoinSecondaryTopics(topicParams), handler) 59 | if err != nil { 60 | fmt.Printf("Error from local consumer: %v\n", err) 61 | } 62 | } 63 | }() 64 | 65 | go func() { 66 | for err := range c.remoteConsumer.Errors() { 67 | fmt.Printf("Error from remote consumer group: %v\n", err) 68 | } 69 | }() 70 | 71 | go func() { 72 | for err := range c.localConsumer.Errors() { 73 | fmt.Printf("Error from local consumer group: %v\n", err) 74 | } 75 | }() 76 | 77 | fmt.Printf("Kafka consumer listens topic: %+v\n", topicParams) 78 | } 79 | 80 | func (c *remoteKafkaConsumer) Unsubscribe() { 81 | c.cancelFunc() 82 | 83 | if err := c.remoteConsumer.Close(); err != nil { 84 | fmt.Printf("Remote consumer wasn't closed: %v\n", err) 85 | } 86 | if err := c.localConsumer.Close(); err != nil { 87 | fmt.Printf("Local consumer wasn't closed: %v\n", err) 88 | } 89 | fmt.Println("Kafka consumer closed") 90 | } 91 | -------------------------------------------------------------------------------- /remote_consumer_test.go: -------------------------------------------------------------------------------- 1 | package kafka_wrapper_test 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "github.com/Trendyol/kafka-wrapper" 6 | "github.com/Trendyol/kafka-wrapper/params" 7 | "github.com/Trendyol/kafka-wrapper/test_utils" 8 | testifyAssert "github.com/stretchr/testify/assert" 9 | "time" 10 | ) 11 | 12 | func (s *testKafkaSuite) Test_consume_when_a_message_sent_to_local() { 13 | // Given 14 | var ( 15 | assert = testifyAssert.New(s.T()) 16 | 17 | remoteConnectionParams = params.ConnectionParameters{ 18 | ConsumerGroupID: "remote-consumer-group", 19 | } 20 | 21 | localConnectionParams = params.ConnectionParameters{ 22 | ConsumerGroupID: "local-consumer-group", 23 | } 24 | 25 | topicParams = params.TopicsParameters{ 26 | Topic: "test-topic", 27 | RetryTopic: "test-topic_retry", 28 | ErrorTopic: "test-topic_error", 29 | } 30 | expectedMessage = "test" 31 | messageChn = make(chan string, 1) 32 | receivedMessage string 33 | ) 34 | 35 | remoteConnectionParams.Brokers = s.RemoteWrapper.GetBrokerAddress() 36 | remoteConnectionParams.Conf = test_utils.CreateBasicConf() 37 | 38 | localConnectionParams.Brokers = s.Wrapper.GetBrokerAddress() 39 | localConnectionParams.Conf = test_utils.CreateBasicConf() 40 | 41 | time.Sleep(5 * time.Second) 42 | 43 | localProducer, _ := kafka_wrapper.NewProducer(localConnectionParams) 44 | 45 | // When 46 | _, _, _ = localProducer.SendMessage(&sarama.ProducerMessage{ 47 | Value: sarama.StringEncoder(expectedMessage + "local"), 48 | Topic: topicParams.ErrorTopic, 49 | }) 50 | 51 | testConsumer, err := kafka_wrapper.NewRemoteConsumer(remoteConnectionParams, localConnectionParams) 52 | assert.NoError(err, "NewRemoteConsumer should not error") 53 | testConsumer.SubscribeToTopic(topicParams, test_utils.NewEventHandler(messageChn)) 54 | receivedMessage = <-messageChn 55 | 56 | // Then 57 | assert.Equal(receivedMessage, expectedMessage) 58 | } 59 | 60 | func (s *testKafkaSuite) Test_consume_when_a_message_sent_to_remote2() { 61 | // Given 62 | var ( 63 | assert = testifyAssert.New(s.T()) 64 | 65 | remoteConnectionParams = params.ConnectionParameters{ 66 | ConsumerGroupID: "remote-consumer-group", 67 | } 68 | 69 | localConnectionParams = params.ConnectionParameters{ 70 | ConsumerGroupID: "local-consumer-group", 71 | } 72 | 73 | topicParams = params.TopicsParameters{ 74 | Topic: "test-topic", 75 | RetryTopic: "test-topic_retry", 76 | ErrorTopic: "test-topic_error", 77 | } 78 | expectedMessage = "test" 79 | messageChn = make(chan string, 1) 80 | receivedMessage string 81 | ) 82 | 83 | remoteConnectionParams.Brokers = s.RemoteWrapper.GetBrokerAddress() 84 | remoteConnectionParams.Conf = test_utils.CreateBasicConf() 85 | 86 | localConnectionParams.Brokers = s.Wrapper.GetBrokerAddress() 87 | localConnectionParams.Conf = test_utils.CreateBasicConf() 88 | 89 | time.Sleep(5 * time.Second) 90 | 91 | remoteProducer, _ := kafka_wrapper.NewProducer(localConnectionParams) 92 | 93 | // When 94 | _, _, _ = remoteProducer.SendMessage(&sarama.ProducerMessage{ 95 | Value: sarama.StringEncoder(expectedMessage + "remote"), 96 | Topic: topicParams.Topic, 97 | }) 98 | 99 | testConsumer, _ := kafka_wrapper.NewRemoteConsumer(localConnectionParams, localConnectionParams) 100 | testConsumer.SubscribeToTopic(topicParams, test_utils.NewEventHandler(messageChn)) 101 | receivedMessage = <-messageChn 102 | 103 | // Then 104 | assert.Equal(receivedMessage, expectedMessage) 105 | } 106 | 107 | func (s *testKafkaSuite) Test_consume_when_a_message_sent_to_remote() { 108 | // Given 109 | var ( 110 | assert = testifyAssert.New(s.T()) 111 | 112 | remoteConnectionParams = params.ConnectionParameters{ 113 | ConsumerGroupID: "remote-consumer-group", 114 | } 115 | 116 | localConnectionParams = params.ConnectionParameters{ 117 | ConsumerGroupID: "local-consumer-group", 118 | } 119 | 120 | topicParams = params.TopicsParameters{ 121 | Topic: "test-topic", 122 | RetryTopic: "test-topic_retry", 123 | ErrorTopic: "test-topic_error", 124 | } 125 | expectedMessage = "test" 126 | messageChn = make(chan string, 1) 127 | receivedMessage string 128 | ) 129 | 130 | remoteConnectionParams.Brokers = s.RemoteWrapper.GetBrokerAddress() 131 | remoteConnectionParams.Conf = test_utils.CreateBasicConf() 132 | 133 | localConnectionParams.Brokers = s.Wrapper.GetBrokerAddress() 134 | localConnectionParams.Conf = test_utils.CreateBasicConf() 135 | 136 | time.Sleep(5 * time.Second) 137 | 138 | remoteProducer, _ := kafka_wrapper.NewProducer(remoteConnectionParams) 139 | 140 | // When 141 | 142 | _, _, _ = remoteProducer.SendMessage(&sarama.ProducerMessage{ 143 | Value: sarama.StringEncoder(expectedMessage), 144 | Topic: topicParams.Topic, 145 | }) 146 | 147 | testConsumer, _ := kafka_wrapper.NewRemoteConsumer(remoteConnectionParams, localConnectionParams) 148 | testConsumer.SubscribeToTopic(topicParams, test_utils.NewEventHandler(messageChn)) 149 | 150 | receivedMessage = <-messageChn 151 | 152 | // Then 153 | assert.Equal(receivedMessage, expectedMessage) 154 | } 155 | -------------------------------------------------------------------------------- /test_utils/fake_event_handler.go: -------------------------------------------------------------------------------- 1 | package test_utils 2 | 3 | import ( 4 | "fmt" 5 | "github.com/IBM/sarama" 6 | "github.com/Trendyol/kafka-wrapper" 7 | "github.com/Trendyol/kafka-wrapper/execution_behaviour" 8 | ) 9 | 10 | func NewEventHandler(message chan string) kafka_wrapper.EventHandler { 11 | return &testEventHandler{ 12 | message: message, 13 | subscriptionStatusCh: make(chan bool), 14 | } 15 | } 16 | 17 | func NewEventHandlerWithError(message chan string) kafka_wrapper.EventHandler { 18 | return &testEventHandler{ 19 | message: message, 20 | subscriptionStatusCh: make(chan bool), 21 | } 22 | } 23 | 24 | type testEventHandler struct { 25 | message chan string 26 | subscriptionStatusCh chan bool 27 | } 28 | 29 | func (ge *testEventHandler) BehavioralSelector() execution_behaviour.BehavioralSelector { 30 | //TODO implement me 31 | panic("implement me") 32 | } 33 | 34 | func (ge *testEventHandler) Setup(sarama.ConsumerGroupSession) error { 35 | return nil 36 | } 37 | 38 | func (ge *testEventHandler) Cleanup(sarama.ConsumerGroupSession) error { 39 | return nil 40 | } 41 | 42 | func (ge *testEventHandler) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { 43 | for message := range claim.Messages() { 44 | fmt.Printf("message: %+v", string(message.Value)) 45 | ge.message <- string(message.Value) 46 | } 47 | 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /test_utils/utils.go: -------------------------------------------------------------------------------- 1 | package test_utils 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "github.com/Trendyol/kafka-wrapper/params" 6 | ) 7 | 8 | func Consume(kafkaConfig params.ConnectionParameters, topic string) (string, string, []*sarama.RecordHeader) { 9 | master, err := sarama.NewConsumer([]string{kafkaConfig.Brokers}, kafkaConfig.Conf) 10 | if err != nil { 11 | panic(err) 12 | } 13 | 14 | defer func() { 15 | if err := master.Close(); err != nil { 16 | panic(err) 17 | } 18 | }() 19 | 20 | partitions, _ := master.Partitions(topic) 21 | partitionConsumer, err := master.ConsumePartition(topic, partitions[0], sarama.OffsetOldest) 22 | if err != nil { 23 | panic(err) 24 | } 25 | msg := <-partitionConsumer.Messages() 26 | return string(msg.Key), string(msg.Value), msg.Headers 27 | } 28 | 29 | func CreateBasicConf() *sarama.Config { 30 | kafkaConfig := sarama.NewConfig() 31 | v, _ := sarama.ParseKafkaVersion("2.2.0") 32 | kafkaConfig.Version = v 33 | kafkaConfig.ClientID = "kafka-client-id" 34 | kafkaConfig.Consumer.Offsets.Initial = sarama.OffsetOldest 35 | kafkaConfig.Producer.Return.Successes = true 36 | kafkaConfig.Consumer.Return.Errors = true 37 | 38 | return kafkaConfig 39 | } 40 | -------------------------------------------------------------------------------- /utils/header.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "github.com/IBM/sarama" 4 | 5 | //go:generate mockgen -destination=../mocks/header_mock.go -package=mocks -source=header.go 6 | 7 | type HeaderOperation interface { 8 | AddIntoHeader(headers []sarama.RecordHeader, key, value string) []sarama.RecordHeader 9 | ExtractHeader(message *sarama.ConsumerMessage) []sarama.RecordHeader 10 | } 11 | 12 | type headerOperator struct { 13 | } 14 | 15 | func NewHeaderOperator() HeaderOperation { 16 | return &headerOperator{} 17 | } 18 | 19 | func (h *headerOperator) AddIntoHeader(headers []sarama.RecordHeader, key, value string) []sarama.RecordHeader { 20 | for i, header := range headers { 21 | if string(header.Key) == key { 22 | headers[i].Value = []byte(value) 23 | return headers 24 | } 25 | } 26 | newHeader := sarama.RecordHeader{ 27 | Key: []byte(key), 28 | Value: []byte(value), 29 | } 30 | return append(headers, newHeader) 31 | } 32 | 33 | func (h *headerOperator) ExtractHeader(message *sarama.ConsumerMessage) []sarama.RecordHeader { 34 | headers := make([]sarama.RecordHeader, 0) 35 | 36 | for i := 0; i < len(message.Headers); i++ { 37 | headers = append(headers, *message.Headers[i]) 38 | } 39 | 40 | return headers 41 | } 42 | -------------------------------------------------------------------------------- /utils/header_test.go: -------------------------------------------------------------------------------- 1 | package utils_test 2 | 3 | import ( 4 | "github.com/IBM/sarama" 5 | "github.com/Trendyol/kafka-wrapper/utils" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func Test_header_operator_should_extract_existing_headers(t *testing.T) { 11 | // Given 12 | headers := make([]*sarama.RecordHeader, 0) 13 | headers = append(headers, &sarama.RecordHeader{ 14 | Key: []byte("key_1"), 15 | Value: []byte("val_1"), 16 | }) 17 | headers = append(headers, &sarama.RecordHeader{ 18 | Key: []byte("key_2"), 19 | Value: []byte("val_2"), 20 | }) 21 | 22 | message := sarama.ConsumerMessage{ 23 | Headers: headers, 24 | } 25 | sut := utils.NewHeaderOperator() 26 | 27 | // When 28 | extracted := sut.ExtractHeader(&message) 29 | 30 | // Then 31 | assert.Equal(t, 2, len(extracted)) 32 | assert.Equal(t, "key_1", string(extracted[0].Key)) 33 | assert.Equal(t, "key_2", string(extracted[1].Key)) 34 | assert.Equal(t, "val_1", string(extracted[0].Value)) 35 | assert.Equal(t, "val_2", string(extracted[1].Value)) 36 | } 37 | 38 | func Test_header_operator_should_add_given_item_to_the_header(t *testing.T) { 39 | // Given 40 | headers := make([]sarama.RecordHeader, 0) 41 | headers = append(headers, sarama.RecordHeader{ 42 | Key: []byte("key_1"), 43 | Value: []byte("val_1"), 44 | }) 45 | headers = append(headers, sarama.RecordHeader{ 46 | Key: []byte("key_2"), 47 | Value: []byte("val_2"), 48 | }) 49 | 50 | sut := utils.NewHeaderOperator() 51 | 52 | // When 53 | extendedHeader := sut.AddIntoHeader(headers, "inserter_key", "inserted_val") 54 | 55 | // Then 56 | assert.Equal(t, 3, len(extendedHeader)) 57 | assert.Equal(t, "inserter_key", string(extendedHeader[2].Key)) 58 | assert.Equal(t, "inserted_val", string(extendedHeader[2].Value)) 59 | } 60 | --------------------------------------------------------------------------------