diff --git a/amp/amp.go b/amp/amp.go index f425bc2..5fa1016 100644 --- a/amp/amp.go +++ b/amp/amp.go @@ -4,16 +4,23 @@ import ( "bytes" "compress/flate" "encoding/json" - "github.com/minus5/svckit/pkg/compress" "io" "net/url" "strings" "sync" "time" + jsoniter "github.com/json-iterator/go" + "github.com/minus5/svckit/pkg/compress" + "github.com/minus5/svckit/log" ) +var ( + jsonSerializer = jsoniter.Config{TagKey: "json"}.Froze() + backendSerializer = jsoniter.Config{TagKey: "backend"}.Froze() +) + // Message types const ( Publish uint8 = iota // stream updated message, view update types below @@ -64,7 +71,7 @@ const ( var ( compressionLenLimit = 8 * 1024 // do not compress messages smaller than - separtor = []byte{10} + separator = []byte{10} ) // Subscriber is the interface for subscribing to the topics @@ -86,17 +93,18 @@ type BodyMarshaler interface { // Msg basic application message structure type Msg struct { - Type uint8 `json:"t,omitempty"` // message type - ReplyTo string `json:"r,omitempty"` // topic to send replay to - CorrelationID uint64 `json:"i,omitempty"` // correlationID between request and response - Error *Error `json:"e,omitempty"` // error description in response message - URI string `json:"u,omitempty"` // has structure: topic/path - Ts int64 `json:"s,omitempty"` // timestamp unix milli - UpdateType uint8 `json:"p,omitempty"` // explains how to handle publish message - Replay uint8 `json:"l,omitempty"` // is this a re-play message (repeated) - Subscriptions map[string]int64 `json:"b,omitempty"` // topics to subscribe to - CacheDepth int `json:"d,omitempty"` // cache depth for append update type messages - Meta map[string]string `json:"m,omitempty"` // client session metadata + Type uint8 `json:"t,omitempty" backend:"t,omitempty"` // message type + ReplyTo string `json:"r,omitempty" backend:"r,omitempty"` // topic to send replay to + CorrelationID uint64 `json:"i,omitempty" backend:"i,omitempty"` // correlationID between request and response + Error *Error `json:"e,omitempty" backend:"e,omitempty"` // error description in response message + URI string `json:"u,omitempty" backend:"u,omitempty"` // has structure: topic/path + Ts int64 `json:"s,omitempty" backend:"s,omitempty"` // timestamp unix milli + UpdateType uint8 `json:"p,omitempty" backend:"p,omitempty"` // explains how to handle publish message + Replay uint8 `json:"l,omitempty" backend:"l,omitempty"` // is this a re-play message (repeated) + Subscriptions map[string]int64 `json:"b,omitempty" backend:"b,omitempty"` // topics to subscribe to + CacheDepth int `json:"d,omitempty" backend:"d,omitempty"` // cache depth for append update type messages + Meta map[string]string `json:"m,omitempty" backend:"m,omitempty"` // client session metadata + BackendHeaders map[string]string `json:"-" backend:"h,omitempty"` // exclusive for communication between backend services body []byte noCompression bool @@ -115,17 +123,28 @@ type Error struct { Code int `json:"c,omitempty"` } -// Parse decodes Msg from []byte +// Parse decodes Msg received from client. func Parse(buf []byte) *Msg { + return parse(buf, jsonSerializer) +} + +// ParseFromBackend parses the message received from another backend service. +func ParseFromBackend(buf []byte) *Msg { + return parse(buf, backendSerializer) +} + +func parse(buf []byte, serializer jsoniter.API) *Msg { if buf == nil { return nil } - parts := bytes.SplitN(buf, separtor, 2) + parts := bytes.SplitN(buf, separator, 2) + m := &Msg{} - if err := json.Unmarshal(parts[0], m); err != nil { + if err := serializer.Unmarshal(parts[0], m); err != nil { log.S("header", string(parts[0])).Error(err) return nil } + if len(parts) > 1 { m.body = parts[1] body, err := compress.GunzipIf(m.body) @@ -136,6 +155,7 @@ func Parse(buf []byte) *Msg { m.body = body } } + return m } @@ -167,19 +187,25 @@ func Undeflate(data []byte) []byte { return out.Bytes() } -// Marshal packs message for sending on the wire +// Marshal the message that will be sent to the client. func (m *Msg) Marshal() []byte { - buf, _ := m.marshal(CompressionNone, CompatibilityVersionDefault) + buf, _ := m.marshal(CompressionNone, CompatibilityVersionDefault, jsonSerializer) + return buf +} + +// MarshalForBackend is used when marshaling Msg for backend to backend communication. +func (m *Msg) MarshalForBackend() []byte { + buf, _ := m.marshal(CompressionNone, CompatibilityVersionDefault, backendSerializer) return buf } -// MarshalDeflate packs and compress message +// MarshalDeflate packs and compress message that will be sent to the client. func (m *Msg) MarshalDeflate() ([]byte, bool) { - return m.marshal(CompressionDeflate, CompatibilityVersionDefault) + return m.marshal(CompressionDeflate, CompatibilityVersionDefault, jsonSerializer) } // marshal encodes message into []byte -func (m *Msg) marshal(supportedCompression, version uint8) ([]byte, bool) { +func (m *Msg) marshal(supportedCompression, version uint8, serializer jsoniter.API) ([]byte, bool) { if version == CompatibilityVersion1 { if m.UpdateType == BurstStart || m.UpdateType == BurstEnd { // unsuported mesage types in this version @@ -188,6 +214,7 @@ func (m *Msg) marshal(supportedCompression, version uint8) ([]byte, bool) { } m.Lock() defer m.Unlock() + compression := supportedCompression if m.noCompression { compression = CompressionNone @@ -198,7 +225,7 @@ func (m *Msg) marshal(supportedCompression, version uint8) ([]byte, bool) { return payload, compression != CompressionNone } - payload := m.payload(version) + payload := m.payload(version, serializer) // decide wather we need compression if len(payload) < compressionLenLimit { m.noCompression = true @@ -217,15 +244,15 @@ func (m *Msg) marshal(supportedCompression, version uint8) ([]byte, bool) { return payload, compression != CompressionNone } -func (m *Msg) payload(version uint8) []byte { +func (m *Msg) payload(version uint8, serializer jsoniter.API) []byte { var header []byte if version == CompatibilityVersion1 { header = m.marshalV1header() } else { - header, _ = json.Marshal(m) + header, _ = serializer.Marshal(m) } buf := bytes.NewBuffer(header) - buf.Write(separtor) + buf.Write(separator) if m.body != nil { buf.Write(m.body) } @@ -265,9 +292,10 @@ func (m *Msg) Unmarshal(v interface{}) error { // Response creates response message from original request func (m *Msg) Response(o interface{}) *Msg { return &Msg{ - Type: Response, - CorrelationID: m.CorrelationID, - src: toBodyMarshaler(o), + Type: Response, + CorrelationID: m.CorrelationID, + src: toBodyMarshaler(o), + BackendHeaders: m.BackendHeaders, } } @@ -317,12 +345,13 @@ func (m *Msg) ResponseError(err error) *Msg { // Request creates request type message from original message func (m *Msg) Request() *Msg { return &Msg{ - Type: Request, - CorrelationID: m.CorrelationID, - URI: m.URI, - Meta: m.Meta, - src: m.src, - body: m.body, + Type: Request, + CorrelationID: m.CorrelationID, + URI: m.URI, + Meta: m.Meta, + BackendHeaders: m.BackendHeaders, + src: m.src, + body: m.body, } } diff --git a/amp/amp_test.go b/amp/amp_test.go index 9919944..6309d4c 100644 --- a/amp/amp_test.go +++ b/amp/amp_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestURI(t *testing.T) { @@ -43,8 +44,70 @@ func TestPublish(t *testing.T) { } func TestParse(t *testing.T) { - m := Parse(nil) - assert.Nil(t, m) + tests := []struct { + name string + in []byte + want *Msg + }{ + { + name: "it should return nil if input is nil", + in: nil, + want: nil, + }, + { + name: "it should parse the message, ignoring values in 'h' field", + in: []byte(`{"t":2,"u":"some.topic/method","i":4,"b":{"topic.one":1},"m":{"a":"b"},"h":{"a":"b"}}`), + want: &Msg{ + Type: Request, + URI: "some.topic/method", + CorrelationID: 4, + Subscriptions: map[string]int64{ + "topic.one": 1, + }, + Meta: map[string]string{ + "a": "b", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := Parse(tt.in) + require.Equal(t, tt.want, got) + }) + } +} + +func TestParseFromBackend(t *testing.T) { + tests := []struct { + name string + in []byte + want *Msg + }{ + { + name: "it should return nil if input is nil", + in: nil, + want: nil, + }, + { + name: "it should parse the message", + in: []byte(`{"t":2,"u":"some.topic/method","i":4,"h":{"a":"b"}}`), + want: &Msg{ + Type: Request, + URI: "some.topic/method", + CorrelationID: 4, + BackendHeaders: map[string]string{ + "a": "b", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := ParseFromBackend(tt.in) + require.Equal(t, tt.want, got) + }) + } } func TestParseV1Subscribe(t *testing.T) { @@ -99,3 +162,166 @@ func TestParseV1Subscriptions(t *testing.T) { assert.Equal(t, m.Subscriptions["sportsbook/s_4"], int64(1)) assert.Equal(t, m.Subscriptions["sportsbook/s_5"], int64(2)) } + +func TestMsg_Marshal(t *testing.T) { + tests := []struct { + name string + in *Msg + want []byte + }{ + { + name: "it should marshal the message without body", + in: &Msg{ + Type: Response, + CorrelationID: 4, + Error: &Error{ + Message: "error", + }, + URI: "some.topic", + Ts: 12, + UpdateType: Full, + Replay: Replay, + CacheDepth: 5, + Meta: map[string]string{ + "a": "b", + }, + }, + want: append( + []byte(`{"t":3,"i":4,"e":{"m":"error"},"u":"some.topic","s":12,"p":1,"l":1,"d":5,"m":{"a":"b"}}`), + []byte("\n")..., + ), + }, + { + name: "it should marshal the message", + in: &Msg{ + Type: Response, + CorrelationID: 4, + Error: &Error{ + Message: "error", + }, + URI: "some.topic", + Ts: 12, + UpdateType: Full, + Replay: Replay, + CacheDepth: 5, + Meta: map[string]string{ + "a": "b", + }, + body: []byte(`{"foo":"bar"}`), + }, + want: append( + append( + []byte(`{"t":3,"i":4,"e":{"m":"error"},"u":"some.topic","s":12,"p":1,"l":1,"d":5,"m":{"a":"b"}}`), + []byte("\n")..., + ), + []byte(`{"foo":"bar"}`)..., + ), + }, + { + name: "it should marshal the message that contains BackendHeaders without adding that headers to the marshalled payload", + in: &Msg{ + Type: Response, + CorrelationID: 4, + Error: &Error{ + Message: "error", + }, + URI: "some.topic", + Ts: 12, + UpdateType: Full, + Replay: Replay, + CacheDepth: 5, + Meta: map[string]string{ + "a": "b", + }, + BackendHeaders: map[string]string{ + "foo": "bar", + "bar": "baz", + }, + body: []byte(`{"foo":"bar"}`), + }, + want: append( + append( + []byte(`{"t":3,"i":4,"e":{"m":"error"},"u":"some.topic","s":12,"p":1,"l":1,"d":5,"m":{"a":"b"}}`), + []byte("\n")..., + ), + []byte(`{"foo":"bar"}`)..., + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.in.Marshal() + require.Equal(t, tt.want, got) + }) + } +} + +func TestMsg_MarshalForBackend(t *testing.T) { + tests := []struct { + name string + in *Msg + want []byte + }{ + { + name: "it should marshal the message without body", + in: &Msg{ + Type: Response, + CorrelationID: 4, + Meta: map[string]string{ + "a": "b", + }, + }, + want: append( + []byte(`{"t":3,"i":4,"m":{"a":"b"}}`), + []byte("\n")..., + ), + }, + { + name: "it should marshal the message", + in: &Msg{ + Type: Response, + CorrelationID: 4, + Meta: map[string]string{ + "a": "b", + }, + body: []byte(`{"foo":"bar"}`), + }, + want: append( + append( + []byte(`{"t":3,"i":4,"m":{"a":"b"}}`), + []byte("\n")..., + ), + + []byte(`{"foo":"bar"}`)..., + ), + }, + { + name: "it should marshal the message that contains BackendHeaders, adding that headers to the marshalled payload", + in: &Msg{ + Type: Response, + CorrelationID: 4, + Meta: map[string]string{ + "a": "b", + }, + BackendHeaders: map[string]string{ + "c": "d", + }, + body: []byte(`{"foo":"bar"}`), + }, + want: append( + append( + []byte(`{"t":3,"i":4,"m":{"a":"b"},"h":{"c":"d"}}`), + []byte("\n")..., + ), + + []byte(`{"foo":"bar"}`)..., + ), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := tt.in.MarshalForBackend() + require.Equal(t, tt.want, got) + }) + } +} diff --git a/amp/nsq/publisher.go b/amp/nsq/publisher.go index 7165ffb..550b5a3 100644 --- a/amp/nsq/publisher.go +++ b/amp/nsq/publisher.go @@ -8,7 +8,7 @@ import ( func Publish(topic string, in <-chan *amp.Msg) chan *amp.Msg { pub := nsq.Pub(topic) publish := func(m *amp.Msg) { - pub.Publish(m.Marshal()) + pub.Publish(m.MarshalForBackend()) } out := make(chan *amp.Msg, 16) go func() { @@ -34,7 +34,7 @@ func (p *Publisher) loop(in <-chan *amp.Msg) { pub := nsq.Pub("") publish := func(m *amp.Msg) { - pub.PublishTo(m.Topic(), m.Marshal()) + pub.PublishTo(m.Topic(), m.MarshalForBackend()) } for m := range in { diff --git a/amp/nsq/requester.go b/amp/nsq/requester.go index 2868f40..a11a124 100644 --- a/amp/nsq/requester.go +++ b/amp/nsq/requester.go @@ -60,7 +60,7 @@ func resposesTopicName() string { } func (r *Requester) responses(nm *nsq.Message) error { - m := amp.Parse(nm.Body) + m := amp.ParseFromBackend(nm.Body) r.reply(m.CorrelationID, m) return nil } @@ -91,7 +91,7 @@ func (r *Requester) Send(e amp.Subscriber, m *amp.Msg) { rm := m.Request() rm.CorrelationID = correlationID rm.ReplyTo = r.topic - buf := rm.Marshal() + buf := rm.MarshalForBackend() go func() { err := r.producer.PublishTo(m.Topic(), buf) diff --git a/amp/nsq/responder.go b/amp/nsq/responder.go index cd89506..ddc1cb8 100644 --- a/amp/nsq/responder.go +++ b/amp/nsq/responder.go @@ -41,7 +41,7 @@ func (r *Responder) loop(in <-chan *amp.Msg) { if rm == nil || m.ReplyTo == "" { continue } - if err := pub.PublishTo(m.ReplyTo, rm.Marshal()); err != nil { + if err := pub.PublishTo(m.ReplyTo, rm.MarshalForBackend()); err != nil { log.Error(err) } } diff --git a/amp/nsq/subscriber.go b/amp/nsq/subscriber.go index c634bdf..743e63e 100644 --- a/amp/nsq/subscriber.go +++ b/amp/nsq/subscriber.go @@ -19,7 +19,7 @@ type subscriber struct { func (s *subscriber) onMessage(m *nsq.Message) error { s.msgs.Add(1) defer s.msgs.Done() - am := amp.Parse(m.Body) + am := amp.ParseFromBackend(m.Body) if am == nil { return nil } diff --git a/amp/session/factory.go b/amp/session/factory.go index 1afe0da..877cd4e 100644 --- a/amp/session/factory.go +++ b/amp/session/factory.go @@ -22,14 +22,16 @@ type broker interface { } type connection interface { - Read() ([]byte, error) // get client message - Write(payload []byte, deflated bool) error // send message to the client - DeflateSupported() bool // does websocket connection support per message deflate - Headers() map[string]string // http headers we got on connection open - No() uint64 // connection identifier (for grouping logs) - Close() error // close connection - Meta() map[string]string // session metadata, set by the client - SetMeta(map[string]string) // set session metadata + Read() ([]byte, error) // get client message + Write(payload []byte, deflated bool) error // send message to the client + DeflateSupported() bool // does websocket connection support per message deflate + Headers() map[string]string // http headers we got on connection open + SetBackendHeaders(headers map[string]string) // set BackendHeaders + GetBackendHeaders() map[string]string // get BackendHeaders + No() uint64 // connection identifier (for grouping logs) + Close() error // close connection + Meta() map[string]string // session metadata, set by the client + SetMeta(map[string]string) // set session metadata GetRemoteIp() string GetCookie() string } diff --git a/amp/session/session.go b/amp/session/session.go index c5fde61..ae46144 100644 --- a/amp/session/session.go +++ b/amp/session/session.go @@ -150,6 +150,7 @@ func (s *session) receive(m *amp.Msg) { return } m.Meta = s.conn.Meta() + m.BackendHeaders = s.conn.GetBackendHeaders() s.requester.Send(s, m) case amp.Subscribe: s.broker.Subscribe(s, m.Subscriptions) diff --git a/amp/session/session_test.go b/amp/session/session_test.go index d01d85f..33badf7 100644 --- a/amp/session/session_test.go +++ b/amp/session/session_test.go @@ -13,8 +13,18 @@ import ( ) type mockConn struct { + t *testing.T + in chan []byte out chan []byte + + WantBackendHeaders int + gotBackendHeadersCalls int + ReturnBackendHeaders map[string]string + + WantMetaCalls int + gotMetaCalls int + ReturnMeta map[string]string } func (c *mockConn) Read() ([]byte, error) { @@ -31,18 +41,42 @@ func (c *mockConn) Write(payload []byte, deflated bool) error { c.out <- payload return nil } -func (c *mockConn) DeflateSupported() bool { return false } +func (c *mockConn) DeflateSupported() bool { return false } + +func (c *mockConn) SetBackendHeaders(_ map[string]string) {} + +func (c *mockConn) GetBackendHeaders() map[string]string { + c.gotBackendHeadersCalls++ + + require.True(c.t, c.WantBackendHeaders >= c.gotBackendHeadersCalls) + + return c.ReturnBackendHeaders +} + func (c *mockConn) Headers() map[string]string { return nil } func (c *mockConn) No() uint64 { return 0 } -func (c *mockConn) Meta() map[string]string { return nil } -func (c *mockConn) GetRemoteIp() string { return "" } -func (c *mockConn) GetCookie() string { return "" } + +func (c *mockConn) Meta() map[string]string { + c.gotMetaCalls++ + + require.True(c.t, c.WantMetaCalls >= c.gotMetaCalls) + + return c.ReturnMeta +} + +func (c *mockConn) GetRemoteIp() string { return "" } +func (c *mockConn) GetCookie() string { return "" } func (c *mockConn) Close() error { close(c.in) return nil } func (c *mockConn) SetMeta(m map[string]string) {} +func (c *mockConn) Assert(t *testing.T) { + require.Equal(t, c.WantBackendHeaders, c.gotBackendHeadersCalls) + require.Equal(t, c.WantMetaCalls, c.gotMetaCalls) +} + type mockBroker struct{} func (b *mockBroker) Subscribe(amp.Sender, map[string]int64) {} @@ -55,12 +89,28 @@ type mockRequester struct { WantSendCalls int gotSendCalls int + WantMsgs []*amp.Msg } -func (r *mockRequester) Send(subscriber amp.Subscriber, msg *amp.Msg) { +func (r *mockRequester) Send(_ amp.Subscriber, msg *amp.Msg) { r.gotSendCalls++ - require.True(r.t, r.WantSendCalls >= r.gotSendCalls) + + want := r.WantMsgs[r.gotSendCalls-1] + + // explicitly comparing only public fields + require.Equal(r.t, want.Type, msg.Type) + require.Equal(r.t, want.ReplyTo, msg.ReplyTo) + require.Equal(r.t, want.CorrelationID, msg.CorrelationID) + require.Equal(r.t, want.Error, msg.Error) + require.Equal(r.t, want.URI, msg.URI) + require.Equal(r.t, want.Ts, msg.Ts) + require.Equal(r.t, want.UpdateType, msg.UpdateType) + require.Equal(r.t, want.Replay, msg.Replay) + require.Equal(r.t, want.Subscriptions, msg.Subscriptions) + require.Equal(r.t, want.CacheDepth, msg.CacheDepth) + require.Equal(r.t, want.Meta, msg.Meta) + require.Equal(r.t, want.BackendHeaders, msg.BackendHeaders) } func (r *mockRequester) Unsubscribe(amp.Subscriber) {} @@ -179,7 +229,6 @@ func Test_session_receive(t *testing.T) { requester mockRequester topicWhitelist []string } - tests := []struct { name string fields fields @@ -191,8 +240,35 @@ func Test_session_receive(t *testing.T) { requester: mockRequester{ t: t, WantSendCalls: 1, + WantMsgs: []*amp.Msg{ + { + Type: amp.Request, + URI: "whitelisted.req/method", + Meta: map[string]string{ + "a": "b", + "c": "d", + }, + BackendHeaders: map[string]string{ + "foo": "bar", + "bar": "baz", + }, + }, + }, }, topicWhitelist: []string{"whitelisted.req"}, + conn: mockConn{ + t: t, + WantMetaCalls: 1, + ReturnMeta: map[string]string{ + "a": "b", + "c": "d", + }, + WantBackendHeaders: 1, + ReturnBackendHeaders: map[string]string{ + "foo": "bar", + "bar": "baz", + }, + }, }, in: &.Msg{ Type: amp.Request, @@ -226,6 +302,7 @@ func Test_session_receive(t *testing.T) { s.receive(tt.in) tt.fields.requester.Assert(t) + tt.fields.conn.Assert(t) }) } } diff --git a/amp/v1_compatibility.go b/amp/v1_compatibility.go index 1b9352e..9c31194 100644 --- a/amp/v1_compatibility.go +++ b/amp/v1_compatibility.go @@ -73,13 +73,13 @@ func ParseV1Subscriptions(buf []byte) *Msg { // Marshal packs message for sending on the wire func (m *Msg) MarshalV1() []byte { - buf, _ := m.marshal(CompressionNone, CompatibilityVersion1) + buf, _ := m.marshal(CompressionNone, CompatibilityVersion1, jsonSerializer) return buf } // MarshalDeflate packs and compress message func (m *Msg) MarshalV1Deflate() ([]byte, bool) { - return m.marshal(CompressionDeflate, CompatibilityVersion1) + return m.marshal(CompressionDeflate, CompatibilityVersion1, jsonSerializer) } func (m *Msg) marshalV1header() []byte { diff --git a/amp/ws/conn.go b/amp/ws/conn.go index 1f2b6d0..ca539c6 100644 --- a/amp/ws/conn.go +++ b/amp/ws/conn.go @@ -18,8 +18,9 @@ type Conn struct { tcpConn net.Conn cap connCap no uint64 - // receive chan []byte - // receiveErr error + + // backendHeaders can only be set and read on the backend. + backendHeaders map[string]string } // connCap connection capabilities and usefull atributes @@ -52,7 +53,6 @@ func newConn(tc net.Conn, cap connCap) *Conn { tcpConn: tc, cap: cap, no: no(), - // receive: make(chan []byte), } return c } @@ -62,6 +62,20 @@ func (c *Conn) Headers() map[string]string { return c.cap.headers } +func (c *Conn) SetBackendHeaders(headers map[string]string) { + if c.backendHeaders == nil { + c.backendHeaders = make(map[string]string) + } + + for k, v := range headers { + c.backendHeaders[k] = v + } +} + +func (c *Conn) GetBackendHeaders() map[string]string { + return c.backendHeaders +} + // Write writes payload to the websocket connection. func (c *Conn) Write(payload []byte, deflated bool) error { var header ws.Header diff --git a/go.mod b/go.mod index 644651e..48ab885 100644 --- a/go.mod +++ b/go.mod @@ -44,11 +44,14 @@ require ( github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/serf v0.10.1 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.13.6 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect github.com/onsi/ginkgo v1.11.0 // indirect github.com/onsi/gomega v1.7.0 // indirect diff --git a/go.sum b/go.sum index d52006d..081c056 100644 --- a/go.sum +++ b/go.sum @@ -116,6 +116,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/klauspost/compress v1.13.6 h1:P76CopJELS0TiO2mebmnzgWaajssP/EszplttgQxcgc= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= @@ -163,9 +165,12 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 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/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +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/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=