diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..38011303d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @cloudevents/sdk-go-maintainers diff --git a/.github/workflows/go-lint.yaml b/.github/workflows/go-lint.yaml index 66f3181a4..33cdf95c9 100644 --- a/.github/workflows/go-lint.yaml +++ b/.github/workflows/go-lint.yaml @@ -30,7 +30,7 @@ jobs: - name: Go Lint on ./v2 if: steps.golangci_configuration.outputs.files_exists == 'true' - uses: golangci/golangci-lint-action@v4 + uses: golangci/golangci-lint-action@v6 with: version: v1.54 working-directory: v2 diff --git a/.github/workflows/integration.yaml b/.github/workflows/integration.yaml index 65840e3aa..c237f1e12 100644 --- a/.github/workflows/integration.yaml +++ b/.github/workflows/integration.yaml @@ -27,6 +27,15 @@ jobs: - 9091:9091 - 9092:9092 + kafka_confluent: + image: confluentinc/confluent-local:7.6.0 + ports: + - "9192:9192" + env: + KAFKA_ADVERTISED_LISTENERS: 'PLAINTEXT://localhost:29192,PLAINTEXT_HOST://localhost:9192' + KAFKA_CONTROLLER_QUORUM_VOTERS: '1@localhost:29193' + KAFKA_LISTENERS: 'PLAINTEXT://localhost:29192,CONTROLLER://localhost:29193,PLAINTEXT_HOST://0.0.0.0:9192' + natss: image: nats-streaming:0.22.1 ports: diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index c165e1ff9..29f563429 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -201,14 +201,14 @@ GEM rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) mercenary (0.3.6) - mini_portile2 (2.8.5) + mini_portile2 (2.8.6) minima (2.5.1) jekyll (>= 3.5, < 5.0) jekyll-feed (~> 0.9) jekyll-seo-tag (~> 2.1) minitest (5.17.0) multipart-post (2.1.1) - nokogiri (1.16.2) + nokogiri (1.16.5) mini_portile2 (~> 2.8.2) racc (~> 1.4) octokit (4.18.0) diff --git a/docs/index.md b/docs/index.md index 23f9b275d..060ad3e91 100644 --- a/docs/index.md +++ b/docs/index.md @@ -124,7 +124,8 @@ err := json.Unmarshal(bytes, &event) | AVRO Event Format | :x: | :x: | | [HTTP Protocol Binding](https://github.com/cloudevents/sdk-go/tree/main/samples/http) | :heavy_check_mark: | :heavy_check_mark: | | [JSON Event Format](event_data_structure.md#marshalunmarshal-event-to-json) | :heavy_check_mark: | :heavy_check_mark: | -| [Kafka Protocol Binding](https://github.com/cloudevents/sdk-go/tree/main/samples/kafka) | :heavy_check_mark: | :heavy_check_mark: | +| [Sarama Kafka Protocol Binding](https://github.com/cloudevents/sdk-go/tree/main/samples/kafka) | :heavy_check_mark: | :heavy_check_mark: | +| [Confluent Kafka Protocol Binding](https://github.com/cloudevents/sdk-go/tree/main/samples/kafka_confluent) | :heavy_check_mark: | :heavy_check_mark: | | MQTT Protocol Binding | :x: | :x: | | [NATS Protocol Binding](https://github.com/cloudevents/sdk-go/tree/main/samples/nats) | :heavy_check_mark: | :heavy_check_mark: | | [STAN Protocol Binding](https://github.com/cloudevents/sdk-go/tree/main/samples/stan) | :heavy_check_mark: | :heavy_check_mark: | diff --git a/observability/opencensus/v2/go.mod b/observability/opencensus/v2/go.mod index 73ce7da13..6b90506b9 100644 --- a/observability/opencensus/v2/go.mod +++ b/observability/opencensus/v2/go.mod @@ -22,7 +22,7 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/observability/opencensus/v2/go.sum b/observability/opencensus/v2/go.sum index a86dbdedf..ec15c5f41 100644 --- a/observability/opencensus/v2/go.sum +++ b/observability/opencensus/v2/go.sum @@ -80,8 +80,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -97,11 +97,11 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/protocol/amqp/v2/message.go b/protocol/amqp/v2/message.go index 698bf041d..0756ddfc7 100644 --- a/protocol/amqp/v2/message.go +++ b/protocol/amqp/v2/message.go @@ -50,8 +50,10 @@ func NewMessage(message *amqp.Message, receiver *amqp.Receiver) *Message { return &Message{AMQP: message, AMQPrcv: receiver, format: fmt, version: vn} } -var _ binding.Message = (*Message)(nil) -var _ binding.MessageMetadataReader = (*Message)(nil) +var ( + _ binding.Message = (*Message)(nil) + _ binding.MessageMetadataReader = (*Message)(nil) +) func getSpecVersion(message *amqp.Message) spec.Version { if sv, ok := message.ApplicationProperties[specs.PrefixedSpecVersionName()]; ok { @@ -74,7 +76,8 @@ func (m *Message) ReadEncoding() binding.Encoding { func (m *Message) ReadStructured(ctx context.Context, encoder binding.StructuredWriter) error { if m.format != nil { - return encoder.SetStructuredEvent(ctx, m.format, bytes.NewReader(m.AMQP.GetData())) + data := m.getAmqpData() + return encoder.SetStructuredEvent(ctx, m.format, bytes.NewReader(data)) } return binding.ErrNotStructured } @@ -106,7 +109,7 @@ func (m *Message) ReadBinary(ctx context.Context, encoder binding.BinaryWriter) } } - data := m.AMQP.GetData() + data := m.getAmqpData() if len(data) != 0 { // Some data err = encoder.SetData(bytes.NewBuffer(data)) if err != nil { @@ -137,3 +140,15 @@ func (m *Message) Finish(err error) error { } return m.AMQPrcv.AcceptMessage(context.Background(), m.AMQP) } + +// fixes: github.com/cloudevents/spec/issues/1275 +func (m *Message) getAmqpData() []byte { + var data []byte + amqpData := m.AMQP.Data + + // TODO: replace with slices.Concat once go mod bumped to 1.22 + for idx := range amqpData { + data = append(data, amqpData[idx]...) + } + return data +} diff --git a/protocol/amqp/v2/message_test.go b/protocol/amqp/v2/message_test.go index 74d3a4bad..71a9d8409 100644 --- a/protocol/amqp/v2/message_test.go +++ b/protocol/amqp/v2/message_test.go @@ -62,3 +62,58 @@ func TestNewMessage_message_unknown(t *testing.T) { got := NewMessage(message, &rcv) require.Equal(t, binding.EncodingUnknown, got.ReadEncoding()) } + +func TestMessage_getAmqpData(t *testing.T) { + tests := []struct { + name string + message *amqp.Message + want []byte + }{ + { + name: "nil data", + message: amqp.NewMessage(nil), + want: nil, + }, + { + name: "empty string", + message: amqp.NewMessage([]byte(`""`)), + want: []byte(`""`), + }, + { + name: "simple string", + message: amqp.NewMessage([]byte("hello world")), + want: []byte("hello world"), + }, + { + name: "multiple data with simple strings", + message: &amqp.Message{Data: [][]byte{ + []byte("hello"), + []byte(" "), + []byte("world"), + }}, + want: []byte("hello world"), + }, + { + name: "multiple data to build JSON array", + message: &amqp.Message{Data: [][]byte{ + []byte("["), + []byte("Foo"), + []byte(","), + []byte("Bar"), + []byte(","), + []byte("Baz"), + []byte("]"), + }}, + want: []byte("[Foo,Bar,Baz]"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + m := &Message{ + AMQP: tt.message, + } + got := m.getAmqpData() + require.Equal(t, tt.want, got) + }) + } +} diff --git a/protocol/kafka_confluent/v2/go.mod b/protocol/kafka_confluent/v2/go.mod new file mode 100644 index 000000000..88690072e --- /dev/null +++ b/protocol/kafka_confluent/v2/go.mod @@ -0,0 +1,25 @@ +module github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2 + +go 1.18 + +replace github.com/cloudevents/sdk-go/v2 => ../../../v2 + +require ( + github.com/cloudevents/sdk-go/v2 v2.15.2 + github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 + github.com/stretchr/testify v1.8.4 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + go.uber.org/atomic v1.4.0 // indirect + go.uber.org/multierr v1.1.0 // indirect + go.uber.org/zap v1.10.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/protocol/kafka_confluent/v2/go.sum b/protocol/kafka_confluent/v2/go.sum new file mode 100644 index 000000000..389b199e2 --- /dev/null +++ b/protocol/kafka_confluent/v2/go.sum @@ -0,0 +1,68 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633 h1:0BOZf6qNozI3pkN3fJLwNubheHJYHhMh91GRFOWWK08= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/protocol/kafka_confluent/v2/message.go b/protocol/kafka_confluent/v2/message.go new file mode 100644 index 000000000..43df8d4ff --- /dev/null +++ b/protocol/kafka_confluent/v2/message.go @@ -0,0 +1,157 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package kafka_confluent + +import ( + "bytes" + "context" + "strconv" + "strings" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + + "github.com/cloudevents/sdk-go/v2/binding" + "github.com/cloudevents/sdk-go/v2/binding/format" + "github.com/cloudevents/sdk-go/v2/binding/spec" +) + +const ( + prefix = "ce_" + contentTypeKey = "content-type" +) + +const ( + KafkaOffsetKey = "kafkaoffset" + KafkaPartitionKey = "kafkapartition" + KafkaTopicKey = "kafkatopic" + KafkaMessageKey = "kafkamessagekey" +) + +var specs = spec.WithPrefix(prefix) + +// Message represents a Kafka message. +// This message *can* be read several times safely +type Message struct { + internal *kafka.Message + properties map[string][]byte + format format.Format + version spec.Version +} + +// Check if Message implements binding.Message +var ( + _ binding.Message = (*Message)(nil) + _ binding.MessageMetadataReader = (*Message)(nil) +) + +// NewMessage returns a binding.Message that holds the provided kafka.Message. +// The returned binding.Message *can* be read several times safely +// This function *doesn't* guarantee that the returned binding.Message is always a kafka_sarama.Message instance +func NewMessage(msg *kafka.Message) *Message { + if msg == nil { + panic("the kafka.Message shouldn't be nil") + } + if msg.TopicPartition.Topic == nil { + panic("the topic of kafka.Message shouldn't be nil") + } + if msg.TopicPartition.Partition < 0 || msg.TopicPartition.Offset < 0 { + panic("the partition or offset of the kafka.Message must be non-negative") + } + + var contentType, contentVersion string + properties := make(map[string][]byte, len(msg.Headers)+3) + for _, header := range msg.Headers { + k := strings.ToLower(string(header.Key)) + if k == strings.ToLower(contentTypeKey) { + contentType = string(header.Value) + } + if k == specs.PrefixedSpecVersionName() { + contentVersion = string(header.Value) + } + properties[k] = header.Value + } + + // add the kafka message key, topic, partition and partition key to the properties + properties[prefix+KafkaOffsetKey] = []byte(strconv.FormatInt(int64(msg.TopicPartition.Offset), 10)) + properties[prefix+KafkaPartitionKey] = []byte(strconv.FormatInt(int64(msg.TopicPartition.Partition), 10)) + properties[prefix+KafkaTopicKey] = []byte(*msg.TopicPartition.Topic) + if msg.Key != nil { + properties[prefix+KafkaMessageKey] = msg.Key + } + + message := &Message{ + internal: msg, + properties: properties, + } + if ft := format.Lookup(contentType); ft != nil { + message.format = ft + } else if v := specs.Version(contentVersion); v != nil { + message.version = v + } + + return message +} + +func (m *Message) ReadEncoding() binding.Encoding { + if m.version != nil { + return binding.EncodingBinary + } + if m.format != nil { + return binding.EncodingStructured + } + return binding.EncodingUnknown +} + +func (m *Message) ReadStructured(ctx context.Context, encoder binding.StructuredWriter) error { + if m.format != nil { + return encoder.SetStructuredEvent(ctx, m.format, bytes.NewReader(m.internal.Value)) + } + return binding.ErrNotStructured +} + +func (m *Message) ReadBinary(ctx context.Context, encoder binding.BinaryWriter) error { + if m.version == nil { + return binding.ErrNotBinary + } + + var err error + for k, v := range m.properties { + if strings.HasPrefix(k, prefix) { + attr := m.version.Attribute(k) + if attr != nil { + err = encoder.SetAttribute(attr, string(v)) + } else { + err = encoder.SetExtension(strings.TrimPrefix(k, prefix), string(v)) + } + } else if k == strings.ToLower(contentTypeKey) { + err = encoder.SetAttribute(m.version.AttributeFromKind(spec.DataContentType), string(v)) + } + if err != nil { + return err + } + } + + if m.internal.Value != nil { + err = encoder.SetData(bytes.NewBuffer(m.internal.Value)) + } + return err +} + +func (m *Message) Finish(error) error { + return nil +} + +func (m *Message) GetAttribute(k spec.Kind) (spec.Attribute, interface{}) { + attr := m.version.AttributeFromKind(k) + if attr == nil { + return nil, nil + } + return attr, m.properties[attr.PrefixedName()] +} + +func (m *Message) GetExtension(name string) interface{} { + return m.properties[prefix+name] +} diff --git a/protocol/kafka_confluent/v2/message_test.go b/protocol/kafka_confluent/v2/message_test.go new file mode 100644 index 000000000..9676fe7ac --- /dev/null +++ b/protocol/kafka_confluent/v2/message_test.go @@ -0,0 +1,131 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package kafka_confluent + +import ( + "context" + "testing" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + "github.com/stretchr/testify/require" + + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/cloudevents/sdk-go/v2/binding" + "github.com/cloudevents/sdk-go/v2/binding/format" + "github.com/cloudevents/sdk-go/v2/test" +) + +var ( + ctx = context.Background() + testEvent = test.FullEvent() + testTopic = "test-topic" + topicPartition = kafka.TopicPartition{ + Topic: &testTopic, + Partition: int32(0), + Offset: kafka.Offset(10), + } + structuredConsumerMessage = &kafka.Message{ + TopicPartition: topicPartition, + Value: func() []byte { + b, _ := format.JSON.Marshal(&testEvent) + return b + }(), + Headers: []kafka.Header{{ + Key: "content-type", + Value: []byte(cloudevents.ApplicationCloudEventsJSON), + }}, + } + binaryConsumerMessage = &kafka.Message{ + TopicPartition: topicPartition, + Value: []byte("hello world!"), + Headers: mapToKafkaHeaders(map[string]string{ + "ce_type": testEvent.Type(), + "ce_source": testEvent.Source(), + "ce_id": testEvent.ID(), + "ce_time": test.Timestamp.String(), + "ce_specversion": "1.0", + "ce_dataschema": test.Schema.String(), + "ce_datacontenttype": "text/json", + "ce_subject": "receiverTopic", + "exta": "someext", + }), + } +) + +func TestNewMessage(t *testing.T) { + tests := []struct { + name string + consumerMessage *kafka.Message + expectedEncoding binding.Encoding + }{ + { + name: "Structured encoding", + consumerMessage: structuredConsumerMessage, + expectedEncoding: binding.EncodingStructured, + }, + { + name: "Binary encoding", + consumerMessage: binaryConsumerMessage, + expectedEncoding: binding.EncodingBinary, + }, + { + name: "Unknown encoding", + consumerMessage: &kafka.Message{ + TopicPartition: topicPartition, + Value: []byte("{}"), + Headers: []kafka.Header{{ + Key: "content-type", + Value: []byte("application/json"), + }}, + }, + expectedEncoding: binding.EncodingUnknown, + }, + { + name: "Binary encoding with empty value", + consumerMessage: &kafka.Message{ + TopicPartition: topicPartition, + Value: nil, + Headers: mapToKafkaHeaders(map[string]string{ + "ce_type": testEvent.Type(), + "ce_source": testEvent.Source(), + "ce_id": testEvent.ID(), + "ce_time": test.Timestamp.String(), + "ce_specversion": "1.0", + "ce_dataschema": test.Schema.String(), + "ce_datacontenttype": "text/json", + "ce_subject": "receiverTopic", + }), + }, + expectedEncoding: binding.EncodingBinary, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + msg := NewMessage(tt.consumerMessage) + require.Equal(t, tt.expectedEncoding, msg.ReadEncoding()) + + var err error + if tt.expectedEncoding == binding.EncodingStructured { + err = msg.ReadStructured(ctx, (*kafkaMessageWriter)(tt.consumerMessage)) + } + + if tt.expectedEncoding == binding.EncodingBinary { + err = msg.ReadBinary(ctx, (*kafkaMessageWriter)(tt.consumerMessage)) + } + require.Nil(t, err) + }) + } +} + +func mapToKafkaHeaders(m map[string]string) []kafka.Header { + res := make([]kafka.Header, len(m)) + i := 0 + for k, v := range m { + res[i] = kafka.Header{Key: k, Value: []byte(v)} + i++ + } + return res +} diff --git a/protocol/kafka_confluent/v2/option.go b/protocol/kafka_confluent/v2/option.go new file mode 100644 index 000000000..7eef74007 --- /dev/null +++ b/protocol/kafka_confluent/v2/option.go @@ -0,0 +1,151 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package kafka_confluent + +import ( + "context" + "errors" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" +) + +// Option is the function signature required to be considered an kafka_confluent.Option. +type Option func(*Protocol) error + +// WithConfigMap sets the configMap to init the kafka client. +func WithConfigMap(config *kafka.ConfigMap) Option { + return func(p *Protocol) error { + if config == nil { + return errors.New("the kafka.ConfigMap option must not be nil") + } + p.kafkaConfigMap = config + return nil + } +} + +// WithSenderTopic sets the defaultTopic for the kafka.Producer. +func WithSenderTopic(defaultTopic string) Option { + return func(p *Protocol) error { + if defaultTopic == "" { + return errors.New("the producer topic option must not be nil") + } + p.producerDefaultTopic = defaultTopic + return nil + } +} + +// WithReceiverTopics sets the topics for the kafka.Consumer. +func WithReceiverTopics(topics []string) Option { + return func(p *Protocol) error { + if topics == nil { + return errors.New("the consumer topics option must not be nil") + } + p.consumerTopics = topics + return nil + } +} + +// WithRebalanceCallBack sets the callback for rebalancing of the consumer group. +func WithRebalanceCallBack(rebalanceCb kafka.RebalanceCb) Option { + return func(p *Protocol) error { + if rebalanceCb == nil { + return errors.New("the consumer group rebalance callback must not be nil") + } + p.consumerRebalanceCb = rebalanceCb + return nil + } +} + +// WithPollTimeout sets timeout of the consumer polling for message or events, return nil on timeout. +func WithPollTimeout(timeoutMs int) Option { + return func(p *Protocol) error { + p.consumerPollTimeout = timeoutMs + return nil + } +} + +// WithSender set a kafka.Producer instance to init the client directly. +func WithSender(producer *kafka.Producer) Option { + return func(p *Protocol) error { + if producer == nil { + return errors.New("the producer option must not be nil") + } + p.producer = producer + return nil + } +} + +// WithErrorHandler provide a func on how to handle the kafka.Error which the kafka.Consumer has polled. +func WithErrorHandler(handler func(ctx context.Context, err kafka.Error)) Option { + return func(p *Protocol) error { + p.consumerErrorHandler = handler + return nil + } +} + +// WithReceiver set a kafka.Consumer instance to init the client directly. +func WithReceiver(consumer *kafka.Consumer) Option { + return func(p *Protocol) error { + if consumer == nil { + return errors.New("the consumer option must not be nil") + } + p.consumer = consumer + return nil + } +} + +// Opaque key type used to store topicPartitionOffsets: assign them from ctx. +type topicPartitionOffsetsType struct{} + +var offsetKey = topicPartitionOffsetsType{} + +// WithTopicPartitionOffsets will set the positions where the consumer starts consuming from. +func WithTopicPartitionOffsets(ctx context.Context, topicPartitionOffsets []kafka.TopicPartition) context.Context { + if len(topicPartitionOffsets) == 0 { + panic("the topicPartitionOffsets cannot be empty") + } + for _, offset := range topicPartitionOffsets { + if offset.Topic == nil || *(offset.Topic) == "" { + panic("the kafka topic cannot be nil or empty") + } + if offset.Partition < 0 || offset.Offset < 0 { + panic("the kafka partition/offset must be non-negative") + } + } + return context.WithValue(ctx, offsetKey, topicPartitionOffsets) +} + +// TopicPartitionOffsetsFrom looks in the given context and returns []kafka.TopicPartition or nil if not set +func TopicPartitionOffsetsFrom(ctx context.Context) []kafka.TopicPartition { + c := ctx.Value(offsetKey) + if c != nil { + if s, ok := c.([]kafka.TopicPartition); ok { + return s + } + } + return nil +} + +// Opaque key type used to store message key +type messageKeyType struct{} + +var keyForMessageKey = messageKeyType{} + +// WithMessageKey returns back a new context with the given messageKey. +func WithMessageKey(ctx context.Context, messageKey string) context.Context { + return context.WithValue(ctx, keyForMessageKey, messageKey) +} + +// MessageKeyFrom looks in the given context and returns `messageKey` as a string if found and valid, otherwise "". +func MessageKeyFrom(ctx context.Context) string { + c := ctx.Value(keyForMessageKey) + if c != nil { + if s, ok := c.(string); ok { + return s + } + } + return "" +} diff --git a/protocol/kafka_confluent/v2/protocol.go b/protocol/kafka_confluent/v2/protocol.go new file mode 100644 index 000000000..c527f1061 --- /dev/null +++ b/protocol/kafka_confluent/v2/protocol.go @@ -0,0 +1,247 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package kafka_confluent + +import ( + "context" + "errors" + "fmt" + "io" + "sync" + + "github.com/cloudevents/sdk-go/v2/binding" + "github.com/cloudevents/sdk-go/v2/protocol" + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + + cecontext "github.com/cloudevents/sdk-go/v2/context" +) + +var ( + _ protocol.Sender = (*Protocol)(nil) + _ protocol.Opener = (*Protocol)(nil) + _ protocol.Receiver = (*Protocol)(nil) + _ protocol.Closer = (*Protocol)(nil) +) + +type Protocol struct { + kafkaConfigMap *kafka.ConfigMap + + consumer *kafka.Consumer + consumerTopics []string + consumerRebalanceCb kafka.RebalanceCb // optional + consumerPollTimeout int // optional + consumerErrorHandler func(ctx context.Context, err kafka.Error) // optional + consumerMux sync.Mutex + consumerIncoming chan *kafka.Message + consumerCtx context.Context + consumerCancel context.CancelFunc + + producer *kafka.Producer + producerDefaultTopic string // optional + + closerMux sync.Mutex +} + +func New(opts ...Option) (*Protocol, error) { + p := &Protocol{ + consumerPollTimeout: 100, + consumerIncoming: make(chan *kafka.Message), + } + if err := p.applyOptions(opts...); err != nil { + return nil, err + } + + if p.kafkaConfigMap != nil { + if p.consumerTopics != nil && p.consumer == nil { + consumer, err := kafka.NewConsumer(p.kafkaConfigMap) + if err != nil { + return nil, err + } + p.consumer = consumer + } + if p.producerDefaultTopic != "" && p.producer == nil { + producer, err := kafka.NewProducer(p.kafkaConfigMap) + if err != nil { + return nil, err + } + p.producer = producer + } + if p.producer == nil && p.consumer == nil { + return nil, errors.New("at least receiver or sender topic must be set") + } + } + if p.producerDefaultTopic != "" && p.producer == nil { + return nil, fmt.Errorf("at least configmap or producer must be set for the sender topic: %s", p.producerDefaultTopic) + } + + if len(p.consumerTopics) > 0 && p.consumer == nil { + return nil, fmt.Errorf("at least configmap or consumer must be set for the receiver topics: %s", p.consumerTopics) + } + + if p.kafkaConfigMap == nil && p.producer == nil && p.consumer == nil { + return nil, errors.New("at least one of the following to initialize the protocol must be set: config, producer, or consumer") + } + return p, nil +} + +// Events returns the events channel used by Confluent Kafka to deliver the result from a produce, i.e., send, operation. +// When using this SDK to produce (send) messages, this channel must be monitored to avoid resource leaks and this channel becoming full. See Confluent SDK for Go for details on the implementation. +func (p *Protocol) Events() (chan kafka.Event, error) { + if p.producer == nil { + return nil, errors.New("producer not set") + } + return p.producer.Events(), nil +} + +func (p *Protocol) applyOptions(opts ...Option) error { + for _, fn := range opts { + if err := fn(p); err != nil { + return err + } + } + return nil +} + +// Send message by kafka.Producer. You must monitor the Events() channel when using this function. +func (p *Protocol) Send(ctx context.Context, in binding.Message, transformers ...binding.Transformer) (err error) { + if p.producer == nil { + return errors.New("producer client must be set") + } + + p.closerMux.Lock() + defer p.closerMux.Unlock() + if p.producer.IsClosed() { + return errors.New("producer is closed") + } + + defer in.Finish(err) + + kafkaMsg := &kafka.Message{ + TopicPartition: kafka.TopicPartition{ + Topic: &p.producerDefaultTopic, + Partition: kafka.PartitionAny, + }, + } + + if topic := cecontext.TopicFrom(ctx); topic != "" { + kafkaMsg.TopicPartition.Topic = &topic + } + + if messageKey := MessageKeyFrom(ctx); messageKey != "" { + kafkaMsg.Key = []byte(messageKey) + } + + if err = WriteProducerMessage(ctx, in, kafkaMsg, transformers...); err != nil { + return fmt.Errorf("create producer message: %w", err) + } + + if err = p.producer.Produce(kafkaMsg, nil); err != nil { + return fmt.Errorf("produce message: %w", err) + } + return nil +} + +func (p *Protocol) OpenInbound(ctx context.Context) error { + if p.consumer == nil { + return errors.New("the consumer client must be set") + } + if p.consumerTopics == nil { + return errors.New("the consumer topics must be set") + } + + p.consumerMux.Lock() + defer p.consumerMux.Unlock() + logger := cecontext.LoggerFrom(ctx) + + // Query committed offsets for each partition + if positions := TopicPartitionOffsetsFrom(ctx); positions != nil { + if err := p.consumer.Assign(positions); err != nil { + return err + } + } + + logger.Infof("Subscribing to topics: %v", p.consumerTopics) + err := p.consumer.SubscribeTopics(p.consumerTopics, p.consumerRebalanceCb) + if err != nil { + return err + } + + p.closerMux.Lock() + p.consumerCtx, p.consumerCancel = context.WithCancel(ctx) + defer p.consumerCancel() + p.closerMux.Unlock() + + defer func() { + if !p.consumer.IsClosed() { + logger.Infof("Closing consumer %v", p.consumerTopics) + if err = p.consumer.Close(); err != nil { + logger.Errorf("failed to close the consumer: %v", err) + } + } + close(p.consumerIncoming) + }() + + for { + select { + case <-p.consumerCtx.Done(): + return p.consumerCtx.Err() + default: + ev := p.consumer.Poll(p.consumerPollTimeout) + if ev == nil { + continue + } + switch e := ev.(type) { + case *kafka.Message: + p.consumerIncoming <- e + case kafka.Error: + // Errors should generally be considered informational, the client will try to automatically recover. + // But in here, we choose to terminate the application if all brokers are down. + logger.Infof("Error %v: %v", e.Code(), e) + if p.consumerErrorHandler != nil { + p.consumerErrorHandler(ctx, e) + } + if e.Code() == kafka.ErrAllBrokersDown { + logger.Error("All broker connections are down") + return e + } + } + } + } +} + +// Receive implements Receiver.Receive +func (p *Protocol) Receive(ctx context.Context) (binding.Message, error) { + select { + case m, ok := <-p.consumerIncoming: + if !ok { + return nil, io.EOF + } + msg := NewMessage(m) + return msg, nil + case <-ctx.Done(): + return nil, io.EOF + } +} + +// Close cleans up resources after use. Must be called to properly close underlying Kafka resources and avoid resource leaks +func (p *Protocol) Close(ctx context.Context) error { + p.closerMux.Lock() + defer p.closerMux.Unlock() + logger := cecontext.LoggerFrom(ctx) + + if p.consumerCancel != nil { + p.consumerCancel() + } + + if p.producer != nil && !p.producer.IsClosed() { + // Flush and close the producer with a 10 seconds timeout (closes Events channel) + for p.producer.Flush(10000) > 0 { + logger.Info("Flushing outstanding messages") + } + p.producer.Close() + } + return nil +} diff --git a/protocol/kafka_confluent/v2/protocol_test.go b/protocol/kafka_confluent/v2/protocol_test.go new file mode 100644 index 000000000..0cc3e769f --- /dev/null +++ b/protocol/kafka_confluent/v2/protocol_test.go @@ -0,0 +1,69 @@ +/* + Copyright 2024 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package kafka_confluent + +import ( + "context" + "testing" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + "github.com/stretchr/testify/assert" +) + +func TestNewProtocol(t *testing.T) { + tests := []struct { + name string + ctx context.Context + options []Option + errorMessage string + }{ + { + name: "invalidated parameters", + options: nil, + errorMessage: "at least one of the following to initialize the protocol must be set: config, producer, or consumer", + }, + { + name: "Insufficient parameters", + options: []Option{ + WithConfigMap(&kafka.ConfigMap{ + "bootstrap.servers": "127.0.0.1:9092", + })}, + errorMessage: "at least receiver or sender topic must be set", + }, + { + name: "Insufficient consumer parameters - group.id", + options: []Option{ + WithConfigMap(&kafka.ConfigMap{ + "bootstrap.servers": "127.0.0.1:9092", + }), + WithReceiverTopics([]string{"topic1", "topic2"}), + }, + errorMessage: "Required property group.id not set", + }, + { + name: "Insufficient consumer parameters - configmap or consumer", + options: []Option{ + WithReceiverTopics([]string{"topic1", "topic2"}), + }, + errorMessage: "at least configmap or consumer must be set for the receiver topics: [topic1 topic2]", + }, + { + name: "Insufficient producer parameters", + options: []Option{ + WithSenderTopic("topic3"), + }, + errorMessage: "at least configmap or producer must be set for the sender topic: topic3", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _, err := New(tt.options...) + if err != nil { + assert.Equal(t, tt.errorMessage, err.Error()) + } + }) + } +} diff --git a/protocol/kafka_confluent/v2/write_producer_message.go b/protocol/kafka_confluent/v2/write_producer_message.go new file mode 100644 index 000000000..e640cbded --- /dev/null +++ b/protocol/kafka_confluent/v2/write_producer_message.go @@ -0,0 +1,125 @@ +/* +Copyright 2023 The CloudEvents Authors +SPDX-License-Identifier: Apache-2.0 +*/ + +package kafka_confluent + +import ( + "bytes" + "context" + "io" + + "github.com/cloudevents/sdk-go/v2/binding" + "github.com/cloudevents/sdk-go/v2/binding/format" + "github.com/cloudevents/sdk-go/v2/binding/spec" + "github.com/cloudevents/sdk-go/v2/types" + "github.com/confluentinc/confluent-kafka-go/v2/kafka" +) + +// extends the kafka.Message to support the interfaces for the converting it to binding.Message +type kafkaMessageWriter kafka.Message + +var ( + _ binding.StructuredWriter = (*kafkaMessageWriter)(nil) + _ binding.BinaryWriter = (*kafkaMessageWriter)(nil) +) + +// WriteProducerMessage fills the provided pubMessage with the message m. +// Using context you can tweak the encoding processing (more details on binding.Write documentation). +func WriteProducerMessage(ctx context.Context, in binding.Message, kafkaMsg *kafka.Message, + transformers ...binding.Transformer, +) error { + structuredWriter := (*kafkaMessageWriter)(kafkaMsg) + binaryWriter := (*kafkaMessageWriter)(kafkaMsg) + + _, err := binding.Write( + ctx, + in, + structuredWriter, + binaryWriter, + transformers..., + ) + return err +} + +func (b *kafkaMessageWriter) SetStructuredEvent(ctx context.Context, f format.Format, event io.Reader) error { + b.Headers = []kafka.Header{{ + Key: contentTypeKey, + Value: []byte(f.MediaType()), + }} + + var buf bytes.Buffer + _, err := io.Copy(&buf, event) + if err != nil { + return err + } + + b.Value = buf.Bytes() + return nil +} + +func (b *kafkaMessageWriter) Start(ctx context.Context) error { + b.Headers = []kafka.Header{} + return nil +} + +func (b *kafkaMessageWriter) End(ctx context.Context) error { + return nil +} + +func (b *kafkaMessageWriter) SetData(reader io.Reader) error { + buf, ok := reader.(*bytes.Buffer) + if !ok { + buf = new(bytes.Buffer) + _, err := io.Copy(buf, reader) + if err != nil { + return err + } + } + b.Value = buf.Bytes() + return nil +} + +func (b *kafkaMessageWriter) SetAttribute(attribute spec.Attribute, value interface{}) error { + if attribute.Kind() == spec.DataContentType { + if value == nil { + b.removeProperty(contentTypeKey) + return nil + } + b.addProperty(contentTypeKey, value) + } else { + key := prefix + attribute.Name() + if value == nil { + b.removeProperty(key) + return nil + } + b.addProperty(key, value) + } + return nil +} + +func (b *kafkaMessageWriter) SetExtension(name string, value interface{}) error { + if value == nil { + b.removeProperty(prefix + name) + } + return b.addProperty(prefix+name, value) +} + +func (b *kafkaMessageWriter) removeProperty(key string) { + for i, v := range b.Headers { + if v.Key == key { + b.Headers = append(b.Headers[:i], b.Headers[i+1:]...) + break + } + } +} + +func (b *kafkaMessageWriter) addProperty(key string, value interface{}) error { + s, err := types.Format(value) + if err != nil { + return err + } + b.Headers = append(b.Headers, kafka.Header{Key: key, Value: []byte(s)}) + return nil +} diff --git a/protocol/kafka_confluent/v2/write_producer_message_test.go b/protocol/kafka_confluent/v2/write_producer_message_test.go new file mode 100644 index 000000000..eb4cd25bf --- /dev/null +++ b/protocol/kafka_confluent/v2/write_producer_message_test.go @@ -0,0 +1,82 @@ +/* +Copyright 2023 The CloudEvents Authors +SPDX-License-Identifier: Apache-2.0 +*/ + +package kafka_confluent + +import ( + "context" + "strconv" + "testing" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + "github.com/stretchr/testify/require" + + "github.com/cloudevents/sdk-go/v2/binding" + . "github.com/cloudevents/sdk-go/v2/binding/test" + "github.com/cloudevents/sdk-go/v2/event" + . "github.com/cloudevents/sdk-go/v2/test" +) + +func TestWriteProducerMessage(t *testing.T) { + tests := []struct { + name string + context context.Context + messageFactory func(e event.Event) binding.Message + expectedEncoding binding.Encoding + }{ + { + name: "Structured to Structured", + context: ctx, + messageFactory: func(e event.Event) binding.Message { + return MustCreateMockStructuredMessage(t, e) + }, + expectedEncoding: binding.EncodingStructured, + }, + { + name: "Binary to Binary", + context: ctx, + messageFactory: MustCreateMockBinaryMessage, + expectedEncoding: binding.EncodingBinary, + }, + } + EachEvent(t, Events(), func(t *testing.T, e event.Event) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := tt.context + topic := "test-topic" + kafkaMessage := &kafka.Message{ + TopicPartition: kafka.TopicPartition{ + Topic: &topic, + Partition: int32(0), + Offset: kafka.Offset(10), + }, + } + + eventIn := ConvertEventExtensionsToString(t, e.Clone()) + messageIn := tt.messageFactory(eventIn) + + err := WriteProducerMessage(ctx, messageIn, kafkaMessage) + require.NoError(t, err) + + messageOut := NewMessage(kafkaMessage) + require.Equal(t, tt.expectedEncoding, messageOut.ReadEncoding()) + + if tt.expectedEncoding == binding.EncodingBinary { + err = messageOut.ReadBinary(ctx, (*kafkaMessageWriter)(kafkaMessage)) + } + require.NoError(t, err) + + eventOut, err := binding.ToEvent(ctx, messageOut) + require.NoError(t, err) + if tt.expectedEncoding == binding.EncodingBinary { + eventIn.SetExtension(KafkaPartitionKey, strconv.FormatInt(int64(kafkaMessage.TopicPartition.Partition), 10)) + eventIn.SetExtension(KafkaOffsetKey, strconv.FormatInt(int64(kafkaMessage.TopicPartition.Offset), 10)) + eventIn.SetExtension(KafkaTopicKey, kafkaMessage.TopicPartition.Topic) + } + AssertEventEquals(t, eventIn, *eventOut) + }) + } + }) +} diff --git a/protocol/kafka_sarama/v2/go.mod b/protocol/kafka_sarama/v2/go.mod index ff2fc11ed..24ed04fe8 100644 --- a/protocol/kafka_sarama/v2/go.mod +++ b/protocol/kafka_sarama/v2/go.mod @@ -37,7 +37,7 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/protocol/kafka_sarama/v2/go.sum b/protocol/kafka_sarama/v2/go.sum index 7a05f314b..38a1dd9ad 100644 --- a/protocol/kafka_sarama/v2/go.sum +++ b/protocol/kafka_sarama/v2/go.sum @@ -76,13 +76,13 @@ go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/protocol/mqtt_paho/v2/message.go b/protocol/mqtt_paho/v2/message.go index 8dd938545..2bd7ed35d 100644 --- a/protocol/mqtt_paho/v2/message.go +++ b/protocol/mqtt_paho/v2/message.go @@ -17,8 +17,7 @@ import ( ) const ( - prefix = "ce-" - contentType = "Content-Type" + prefix = "ce-" ) var specs = spec.WithPrefix(prefix) @@ -41,8 +40,7 @@ func NewMessage(msg *paho.Publish) *Message { var f format.Format var v spec.Version if msg.Properties != nil { - // Use properties.User["Content-type"] to determine if message is structured - if s := msg.Properties.User.Get(contentType); format.IsFormat(s) { + if s := msg.Properties.ContentType; format.IsFormat(s) { f = format.Lookup(s) } else if s := msg.Properties.User.Get(specs.PrefixedSpecVersionName()); s != "" { v = specs.Version(s) @@ -88,14 +86,20 @@ func (m *Message) ReadBinary(ctx context.Context, encoder binding.BinaryWriter) } else { err = encoder.SetExtension(strings.TrimPrefix(userProperty.Key, prefix), userProperty.Value) } - } else if userProperty.Key == contentType { - err = encoder.SetAttribute(m.version.AttributeFromKind(spec.DataContentType), string(userProperty.Value)) } if err != nil { return } } + contentType := m.internal.Properties.ContentType + if contentType != "" { + err = encoder.SetAttribute(m.version.AttributeFromKind(spec.DataContentType), contentType) + if err != nil { + return err + } + } + if m.internal.Payload != nil { return encoder.SetData(bytes.NewBuffer(m.internal.Payload)) } diff --git a/protocol/mqtt_paho/v2/message_test.go b/protocol/mqtt_paho/v2/message_test.go index 757f81f5c..ff51a0a0c 100644 --- a/protocol/mqtt_paho/v2/message_test.go +++ b/protocol/mqtt_paho/v2/message_test.go @@ -32,7 +32,7 @@ func TestReadStructured(t *testing.T) { msg: &paho.Publish{ Payload: []byte(""), Properties: &paho.PublishProperties{ - User: []paho.UserProperty{{Key: contentType, Value: event.ApplicationCloudEventsJSON}}, + ContentType: event.ApplicationCloudEventsJSON, }, }, }, diff --git a/protocol/mqtt_paho/v2/write_message.go b/protocol/mqtt_paho/v2/write_message.go index a4b87f4aa..9db47e918 100644 --- a/protocol/mqtt_paho/v2/write_message.go +++ b/protocol/mqtt_paho/v2/write_message.go @@ -42,11 +42,9 @@ var ( func (b *pubMessageWriter) SetStructuredEvent(ctx context.Context, f format.Format, event io.Reader) error { if b.Properties == nil { - b.Properties = &paho.PublishProperties{ - User: make([]paho.UserProperty, 0), - } + b.Properties = &paho.PublishProperties{} } - b.Properties.User.Add(contentType, f.MediaType()) + b.Properties.ContentType = f.MediaType() var buf bytes.Buffer _, err := io.Copy(&buf, event) if err != nil { @@ -85,15 +83,13 @@ func (b *pubMessageWriter) SetData(reader io.Reader) error { func (b *pubMessageWriter) SetAttribute(attribute spec.Attribute, value interface{}) error { if attribute.Kind() == spec.DataContentType { if value == nil { - b.removeProperty(contentType) + b.Properties.ContentType = "" } s, err := types.Format(value) if err != nil { return err } - if err := b.addProperty(contentType, s); err != nil { - return err - } + b.Properties.ContentType = s } else { if value == nil { b.removeProperty(prefix + attribute.Name()) diff --git a/protocol/pubsub/v2/go.mod b/protocol/pubsub/v2/go.mod index 663693de4..e9cd57d6d 100644 --- a/protocol/pubsub/v2/go.mod +++ b/protocol/pubsub/v2/go.mod @@ -33,10 +33,10 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/protocol/pubsub/v2/go.sum b/protocol/pubsub/v2/go.sum index 84de05242..2928930ad 100644 --- a/protocol/pubsub/v2/go.sum +++ b/protocol/pubsub/v2/go.sum @@ -96,8 +96,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= @@ -110,13 +110,13 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= diff --git a/samples/http/go.mod b/samples/http/go.mod index acd3f66bc..a590f18e2 100644 --- a/samples/http/go.mod +++ b/samples/http/go.mod @@ -8,7 +8,7 @@ require ( github.com/cloudevents/sdk-go/observability/opencensus/v2 v2.5.0 github.com/cloudevents/sdk-go/observability/opentelemetry/v2 v2.5.0 github.com/cloudevents/sdk-go/v2 v2.5.0 - github.com/gin-gonic/gin v1.8.2 + github.com/gin-gonic/gin v1.9.1 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.7.3 github.com/kelseyhightower/envconfig v1.4.0 @@ -24,41 +24,47 @@ require ( require ( github.com/beorn7/perks v1.0.1 // indirect + github.com/bytedance/sonic v1.9.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-playground/locales v0.14.0 // indirect - github.com/go-playground/universal-translator v0.18.0 // indirect - github.com/go-playground/validator/v10 v10.11.1 // indirect - github.com/goccy/go-json v0.9.11 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/leodido/go-urn v1.2.1 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.6 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/prometheus/client_golang v1.11.1 // indirect github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.26.0 // indirect github.com/prometheus/procfs v0.6.0 // indirect - github.com/ugorji/go/codec v1.2.7 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect go.opentelemetry.io/otel/metric v1.18.0 // indirect go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.56.3 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace github.com/cloudevents/sdk-go/v2 => ../../v2 diff --git a/samples/http/go.sum b/samples/http/go.sum index 8c25ac366..ab2d5dcfd 100644 --- a/samples/http/go.sum +++ b/samples/http/go.sum @@ -12,21 +12,28 @@ github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24 github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY= -github.com/gin-gonic/gin v1.8.2/go.mod h1:qw5AYuDrzRTnhvusDsrov+fDIxp9Dleuu12h8nfB398= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= @@ -38,17 +45,16 @@ github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= -github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -92,25 +98,24 @@ github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7V github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lightstep/tracecontext.go v0.0.0-20181129014701-1757c391b1ac h1:+2b6iGRJe3hvV/yVXrd41yVEjxuFHxasJqDhkIjS4gk= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -122,9 +127,9 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= -github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -152,9 +157,6 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.0 h1:Zes4hju04hjbvkVkOhdl2HpZa+0PmVwigmo8XoORE5w= github.com/rs/zerolog v1.29.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= @@ -169,15 +171,17 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= @@ -202,12 +206,14 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -223,9 +229,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -245,23 +250,17 @@ golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= @@ -293,20 +292,15 @@ google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGm google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/samples/kafka/go.mod b/samples/kafka/go.mod index d30381549..e42c46f64 100644 --- a/samples/kafka/go.mod +++ b/samples/kafka/go.mod @@ -32,8 +32,8 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect ) replace github.com/cloudevents/sdk-go/v2 => ../../v2 diff --git a/samples/kafka/go.sum b/samples/kafka/go.sum index 15429c866..7db1f7227 100644 --- a/samples/kafka/go.sum +++ b/samples/kafka/go.sum @@ -70,13 +70,13 @@ go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/samples/kafka_confluent/README.md b/samples/kafka_confluent/README.md new file mode 100644 index 000000000..969cafb97 --- /dev/null +++ b/samples/kafka_confluent/README.md @@ -0,0 +1,9 @@ +# Confluent kafka samples + +To run the samples, you need a running Kafka cluster. + +To run a sample Kafka cluster using docker: + +``` +docker run --rm --net=host confluentinc/confluent-local +``` \ No newline at end of file diff --git a/samples/kafka_confluent/go.mod b/samples/kafka_confluent/go.mod new file mode 100644 index 000000000..17b2b4ea1 --- /dev/null +++ b/samples/kafka_confluent/go.mod @@ -0,0 +1,23 @@ +module github.com/cloudevents/sdk-go/samples/kafka_confluent + +go 1.18 + +replace github.com/cloudevents/sdk-go/v2 => ../../v2 + +replace github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2 => ../../protocol/kafka_confluent/v2 + +require ( + github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2 v2.0.0-00010101000000-000000000000 + github.com/cloudevents/sdk-go/v2 v2.15.2 + github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 +) + +require ( + github.com/google/uuid v1.3.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + go.uber.org/atomic v1.4.0 // indirect + go.uber.org/multierr v1.1.0 // indirect + go.uber.org/zap v1.10.0 // indirect +) diff --git a/samples/kafka_confluent/go.sum b/samples/kafka_confluent/go.sum new file mode 100644 index 000000000..b0200c98f --- /dev/null +++ b/samples/kafka_confluent/go.sum @@ -0,0 +1,61 @@ +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633 h1:0BOZf6qNozI3pkN3fJLwNubheHJYHhMh91GRFOWWK08= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/samples/kafka_confluent/receiver-assign/main.go b/samples/kafka_confluent/receiver-assign/main.go new file mode 100644 index 000000000..c1ce92610 --- /dev/null +++ b/samples/kafka_confluent/receiver-assign/main.go @@ -0,0 +1,60 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "context" + "fmt" + "log" + + confluent "github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/cloudevents/sdk-go/v2/client" + "github.com/confluentinc/confluent-kafka-go/v2/kafka" +) + +var topic = "test-confluent-topic" + +func main() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + receiver, err := confluent.New(confluent.WithConfigMap(&kafka.ConfigMap{ + "bootstrap.servers": "127.0.0.1:9092", + "group.id": "test-confluent-offset-id", + // "auto.offset.reset": "earliest", + "enable.auto.commit": "true", + }), confluent.WithReceiverTopics([]string{topic})) + if err != nil { + log.Fatalf("failed to create kafka protocol, %v", err) + } + defer receiver.Close(ctx) + + // Setting the 'client.WithPollGoroutines(1)' to make sure the events from kafka partition are processed in order + c, err := cloudevents.NewClient(receiver, client.WithPollGoroutines(1)) + if err != nil { + log.Fatalf("failed to create client, %v", err) + } + + offsetToStart := []kafka.TopicPartition{ + {Topic: &topic, Partition: 0, Offset: 3}, + } + + log.Printf("will listen consuming topic %s\n", topic) + err = c.StartReceiver(confluent.WithTopicPartitionOffsets(ctx, offsetToStart), receive) + if err != nil { + log.Fatalf("failed to start receiver: %s", err) + } else { + log.Printf("receiver stopped\n") + } +} + +func receive(ctx context.Context, event cloudevents.Event) { + ext := event.Extensions() + + fmt.Printf("%s[%s:%s] \n", ext[confluent.KafkaTopicKey], + ext[confluent.KafkaPartitionKey], ext[confluent.KafkaOffsetKey]) +} diff --git a/samples/kafka_confluent/receiver/main.go b/samples/kafka_confluent/receiver/main.go new file mode 100644 index 000000000..3b9efab6c --- /dev/null +++ b/samples/kafka_confluent/receiver/main.go @@ -0,0 +1,54 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "context" + "fmt" + "log" + + confluent "github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/cloudevents/sdk-go/v2/client" + "github.com/confluentinc/confluent-kafka-go/v2/kafka" +) + +const topic = "test-confluent-topic" + +func main() { + ctx := context.Background() + + receiver, err := confluent.New(confluent.WithConfigMap(&kafka.ConfigMap{ + "bootstrap.servers": "127.0.0.1:9092", + "group.id": "test-confluent-group-id", + "auto.offset.reset": "earliest", // only validated when the consumer group offset has saved before + "enable.auto.commit": "true", + }), confluent.WithReceiverTopics([]string{topic})) + + if err != nil { + log.Fatalf("failed to create receiver, %v", err) + } + defer receiver.Close(ctx) + + // The 'WithBlockingCallback()' is to make event processing serialized (no concurrency), use this option along with WithPollGoroutines(1). + // These two options make sure the events from kafka partition are processed in order + c, err := cloudevents.NewClient(receiver, client.WithBlockingCallback(), client.WithPollGoroutines(1)) + if err != nil { + log.Fatalf("failed to create client, %v", err) + } + + log.Printf("will listen consuming topic %s\n", topic) + err = c.StartReceiver(ctx, receive) + if err != nil { + log.Fatalf("failed to start receiver: %s", err) + } else { + log.Printf("receiver stopped\n") + } +} + +func receive(ctx context.Context, event cloudevents.Event) { + fmt.Printf("%s", event) +} diff --git a/samples/kafka_confluent/sender/main.go b/samples/kafka_confluent/sender/main.go new file mode 100644 index 000000000..777509224 --- /dev/null +++ b/samples/kafka_confluent/sender/main.go @@ -0,0 +1,97 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package main + +import ( + "context" + "log" + "sync" + + confluent "github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2" + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + + cloudevents "github.com/cloudevents/sdk-go/v2" +) + +const ( + count = 10 + topic = "test-confluent-topic" +) + +func main() { + ctx := context.Background() + + sender, err := confluent.New(confluent.WithConfigMap(&kafka.ConfigMap{ + "bootstrap.servers": "127.0.0.1:9092", + }), confluent.WithSenderTopic(topic)) + if err != nil { + log.Fatalf("failed to create protocol, %v", err) + } + + var wg sync.WaitGroup + wg.Add(1) + + // Listen to all the events on the default events channel + // It's important to read these events otherwise the events channel will eventually fill up + go func() { + defer wg.Done() + eventChan, err := sender.Events() + if err != nil { + log.Fatalf("failed to get events channel for sender, %v", err) + } + for e := range eventChan { + switch ev := e.(type) { + case *kafka.Message: + // The message delivery report, indicating success or + // permanent failure after retries have been exhausted. + // Application level retries won't help since the client + // is already configured to do that. + m := ev + if m.TopicPartition.Error != nil { + log.Printf("Delivery failed: %v\n", m.TopicPartition.Error) + } else { + log.Printf("Delivered message to topic %s [%d] at offset %v\n", + *m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset) + } + case kafka.Error: + // Generic client instance-level errors, such as + // broker connection failures, authentication issues, etc. + // + // These errors should generally be considered informational + // as the underlying client will automatically try to + // recover from any errors encountered, the application + // does not need to take action on them. + log.Printf("Error: %v\n", ev) + default: + log.Printf("Ignored event: %v\n", ev) + } + } + }() + + c, err := cloudevents.NewClient(sender, cloudevents.WithTimeNow(), cloudevents.WithUUIDs()) + if err != nil { + log.Fatalf("failed to create client, %v", err) + } + + for i := 0; i < count; i++ { + e := cloudevents.NewEvent() + e.SetType("com.cloudevents.sample.sent") + e.SetSource("https://github.com/cloudevents/sdk-go/samples/kafka_confluent/sender") + _ = e.SetData(cloudevents.ApplicationJSON, map[string]interface{}{ + "id": i, + "message": "Hello, World!", + }) + + if result := c.Send(confluent.WithMessageKey(ctx, e.ID()), e); cloudevents.IsUndelivered(result) { + log.Printf("failed to send: %v", result) + } else { + log.Printf("sent: %d, accepted: %t", i, cloudevents.IsACK(result)) + } + } + + sender.Close(ctx) + wg.Wait() +} diff --git a/samples/pubsub/go.mod b/samples/pubsub/go.mod index 7b8630fcc..2883ec801 100644 --- a/samples/pubsub/go.mod +++ b/samples/pubsub/go.mod @@ -27,11 +27,11 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.7.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/text v0.13.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect google.golang.org/api v0.114.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect diff --git a/samples/pubsub/go.sum b/samples/pubsub/go.sum index 3625d93fd..1db53d982 100644 --- a/samples/pubsub/go.sum +++ b/samples/pubsub/go.sum @@ -98,8 +98,8 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= @@ -112,13 +112,13 @@ golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/sql/v2/README.md b/sql/v2/README.md index f45641d97..948f48f41 100644 --- a/sql/v2/README.md +++ b/sql/v2/README.md @@ -18,6 +18,54 @@ expression, err := cesqlparser.Parse("subject = 'Hello world'") res, err := expression.Evaluate(event) ``` +Add a user defined function +```go +import ( + cesql "github.com/cloudevents/sdk-go/sql/v2" + cefn "github.com/cloudevents/sdk-go/sql/v2/function" + cesqlparser "github.com/cloudevents/sdk-go/sql/v2/parser" + ceruntime "github.com/cloudevents/sdk-go/sql/v2/runtime" + cloudevents "github.com/cloudevents/sdk-go/v2" +) + +// Create a test event +event := cloudevents.NewEvent() +event.SetID("aaaa-bbbb-dddd") +event.SetSource("https://my-source") +event.SetType("dev.tekton.event") + +// Create and add a new user defined function +var HasPrefixFunction cesql.Function = cefn.NewFunction( + "HASPREFIX", + []cesql.Type{cesql.StringType, cesql.StringType}, + nil, + func(event cloudevents.Event, i []interface{}) (interface{}, error) { + str := i[0].(string) + prefix := i[1].(string) + + return strings.HasPrefix(str, prefix), nil + }, +) + +err := ceruntime.AddFunction(HasPrefixFunction) + +// parse the expression +expression, err := cesqlparser.Parse("HASPREFIX(type, 'dev.tekton.event')") + if err != nil { + fmt.Println("parser err: ", err) + os.Exit(1) + } + +// Evalute the expression with the test event +res, err := expression.Evaluate(event) + +if res.(bool) { + fmt.Println("Event type has the prefix") +} else { + fmt.Println("Event type doesn't have the prefix") +} +``` + ## Development guide To regenerate the parser, make sure you have [ANTLR4 installed](https://github.com/antlr/antlr4/blob/master/doc/getting-started.md) and then run: diff --git a/sql/v2/expression/like_expression.go b/sql/v2/expression/like_expression.go index 5f557fa5a..01734852a 100644 --- a/sql/v2/expression/like_expression.go +++ b/sql/v2/expression/like_expression.go @@ -6,9 +6,6 @@ package expression import ( - "regexp" - "strings" - cesql "github.com/cloudevents/sdk-go/sql/v2" "github.com/cloudevents/sdk-go/sql/v2/utils" cloudevents "github.com/cloudevents/sdk-go/v2" @@ -16,7 +13,7 @@ import ( type likeExpression struct { baseUnaryExpression - pattern *regexp.Regexp + pattern string } func (l likeExpression) Evaluate(event cloudevents.Event) (interface{}, error) { @@ -30,70 +27,65 @@ func (l likeExpression) Evaluate(event cloudevents.Event) (interface{}, error) { return nil, err } - return l.pattern.MatchString(val.(string)), nil + return matchString(val.(string), l.pattern), nil + } func NewLikeExpression(child cesql.Expression, pattern string) (cesql.Expression, error) { - // Converting to regex is not the most performant impl, but it works - p, err := convertLikePatternToRegex(pattern) - if err != nil { - return nil, err - } - return likeExpression{ baseUnaryExpression: baseUnaryExpression{ child: child, }, - pattern: p, + pattern: pattern, }, nil } -func convertLikePatternToRegex(pattern string) (*regexp.Regexp, error) { - var chunks []string - chunks = append(chunks, "^") +func matchString(text, pattern string) bool { + textLen := len(text) + patternLen := len(pattern) + textIdx := 0 + patternIdx := 0 + lastWildcardIdx := -1 + lastMatchIdx := 0 - var chunk strings.Builder + if patternLen == 0 { + return patternLen == textLen + } - for i := 0; i < len(pattern); i++ { - if pattern[i] == '\\' && i < len(pattern)-1 { - if pattern[i+1] == '%' { - // \% case - chunk.WriteRune('%') - chunks = append(chunks, "\\Q"+chunk.String()+"\\E") - chunk.Reset() - i++ - continue - } else if pattern[i+1] == '_' { - // \_ case - chunk.WriteRune('_') - chunks = append(chunks, "\\Q"+chunk.String()+"\\E") - chunk.Reset() - i++ - continue - } else { - // if there is an actual literal \ character, we need to include that in the string - chunk.WriteRune('\\') - } - } else if pattern[i] == '_' { - // replace with . - chunks = append(chunks, "\\Q"+chunk.String()+"\\E") - chunk.Reset() - chunks = append(chunks, ".") - } else if pattern[i] == '%' { - // replace with .* - chunks = append(chunks, "\\Q"+chunk.String()+"\\E") - chunk.Reset() - chunks = append(chunks, ".*") + for textIdx < textLen { + if patternIdx < patternLen-1 && pattern[patternIdx] == '\\' && + ((pattern[patternIdx+1] == '_' || pattern[patternIdx+1] == '%') && + pattern[patternIdx+1] == text[textIdx]) { + // handle escaped characters -> pattern needs to increment two places here + patternIdx += 2 + textIdx += 1 + } else if patternIdx < patternLen && (pattern[patternIdx] == '_' || pattern[patternIdx] == text[textIdx]) { + // handle non escaped characters + textIdx += 1 + patternIdx += 1 + } else if patternIdx < patternLen && pattern[patternIdx] == '%' { + // handle wildcard characters + lastWildcardIdx = patternIdx + lastMatchIdx = textIdx + patternIdx += 1 + } else if lastWildcardIdx != -1 { + // greedy match didn't work, try again from the last known match + patternIdx = lastWildcardIdx + 1 + lastMatchIdx += 1 + textIdx = lastMatchIdx } else { - chunk.WriteByte(pattern[i]) + return false } } - if chunk.Len() != 0 { - chunks = append(chunks, "\\Q"+chunk.String()+"\\E") - } + // consume remaining pattern characters as long as they are wildcards + for patternIdx < patternLen { + if pattern[patternIdx] != '%' { + return false + } - chunks = append(chunks, "$") + patternIdx += 1 + } - return regexp.Compile(strings.Join(chunks, "")) + return true } diff --git a/sql/v2/function/function.go b/sql/v2/function/function.go index 4ad61faed..f43db3e9d 100644 --- a/sql/v2/function/function.go +++ b/sql/v2/function/function.go @@ -10,11 +10,13 @@ import ( cloudevents "github.com/cloudevents/sdk-go/v2" ) +type FuncType func(cloudevents.Event, []interface{}) (interface{}, error) + type function struct { name string fixedArgs []cesql.Type variadicArgs *cesql.Type - fn func(cloudevents.Event, []interface{}) (interface{}, error) + fn FuncType } func (f function) Name() string { @@ -39,3 +41,15 @@ func (f function) ArgType(index int) *cesql.Type { func (f function) Run(event cloudevents.Event, arguments []interface{}) (interface{}, error) { return f.fn(event, arguments) } + +func NewFunction(name string, + fixedargs []cesql.Type, + variadicArgs *cesql.Type, + fn FuncType) cesql.Function { + return function{ + name: name, + fixedArgs: fixedargs, + variadicArgs: variadicArgs, + fn: fn, + } +} diff --git a/sql/v2/go.mod b/sql/v2/go.mod index 86d00e196..631a5b536 100644 --- a/sql/v2/go.mod +++ b/sql/v2/go.mod @@ -6,6 +6,7 @@ require ( github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 github.com/cloudevents/sdk-go/v2 v2.5.0 github.com/stretchr/testify v1.8.0 + gopkg.in/yaml.v2 v2.4.0 sigs.k8s.io/yaml v1.3.0 ) @@ -20,7 +21,6 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/sql/v2/parser/expression_visitor.go b/sql/v2/parser/expression_visitor.go index 8ca5fd07e..4fb863ac2 100644 --- a/sql/v2/parser/expression_visitor.go +++ b/sql/v2/parser/expression_visitor.go @@ -6,6 +6,7 @@ package parser import ( + "fmt" "strconv" "strings" @@ -175,9 +176,13 @@ func (v *expressionVisitor) VisitLikeExpression(ctx *gen.LikeExpressionContext) if patternContext.DQUOTED_STRING_LITERAL() != nil { // Parse double quoted string pattern = dQuotedStringToString(patternContext.DQUOTED_STRING_LITERAL().GetText()) - } else { + } else if patternContext.SQUOTED_STRING_LITERAL() != nil { // Parse single quoted string pattern = sQuotedStringToString(patternContext.SQUOTED_STRING_LITERAL().GetText()) + } else { + // not a string, return an error + v.parsingErrors = append(v.parsingErrors, fmt.Errorf("failed to parse LIKE expression: the pattern was not a string literal")) + return noopExpression{} } likeExpression, err := expression.NewLikeExpression(v.Visit(ctx.Expression()).(cesql.Expression), pattern) diff --git a/sql/v2/runtime/functions_resolver.go b/sql/v2/runtime/functions_resolver.go index b80136842..5ab964fb7 100644 --- a/sql/v2/runtime/functions_resolver.go +++ b/sql/v2/runtime/functions_resolver.go @@ -58,6 +58,11 @@ func (table functionTable) AddFunction(function cesql.Function) error { } } +// Adds user defined function +func AddFunction(fn cesql.Function) error { + return globalFunctionTable.AddFunction(fn) +} + func (table functionTable) ResolveFunction(name string, args int) cesql.Function { item := table[strings.ToUpper(name)] if item == nil { diff --git a/sql/v2/runtime/test/tck/user_defined_functions.yaml b/sql/v2/runtime/test/tck/user_defined_functions.yaml new file mode 100644 index 000000000..c2a3a922e --- /dev/null +++ b/sql/v2/runtime/test/tck/user_defined_functions.yaml @@ -0,0 +1,27 @@ +name: User defined functions +tests: + - name: HASPREFIX (1) + expression: "HASPREFIX('abcdef', 'ab')" + result: true + - name: HASPREFIX (2) + expression: "HASPREFIX('abcdef', 'abcdef')" + result: true + - name: HASPREFIX (3) + expression: "HASPREFIX('abcdef', '')" + result: true + - name: HASPREFIX (4) + expression: "HASPREFIX('abcdef', 'gh')" + result: false + - name: HASPREFIX (5) + expression: "HASPREFIX('abcdef', 'abcdefg')" + result: false + + - name: KONKAT (1) + expression: "KONKAT('a', 'b', 'c')" + result: abc + - name: KONKAT (2) + expression: "KONKAT()" + result: "" + - name: KONKAT (3) + expression: "KONKAT('a')" + result: "a" \ No newline at end of file diff --git a/sql/v2/runtime/test/user_defined_functions_test.go b/sql/v2/runtime/test/user_defined_functions_test.go new file mode 100644 index 000000000..944ba98dd --- /dev/null +++ b/sql/v2/runtime/test/user_defined_functions_test.go @@ -0,0 +1,209 @@ +/* + Copyright 2024 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package runtime_test + +import ( + "io" + "os" + "path" + "runtime" + "strings" + "testing" + + cesql "github.com/cloudevents/sdk-go/sql/v2" + "github.com/cloudevents/sdk-go/sql/v2/function" + "github.com/cloudevents/sdk-go/sql/v2/parser" + ceruntime "github.com/cloudevents/sdk-go/sql/v2/runtime" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/cloudevents/sdk-go/v2/binding/spec" + "github.com/cloudevents/sdk-go/v2/event" + "github.com/cloudevents/sdk-go/v2/test" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v2" +) + +var TCKFileNames = []string{ + "user_defined_functions", +} + +var TCKUserDefinedFunctions = []cesql.Function{ + function.NewFunction( + "HASPREFIX", + []cesql.Type{cesql.StringType, cesql.StringType}, + nil, + func(event cloudevents.Event, i []interface{}) (interface{}, error) { + str := i[0].(string) + prefix := i[1].(string) + + return strings.HasPrefix(str, prefix), nil + }, + ), + function.NewFunction( + "KONKAT", + []cesql.Type{}, + cesql.TypePtr(cesql.StringType), + func(event cloudevents.Event, i []interface{}) (interface{}, error) { + var sb strings.Builder + for _, v := range i { + sb.WriteString(v.(string)) + } + return sb.String(), nil + }, + ), +} + +type ErrorType string + +const ( + ParseError ErrorType = "parse" + MathError ErrorType = "math" + CastError ErrorType = "cast" + MissingAttributeError ErrorType = "missingAttribute" + MissingFunctionError ErrorType = "missingFunction" + FunctionEvaluationError ErrorType = "functionEvaluation" +) + +type TckFile struct { + Name string `json:"name"` + Tests []TckTestCase `json:"tests"` +} + +type TckTestCase struct { + Name string `json:"name"` + Expression string `json:"expression"` + + Result interface{} `json:"result"` + Error ErrorType `json:"error"` + + Event *cloudevents.Event `json:"event"` + EventOverrides map[string]interface{} `json:"eventOverrides"` +} + +func (tc TckTestCase) InputEvent(t *testing.T) cloudevents.Event { + var inputEvent cloudevents.Event + if tc.Event != nil { + inputEvent = *tc.Event + } else { + inputEvent = test.FullEvent() + } + + // Make sure the event is v1 + inputEvent.SetSpecVersion(event.CloudEventsVersionV1) + + for k, v := range tc.EventOverrides { + require.NoError(t, spec.V1.SetAttribute(inputEvent.Context, k, v)) + } + + return inputEvent +} + +func (tc TckTestCase) ExpectedResult() interface{} { + switch tc.Result.(type) { + case int: + return int32(tc.Result.(int)) + case float64: + return int32(tc.Result.(float64)) + case bool: + return tc.Result.(bool) + } + return tc.Result +} + +func TestFunctionTableAddFunction(t *testing.T) { + + type args struct { + functions []cesql.Function + } + tests := []struct { + name string + args args + wantErr bool + }{ + { + name: "Add user functions to global table", + + args: args{ + functions: TCKUserDefinedFunctions, + }, + wantErr: false, + }, + { + name: "Fail add user functions to global table", + args: args{ + functions: TCKUserDefinedFunctions, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, fn := range tt.args.functions { + if err := ceruntime.AddFunction(fn); (err != nil) != tt.wantErr { + t.Errorf("AddFunction() error = %v, wantErr %v", err, tt.wantErr) + } + } + }) + } +} + +func TestUserFunctions(t *testing.T) { + tckFiles := make([]TckFile, 0, len(TCKFileNames)) + + _, basePath, _, _ := runtime.Caller(0) + basePath, _ = path.Split(basePath) + + for _, testFile := range TCKFileNames { + testFilePath := path.Join(basePath, "tck", testFile+".yaml") + + t.Logf("Loading file %s", testFilePath) + file, err := os.Open(testFilePath) + require.NoError(t, err) + + fileBytes, err := io.ReadAll(file) + require.NoError(t, err) + + tckFileModel := TckFile{} + require.NoError(t, yaml.Unmarshal(fileBytes, &tckFileModel)) + + tckFiles = append(tckFiles, tckFileModel) + } + + for i, file := range tckFiles { + i := i + t.Run(file.Name, func(t *testing.T) { + for j, testCase := range tckFiles[i].Tests { + j := j + testCase := testCase + t.Run(testCase.Name, func(t *testing.T) { + t.Parallel() + testCase := tckFiles[i].Tests[j] + + t.Logf("Test expression: '%s'", testCase.Expression) + + if testCase.Error == ParseError { + _, err := parser.Parse(testCase.Expression) + require.NotNil(t, err) + return + } + + expr, err := parser.Parse(testCase.Expression) + require.NoError(t, err) + require.NotNil(t, expr) + + inputEvent := testCase.InputEvent(t) + result, err := expr.Evaluate(inputEvent) + + if testCase.Error != "" { + require.NotNil(t, err) + } else { + require.NoError(t, err) + require.Equal(t, testCase.ExpectedResult(), result) + } + }) + } + }) + } +} diff --git a/sql/v2/test/tck/like_expression.yaml b/sql/v2/test/tck/like_expression.yaml index b6bc5a18b..31e202638 100644 --- a/sql/v2/test/tck/like_expression.yaml +++ b/sql/v2/test/tck/like_expression.yaml @@ -115,4 +115,11 @@ tests: result: false - name: With type coercion from bool (4) expression: "FALSE LIKE 'fal%'" - result: true + result: true + + - name: Invalid string literal in comparison causes parse error + expression: "x LIKE 123" + result: false + error: parse + eventOverrides: + x: "123" diff --git a/sql/v2/test/tck_test.go b/sql/v2/test/tck_test.go index f215c8db4..d22555517 100644 --- a/sql/v2/test/tck_test.go +++ b/sql/v2/test/tck_test.go @@ -6,6 +6,7 @@ package test import ( + "fmt" "io" "os" "path" @@ -70,7 +71,7 @@ type TckTestCase struct { EventOverrides map[string]interface{} `json:"eventOverrides"` } -func (tc TckTestCase) InputEvent(t *testing.T) cloudevents.Event { +func (tc TckTestCase) InputEvent(tb testing.TB) cloudevents.Event { var inputEvent cloudevents.Event if tc.Event != nil { inputEvent = *tc.Event @@ -82,7 +83,7 @@ func (tc TckTestCase) InputEvent(t *testing.T) cloudevents.Event { inputEvent.SetSpecVersion(event.CloudEventsVersionV1) for k, v := range tc.EventOverrides { - require.NoError(t, spec.V1.SetAttribute(inputEvent.Context, k, v)) + require.NoError(tb, spec.V1.SetAttribute(inputEvent.Context, k, v)) } return inputEvent @@ -159,3 +160,60 @@ func TestTCK(t *testing.T) { }) } } + +func BenchmarkTCK(b *testing.B) { + tckFiles := make([]TckFile, 0, len(TCKFileNames)) + + _, basePath, _, _ := runtime.Caller(0) + basePath, _ = path.Split(basePath) + + for _, testFile := range TCKFileNames { + testFilePath := path.Join(basePath, "tck", testFile+".yaml") + + b.Logf("Loading file %s", testFilePath) + + file, err := os.Open(testFilePath) + require.NoError(b, err) + + fileBytes, err := io.ReadAll(file) + require.NoError(b, err) + + tckFileModel := TckFile{} + require.NoError(b, yaml.Unmarshal(fileBytes, &tckFileModel)) + + tckFiles = append(tckFiles, tckFileModel) + } + + for i, file := range tckFiles { + i := i + b.Run(file.Name, func(b *testing.B) { + for j, testCase := range tckFiles[i].Tests { + j := j + testCase := testCase + b.Run(fmt.Sprintf("%v parse", testCase.Name), func(b *testing.B) { + testCase := tckFiles[i].Tests[j] + for k := 0; k < b.N; k++ { + _, _ = parser.Parse(testCase.Expression) + } + }) + + if testCase.Error == ParseError { + return + } + + b.Run(fmt.Sprintf("%v evaluate", testCase.Name), func(b *testing.B) { + testCase := tckFiles[i].Tests[j] + + expr, _ := parser.Parse(testCase.Expression) + + inputEvent := testCase.InputEvent(b) + + for k := 0; k < b.N; k++ { + _, _ = expr.Evaluate(inputEvent) + } + + }) + } + }) + } +} diff --git a/test/benchmark/go.mod b/test/benchmark/go.mod index 0b8868605..fac555be1 100644 --- a/test/benchmark/go.mod +++ b/test/benchmark/go.mod @@ -49,7 +49,7 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/benchmark/go.sum b/test/benchmark/go.sum index 2b4e21ec9..baf8b10d9 100644 --- a/test/benchmark/go.sum +++ b/test/benchmark/go.sum @@ -78,13 +78,13 @@ go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/test/conformance/go.mod b/test/conformance/go.mod index 484123e6b..66f9e4d7c 100644 --- a/test/conformance/go.mod +++ b/test/conformance/go.mod @@ -49,6 +49,6 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect ) diff --git a/test/conformance/go.sum b/test/conformance/go.sum index ae068f9ff..35cf0a280 100644 --- a/test/conformance/go.sum +++ b/test/conformance/go.sum @@ -99,8 +99,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -110,8 +110,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/test/integration/go.mod b/test/integration/go.mod index 1fe9e1be4..b80046249 100644 --- a/test/integration/go.mod +++ b/test/integration/go.mod @@ -18,19 +18,23 @@ replace github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2 => ../../protocol replace github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2 => ../../protocol/mqtt_paho/v2 +replace github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2 => ../../protocol/kafka_confluent/v2 + require ( github.com/Azure/go-amqp v0.17.0 github.com/IBM/sarama v1.40.1 github.com/cloudevents/sdk-go/protocol/amqp/v2 v2.5.0 + github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2 v2.0.0-00010101000000-000000000000 github.com/cloudevents/sdk-go/protocol/kafka_sarama/v2 v2.5.0 github.com/cloudevents/sdk-go/protocol/mqtt_paho/v2 v2.0.0-00010101000000-000000000000 github.com/cloudevents/sdk-go/protocol/nats/v2 v2.5.0 github.com/cloudevents/sdk-go/protocol/nats_jetstream/v2 v2.0.0-00010101000000-000000000000 github.com/cloudevents/sdk-go/protocol/stan/v2 v2.5.0 - github.com/cloudevents/sdk-go/v2 v2.14.0 + github.com/cloudevents/sdk-go/v2 v2.15.2 + github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 github.com/eclipse/paho.golang v0.12.0 github.com/google/go-cmp v0.6.0 - github.com/google/uuid v1.1.1 + github.com/google/uuid v1.3.0 github.com/nats-io/nats.go v1.31.0 github.com/nats-io/stan.go v0.10.4 github.com/stretchr/testify v1.8.4 @@ -73,10 +77,10 @@ require ( go.etcd.io/bbolt v1.3.6 // indirect go.uber.org/multierr v1.1.0 // indirect go.uber.org/zap v1.10.0 // indirect - golang.org/x/crypto v0.17.0 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.4.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/test/integration/go.sum b/test/integration/go.sum index 084a0ea60..422913dd6 100644 --- a/test/integration/go.sum +++ b/test/integration/go.sum @@ -1,17 +1,29 @@ github.com/Azure/go-amqp v0.17.0 h1:HHXa3149nKrI0IZwyM7DRcRy5810t9ZICDutn4BYzj4= github.com/Azure/go-amqp v0.17.0/go.mod h1:9YJ3RhxRT1gquYnzpZO1vcYMMpAdJT+QEg6fwmw9Zlg= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/DataDog/datadog-go v2.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/IBM/sarama v1.40.1 h1:lL01NNg/iBeigUbT+wpPysuTYW6roHo6kc1QrffRf0k= github.com/IBM/sarama v1.40.1/go.mod h1:+5OFwA5Du9I6QrznhaMHsuwWdWZNMjaBSIxEWEgKOYE= +github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= +github.com/Microsoft/hcsshim v0.9.4 h1:mnUj0ivWy6UzbB1uLFqKR6F+ZyiDc7j4iGgHTpO+5+I= github.com/Shopify/toxiproxy/v2 v2.5.0 h1:i4LPT+qrSlKNtQf5QliVjdP08GyAH8+BUIc9gT0eahc= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878 h1:EFSB7Zo9Eg91v7MJPVsifUysc/wPdN+NOnVe6bWbdBM= github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0 h1:icCHutJouWlQREayFwCc7lxDAhws08td+W3/gdqgZts= +github.com/confluentinc/confluent-kafka-go/v2 v2.3.0/go.mod h1:/VTy8iEpe6mD9pkCH5BhijlUl8ulUXymKv1Qig5Rgb8= +github.com/containerd/cgroups v1.0.4 h1:jN/mbWBEaz+T1pi5OFtnkQ+8qnmEbAr1Oo1FRm5B0dA= +github.com/containerd/containerd v1.6.8 h1:h4dOFDwzHmqFEP754PgfgTeVXFnLiRc6kiqC7tplDJs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= +github.com/docker/docker v20.10.17+incompatible h1:JYCuMrWaVNophQTOrMMoSwudOVEfcegoZZrleKc1xwE= +github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/eapache/go-resiliency v1.3.0 h1:RRL0nge+cWGlxXbUzJ7yMcq6w2XBEr19dCN6HECGaT0= github.com/eapache/go-resiliency v1.3.0/go.mod h1:5yPzW0MIvSe0JDsv0v+DvcjEv2FyD6iZYSs1ZI+iQho= github.com/eapache/go-xerial-snappy v0.0.0-20230111030713-bf00bc1b83b6 h1:8yY/I9ndfrgrXUbOGObLHKBR4Fl3nZXwM2c7OYTT8hM= @@ -26,15 +38,17 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= @@ -80,6 +94,7 @@ github.com/klauspost/compress v1.17.2 h1:RlWWUY/Dr4fL8qk9YG7DTZ7PDgME2V4csBXA8L/ github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= @@ -90,11 +105,15 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/minio/highwayhash v1.0.2 h1:Aak5U0nElisjDCfPSG79Tgzkn2gl66NxOMspRrKnA/g= github.com/minio/highwayhash v1.0.2/go.mod h1:BQskDq+xkJ12lmlUUi7U0M5Swg3EWR+dLTk+kldvVxY= +github.com/moby/sys/mount v0.3.3 h1:fX1SVkXFJ47XWDoeFW4Sq7PdQJnV2QIDZAqjNqgEjUs= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296 h1:vU9tpM3apjYlLLeY23zRWJ9Zktr5jp+mloR942LEOpY= github.com/nats-io/jwt/v2 v2.2.1-0.20220113022732-58e87895b296/go.mod h1:0tqz9Hlu6bCBFLWAASKhE5vUA4c24L9KPUUgvwumE/k= github.com/nats-io/nats-server/v2 v2.9.23 h1:6Wj6H6QpP9FMlpCyWUaNu2yeZ/qGj+mdRkZ1wbikExU= @@ -109,6 +128,9 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/stan.go v0.10.4 h1:19GS/eD1SeQJaVkeM9EkvEYattnvnWrZ3wkSWSw4uXw= github.com/nats-io/stan.go v0.10.4/go.mod h1:3XJXH8GagrGqajoO/9+HgPyKV5MWsv7S5ccdda+pc6k= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= +github.com/opencontainers/runc v1.1.3 h1:vIXrkId+0/J2Ymu2m7VjGvbSlAId9XNRPhn2p4b+d8w= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= @@ -125,6 +147,7 @@ github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0ua github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -134,6 +157,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/testcontainers/testcontainers-go v0.14.0 h1:h0D5GaYG9mhOWr2qHdEKDXpkce/VlvaYOCzTRi6UBi8= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -141,6 +165,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.etcd.io/bbolt v1.3.6 h1:/ecaJf0sk1l4l6V4awd65v2C3ILy7MSj+s/x1ADCIMU= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= @@ -154,8 +179,8 @@ golang.org/x/crypto v0.0.0-20210314154223-e6e6c4f2bb5b/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= -golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= @@ -171,8 +196,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220725212005-46097bf591d3/go.mod h1:AaygXjzTFtRAg2ttMY5RMuhpJ3cNnI0XpyFJD1iQRSM= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -194,8 +219,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= @@ -216,6 +241,9 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633 h1:0BOZf6qNozI3pkN3fJLwNubheHJYHhMh91GRFOWWK08= +google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/test/integration/kafka_confluent/kafka_test.go b/test/integration/kafka_confluent/kafka_test.go new file mode 100644 index 000000000..5ae429b2e --- /dev/null +++ b/test/integration/kafka_confluent/kafka_test.go @@ -0,0 +1,139 @@ +/* + Copyright 2023 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + +package kafka_confluent + +import ( + "context" + "testing" + "time" + + "github.com/confluentinc/confluent-kafka-go/v2/kafka" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + confluent "github.com/cloudevents/sdk-go/protocol/kafka_confluent/v2" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/cloudevents/sdk-go/v2/event" + "github.com/cloudevents/sdk-go/v2/test" +) + +const ( + TEST_GROUP_ID = "test_confluent_group_id" + BOOTSTRAP_SERVER = "localhost:9192" +) + +type receiveEvent struct { + event cloudevents.Event + err error +} + +func TestSendEvent(t *testing.T) { + test.EachEvent(t, test.Events(), func(t *testing.T, eventIn event.Event) { + ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) + defer cancel() + + topicName := "test-ce-confluent-" + uuid.New().String() + // create the topic with kafka.AdminClient manually + admin, err := kafka.NewAdminClient(&kafka.ConfigMap{"bootstrap.servers": BOOTSTRAP_SERVER}) + require.NoError(t, err) + + _, err = admin.CreateTopics(ctx, []kafka.TopicSpecification{{ + Topic: topicName, + NumPartitions: 1, + ReplicationFactor: 1}}) + require.NoError(t, err) + + eventIn = test.ConvertEventExtensionsToString(t, eventIn) + + // start a cloudevents receiver client go to receive the event + eventChan := make(chan receiveEvent) + + receiverReady := make(chan bool) + go func() { + p, err := protocolFactory("", []string{topicName}) + if err != nil { + eventChan <- receiveEvent{err: err} + return + } + defer p.Close(ctx) + + client, err := cloudevents.NewClient(p) + if err != nil { + eventChan <- receiveEvent{err: err} + } + + receiverReady <- true + err = client.StartReceiver(ctx, func(event cloudevents.Event) { + eventChan <- receiveEvent{event: event} + }) + if err != nil { + eventChan <- receiveEvent{err: err} + } + }() + + <-receiverReady + + // start a cloudevents sender client go to send the event + p, err := protocolFactory(topicName, nil) + require.NoError(t, err) + defer p.Close(ctx) + + client, err := cloudevents.NewClient(p) + require.NoError(t, err) + res := client.Send(ctx, eventIn) + require.NoError(t, res) + + // check the received event + receivedEvent := <-eventChan + require.NoError(t, receivedEvent.err) + eventOut := test.ConvertEventExtensionsToString(t, receivedEvent.event) + + // test.AssertEventEquals(t, eventIn, receivedEvent.event) + err = test.AllOf( + test.HasExactlyAttributesEqualTo(eventIn.Context), + test.HasData(eventIn.Data()), + test.HasExtensionKeys([]string{confluent.KafkaPartitionKey, confluent.KafkaOffsetKey}), + test.HasExtension(confluent.KafkaTopicKey, topicName), + )(eventOut) + require.NoError(t, err) + }) +} + +// To start a local environment for testing: +// Option 1: Start it on port 9092 +// +// docker run --rm --net=host -p 9092:9092 confluentinc/confluent-local +// +// Option 2: Start it on port 9192 +// docker run --rm \ +// --name broker \ +// --hostname broker \ +// -p 9192:9192 \ +// -e KAFKA_ADVERTISED_LISTENERS='PLAINTEXT://broker:29192,PLAINTEXT_HOST://localhost:9192' \ +// -e KAFKA_CONTROLLER_QUORUM_VOTERS='1@broker:29193' \ +// -e KAFKA_LISTENERS='PLAINTEXT://broker:29192,CONTROLLER://broker:29193,PLAINTEXT_HOST://0.0.0.0:9192' \ +// confluentinc/confluent-local:latest +func protocolFactory(sendTopic string, receiveTopic []string, +) (*confluent.Protocol, error) { + + var p *confluent.Protocol + var err error + if receiveTopic != nil { + p, err = confluent.New(confluent.WithConfigMap(&kafka.ConfigMap{ + "bootstrap.servers": BOOTSTRAP_SERVER, + "group.id": TEST_GROUP_ID, + "auto.offset.reset": "earliest", + "enable.auto.commit": "true", + }), confluent.WithReceiverTopics(receiveTopic)) + } + if sendTopic != "" { + p, err = confluent.New(confluent.WithConfigMap(&kafka.ConfigMap{ + "bootstrap.servers": BOOTSTRAP_SERVER, + "go.delivery.reports": false, + }), confluent.WithSenderTopic(sendTopic)) + } + return p, err +} diff --git a/v2/alias.go b/v2/alias.go index 2fbfaa9a7..0f484b33b 100644 --- a/v2/alias.go +++ b/v2/alias.go @@ -173,6 +173,7 @@ var ( WithTarget = http.WithTarget WithHeader = http.WithHeader + WithHost = http.WithHost WithShutdownTimeout = http.WithShutdownTimeout //WithEncoding = http.WithEncoding //WithStructuredEncoding = http.WithStructuredEncoding // TODO: expose new way diff --git a/v2/event/datacodec/codec.go b/v2/event/datacodec/codec.go index 3e077740b..6f5d1f4c5 100644 --- a/v2/event/datacodec/codec.go +++ b/v2/event/datacodec/codec.go @@ -8,6 +8,7 @@ package datacodec import ( "context" "fmt" + "strings" "github.com/cloudevents/sdk-go/v2/event/datacodec/json" "github.com/cloudevents/sdk-go/v2/event/datacodec/text" @@ -26,9 +27,20 @@ type Encoder func(ctx context.Context, in interface{}) ([]byte, error) var decoder map[string]Decoder var encoder map[string]Encoder +// ssDecoder is a map of content-type structured suffixes as defined in +// [Structured Syntax Suffixes](https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml), +// which may be used to match content types such as application/vnd.custom-app+json +var ssDecoder map[string]Decoder + +// ssEncoder is a map of content-type structured suffixes similar to ssDecoder. +var ssEncoder map[string]Encoder + func init() { decoder = make(map[string]Decoder, 10) + ssDecoder = make(map[string]Decoder, 10) + encoder = make(map[string]Encoder, 10) + ssEncoder = make(map[string]Encoder, 10) AddDecoder("", json.Decode) AddDecoder("application/json", json.Decode) @@ -37,12 +49,18 @@ func init() { AddDecoder("text/xml", xml.Decode) AddDecoder("text/plain", text.Decode) + AddStructuredSuffixDecoder("json", json.Decode) + AddStructuredSuffixDecoder("xml", xml.Decode) + AddEncoder("", json.Encode) AddEncoder("application/json", json.Encode) AddEncoder("text/json", json.Encode) AddEncoder("application/xml", xml.Encode) AddEncoder("text/xml", xml.Encode) AddEncoder("text/plain", text.Encode) + + AddStructuredSuffixEncoder("json", json.Encode) + AddStructuredSuffixEncoder("xml", xml.Encode) } // AddDecoder registers a decoder for a given content type. The codecs will use @@ -51,12 +69,34 @@ func AddDecoder(contentType string, fn Decoder) { decoder[contentType] = fn } +// AddStructuredSuffixDecoder registers a decoder for content-types which match the given structured +// syntax suffix as defined by +// [Structured Syntax Suffixes](https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml). +// This allows users to register custom decoders for non-standard content types which follow the +// structured syntax suffix standard (e.g. application/vnd.custom-app+json). +// +// Suffix should not include the "+" character, and "json" and "xml" are registered by default. +func AddStructuredSuffixDecoder(suffix string, fn Decoder) { + ssDecoder[suffix] = fn +} + // AddEncoder registers an encoder for a given content type. The codecs will // use these to encode the data payload for a cloudevent.Event object. func AddEncoder(contentType string, fn Encoder) { encoder[contentType] = fn } +// AddStructuredSuffixEncoder registers an encoder for content-types which match the given +// structured syntax suffix as defined by +// [Structured Syntax Suffixes](https://www.iana.org/assignments/media-type-structured-suffix/media-type-structured-suffix.xhtml). +// This allows users to register custom encoders for non-standard content types which follow the +// structured syntax suffix standard (e.g. application/vnd.custom-app+json). +// +// Suffix should not include the "+" character, and "json" and "xml" are registered by default. +func AddStructuredSuffixEncoder(suffix string, fn Encoder) { + ssEncoder[suffix] = fn +} + // Decode looks up and invokes the decoder registered for the given content // type. An error is returned if no decoder is registered for the given // content type. @@ -64,6 +104,11 @@ func Decode(ctx context.Context, contentType string, in []byte, out interface{}) if fn, ok := decoder[contentType]; ok { return fn(ctx, in, out) } + + if fn, ok := ssDecoder[structuredSuffix(contentType)]; ok { + return fn(ctx, in, out) + } + return fmt.Errorf("[decode] unsupported content type: %q", contentType) } @@ -74,5 +119,19 @@ func Encode(ctx context.Context, contentType string, in interface{}) ([]byte, er if fn, ok := encoder[contentType]; ok { return fn(ctx, in) } + + if fn, ok := ssEncoder[structuredSuffix(contentType)]; ok { + return fn(ctx, in) + } + return nil, fmt.Errorf("[encode] unsupported content type: %q", contentType) } + +func structuredSuffix(contentType string) string { + parts := strings.Split(contentType, "+") + if len(parts) >= 2 { + return parts[len(parts)-1] + } + + return "" +} diff --git a/v2/event/datacodec/codec_test.go b/v2/event/datacodec/codec_test.go index 0fd96ef5d..bc6ec3558 100644 --- a/v2/event/datacodec/codec_test.go +++ b/v2/event/datacodec/codec_test.go @@ -11,9 +11,10 @@ import ( "strings" "testing" + "github.com/google/go-cmp/cmp" + "github.com/cloudevents/sdk-go/v2/event/datacodec" "github.com/cloudevents/sdk-go/v2/types" - "github.com/google/go-cmp/cmp" ) func strptr(s string) *string { return &s } @@ -25,11 +26,12 @@ type Example struct { func TestCodecDecode(t *testing.T) { testCases := map[string]struct { - contentType string - decoder datacodec.Decoder - in []byte - want interface{} - wantErr string + contentType string + decoder datacodec.Decoder + structuredSuffix string + in []byte + want interface{} + wantErr string }{ "empty": {}, "invalid content type": { @@ -50,12 +52,24 @@ func TestCodecDecode(t *testing.T) { "b": "banana", }, }, + "application/vnd.custom-type+json": { + contentType: "application/vnd.custom-type+json", + in: []byte(`{"a":"apple","b":"banana"}`), + want: &map[string]string{ + "a": "apple", + "b": "banana", + }, + }, "application/xml": { contentType: "application/xml", in: []byte(`7Hello, Structured Encoding v1.0!`), want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"}, }, - + "application/vnd.custom-type+xml": { + contentType: "application/vnd.custom-type+xml", + in: []byte(`7Hello, Structured Encoding v1.0!`), + want: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"}, + }, "custom content type": { contentType: "unit/testing", in: []byte("Hello, Testing"), @@ -82,12 +96,44 @@ func TestCodecDecode(t *testing.T) { }, wantErr: "expecting unit test error", }, + "custom structured suffix": { + contentType: "unit/testing+custom", + structuredSuffix: "custom", + in: []byte("Hello, Testing"), + decoder: func(ctx context.Context, in []byte, out interface{}) error { + if s, k := out.(*map[string]string); k { + if (*s) == nil { + (*s) = make(map[string]string) + } + (*s)["upper"] = strings.ToUpper(string(in)) + (*s)["lower"] = strings.ToLower(string(in)) + } + return nil + }, + want: &map[string]string{ + "upper": "HELLO, TESTING", + "lower": "hello, testing", + }, + }, + "custom structured suffix error": { + contentType: "unit/testing+custom", + structuredSuffix: "custom", + in: []byte("Hello, Testing"), + decoder: func(ctx context.Context, in []byte, out interface{}) error { + return fmt.Errorf("expecting unit test error") + }, + wantErr: "expecting unit test error", + }, } for n, tc := range testCases { t.Run(n, func(t *testing.T) { if tc.decoder != nil { - datacodec.AddDecoder(tc.contentType, tc.decoder) + if tc.structuredSuffix == "" { + datacodec.AddDecoder(tc.contentType, tc.decoder) + } else { + datacodec.AddStructuredSuffixDecoder(tc.structuredSuffix, tc.decoder) + } } got, _ := types.Allocate(tc.want) @@ -111,11 +157,12 @@ func TestCodecDecode(t *testing.T) { func TestCodecEncode(t *testing.T) { testCases := map[string]struct { - contentType string - encoder datacodec.Encoder - in interface{} - want []byte - wantErr string + contentType string + structuredSuffix string + encoder datacodec.Encoder + in interface{} + want []byte + wantErr string }{ "empty": {}, "invalid content type": { @@ -138,11 +185,24 @@ func TestCodecEncode(t *testing.T) { }, want: []byte(`{"a":"apple","b":"banana"}`), }, + "application/vnd.custom-type+json": { + contentType: "application/vnd.custom-type+json", + in: map[string]string{ + "a": "apple", + "b": "banana", + }, + want: []byte(`{"a":"apple","b":"banana"}`), + }, "application/xml": { contentType: "application/xml", in: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"}, want: []byte(`7Hello, Structured Encoding v1.0!`), }, + "application/vnd.custom-type+xml": { + contentType: "application/vnd.custom-type+xml", + in: &Example{Sequence: 7, Message: "Hello, Structured Encoding v1.0!"}, + want: []byte(`7Hello, Structured Encoding v1.0!`), + }, "custom content type": { contentType: "unit/testing", @@ -173,12 +233,47 @@ func TestCodecEncode(t *testing.T) { }, wantErr: "expecting unit test error", }, + "custom structured suffix": { + contentType: "unit/testing+custom", + structuredSuffix: "custom", + in: []string{ + "Hello,", + "Testing", + }, + encoder: func(ctx context.Context, in interface{}) ([]byte, error) { + if s, ok := in.([]string); ok { + sb := strings.Builder{} + for _, v := range s { + if sb.Len() > 0 { + sb.WriteString(" ") + } + sb.WriteString(v) + } + return []byte(sb.String()), nil + } + return nil, fmt.Errorf("don't get here") + }, + want: []byte("Hello, Testing"), + }, + "custom structured suffix error": { + contentType: "unit/testing+custom", + structuredSuffix: "custom", + in: []byte("Hello, Testing"), + encoder: func(ctx context.Context, in interface{}) ([]byte, error) { + return nil, fmt.Errorf("expecting unit test error") + }, + wantErr: "expecting unit test error", + }, } for n, tc := range testCases { t.Run(n, func(t *testing.T) { if tc.encoder != nil { - datacodec.AddEncoder(tc.contentType, tc.encoder) + if tc.structuredSuffix == "" { + datacodec.AddEncoder(tc.contentType, tc.encoder) + } else { + datacodec.AddStructuredSuffixEncoder(tc.structuredSuffix, tc.encoder) + } } got, err := datacodec.Encode(context.TODO(), tc.contentType, tc.in) diff --git a/v2/protocol/http/options.go b/v2/protocol/http/options.go index 6582af3ea..91a45ce36 100644 --- a/v2/protocol/http/options.go +++ b/v2/protocol/http/options.go @@ -72,6 +72,26 @@ func WithHeader(key, value string) Option { } } +// WithHost sets the outbound host header for all cloud events when using an HTTP request +func WithHost(value string) Option { + return func(p *Protocol) error { + if p == nil { + return fmt.Errorf("http host option can not set nil protocol") + } + value = strings.TrimSpace(value) + if value != "" { + if p.RequestTemplate == nil { + p.RequestTemplate = &nethttp.Request{ + Method: nethttp.MethodPost, + } + } + p.RequestTemplate.Host = value + return nil + } + return fmt.Errorf("http host option was empty string") + } +} + // WithShutdownTimeout sets the shutdown timeout when the http server is being shutdown. func WithShutdownTimeout(timeout time.Duration) Option { return func(p *Protocol) error { @@ -83,6 +103,38 @@ func WithShutdownTimeout(timeout time.Duration) Option { } } +// WithReadTimeout overwrites the default read timeout (600s) of the http +// server. The specified timeout must not be negative. A timeout of 0 disables +// read timeouts in the http server. +func WithReadTimeout(timeout time.Duration) Option { + return func(p *Protocol) error { + if p == nil { + return fmt.Errorf("http read timeout option can not set nil protocol") + } + if timeout < 0 { + return fmt.Errorf("http read timeout must not be negative") + } + p.readTimeout = &timeout + return nil + } +} + +// WithWriteTimeout overwrites the default write timeout (600s) of the http +// server. The specified timeout must not be negative. A timeout of 0 disables +// write timeouts in the http server. +func WithWriteTimeout(timeout time.Duration) Option { + return func(p *Protocol) error { + if p == nil { + return fmt.Errorf("http write timeout option can not set nil protocol") + } + if timeout < 0 { + return fmt.Errorf("http write timeout must not be negative") + } + p.writeTimeout = &timeout + return nil + } +} + func checkListen(p *Protocol, prefix string) error { switch { case p.listener.Load() != nil: diff --git a/v2/protocol/http/options_test.go b/v2/protocol/http/options_test.go index fd0af7fcf..58d12d91a 100644 --- a/v2/protocol/http/options_test.go +++ b/v2/protocol/http/options_test.go @@ -271,6 +271,77 @@ func TestWithHeader(t *testing.T) { } } +func TestWithHost(t *testing.T) { + testCases := map[string]struct { + t *Protocol + value string + want *Protocol + wantErr string + }{ + "valid host": { + t: &Protocol{ + RequestTemplate: &http.Request{}, + }, + value: "test", + want: &Protocol{ + RequestTemplate: &http.Request{ + Host: "test", + }, + }, + }, + "valid host, unset req": { + t: &Protocol{}, + value: "test", + want: &Protocol{ + RequestTemplate: &http.Request{ + Method: http.MethodPost, + Host: "test", + }, + }, + }, + "empty host value": { + t: &Protocol{ + RequestTemplate: &http.Request{}, + }, + wantErr: `http host option was empty string`, + }, + "whitespace key": { + t: &Protocol{ + RequestTemplate: &http.Request{}, + }, + value: " \t\n", + wantErr: `http host option was empty string`, + }, + "nil protocol": { + wantErr: `http host option can not set nil protocol`, + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + + err := tc.t.applyOptions(WithHost(tc.value)) + + if tc.wantErr != "" || err != nil { + var gotErr string + if err != nil { + gotErr = err.Error() + } + if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { + t.Errorf("unexpected error (-want, +got) = %v", diff) + } + return + } + + got := tc.t + + if diff := cmp.Diff(tc.want, got, + cmpopts.IgnoreUnexported(Protocol{}), cmpopts.IgnoreUnexported(http.Request{})); diff != "" { + t.Errorf("unexpected (-want, +got) = %v", diff) + } + }) + } +} + func TestWithShutdownTimeout(t *testing.T) { testCases := map[string]struct { t *Protocol @@ -315,6 +386,106 @@ func TestWithShutdownTimeout(t *testing.T) { } } +func TestWithReadTimeout(t *testing.T) { + expected := time.Minute * 4 + testCases := map[string]struct { + t *Protocol + timeout time.Duration + want *Protocol + wantErr string + }{ + "valid timeout": { + t: &Protocol{}, + timeout: time.Minute * 4, + want: &Protocol{ + readTimeout: &expected, + }, + }, + "negative timeout": { + t: &Protocol{}, + timeout: -1, + wantErr: "http read timeout must not be negative", + }, + "nil protocol": { + wantErr: "http read timeout option can not set nil protocol", + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + + err := tc.t.applyOptions(WithReadTimeout(tc.timeout)) + + if tc.wantErr != "" || err != nil { + var gotErr string + if err != nil { + gotErr = err.Error() + } + if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { + t.Errorf("unexpected error (-want, +got) = %v", diff) + } + return + } + + got := tc.t + + if diff := cmp.Diff(tc.want, got, + cmpopts.IgnoreUnexported(Protocol{})); diff != "" { + t.Errorf("unexpected (-want, +got) = %v", diff) + } + }) + } +} + +func TestWithWriteTimeout(t *testing.T) { + expected := time.Minute * 4 + + testCases := map[string]struct { + t *Protocol + timeout time.Duration + want *Protocol + wantErr string + }{ + "valid timeout": { + t: &Protocol{}, + timeout: time.Minute * 4, + want: &Protocol{ + writeTimeout: &expected, + }, + }, + "negative timeout": { + t: &Protocol{}, + timeout: -1, + wantErr: "http write timeout must not be negative", + }, + "nil protocol": { + wantErr: "http write timeout option can not set nil protocol", + }, + } + for n, tc := range testCases { + t.Run(n, func(t *testing.T) { + + err := tc.t.applyOptions(WithWriteTimeout(tc.timeout)) + + if tc.wantErr != "" || err != nil { + var gotErr string + if err != nil { + gotErr = err.Error() + } + if diff := cmp.Diff(tc.wantErr, gotErr); diff != "" { + t.Errorf("unexpected error (-want, +got) = %v", diff) + } + return + } + + got := tc.t + + if diff := cmp.Diff(tc.want, got, + cmpopts.IgnoreUnexported(Protocol{})); diff != "" { + t.Errorf("unexpected (-want, +got) = %v", diff) + } + }) + } +} func TestWithPort(t *testing.T) { testCases := map[string]struct { t *Protocol @@ -389,9 +560,19 @@ func forceClose(tr *Protocol) { } func TestWithPort0(t *testing.T) { + noReadWriteTimeout := time.Duration(0) + testCases := map[string]func() (*Protocol, error){ - "WithPort0": func() (*Protocol, error) { return New(WithPort(0)) }, - "SetPort0": func() (*Protocol, error) { return &Protocol{Port: 0}, nil }, + "WithPort0": func() (*Protocol, error) { + return New(WithPort(0)) + }, + "SetPort0": func() (*Protocol, error) { + return &Protocol{ + Port: 0, + readTimeout: &noReadWriteTimeout, + writeTimeout: &noReadWriteTimeout, + }, nil + }, } for name, f := range testCases { t.Run(name, func(t *testing.T) { diff --git a/v2/protocol/http/protocol.go b/v2/protocol/http/protocol.go index 7ee3b8fe1..18bd604a6 100644 --- a/v2/protocol/http/protocol.go +++ b/v2/protocol/http/protocol.go @@ -70,6 +70,18 @@ type Protocol struct { // If 0, DefaultShutdownTimeout is used. ShutdownTimeout time.Duration + // readTimeout defines the http.Server ReadTimeout It is the maximum duration + // for reading the entire request, including the body. If not overwritten by an + // option, the default value (600s) is used + readTimeout *time.Duration + + // writeTimeout defines the http.Server WriteTimeout It is the maximum duration + // before timing out writes of the response. It is reset whenever a new + // request's header is read. Like ReadTimeout, it does not let Handlers make + // decisions on a per-request basis. If not overwritten by an option, the + // default value (600s) is used + writeTimeout *time.Duration + // Port is the port configured to bind the receiver to. Defaults to 8080. // If you want to know the effective port you're listening to, use GetListeningPort() Port int @@ -116,6 +128,17 @@ func New(opts ...Option) (*Protocol, error) { p.ShutdownTimeout = DefaultShutdownTimeout } + // use default timeout from abuse protection value + defaultTimeout := DefaultTimeout + + if p.readTimeout == nil { + p.readTimeout = &defaultTimeout + } + + if p.writeTimeout == nil { + p.writeTimeout = &defaultTimeout + } + if p.isRetriableFunc == nil { p.isRetriableFunc = defaultIsRetriableFunc } diff --git a/v2/protocol/http/protocol_lifecycle.go b/v2/protocol/http/protocol_lifecycle.go index 04ef96915..7551c31c5 100644 --- a/v2/protocol/http/protocol_lifecycle.go +++ b/v2/protocol/http/protocol_lifecycle.go @@ -40,8 +40,8 @@ func (p *Protocol) OpenInbound(ctx context.Context) error { p.server = &http.Server{ Addr: listener.Addr().String(), Handler: attachMiddleware(p.Handler, p.middleware), - ReadTimeout: DefaultTimeout, - WriteTimeout: DefaultTimeout, + ReadTimeout: *p.readTimeout, + WriteTimeout: *p.writeTimeout, } // Shutdown diff --git a/v2/protocol/http/protocol_test.go b/v2/protocol/http/protocol_test.go index 818ef60c2..4014989e6 100644 --- a/v2/protocol/http/protocol_test.go +++ b/v2/protocol/http/protocol_test.go @@ -26,6 +26,7 @@ import ( func TestNew(t *testing.T) { dst := DefaultShutdownTimeout + ot := DefaultTimeout testCases := map[string]struct { opts []Option @@ -36,6 +37,8 @@ func TestNew(t *testing.T) { want: &Protocol{ Client: http.DefaultClient, ShutdownTimeout: dst, + readTimeout: &ot, + writeTimeout: &ot, Port: -1, }, },