Merge branch 'master' into jjcollinge/pubsub-context
This commit is contained in:
commit
f868dc9497
|
|
@ -1,2 +1,3 @@
|
|||
/dist
|
||||
.idea
|
||||
.idea
|
||||
.vscode
|
||||
|
|
@ -3,7 +3,7 @@
|
|||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package twitter
|
||||
package cron
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package twitter
|
||||
package cron
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/dapr/components-contrib/bindings"
|
||||
"github.com/dapr/dapr/pkg/logger"
|
||||
|
|
@ -20,28 +22,22 @@ import (
|
|||
"github.com/dghubble/oauth1"
|
||||
)
|
||||
|
||||
type twitterInput struct {
|
||||
consumerKey string
|
||||
consumerSecret string
|
||||
accessToken string
|
||||
accessSecret string
|
||||
query string
|
||||
logger logger.Logger
|
||||
// Binding represents Twitter input/output binding
|
||||
type Binding struct {
|
||||
client *twitter.Client
|
||||
query string
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
var _ = bindings.InputBinding(&twitterInput{})
|
||||
var _ = bindings.InputBinding(&Binding{})
|
||||
|
||||
// NewTwitter returns a new Twitter event input binding
|
||||
func NewTwitter(logger logger.Logger) bindings.InputBinding {
|
||||
return &twitterInput{logger: logger}
|
||||
func NewTwitter(logger logger.Logger) *Binding {
|
||||
return &Binding{logger: logger}
|
||||
}
|
||||
|
||||
// Init initializes the Twitter binding
|
||||
func (t *twitterInput) Init(metadata bindings.Metadata) error {
|
||||
return t.parseMetadata(metadata)
|
||||
}
|
||||
|
||||
func (t *twitterInput) parseMetadata(metadata bindings.Metadata) error {
|
||||
func (t *Binding) Init(metadata bindings.Metadata) error {
|
||||
ck, f := metadata.Properties["consumerKey"]
|
||||
if !f || ck == "" {
|
||||
return fmt.Errorf("consumerKey not set")
|
||||
|
|
@ -58,36 +54,34 @@ func (t *twitterInput) parseMetadata(metadata bindings.Metadata) error {
|
|||
if !f || as == "" {
|
||||
return fmt.Errorf("accessSecret not set")
|
||||
}
|
||||
|
||||
// set query only in an input binding case
|
||||
q, f := metadata.Properties["query"]
|
||||
if !f || q == "" {
|
||||
return fmt.Errorf("query not set")
|
||||
if f {
|
||||
t.query = q
|
||||
}
|
||||
|
||||
t.consumerKey = ck
|
||||
t.consumerSecret = cs
|
||||
t.accessToken = at
|
||||
t.accessSecret = as
|
||||
t.query = q
|
||||
config := oauth1.NewConfig(ck, cs)
|
||||
token := oauth1.NewToken(at, as)
|
||||
|
||||
httpClient := config.Client(oauth1.NoContext, token)
|
||||
|
||||
t.client = twitter.NewClient(httpClient)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Operations returns list of operations supported by twitter binding
|
||||
func (t *Binding) Operations() []bindings.OperationKind {
|
||||
return []bindings.OperationKind{bindings.GetOperation}
|
||||
}
|
||||
|
||||
// Read triggers the Twitter search and events on each result tweet
|
||||
func (t *twitterInput) Read(handler func(*bindings.ReadResponse) error) error {
|
||||
config := oauth1.NewConfig(t.consumerKey, t.consumerSecret)
|
||||
token := oauth1.NewToken(t.accessToken, t.accessSecret)
|
||||
|
||||
httpClient := config.Client(oauth1.NoContext, token)
|
||||
client := twitter.NewClient(httpClient)
|
||||
|
||||
limit, _, err := client.RateLimits.Status(nil)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error checking rate status: %v", err)
|
||||
func (t *Binding) Read(handler func(*bindings.ReadResponse) error) error {
|
||||
if t.query == "" {
|
||||
return nil
|
||||
}
|
||||
t.logger.Debugf("rate limit: %+v", limit)
|
||||
|
||||
demux := twitter.NewSwitchDemux()
|
||||
|
||||
demux.Tweet = func(tweet *twitter.Tweet) {
|
||||
t.logger.Debugf("raw tweet: %+v", tweet)
|
||||
data, marshalErr := json.Marshal(tweet)
|
||||
|
|
@ -117,7 +111,7 @@ func (t *twitterInput) Read(handler func(*bindings.ReadResponse) error) error {
|
|||
}
|
||||
|
||||
t.logger.Debug("starting stream for query: %s", t.query)
|
||||
stream, err := client.Streams.Filter(filterParams)
|
||||
stream, err := t.client.Streams.Filter(filterParams)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error executing stream filter: %+v", filterParams)
|
||||
}
|
||||
|
|
@ -147,3 +141,75 @@ func (t *twitterInput) Read(handler func(*bindings.ReadResponse) error) error {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Invoke handles all operations
|
||||
func (t *Binding) Invoke(req *bindings.InvokeRequest) (*bindings.InvokeResponse, error) {
|
||||
t.logger.Debugf("operation: %v", req.Operation)
|
||||
if req.Metadata == nil {
|
||||
return nil, fmt.Errorf("metadata not set")
|
||||
}
|
||||
// required
|
||||
q, f := req.Metadata["query"]
|
||||
if !f || q == "" {
|
||||
return nil, fmt.Errorf("query not set")
|
||||
}
|
||||
|
||||
// optionals
|
||||
l, f := req.Metadata["lang"]
|
||||
if !f || l == "" {
|
||||
l = "en"
|
||||
}
|
||||
|
||||
r, f := req.Metadata["result"]
|
||||
if !f || r == "" {
|
||||
// mixed : Include both popular and real time results in the response
|
||||
// recent : return only the most recent results in the response
|
||||
// popular : return only the most popular results in the response
|
||||
r = "recent"
|
||||
}
|
||||
|
||||
var sinceID int64
|
||||
s, f := req.Metadata["since_id"]
|
||||
if f && s != "" {
|
||||
i, err := strconv.ParseInt(s, 10, 64)
|
||||
if err == nil {
|
||||
sinceID = i
|
||||
}
|
||||
}
|
||||
|
||||
sq := &twitter.SearchTweetParams{
|
||||
Count: 100, // max
|
||||
Lang: l,
|
||||
SinceID: sinceID,
|
||||
Query: q,
|
||||
ResultType: r,
|
||||
IncludeEntities: twitter.Bool(true),
|
||||
}
|
||||
|
||||
t.logger.Debug("starting stream for: %+v", sq)
|
||||
search, _, err := t.client.Search.Tweets(sq)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error executing search filter: %+v", sq)
|
||||
}
|
||||
if search == nil || search.Statuses == nil {
|
||||
return nil, errors.Wrapf(err, "nil search result from: %+v", sq)
|
||||
}
|
||||
|
||||
t.logger.Debugf("raw response: %+v", search.Statuses)
|
||||
data, marshalErr := json.Marshal(search.Statuses)
|
||||
if marshalErr != nil {
|
||||
t.logger.Errorf("error marshaling tweet: %v", marshalErr)
|
||||
return nil, errors.Wrapf(err, "error parsing response from: %+v", sq)
|
||||
}
|
||||
|
||||
req.Metadata["max_tweet_id"] = search.Metadata.MaxIDStr
|
||||
req.Metadata["tweet_count"] = string(search.Metadata.Count)
|
||||
req.Metadata["search_ts"] = time.Now().UTC().String()
|
||||
|
||||
ir := &bindings.InvokeResponse{
|
||||
Data: data,
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
return ir, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ const (
|
|||
testTwitterConsumerSecret = "test-consumerSecret"
|
||||
testTwitterAccessToken = "test-accessToken"
|
||||
testTwitterAccessSecret = "test-accessSecret"
|
||||
testTwitterQuery = "test-query"
|
||||
)
|
||||
|
||||
func getTestMetadata() bindings.Metadata {
|
||||
|
|
@ -31,31 +30,20 @@ func getTestMetadata() bindings.Metadata {
|
|||
"consumerSecret": testTwitterConsumerSecret,
|
||||
"accessToken": testTwitterAccessToken,
|
||||
"accessSecret": testTwitterAccessSecret,
|
||||
"query": testTwitterQuery,
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func TestParseMetadata(t *testing.T) {
|
||||
m := getTestMetadata()
|
||||
i := twitterInput{logger: logger.NewLogger("test")}
|
||||
err := i.parseMetadata(m)
|
||||
assert.Nilf(t, err, "error parsing valid metadata properties")
|
||||
assert.Equal(t, testTwitterConsumerKey, i.consumerKey, "consumerKey should be the same")
|
||||
assert.Equal(t, testTwitterConsumerSecret, i.consumerSecret, "consumerSecret should be the same")
|
||||
assert.Equal(t, testTwitterAccessToken, i.accessToken, "accessToken should be the same")
|
||||
assert.Equal(t, testTwitterAccessSecret, i.accessSecret, "accessSecret should be the same")
|
||||
|
||||
m.Properties["consumerKey"] = ""
|
||||
err = i.parseMetadata(m)
|
||||
assert.NotNilf(t, err, "no error parsing invalid metadata properties")
|
||||
|
||||
m.Properties["consumerKey"] = testTwitterConsumerKey
|
||||
m.Properties["query"] = ""
|
||||
err = i.parseMetadata(m)
|
||||
assert.NotNilf(t, err, "no error parsing invalid metadata properties")
|
||||
func getRuntimeMetadata() map[string]string {
|
||||
return map[string]string{
|
||||
"consumerKey": os.Getenv("CONSUMER_KEY"),
|
||||
"consumerSecret": os.Getenv("CONSUMER_SECRET"),
|
||||
"accessToken": os.Getenv("ACCESS_TOKEN"),
|
||||
"accessSecret": os.Getenv("ACCESS_SECRET"),
|
||||
}
|
||||
}
|
||||
|
||||
// go test -v -count=1 ./bindings/twitter/
|
||||
func TestInit(t *testing.T) {
|
||||
m := getTestMetadata()
|
||||
tw := NewTwitter(logger.NewLogger("test"))
|
||||
|
|
@ -66,8 +54,8 @@ func TestInit(t *testing.T) {
|
|||
// TestReadError excutes the Read method and fails before the Twitter API call
|
||||
// go test -v -count=1 -run TestReadError ./bindings/twitter/
|
||||
func TestReadError(t *testing.T) {
|
||||
m := getTestMetadata()
|
||||
tw := NewTwitter(logger.NewLogger("test"))
|
||||
m := getTestMetadata()
|
||||
err := tw.Init(m)
|
||||
assert.Nilf(t, err, "error initializing valid metadata properties")
|
||||
|
||||
|
|
@ -79,21 +67,19 @@ func TestReadError(t *testing.T) {
|
|||
}
|
||||
|
||||
// TestRead executes the Read method which calls Twiter API
|
||||
// test tokens must be set
|
||||
// go test -v -count=1 -run TestReed ./bindings/twitter/
|
||||
// env RUN_LIVE_TW_TEST=true go test -v -count=1 -run TestReed ./bindings/twitter/
|
||||
func TestReed(t *testing.T) {
|
||||
t.SkipNow() // skip this test until able to read credentials in test infra
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = map[string]string{
|
||||
"consumerKey": os.Getenv("CONSUMER_KEY"),
|
||||
"consumerSecret": os.Getenv("CONSUMER_SECRET"),
|
||||
"accessToken": os.Getenv("ACCESS_TOKEN"),
|
||||
"accessSecret": os.Getenv("ACCESS_SECRET"),
|
||||
"query": "microsoft",
|
||||
if os.Getenv("RUN_LIVE_TW_TEST") != "true" {
|
||||
t.SkipNow() // skip this test until able to read credentials in test infra
|
||||
}
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = getRuntimeMetadata()
|
||||
// add query
|
||||
m.Properties["query"] = "microsoft"
|
||||
tw := NewTwitter(logger.NewLogger("test"))
|
||||
tw.logger.SetOutputLevel(logger.DebugLevel)
|
||||
err := tw.Init(m)
|
||||
assert.Nilf(t, err, "error initializing valid metadata properties")
|
||||
assert.Nilf(t, err, "error initializing read")
|
||||
|
||||
counter := 0
|
||||
err = tw.Read(func(res *bindings.ReadResponse) error {
|
||||
|
|
@ -102,7 +88,33 @@ func TestReed(t *testing.T) {
|
|||
var tweet twitter.Tweet
|
||||
json.Unmarshal(res.Data, &tweet)
|
||||
assert.NotEmpty(t, tweet.IDStr, "tweet should have an ID")
|
||||
os.Exit(0)
|
||||
return nil
|
||||
})
|
||||
assert.Nilf(t, err, "error on read")
|
||||
}
|
||||
|
||||
// TestInvoke executes the Invoke method which calls Twiter API
|
||||
// test tokens must be set
|
||||
// env RUN_LIVE_TW_TEST=true go test -v -count=1 -run TestInvoke ./bindings/twitter/
|
||||
func TestInvoke(t *testing.T) {
|
||||
if os.Getenv("RUN_LIVE_TW_TEST") != "true" {
|
||||
t.SkipNow() // skip this test until able to read credentials in test infra
|
||||
}
|
||||
m := bindings.Metadata{}
|
||||
m.Properties = getRuntimeMetadata()
|
||||
tw := NewTwitter(logger.NewLogger("test"))
|
||||
tw.logger.SetOutputLevel(logger.DebugLevel)
|
||||
err := tw.Init(m)
|
||||
assert.Nilf(t, err, "error initializing Invoke")
|
||||
|
||||
req := &bindings.InvokeRequest{
|
||||
Metadata: map[string]string{
|
||||
"query": "microsoft",
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := tw.Invoke(req)
|
||||
assert.Nilf(t, err, "error on invoke")
|
||||
assert.NotNil(t, resp)
|
||||
}
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -44,6 +44,7 @@ require (
|
|||
github.com/hashicorp/consul/api v1.2.0
|
||||
github.com/hashicorp/go-multierror v1.0.0
|
||||
github.com/hazelcast/hazelcast-go-client v0.0.0-20190530123621-6cf767c2f31a
|
||||
github.com/jackc/pgx/v4 v4.6.0
|
||||
github.com/json-iterator/go v1.1.8
|
||||
github.com/kubernetes-client/go v0.0.0-20190625181339-cd8e39e789c7
|
||||
github.com/mitchellh/mapstructure v1.3.2 // indirect
|
||||
|
|
@ -61,7 +62,7 @@ require (
|
|||
github.com/sendgrid/sendgrid-go v3.5.0+incompatible
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271
|
||||
github.com/stretchr/testify v1.4.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/tidwall/pretty v1.0.1 // indirect
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20200122045848-3419fae592fc // indirect
|
||||
github.com/valyala/fasthttp v1.6.0
|
||||
|
|
|
|||
78
go.sum
78
go.sum
|
|
@ -175,6 +175,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
|||
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/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
|
||||
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
|
||||
github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY=
|
||||
github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
|
|
@ -186,11 +188,13 @@ github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHo
|
|||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c=
|
||||
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/dapr/components-contrib v0.0.0-20200219164914-5b75f4d0fbc6/go.mod h1:AZi8IGs8LFdywJg/YGwDs7MAxJkvGa8RgHN4NoJSKt0=
|
||||
github.com/dapr/dapr v0.4.1-0.20200228055659-71892bc0111e h1:njRp/SZ/zgqjSDywmy+Dn9oikkZqkqAHWGbfMarUuwo=
|
||||
github.com/dapr/dapr v0.4.1-0.20200228055659-71892bc0111e/go.mod h1:c60DJ9TdSdpbLjgqP55A5u4ZCYChFwa9UGYIXd9pmm4=
|
||||
|
|
@ -292,6 +296,8 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
|||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gocql/gocql v0.0.0-20191018090344-07ace3bab0f8 h1:ZyxBBeTImqFLu9mLtQUnXrO8K/SryXE/xjG/ygl0DxQ=
|
||||
github.com/gocql/gocql v0.0.0-20191018090344-07ace3bab0f8/go.mod h1:DL0ekTmBSTdlNF25Orwt/JMzqIq3EJ4MVa/J/uK64OY=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415 h1:WSBJMqJbLxsn+bTCPyPYZfqHdJmc8MK4wrBjMft6BAM=
|
||||
github.com/gogo/protobuf v0.0.0-20171007142547-342cbe0a0415/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
|
|
@ -443,6 +449,47 @@ github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N
|
|||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/improbable-eng/go-httpwares v0.0.0-20191126155631-6144c42a79c9/go.mod h1:LE9Hs6fsYQ7RoDuFUQlYmlRAku9vUlSlO++jWNj+D0I=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
|
||||
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
|
||||
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
|
||||
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
|
||||
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
|
||||
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
|
||||
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
|
||||
github.com/jackc/pgconn v1.5.0 h1:oFSOilzIZkyg787M1fEmyMfOUUvwj0daqYMfaWwNL4o=
|
||||
github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI=
|
||||
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2 h1:JVX6jT/XfzNqIjye4717ITLaNwV9mWbJx0dLCpcRzdA=
|
||||
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
|
||||
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1 h1:Rdjp4NFjwHnEslx2b66FfCI2S0LhO4itac3hXz6WX9M=
|
||||
github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8 h1:Q3tB+ExeflWUW7AFcAhXqk40s9mnNYLk1nOkKNZ5GnU=
|
||||
github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
|
||||
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
|
||||
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
|
||||
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
|
||||
github.com/jackc/pgtype v1.3.0 h1:l8JvKrby3RI7Kg3bYEeU9TA4vqC38QDpFCfcrC7KuN0=
|
||||
github.com/jackc/pgtype v1.3.0/go.mod h1:b0JqxHvPmljG+HQ5IsvQ0yqeSi4nGcDTVjFoiLDb0Ik=
|
||||
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
|
||||
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
|
||||
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
|
||||
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
|
||||
github.com/jackc/pgx/v4 v4.6.0 h1:Fh0O9GdlG4gYpjpwOqjdEodJUQM9jzN3Hdv7PN0xmm0=
|
||||
github.com/jackc/pgx/v4 v4.6.0/go.mod h1:vPh43ZzxijXUVJ+t/EmXBtFmbFVO72cuneCT9oAlxAg=
|
||||
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
|
||||
github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03 h1:FUwcHNlEqkqLjLBdCp5PRlCFijNjvcYANOZXzCfXwCM=
|
||||
github.com/jcmturner/gofork v0.0.0-20190328161633-dc7c13fece03/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o=
|
||||
github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8=
|
||||
|
|
@ -500,12 +547,15 @@ github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
|||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kubernetes-client/go v0.0.0-20190625181339-cd8e39e789c7 h1:NZlvd1Qf3MwoRhh87iVkJSHK3R31fX3D7kQfdJy6LnQ=
|
||||
github.com/kubernetes-client/go v0.0.0-20190625181339-cd8e39e789c7/go.mod h1:ks4KCmmxdXksTSu2dlnUanEOqNd/dsoyS6/7bay2RQ8=
|
||||
github.com/kubernetes-client/go v0.0.0-20190928040339-c757968c4c36 h1:/VKCfQgtQxBXEVU9UAJkW/ybm/070TBG57x2wxYUtXI=
|
||||
github.com/kubernetes-client/go v0.0.0-20190928040339-c757968c4c36/go.mod h1:ks4KCmmxdXksTSu2dlnUanEOqNd/dsoyS6/7bay2RQ8=
|
||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0=
|
||||
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
|
||||
|
|
@ -518,10 +568,18 @@ github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN
|
|||
github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149 h1:HfxbT6/JcvIljmERptWhwa8XzP7H3T+Z2N26gTsaDaA=
|
||||
github.com/mattn/go-ieproxy v0.0.0-20190610004146-91bb50d98149/go.mod h1:31jz6HNzdxOmlERGGEc4v/dMssOfmp2p5bT/okiKFFc=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
|
||||
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
|
||||
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/mediocregopher/radix.v2 v0.0.0-20181115013041-b67df6e626f9 h1:ViNuGS149jgnttqhc6XQNPwdupEMBXqCx9wtlW7P3sA=
|
||||
|
|
@ -676,6 +734,9 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
|||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v2.0.0+incompatible h1:cBXrhZNUf9C+La9/YpS+UHpUT8YD6Td9ZMSU9APFcsk=
|
||||
github.com/russross/blackfriday v2.0.0+incompatible/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
|
|
@ -694,6 +755,8 @@ github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
|||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
|
||||
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
|
||||
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.1.1 h1:VzGj7lhU7KEB9e9gMpAV/v5XT2NVSvLJhJLCWbnkgXg=
|
||||
|
|
@ -737,6 +800,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
|
|||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8=
|
||||
|
|
@ -777,6 +842,7 @@ github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcm
|
|||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb h1:ZkM6LRnq40pR1Ox0hTHlnpkcOTuFIDQpZ1IN8rKKhX0=
|
||||
github.com/yuin/gopher-lua v0.0.0-20191220021717-ab39c6098bdb/go.mod h1:gqRgreBUhTSL0GeU64rtZ3Uq3wtjOa/TB2YfrtkCbVQ=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/etcd v3.3.17+incompatible h1:g8iRku1SID8QAW8cDlV0L/PkZlw63LSiYEHYHoE6j/s=
|
||||
|
|
@ -791,11 +857,13 @@ go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs=
|
|||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
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/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/multierr v1.2.0 h1:6I+W7f5VwC5SV9dNrZ3qXrDB9mD0dyGOi/ZJmYw03T4=
|
||||
go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
go.uber.org/zap v1.11.0 h1:gSmpCfs+R47a4yQPAI4xJ0IPDLTRGXskm6UelqNXpqE=
|
||||
|
|
@ -809,6 +877,7 @@ golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnf
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190418165655-df01cb2cc480/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
|
@ -821,6 +890,7 @@ golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vK
|
|||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a h1:aczoJ0HPNE92XKa7DrIzkNN6esOKO2TBwiiYoKcINhA=
|
||||
golang.org/x/crypto v0.0.0-20200206161412-a0c6ece9d31a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
|
|
@ -870,6 +940,7 @@ golang.org/x/net v0.0.0-20190619014844-b5b0513f8c1b/go.mod h1:z5CRVTTTmAJ677TzLL
|
|||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190812203447-cdfb69ac37fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -907,6 +978,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -921,6 +993,7 @@ golang.org/x/sys v0.0.0-20190620070143-6f217b454f45/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47 h1:/XfQ9z7ib8eEJX2hdgFTZJ/ntt0swNk5oYBziWeTCvY=
|
||||
golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
@ -955,6 +1028,7 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3
|
|||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
|
|
@ -962,11 +1036,14 @@ golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgw
|
|||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c h1:2EA2K0k9bcvvEDlqD8xdlOhCOqq+O/p9Voqi4x9W1YU=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
|
@ -1039,6 +1116,7 @@ gopkg.in/couchbaselabs/jsonx.v1 v1.0.0/go.mod h1:oR201IRovxvLW/eISevH12/+MiKHtNQ
|
|||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
|
||||
gopkg.in/inf.v0 v0.9.0 h1:3zYtXIO92bvsdS3ggAdA8Gb4Azj0YU+TVY1uGYNFA8o=
|
||||
gopkg.in/inf.v0 v0.9.0/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
|
|
@ -13,9 +14,11 @@ const (
|
|||
// DefaultCloudEventType is the default event type for an Dapr published event
|
||||
DefaultCloudEventType = "com.dapr.event.sent"
|
||||
// CloudEventsSpecVersion is the specversion used by Dapr for the cloud events implementation
|
||||
CloudEventsSpecVersion = "0.3"
|
||||
CloudEventsSpecVersion = "1.0"
|
||||
//ContentType is the Cloud Events HTTP content type
|
||||
ContentType = "application/cloudevents+json"
|
||||
// DefaultCloudEventSource is the default event source
|
||||
DefaultCloudEventSource = "Dapr"
|
||||
)
|
||||
|
||||
// CloudEventsEnvelope describes the Dapr implementation of the Cloud Events spec
|
||||
|
|
@ -28,31 +31,81 @@ type CloudEventsEnvelope struct {
|
|||
DataContentType string `json:"datacontenttype"`
|
||||
Data interface{} `json:"data"`
|
||||
Subject string `json:"subject"`
|
||||
Topic string `json:"topic"`
|
||||
}
|
||||
|
||||
// NewCloudEventsEnvelope returns a new CloudEventsEnvelope
|
||||
func NewCloudEventsEnvelope(id, source, eventType, subject string, data []byte) *CloudEventsEnvelope {
|
||||
// NewCloudEventsEnvelope returns CloudEventsEnvelope from data or a new one when data content was not
|
||||
func NewCloudEventsEnvelope(id, source, eventType, subject string, topic string, data []byte) *CloudEventsEnvelope {
|
||||
// defaults
|
||||
if id == "" {
|
||||
id = uuid.New().String()
|
||||
}
|
||||
if source == "" {
|
||||
source = DefaultCloudEventSource
|
||||
}
|
||||
if eventType == "" {
|
||||
eventType = DefaultCloudEventType
|
||||
}
|
||||
contentType := ""
|
||||
|
||||
var i interface{}
|
||||
err := jsoniter.Unmarshal(data, &i)
|
||||
if err != nil {
|
||||
i = string(data)
|
||||
contentType = "text/plain"
|
||||
} else {
|
||||
contentType = "application/json"
|
||||
if subject == "" {
|
||||
subject = DefaultCloudEventSource
|
||||
}
|
||||
|
||||
// check if JSON
|
||||
var j interface{}
|
||||
err := jsoniter.Unmarshal(data, &j)
|
||||
if err != nil {
|
||||
// not JSON, return new envelope
|
||||
return &CloudEventsEnvelope{
|
||||
ID: id,
|
||||
SpecVersion: CloudEventsSpecVersion,
|
||||
DataContentType: "text/plain",
|
||||
Source: source,
|
||||
Type: eventType,
|
||||
Subject: subject,
|
||||
Topic: topic,
|
||||
Data: string(data),
|
||||
}
|
||||
}
|
||||
|
||||
// handle CloudEvent
|
||||
m, isMap := j.(map[string]interface{})
|
||||
if isMap {
|
||||
if _, isCE := m["specversion"]; isCE {
|
||||
ce := &CloudEventsEnvelope{
|
||||
ID: getStrVal(m, "id"),
|
||||
SpecVersion: getStrVal(m, "specversion"),
|
||||
DataContentType: getStrVal(m, "datacontenttype"),
|
||||
Source: getStrVal(m, "source"),
|
||||
Type: getStrVal(m, "type"),
|
||||
Subject: getStrVal(m, "subject"),
|
||||
Topic: topic,
|
||||
Data: m["data"],
|
||||
}
|
||||
// check if CE is valid
|
||||
if ce.ID != "" && ce.SpecVersion != "" && ce.DataContentType != "" {
|
||||
return ce
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// content was JSON but not a valid CloudEvent, make one
|
||||
return &CloudEventsEnvelope{
|
||||
ID: id,
|
||||
SpecVersion: CloudEventsSpecVersion,
|
||||
DataContentType: "application/json",
|
||||
Source: source,
|
||||
Type: eventType,
|
||||
Data: i,
|
||||
SpecVersion: CloudEventsSpecVersion,
|
||||
DataContentType: contentType,
|
||||
Subject: subject,
|
||||
Topic: topic,
|
||||
Data: j,
|
||||
}
|
||||
}
|
||||
|
||||
func getStrVal(m map[string]interface{}, key string) string {
|
||||
if v, k := m[key]; k {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,45 +6,92 @@
|
|||
package pubsub
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCreateCloudEventsEnvelope(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "eventType", "", nil)
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "eventType", "", "", nil)
|
||||
assert.NotNil(t, envelope)
|
||||
}
|
||||
|
||||
func TestEnvelopeUsingExistingCloudEvents(t *testing.T) {
|
||||
t.Run("cloud event content", func(t *testing.T) {
|
||||
str := `{
|
||||
"specversion" : "1.0",
|
||||
"type" : "com.github.pull.create",
|
||||
"source" : "https://github.com/cloudevents/spec/pull",
|
||||
"subject" : "123",
|
||||
"id" : "A234-1234-1234",
|
||||
"time" : "2018-04-05T17:31:00Z",
|
||||
"comexampleextension1" : "value",
|
||||
"comexampleothervalue" : 5,
|
||||
"datacontenttype" : "text/xml",
|
||||
"data" : "<much wow=\"xml\"/>"
|
||||
}`
|
||||
envelope := NewCloudEventsEnvelope("a", "", "", "", "routed.topic", []byte(str))
|
||||
assert.Equal(t, "A234-1234-1234", envelope.ID)
|
||||
assert.Equal(t, "text/xml", envelope.DataContentType)
|
||||
assert.Equal(t, "1.0", envelope.SpecVersion)
|
||||
assert.Equal(t, "routed.topic", envelope.Topic)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateFromJSON(t *testing.T) {
|
||||
t.Run("has JSON object", func(t *testing.T) {
|
||||
obj1 := struct {
|
||||
Val1 string
|
||||
Val2 int
|
||||
}{
|
||||
"test",
|
||||
1,
|
||||
}
|
||||
data, _ := json.Marshal(obj1)
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", "", data)
|
||||
t.Logf("data: %v", envelope.Data)
|
||||
assert.Equal(t, "application/json", envelope.DataContentType)
|
||||
|
||||
obj2 := struct {
|
||||
Val1 string
|
||||
Val2 int
|
||||
}{}
|
||||
err := json.Unmarshal(data, &obj2)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, obj1.Val1, obj2.Val1)
|
||||
assert.Equal(t, obj1.Val2, obj2.Val2)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCreateCloudEventsEnvelopeDefaults(t *testing.T) {
|
||||
t.Run("default event type", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", nil)
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", "", nil)
|
||||
assert.Equal(t, DefaultCloudEventType, envelope.Type)
|
||||
})
|
||||
|
||||
t.Run("non-default event type", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "e1", "", nil)
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "e1", "", "", nil)
|
||||
assert.Equal(t, "e1", envelope.Type)
|
||||
})
|
||||
|
||||
t.Run("spec version", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", nil)
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", "", nil)
|
||||
assert.Equal(t, CloudEventsSpecVersion, envelope.SpecVersion)
|
||||
})
|
||||
|
||||
t.Run("has data", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", []byte("data"))
|
||||
assert.Equal(t, "data", envelope.Data.(string))
|
||||
t.Run("quoted data", func(t *testing.T) {
|
||||
list := []string{"v1", "v2", "v3"}
|
||||
data := strings.Join(list, ",")
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", "", []byte(data))
|
||||
t.Logf("data: %v", envelope.Data)
|
||||
assert.Equal(t, "text/plain", envelope.DataContentType)
|
||||
assert.Equal(t, data, envelope.Data.(string))
|
||||
})
|
||||
|
||||
t.Run("string data content type", func(t *testing.T) {
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", []byte("data"))
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", "", []byte("data"))
|
||||
assert.Equal(t, "text/plain", envelope.DataContentType)
|
||||
})
|
||||
|
||||
t.Run("json data content type", func(t *testing.T) {
|
||||
str := `{ "data": "1" }`
|
||||
envelope := NewCloudEventsEnvelope("a", "source", "", "", []byte(str))
|
||||
assert.Equal(t, "application/json", envelope.DataContentType)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,263 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
/*
|
||||
Azure Blob Storage state store.
|
||||
|
||||
Sample configuration in yaml:
|
||||
|
||||
apiVersion: dapr.io/v1alpha1
|
||||
kind: Component
|
||||
metadata:
|
||||
name: statestore
|
||||
spec:
|
||||
type: state.azure.blobstorage
|
||||
metadata:
|
||||
- name: accountName
|
||||
value: <storage account name>
|
||||
- name: accountKey
|
||||
value: <key>
|
||||
- name: containerName
|
||||
value: <container Name>
|
||||
|
||||
Concurrency is supported with ETags according to https://docs.microsoft.com/en-us/azure/storage/common/storage-concurrency#managing-concurrency-in-blob-storage
|
||||
*/
|
||||
|
||||
package blobstorage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/Azure/azure-storage-blob-go/azblob"
|
||||
"github.com/dapr/components-contrib/state"
|
||||
"github.com/dapr/dapr/pkg/logger"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
)
|
||||
|
||||
const (
|
||||
keyDelimiter = "||"
|
||||
accountNameKey = "accountName"
|
||||
accountKeyKey = "accountKey"
|
||||
containerNameKey = "containerName"
|
||||
)
|
||||
|
||||
// StateStore Type
|
||||
type StateStore struct {
|
||||
containerURL azblob.ContainerURL
|
||||
json jsoniter.API
|
||||
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
type blobStorageMetadata struct {
|
||||
accountName string
|
||||
accountKey string
|
||||
containerName string
|
||||
}
|
||||
|
||||
// Init the connection to blob storage, optionally creates a blob container if it doesn't exist.
|
||||
func (r *StateStore) Init(metadata state.Metadata) error {
|
||||
meta, err := getBlobStorageMetadata(metadata.Properties)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
credential, err := azblob.NewSharedKeyCredential(meta.accountName, meta.accountKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid credentials with error: %s", err.Error())
|
||||
}
|
||||
|
||||
p := azblob.NewPipeline(credential, azblob.PipelineOptions{})
|
||||
|
||||
URL, _ := url.Parse(fmt.Sprintf("https://%s.blob.core.windows.net/%s", meta.accountName, meta.containerName))
|
||||
containerURL := azblob.NewContainerURL(*URL, p)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err = containerURL.Create(ctx, azblob.Metadata{}, azblob.PublicAccessNone)
|
||||
r.logger.Debugf("error creating container: %s", err)
|
||||
|
||||
r.containerURL = containerURL
|
||||
r.logger.Debugf("using container '%s'", meta.containerName)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete the state
|
||||
func (r *StateStore) Delete(req *state.DeleteRequest) error {
|
||||
r.logger.Debugf("delete %s", req.Key)
|
||||
return r.deleteFile(req)
|
||||
}
|
||||
|
||||
// BulkDelete the state
|
||||
func (r *StateStore) BulkDelete(req []state.DeleteRequest) error {
|
||||
r.logger.Debugf("bulk delete %v key(s)", len(req))
|
||||
for _, s := range req {
|
||||
err := r.Delete(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the state
|
||||
func (r *StateStore) Get(req *state.GetRequest) (*state.GetResponse, error) {
|
||||
r.logger.Debugf("fetching %s", req.Key)
|
||||
data, etag, err := r.readFile(req)
|
||||
if err != nil {
|
||||
r.logger.Debugf("error %s", err)
|
||||
|
||||
if isNotFoundError(err) {
|
||||
return &state.GetResponse{}, nil
|
||||
}
|
||||
|
||||
return &state.GetResponse{}, err
|
||||
}
|
||||
|
||||
return &state.GetResponse{
|
||||
Data: data,
|
||||
ETag: etag,
|
||||
}, err
|
||||
}
|
||||
|
||||
// Set the state
|
||||
func (r *StateStore) Set(req *state.SetRequest) error {
|
||||
r.logger.Debugf("saving %s", req.Key)
|
||||
return r.writeFile(req)
|
||||
}
|
||||
|
||||
// BulkSet the state
|
||||
func (r *StateStore) BulkSet(req []state.SetRequest) error {
|
||||
r.logger.Debugf("bulk set %v key(s)", len(req))
|
||||
|
||||
for _, s := range req {
|
||||
err := r.Set(&s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAzureBlobStorageStore instance
|
||||
func NewAzureBlobStorageStore(logger logger.Logger) *StateStore {
|
||||
return &StateStore{
|
||||
json: jsoniter.ConfigFastest,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func getBlobStorageMetadata(metadata map[string]string) (*blobStorageMetadata, error) {
|
||||
meta := blobStorageMetadata{}
|
||||
|
||||
if val, ok := metadata[accountNameKey]; ok && val != "" {
|
||||
meta.accountName = val
|
||||
} else {
|
||||
return nil, fmt.Errorf("missing or empty %s field from metadata", accountNameKey)
|
||||
}
|
||||
|
||||
if val, ok := metadata[accountKeyKey]; ok && val != "" {
|
||||
meta.accountKey = val
|
||||
} else {
|
||||
return nil, fmt.Errorf("missing of empty %s field from metadata", accountKeyKey)
|
||||
}
|
||||
|
||||
if val, ok := metadata[containerNameKey]; ok && val != "" {
|
||||
meta.containerName = val
|
||||
} else {
|
||||
return nil, fmt.Errorf("missing of empty %s field from metadata", containerNameKey)
|
||||
}
|
||||
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func (r *StateStore) readFile(req *state.GetRequest) ([]byte, string, error) {
|
||||
blobURL := r.containerURL.NewBlockBlobURL(getFileName(req.Key))
|
||||
|
||||
resp, err := blobURL.Download(context.Background(), 0, azblob.CountToEnd, azblob.BlobAccessConditions{}, false)
|
||||
if err != nil {
|
||||
r.logger.Debugf("download file %s, err %s", req.Key, err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
bodyStream := resp.Body(azblob.RetryReaderOptions{})
|
||||
data := bytes.Buffer{}
|
||||
_, err = data.ReadFrom(bodyStream)
|
||||
|
||||
if err != nil {
|
||||
r.logger.Debugf("read file %s, err %s", req.Key, err)
|
||||
return nil, "", err
|
||||
}
|
||||
return data.Bytes(), string(resp.ETag()), nil
|
||||
}
|
||||
|
||||
func (r *StateStore) writeFile(req *state.SetRequest) error {
|
||||
blobURL := r.containerURL.NewBlockBlobURL(getFileName(req.Key))
|
||||
|
||||
accessConditions := azblob.BlobAccessConditions{}
|
||||
|
||||
if req.Options.Concurrency == state.LastWrite {
|
||||
accessConditions.IfMatch = azblob.ETag(req.ETag)
|
||||
}
|
||||
|
||||
_, err := azblob.UploadBufferToBlockBlob(context.Background(), r.marshal(req), blobURL, azblob.UploadToBlockBlobOptions{
|
||||
Parallelism: 16,
|
||||
Metadata: req.Metadata,
|
||||
AccessConditions: accessConditions,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
r.logger.Debugf("write file %s, err %s", req.Key, err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *StateStore) deleteFile(req *state.DeleteRequest) error {
|
||||
blobURL := r.containerURL.NewBlockBlobURL(getFileName((req.Key)))
|
||||
accessConditions := azblob.BlobAccessConditions{}
|
||||
|
||||
if req.Options.Concurrency == state.LastWrite {
|
||||
accessConditions.IfMatch = azblob.ETag(req.ETag)
|
||||
}
|
||||
|
||||
_, err := blobURL.Delete(context.Background(), azblob.DeleteSnapshotsOptionNone, accessConditions)
|
||||
|
||||
if err != nil {
|
||||
r.logger.Debugf("delete file %s, err %s", req.Key, err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFileName(key string) string {
|
||||
pr := strings.Split(key, keyDelimiter)
|
||||
if len(pr) != 2 {
|
||||
return pr[0]
|
||||
}
|
||||
return pr[1]
|
||||
}
|
||||
|
||||
func (r *StateStore) marshal(req *state.SetRequest) []byte {
|
||||
var v string
|
||||
b, ok := req.Value.([]byte)
|
||||
if ok {
|
||||
v = string(b)
|
||||
} else {
|
||||
v, _ = jsoniter.MarshalToString(req.Value)
|
||||
}
|
||||
return []byte(v)
|
||||
}
|
||||
|
||||
func isNotFoundError(err error) bool {
|
||||
azureError, ok := err.(azblob.StorageError)
|
||||
return ok && azureError.ServiceCode() == azblob.ServiceCodeBlobNotFound
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package blobstorage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/dapr/components-contrib/state"
|
||||
"github.com/dapr/dapr/pkg/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestInit(t *testing.T) {
|
||||
m := state.Metadata{}
|
||||
s := NewAzureBlobStorageStore(logger.NewLogger("logger"))
|
||||
t.Run("Init with valid metadata", func(t *testing.T) {
|
||||
m.Properties = map[string]string{
|
||||
"accountName": "acc",
|
||||
"accountKey": "e+Dnvl8EOxYxV94nurVaRQ==",
|
||||
"containerName": "dapr",
|
||||
}
|
||||
err := s.Init(m)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "acc.blob.core.windows.net", s.containerURL.URL().Host)
|
||||
assert.Equal(t, "/dapr", s.containerURL.URL().Path)
|
||||
})
|
||||
|
||||
t.Run("Init with missing metadata", func(t *testing.T) {
|
||||
m.Properties = map[string]string{
|
||||
"invalidValue": "a",
|
||||
}
|
||||
err := s.Init(m)
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, err, fmt.Errorf("missing or empty accountName field from metadata"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestGetBlobStorageMetaData(t *testing.T) {
|
||||
t.Run("Nothing at all passed", func(t *testing.T) {
|
||||
m := make(map[string]string)
|
||||
_, err := getBlobStorageMetadata(m)
|
||||
|
||||
assert.NotNil(t, err)
|
||||
})
|
||||
|
||||
t.Run("All parameters passed and parsed", func(t *testing.T) {
|
||||
m := make(map[string]string)
|
||||
m["accountName"] = "acc"
|
||||
m["accountKey"] = "key"
|
||||
m["containerName"] = "dapr"
|
||||
meta, err := getBlobStorageMetadata(m)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "acc", meta.accountName)
|
||||
assert.Equal(t, "key", meta.accountKey)
|
||||
assert.Equal(t, "dapr", meta.containerName)
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileName(t *testing.T) {
|
||||
t.Run("Valid composite key", func(t *testing.T) {
|
||||
key := getFileName("app_id||key")
|
||||
assert.Equal(t, "key", key)
|
||||
})
|
||||
|
||||
t.Run("No delimiter present", func(t *testing.T) {
|
||||
key := getFileName("key")
|
||||
assert.Equal(t, "key", key)
|
||||
})
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@
|
|||
package cosmosdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -43,6 +44,16 @@ type CosmosItem struct {
|
|||
PartitionKey string `json:"partitionKey"`
|
||||
}
|
||||
|
||||
// CosmosItemWithRawMessage is a version of CosmosItem with a Value of RawMessage so this field
|
||||
// is not marshalled. If it is marshalled it will end up in a form that
|
||||
// cannot be unmarshalled. This type is used when SetRequest.Value arrives as bytes.
|
||||
type CosmosItemWithRawMessage struct {
|
||||
documentdb.Document
|
||||
ID string `json:"id"`
|
||||
Value jsoniter.RawMessage `json:"value"`
|
||||
PartitionKey string `json:"partitionKey"`
|
||||
}
|
||||
|
||||
type storedProcedureDefinition struct {
|
||||
ID string `json:"id"`
|
||||
Body string `json:"body"`
|
||||
|
|
@ -196,7 +207,20 @@ func (c *StateStore) Set(req *state.SetRequest) error {
|
|||
options = append(options, documentdb.ConsistencyLevel(documentdb.Eventual))
|
||||
}
|
||||
|
||||
_, err = c.client.UpsertDocument(c.collection.Self, CosmosItem{ID: req.Key, Value: req.Value, PartitionKey: partitionKey}, options...)
|
||||
b, ok := req.Value.([]uint8)
|
||||
if ok {
|
||||
// data arrived in bytes and already json. Don't marshal the Value field again.
|
||||
item := CosmosItemWithRawMessage{ID: req.Key, Value: b, PartitionKey: partitionKey}
|
||||
var marshalled []byte
|
||||
marshalled, err = convertToJSONWithoutEscapes(item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = c.client.UpsertDocument(c.collection.Self, marshalled, options...)
|
||||
} else {
|
||||
// data arrived as non-bytes, just pass it through.
|
||||
_, err = c.client.UpsertDocument(c.collection.Self, CosmosItem{ID: req.Key, Value: req.Value, PartitionKey: partitionKey}, options...)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -332,3 +356,11 @@ func populatePartitionMetadata(key string, requestMetadata map[string]string) st
|
|||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func convertToJSONWithoutEscapes(t interface{}) ([]byte, error) {
|
||||
buffer := &bytes.Buffer{}
|
||||
encoder := jsoniter.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(t)
|
||||
return buffer.Bytes(), err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"github.com/dapr/components-contrib/state"
|
||||
)
|
||||
|
||||
// dbAccess is a private interface which enables unit testing of PostgreSQL
|
||||
type dbAccess interface {
|
||||
Init(metadata state.Metadata) error
|
||||
Set(req *state.SetRequest) error
|
||||
Get(req *state.GetRequest) (*state.GetResponse, error)
|
||||
Delete(req *state.DeleteRequest) error
|
||||
ExecuteMulti(sets []state.SetRequest, deletes []state.DeleteRequest) error
|
||||
Close() error // io.Closer
|
||||
}
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/dapr/components-contrib/state"
|
||||
"github.com/dapr/dapr/pkg/logger"
|
||||
|
||||
// Blank import for the underlying PostgreSQL driver
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
const (
|
||||
connectionStringKey = "connectionString"
|
||||
errMissingConnectionString = "missing connection string"
|
||||
tableName = "state"
|
||||
)
|
||||
|
||||
// postgresDBAccess implements dbaccess
|
||||
type postgresDBAccess struct {
|
||||
logger logger.Logger
|
||||
metadata state.Metadata
|
||||
db *sql.DB
|
||||
connectionString string
|
||||
}
|
||||
|
||||
// newPostgresDBAccess creates a new instance of postgresAccess
|
||||
func newPostgresDBAccess(logger logger.Logger) *postgresDBAccess {
|
||||
logger.Debug("Instantiating new PostgreSQL state store")
|
||||
return &postgresDBAccess{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Init sets up PostgreSQL connection and ensures that the state table exists
|
||||
func (p *postgresDBAccess) Init(metadata state.Metadata) error {
|
||||
p.logger.Debug("Initializing PostgreSQL state store")
|
||||
p.metadata = metadata
|
||||
|
||||
if val, ok := metadata.Properties[connectionStringKey]; ok && val != "" {
|
||||
p.connectionString = val
|
||||
} else {
|
||||
p.logger.Error("Missing postgreSQL connection string")
|
||||
return fmt.Errorf(errMissingConnectionString)
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", p.connectionString)
|
||||
if err != nil {
|
||||
p.logger.Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
p.db = db
|
||||
|
||||
pingErr := db.Ping()
|
||||
if pingErr != nil {
|
||||
return pingErr
|
||||
}
|
||||
|
||||
err = p.ensureStateTable(tableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set makes an insert or update to the database.
|
||||
func (p *postgresDBAccess) Set(req *state.SetRequest) error {
|
||||
return state.SetWithRetries(p.setValue, req)
|
||||
}
|
||||
|
||||
// setValue is an internal implementation of set to enable passing the logic to state.SetWithRetries as a func.
|
||||
func (p *postgresDBAccess) setValue(req *state.SetRequest) error {
|
||||
p.logger.Debug("Setting state value in PostgreSQL")
|
||||
|
||||
err := state.CheckSetRequestOptions(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.Key == "" {
|
||||
return fmt.Errorf("missing key in set operation")
|
||||
}
|
||||
|
||||
var valueBytes []byte
|
||||
|
||||
// Convert to json string
|
||||
valueBytes, err = json.Marshal(req.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value := string(valueBytes)
|
||||
|
||||
var result sql.Result
|
||||
|
||||
// Sprintf is required for table name because sql.DB does not substitute parameters for table names.
|
||||
// Other parameters use sql.DB parameter substitution.
|
||||
if req.ETag == "" {
|
||||
result, err = p.db.Exec(fmt.Sprintf(
|
||||
`INSERT INTO %s (key, value) VALUES ($1, $2)
|
||||
ON CONFLICT (key) DO UPDATE SET value = $2, updatedate = NOW();`,
|
||||
tableName), req.Key, value)
|
||||
} else {
|
||||
// Convert req.ETag to integer for postgres compatibility
|
||||
var etag int
|
||||
etag, err = strconv.Atoi(req.ETag)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// When an etag is provided do an update - no insert
|
||||
result, err = p.db.Exec(fmt.Sprintf(
|
||||
`UPDATE %s SET value = $1, updatedate = NOW()
|
||||
WHERE key = $2 AND xmin = $3;`,
|
||||
tableName), value, req.Key, etag)
|
||||
}
|
||||
|
||||
return p.returnSingleDBResult(result, err)
|
||||
}
|
||||
|
||||
// Get returns data from the database. If data does not exist for the key an empty state.GetResponse will be returned.
|
||||
func (p *postgresDBAccess) Get(req *state.GetRequest) (*state.GetResponse, error) {
|
||||
p.logger.Debug("Getting state value from PostgreSQL")
|
||||
if req.Key == "" {
|
||||
return nil, fmt.Errorf("missing key in get operation")
|
||||
}
|
||||
|
||||
var value string
|
||||
var etag int
|
||||
err := p.db.QueryRow(fmt.Sprintf("SELECT value, xmin as etag FROM %s WHERE key = $1", tableName), req.Key).Scan(&value, &etag)
|
||||
if err != nil {
|
||||
// If no rows exist, return an empty response, otherwise return the error.
|
||||
if err == sql.ErrNoRows {
|
||||
return &state.GetResponse{}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &state.GetResponse{
|
||||
Data: []byte(value),
|
||||
ETag: strconv.Itoa(etag),
|
||||
Metadata: req.Metadata,
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Delete removes an item from the state store.
|
||||
func (p *postgresDBAccess) Delete(req *state.DeleteRequest) error {
|
||||
return state.DeleteWithRetries(p.deleteValue, req)
|
||||
}
|
||||
|
||||
// deleteValue is an internal implementation of delete to enable passing the logic to state.DeleteWithRetries as a func.
|
||||
func (p *postgresDBAccess) deleteValue(req *state.DeleteRequest) error {
|
||||
p.logger.Debug("Deleting state value from PostgreSQL")
|
||||
if req.Key == "" {
|
||||
return fmt.Errorf("missing key in delete operation")
|
||||
}
|
||||
|
||||
var result sql.Result
|
||||
var err error
|
||||
|
||||
if req.ETag == "" {
|
||||
result, err = p.db.Exec("DELETE FROM state WHERE key = $1", req.Key)
|
||||
} else {
|
||||
// Convert req.ETag to integer for postgres compatibility
|
||||
etag, conversionError := strconv.Atoi(req.ETag)
|
||||
if conversionError != nil {
|
||||
return conversionError
|
||||
}
|
||||
|
||||
result, err = p.db.Exec("DELETE FROM state WHERE key = $1 and xmin = $2", req.Key, etag)
|
||||
}
|
||||
|
||||
return p.returnSingleDBResult(result, err)
|
||||
}
|
||||
|
||||
func (p *postgresDBAccess) ExecuteMulti(sets []state.SetRequest, deletes []state.DeleteRequest) error {
|
||||
p.logger.Debug("Executing multiple PostgreSQL operations")
|
||||
tx, err := p.db.Begin()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(deletes) > 0 {
|
||||
for _, d := range deletes {
|
||||
da := d // Fix for gosec G601: Implicit memory aliasing in for loop.
|
||||
err = p.Delete(&da)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(sets) > 0 {
|
||||
for _, s := range sets {
|
||||
sa := s // Fix for gosec G601: Implicit memory aliasing in for loop.
|
||||
err = p.Set(&sa)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
return err
|
||||
}
|
||||
|
||||
// Verifies that the sql.Result affected only one row and no errors exist
|
||||
func (p *postgresDBAccess) returnSingleDBResult(result sql.Result, err error) error {
|
||||
if err != nil {
|
||||
p.logger.Debug(err)
|
||||
return err
|
||||
}
|
||||
|
||||
rowsAffected, resultErr := result.RowsAffected()
|
||||
|
||||
if resultErr != nil {
|
||||
p.logger.Error(resultErr)
|
||||
return resultErr
|
||||
}
|
||||
|
||||
if rowsAffected == 0 {
|
||||
noRowsErr := errors.New("database operation failed: no rows match given key and etag")
|
||||
p.logger.Error(noRowsErr)
|
||||
return noRowsErr
|
||||
}
|
||||
|
||||
if rowsAffected > 1 {
|
||||
tooManyRowsErr := errors.New("database operation failed: more than one row affected, expected one")
|
||||
p.logger.Error(tooManyRowsErr)
|
||||
return tooManyRowsErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements io.Close
|
||||
func (p *postgresDBAccess) Close() error {
|
||||
if p.db != nil {
|
||||
return p.db.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *postgresDBAccess) ensureStateTable(stateTableName string) error {
|
||||
exists, err := tableExists(p.db, stateTableName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !exists {
|
||||
p.logger.Info("Creating PostgreSQL state table")
|
||||
createTable := fmt.Sprintf(`CREATE TABLE %s (
|
||||
key text NOT NULL PRIMARY KEY,
|
||||
value json NOT NULL,
|
||||
insertdate TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
|
||||
updatedate TIMESTAMP WITH TIME ZONE NULL);`, stateTableName)
|
||||
_, err = p.db.Exec(createTable)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func tableExists(db *sql.DB, tableName string) (bool, error) {
|
||||
var exists bool = false
|
||||
err := db.QueryRow("SELECT EXISTS (SELECT FROM pg_tables where tablename = $1)", tableName).Scan(&exists)
|
||||
return exists, err
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/dapr/components-contrib/state"
|
||||
"github.com/dapr/dapr/pkg/logger"
|
||||
)
|
||||
|
||||
// PostgreSQL state store
|
||||
type PostgreSQL struct {
|
||||
logger logger.Logger
|
||||
dbaccess dbAccess
|
||||
}
|
||||
|
||||
// NewPostgreSQLStateStore creates a new instance of PostgreSQL state store
|
||||
func NewPostgreSQLStateStore(logger logger.Logger) *PostgreSQL {
|
||||
dba := newPostgresDBAccess(logger)
|
||||
return newPostgreSQLStateStore(logger, dba)
|
||||
}
|
||||
|
||||
// newPostgreSQLStateStore creates a newPostgreSQLStateStore instance of a PostgreSQL state store.
|
||||
// This unexported constructor allows injecting a dbAccess instance for unit testing.
|
||||
func newPostgreSQLStateStore(logger logger.Logger, dba dbAccess) *PostgreSQL {
|
||||
return &PostgreSQL{
|
||||
logger: logger,
|
||||
dbaccess: dba,
|
||||
}
|
||||
}
|
||||
|
||||
// Init initializes the SQL server state store
|
||||
func (p *PostgreSQL) Init(metadata state.Metadata) error {
|
||||
return p.dbaccess.Init(metadata)
|
||||
}
|
||||
|
||||
// Delete removes an entity from the store
|
||||
func (p *PostgreSQL) Delete(req *state.DeleteRequest) error {
|
||||
return p.dbaccess.Delete(req)
|
||||
}
|
||||
|
||||
// BulkDelete removes multiple entries from the store
|
||||
func (p *PostgreSQL) BulkDelete(req []state.DeleteRequest) error {
|
||||
return p.dbaccess.ExecuteMulti(nil, req)
|
||||
}
|
||||
|
||||
// Get returns an entity from store
|
||||
func (p *PostgreSQL) Get(req *state.GetRequest) (*state.GetResponse, error) {
|
||||
return p.dbaccess.Get(req)
|
||||
}
|
||||
|
||||
// Set adds/updates an entity on store
|
||||
func (p *PostgreSQL) Set(req *state.SetRequest) error {
|
||||
return p.dbaccess.Set(req)
|
||||
}
|
||||
|
||||
// BulkSet adds/updates multiple entities on store
|
||||
func (p *PostgreSQL) BulkSet(req []state.SetRequest) error {
|
||||
return p.dbaccess.ExecuteMulti(req, nil)
|
||||
}
|
||||
|
||||
// Multi handles multiple transactions. Implements TransactionalStore.
|
||||
func (p *PostgreSQL) Multi(reqs []state.TransactionalRequest) error {
|
||||
var deletes []state.DeleteRequest
|
||||
var sets []state.SetRequest
|
||||
for _, req := range reqs {
|
||||
switch req.Operation {
|
||||
case state.Upsert:
|
||||
if setReq, ok := req.Request.(state.SetRequest); ok {
|
||||
sets = append(sets, setReq)
|
||||
} else {
|
||||
return fmt.Errorf("expecting set request")
|
||||
}
|
||||
|
||||
case state.Delete:
|
||||
if delReq, ok := req.Request.(state.DeleteRequest); ok {
|
||||
deletes = append(deletes, delReq)
|
||||
} else {
|
||||
return fmt.Errorf("expecting delete request")
|
||||
}
|
||||
|
||||
default:
|
||||
return fmt.Errorf("unsupported operation: %s", req.Operation)
|
||||
}
|
||||
}
|
||||
|
||||
if len(sets) > 0 || len(deletes) > 0 {
|
||||
return p.dbaccess.ExecuteMulti(sets, deletes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close implements io.Closer
|
||||
func (p *PostgreSQL) Close() error {
|
||||
if p.dbaccess != nil {
|
||||
return p.dbaccess.Close()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,566 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/dapr/components-contrib/state"
|
||||
"github.com/dapr/dapr/pkg/logger"
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
connectionStringEnvKey = "DAPR_TEST_POSTGRES_CONNSTRING" // Environment variable containing the connection string
|
||||
)
|
||||
|
||||
type fakeItem struct {
|
||||
Color string
|
||||
}
|
||||
|
||||
func TestPostgreSQLIntegration(t *testing.T) {
|
||||
connectionString := getConnectionString()
|
||||
if connectionString == "" {
|
||||
t.Skipf("PostgreSQL state integration tests skipped. To enable define the connection string using environment variable '%s' (example 'export %s=\"host=localhost user=postgres password=example port=5432 connect_timeout=10 database=dapr_test\")", connectionStringEnvKey, connectionStringEnvKey)
|
||||
}
|
||||
|
||||
t.Run("Test init configurations", func(t *testing.T) {
|
||||
testInitConfiguration(t)
|
||||
})
|
||||
|
||||
metadata := state.Metadata{
|
||||
Properties: map[string]string{connectionStringKey: connectionString},
|
||||
}
|
||||
|
||||
pgs := NewPostgreSQLStateStore(logger.NewLogger("test"))
|
||||
t.Cleanup(func() {
|
||||
defer pgs.Close()
|
||||
})
|
||||
|
||||
error := pgs.Init(metadata)
|
||||
if error != nil {
|
||||
t.Fatal(error)
|
||||
}
|
||||
|
||||
t.Run("Create table succeeds", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testCreateTable(t, pgs.dbaccess.(*postgresDBAccess))
|
||||
})
|
||||
|
||||
t.Run("Get Set Delete one item", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
setGetUpdateDeleteOneItem(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Get item that does not exist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getItemThatDoesNotExist(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Get item with no key fails", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
getItemWithNoKey(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Set updates the updatedate field", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
setUpdatesTheUpdatedateField(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Set item with no key fails", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
setItemWithNoKey(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Bulk set and bulk delete", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testBulkSetAndBulkDelete(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Update and delete with etag succeeds", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
updateAndDeleteWithEtagSucceeds(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Update with old etag fails", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
updateWithOldEtagFails(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Insert with etag fails", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
newItemWithEtagFails(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Delete with invalid etag fails", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
deleteWithInvalidEtagFails(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Delete item with no key fails", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
deleteWithNoKeyFails(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Delete an item that does not exist", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
deleteItemThatDoesNotExist(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Multi with delete and set", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
multiWithDeleteAndSet(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Multi with delete only", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
multiWithDeleteOnly(t, pgs)
|
||||
})
|
||||
|
||||
t.Run("Multi with set only", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
multiWithSetOnly(t, pgs)
|
||||
})
|
||||
}
|
||||
|
||||
// setGetUpdateDeleteOneItem validates setting one item, getting it, and deleting it.
|
||||
func setGetUpdateDeleteOneItem(t *testing.T, pgs *PostgreSQL) {
|
||||
key := randomKey()
|
||||
//value := `{"something": "DKbLaZwrlCAZ"}`
|
||||
value := &fakeItem{Color: "yellow"}
|
||||
|
||||
setItem(t, pgs, key, value, "")
|
||||
|
||||
getResponse, outputObject := getItem(t, pgs, key)
|
||||
assert.Equal(t, value, outputObject)
|
||||
|
||||
newValue := &fakeItem{Color: "green"}
|
||||
setItem(t, pgs, key, newValue, getResponse.ETag)
|
||||
getResponse, outputObject = getItem(t, pgs, key)
|
||||
assert.Equal(t, newValue, outputObject)
|
||||
|
||||
deleteItem(t, pgs, key, getResponse.ETag)
|
||||
}
|
||||
|
||||
// testCreateTable tests the ability to create the state table.
|
||||
func testCreateTable(t *testing.T, dba *postgresDBAccess) {
|
||||
tableName := "test_state"
|
||||
|
||||
// Drop the table if it already exists
|
||||
exists, err := tableExists(dba.db, tableName)
|
||||
assert.Nil(t, err)
|
||||
if exists {
|
||||
dropTable(t, dba.db, tableName)
|
||||
}
|
||||
|
||||
// Create the state table and test for its existence
|
||||
err = dba.ensureStateTable(tableName)
|
||||
assert.Nil(t, err)
|
||||
exists, err = tableExists(dba.db, tableName)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
// Drop the state table
|
||||
dropTable(t, dba.db, tableName)
|
||||
}
|
||||
|
||||
func dropTable(t *testing.T, db *sql.DB, tableName string) {
|
||||
_, err := db.Exec(fmt.Sprintf("DROP TABLE %s", tableName))
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func deleteItemThatDoesNotExist(t *testing.T, pgs *PostgreSQL) {
|
||||
// Delete the item with a fake etag
|
||||
deleteReq := &state.DeleteRequest{
|
||||
Key: randomKey(),
|
||||
}
|
||||
err := pgs.Delete(deleteReq)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func multiWithSetOnly(t *testing.T, pgs *PostgreSQL) {
|
||||
var multiRequest []state.TransactionalRequest
|
||||
var setRequests []state.SetRequest
|
||||
for i := 0; i < 3; i++ {
|
||||
req := state.SetRequest{
|
||||
Key: randomKey(),
|
||||
Value: randomJSON(),
|
||||
}
|
||||
setRequests = append(setRequests, req)
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: state.Upsert,
|
||||
Request: req,
|
||||
})
|
||||
}
|
||||
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, set := range setRequests {
|
||||
assert.True(t, storeItemExists(t, set.Key))
|
||||
deleteItem(t, pgs, set.Key, "")
|
||||
}
|
||||
}
|
||||
|
||||
func multiWithDeleteOnly(t *testing.T, pgs *PostgreSQL) {
|
||||
var multiRequest []state.TransactionalRequest
|
||||
var deleteRequests []state.DeleteRequest
|
||||
for i := 0; i < 3; i++ {
|
||||
req := state.DeleteRequest{Key: randomKey()}
|
||||
|
||||
// Add the item to the database
|
||||
setItem(t, pgs, req.Key, randomJSON(), "") // Add the item to the database
|
||||
|
||||
// Add the item to a slice of delete requests
|
||||
deleteRequests = append(deleteRequests, req)
|
||||
|
||||
// Add the item to the multi transaction request
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: state.Delete,
|
||||
Request: req,
|
||||
})
|
||||
}
|
||||
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, delete := range deleteRequests {
|
||||
assert.False(t, storeItemExists(t, delete.Key))
|
||||
}
|
||||
}
|
||||
|
||||
func multiWithDeleteAndSet(t *testing.T, pgs *PostgreSQL) {
|
||||
var multiRequest []state.TransactionalRequest
|
||||
var deleteRequests []state.DeleteRequest
|
||||
for i := 0; i < 3; i++ {
|
||||
req := state.DeleteRequest{Key: randomKey()}
|
||||
|
||||
// Add the item to the database
|
||||
setItem(t, pgs, req.Key, randomJSON(), "") // Add the item to the database
|
||||
|
||||
// Add the item to a slice of delete requests
|
||||
deleteRequests = append(deleteRequests, req)
|
||||
|
||||
// Add the item to the multi transaction request
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: state.Delete,
|
||||
Request: req,
|
||||
})
|
||||
}
|
||||
|
||||
// Create the set requests
|
||||
var setRequests []state.SetRequest
|
||||
for i := 0; i < 3; i++ {
|
||||
req := state.SetRequest{
|
||||
Key: randomKey(),
|
||||
Value: randomJSON(),
|
||||
}
|
||||
setRequests = append(setRequests, req)
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: state.Upsert,
|
||||
Request: req,
|
||||
})
|
||||
}
|
||||
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.Nil(t, err)
|
||||
|
||||
for _, delete := range deleteRequests {
|
||||
assert.False(t, storeItemExists(t, delete.Key))
|
||||
}
|
||||
|
||||
for _, set := range setRequests {
|
||||
assert.True(t, storeItemExists(t, set.Key))
|
||||
deleteItem(t, pgs, set.Key, "")
|
||||
}
|
||||
}
|
||||
|
||||
func deleteWithInvalidEtagFails(t *testing.T, pgs *PostgreSQL) {
|
||||
// Create new item
|
||||
key := randomKey()
|
||||
value := &fakeItem{Color: "mauve"}
|
||||
setItem(t, pgs, key, value, "")
|
||||
|
||||
// Delete the item with a fake etag
|
||||
deleteReq := &state.DeleteRequest{
|
||||
Key: key,
|
||||
ETag: "1234",
|
||||
}
|
||||
err := pgs.Delete(deleteReq)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func deleteWithNoKeyFails(t *testing.T, pgs *PostgreSQL) {
|
||||
deleteReq := &state.DeleteRequest{
|
||||
Key: "",
|
||||
}
|
||||
err := pgs.Delete(deleteReq)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
// newItemWithEtagFails creates a new item and also supplies an ETag, which is invalid - expect failure
|
||||
func newItemWithEtagFails(t *testing.T, pgs *PostgreSQL) {
|
||||
value := &fakeItem{Color: "teal"}
|
||||
invalidEtag := "12345"
|
||||
|
||||
setReq := &state.SetRequest{
|
||||
Key: randomKey(),
|
||||
ETag: invalidEtag,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
err := pgs.Set(setReq)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func updateWithOldEtagFails(t *testing.T, pgs *PostgreSQL) {
|
||||
// Create and retrieve new item
|
||||
key := randomKey()
|
||||
value := &fakeItem{Color: "gray"}
|
||||
setItem(t, pgs, key, value, "")
|
||||
getResponse, _ := getItem(t, pgs, key)
|
||||
assert.NotNil(t, getResponse.ETag)
|
||||
originalEtag := getResponse.ETag
|
||||
|
||||
// Change the value and get the updated etag
|
||||
newValue := &fakeItem{Color: "silver"}
|
||||
setItem(t, pgs, key, newValue, originalEtag)
|
||||
_, updatedItem := getItem(t, pgs, key)
|
||||
assert.Equal(t, newValue, updatedItem)
|
||||
|
||||
// Update again with the original etag - expect udpate failure
|
||||
newValue = &fakeItem{Color: "maroon"}
|
||||
setReq := &state.SetRequest{
|
||||
Key: key,
|
||||
ETag: originalEtag,
|
||||
Value: newValue,
|
||||
}
|
||||
err := pgs.Set(setReq)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func updateAndDeleteWithEtagSucceeds(t *testing.T, pgs *PostgreSQL) {
|
||||
// Create and retrieve new item
|
||||
key := randomKey()
|
||||
value := &fakeItem{Color: "hazel"}
|
||||
setItem(t, pgs, key, value, "")
|
||||
getResponse, _ := getItem(t, pgs, key)
|
||||
assert.NotNil(t, getResponse.ETag)
|
||||
|
||||
// Change the value and compare
|
||||
value.Color = "purple"
|
||||
setItem(t, pgs, key, value, getResponse.ETag)
|
||||
updateResponse, updatedItem := getItem(t, pgs, key)
|
||||
assert.Equal(t, value, updatedItem)
|
||||
|
||||
// ETag should change when item is updated
|
||||
assert.NotEqual(t, getResponse.ETag, updateResponse.ETag)
|
||||
|
||||
// Delete
|
||||
deleteItem(t, pgs, key, updateResponse.ETag)
|
||||
|
||||
// Item is not in the data store
|
||||
assert.False(t, storeItemExists(t, key))
|
||||
}
|
||||
|
||||
// getItemThatDoesNotExist validates the behavior of retrieving an item that does not exist.
|
||||
func getItemThatDoesNotExist(t *testing.T, pgs *PostgreSQL) {
|
||||
key := randomKey()
|
||||
response, outputObject := getItem(t, pgs, key)
|
||||
assert.Nil(t, response.Data)
|
||||
assert.Equal(t, "", outputObject.Color)
|
||||
}
|
||||
|
||||
// getItemWithNoKey validates that attempting a Get operation without providing a key will return an error.
|
||||
func getItemWithNoKey(t *testing.T, pgs *PostgreSQL) {
|
||||
getReq := &state.GetRequest{
|
||||
Key: "",
|
||||
}
|
||||
|
||||
response, getErr := pgs.Get(getReq)
|
||||
assert.NotNil(t, getErr)
|
||||
assert.Nil(t, response)
|
||||
}
|
||||
|
||||
// setUpdatesTheUpdatedateField proves that the updateddate is set for an update, and not set upon insert.
|
||||
func setUpdatesTheUpdatedateField(t *testing.T, pgs *PostgreSQL) {
|
||||
key := randomKey()
|
||||
value := &fakeItem{Color: "orange"}
|
||||
setItem(t, pgs, key, value, "")
|
||||
|
||||
// insertdate should have a value and updatedate should be nil
|
||||
_, insertdate, updatedate := getRowData(t, key)
|
||||
assert.NotNil(t, insertdate)
|
||||
assert.Equal(t, "", updatedate.String)
|
||||
|
||||
// insertdate should not change, updatedate should have a value
|
||||
value = &fakeItem{Color: "aqua"}
|
||||
setItem(t, pgs, key, value, "")
|
||||
_, newinsertdate, updatedate := getRowData(t, key)
|
||||
assert.Equal(t, insertdate, newinsertdate) // The insertdate should not change.
|
||||
assert.NotEqual(t, "", updatedate.String)
|
||||
|
||||
deleteItem(t, pgs, key, "")
|
||||
}
|
||||
|
||||
func setItemWithNoKey(t *testing.T, pgs *PostgreSQL) {
|
||||
setReq := &state.SetRequest{
|
||||
Key: "",
|
||||
}
|
||||
|
||||
err := pgs.Set(setReq)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
// Tests valid bulk sets and deletes
|
||||
func testBulkSetAndBulkDelete(t *testing.T, pgs *PostgreSQL) {
|
||||
setReq := []state.SetRequest{
|
||||
{
|
||||
Key: randomKey(),
|
||||
Value: &fakeItem{Color: "blue"},
|
||||
},
|
||||
{
|
||||
Key: randomKey(),
|
||||
Value: &fakeItem{Color: "red"},
|
||||
},
|
||||
}
|
||||
|
||||
err := pgs.BulkSet(setReq)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, storeItemExists(t, setReq[0].Key))
|
||||
assert.True(t, storeItemExists(t, setReq[1].Key))
|
||||
|
||||
deleteReq := []state.DeleteRequest{
|
||||
{
|
||||
Key: setReq[0].Key,
|
||||
},
|
||||
{
|
||||
Key: setReq[1].Key,
|
||||
},
|
||||
}
|
||||
|
||||
err = pgs.BulkDelete(deleteReq)
|
||||
assert.Nil(t, err)
|
||||
assert.False(t, storeItemExists(t, setReq[0].Key))
|
||||
assert.False(t, storeItemExists(t, setReq[1].Key))
|
||||
}
|
||||
|
||||
// testInitConfiguration tests valid and invalid config settings
|
||||
func testInitConfiguration(t *testing.T) {
|
||||
logger := logger.NewLogger("test")
|
||||
tests := []struct {
|
||||
name string
|
||||
props map[string]string
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
props: map[string]string{},
|
||||
expectedErr: errMissingConnectionString,
|
||||
},
|
||||
{
|
||||
name: "Valid connection string",
|
||||
props: map[string]string{connectionStringKey: getConnectionString()},
|
||||
expectedErr: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
p := NewPostgreSQLStateStore(logger)
|
||||
defer p.Close()
|
||||
|
||||
metadata := state.Metadata{
|
||||
Properties: tt.props,
|
||||
}
|
||||
|
||||
err := p.Init(metadata)
|
||||
if tt.expectedErr == "" {
|
||||
assert.Nil(t, err)
|
||||
} else {
|
||||
assert.NotNil(t, err)
|
||||
assert.Equal(t, err.Error(), tt.expectedErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getConnectionString() string {
|
||||
return os.Getenv(connectionStringEnvKey)
|
||||
}
|
||||
|
||||
func setItem(t *testing.T, pgs *PostgreSQL, key string, value interface{}, etag string) {
|
||||
setReq := &state.SetRequest{
|
||||
Key: key,
|
||||
ETag: etag,
|
||||
Value: value,
|
||||
}
|
||||
|
||||
err := pgs.Set(setReq)
|
||||
assert.Nil(t, err)
|
||||
itemExists := storeItemExists(t, key)
|
||||
assert.True(t, itemExists)
|
||||
}
|
||||
|
||||
func getItem(t *testing.T, pgs *PostgreSQL, key string) (*state.GetResponse, *fakeItem) {
|
||||
getReq := &state.GetRequest{
|
||||
Key: key,
|
||||
Options: state.GetStateOption{},
|
||||
}
|
||||
|
||||
response, getErr := pgs.Get(getReq)
|
||||
assert.Nil(t, getErr)
|
||||
assert.NotNil(t, response)
|
||||
outputObject := &fakeItem{}
|
||||
_ = json.Unmarshal(response.Data, outputObject)
|
||||
return response, outputObject
|
||||
}
|
||||
|
||||
func deleteItem(t *testing.T, pgs *PostgreSQL, key string, etag string) {
|
||||
deleteReq := &state.DeleteRequest{
|
||||
Key: key,
|
||||
ETag: etag,
|
||||
Options: state.DeleteStateOption{},
|
||||
}
|
||||
|
||||
deleteErr := pgs.Delete(deleteReq)
|
||||
assert.Nil(t, deleteErr)
|
||||
assert.False(t, storeItemExists(t, key))
|
||||
}
|
||||
|
||||
func storeItemExists(t *testing.T, key string) bool {
|
||||
db, err := sql.Open("pgx", getConnectionString())
|
||||
assert.Nil(t, err)
|
||||
defer db.Close()
|
||||
|
||||
var exists bool = false
|
||||
statement := fmt.Sprintf(`SELECT EXISTS (SELECT FROM %s WHERE key = $1)`, tableName)
|
||||
err = db.QueryRow(statement, key).Scan(&exists)
|
||||
assert.Nil(t, err)
|
||||
return exists
|
||||
}
|
||||
|
||||
func getRowData(t *testing.T, key string) (returnValue string, insertdate sql.NullString, updatedate sql.NullString) {
|
||||
db, err := sql.Open("pgx", getConnectionString())
|
||||
assert.Nil(t, err)
|
||||
defer db.Close()
|
||||
|
||||
err = db.QueryRow(fmt.Sprintf("SELECT value, insertdate, updatedate FROM %s WHERE key = $1", tableName), key).Scan(&returnValue, &insertdate, &updatedate)
|
||||
assert.Nil(t, err)
|
||||
return returnValue, insertdate, updatedate
|
||||
}
|
||||
|
||||
func randomKey() string {
|
||||
return uuid.New().String()
|
||||
}
|
||||
|
||||
func randomJSON() *fakeItem {
|
||||
return &fakeItem{Color: randomKey()}
|
||||
}
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
// ------------------------------------------------------------
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
// ------------------------------------------------------------
|
||||
package postgresql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/dapr/components-contrib/state"
|
||||
"github.com/dapr/dapr/pkg/logger"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
fakeConnectionString = "not a real connection"
|
||||
)
|
||||
|
||||
// Fake implementation of interface postgressql.dbaccess
|
||||
type fakeDBaccess struct {
|
||||
logger logger.Logger
|
||||
initExecuted bool
|
||||
setExecuted bool
|
||||
getExecuted bool
|
||||
}
|
||||
|
||||
func (m *fakeDBaccess) Init(metadata state.Metadata) error {
|
||||
m.initExecuted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeDBaccess) Set(req *state.SetRequest) error {
|
||||
m.setExecuted = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeDBaccess) Get(req *state.GetRequest) (*state.GetResponse, error) {
|
||||
m.getExecuted = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *fakeDBaccess) Delete(req *state.DeleteRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeDBaccess) ExecuteMulti(sets []state.SetRequest, deletes []state.DeleteRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *fakeDBaccess) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Proves that the Init method runs the init method
|
||||
func TestInitRunsDBAccessInit(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, fake := createPostgreSQLWithFake(t)
|
||||
assert.True(t, fake.initExecuted)
|
||||
}
|
||||
|
||||
func TestMultiWithNoRequestsReturnsNil(t *testing.T) {
|
||||
t.Parallel()
|
||||
var multiRequest []state.TransactionalRequest
|
||||
pgs := createPostgreSQL(t)
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidMultiAction(t *testing.T) {
|
||||
t.Parallel()
|
||||
var multiRequest []state.TransactionalRequest
|
||||
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: "Something invalid",
|
||||
Request: createSetRequest(),
|
||||
})
|
||||
|
||||
pgs := createPostgreSQL(t)
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestValidSetRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
var multiRequest []state.TransactionalRequest
|
||||
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: state.Upsert,
|
||||
Request: createSetRequest(),
|
||||
})
|
||||
|
||||
pgs := createPostgreSQL(t)
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidMultiSetRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
var multiRequest []state.TransactionalRequest
|
||||
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: state.Upsert,
|
||||
Request: createDeleteRequest(), // Delete request is not valid for Upsert operation
|
||||
})
|
||||
|
||||
pgs := createPostgreSQL(t)
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func TestValidMultiDeleteRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
var multiRequest []state.TransactionalRequest
|
||||
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: state.Delete,
|
||||
Request: createDeleteRequest(),
|
||||
})
|
||||
|
||||
pgs := createPostgreSQL(t)
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestInvalidMultiDeleteRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
var multiRequest []state.TransactionalRequest
|
||||
|
||||
multiRequest = append(multiRequest, state.TransactionalRequest{
|
||||
Operation: state.Delete,
|
||||
Request: createSetRequest(), // Set request is not valid for Delete operation
|
||||
})
|
||||
|
||||
pgs := createPostgreSQL(t)
|
||||
err := pgs.Multi(multiRequest)
|
||||
assert.NotNil(t, err)
|
||||
}
|
||||
|
||||
func createSetRequest() state.SetRequest {
|
||||
return state.SetRequest{
|
||||
Key: randomKey(),
|
||||
Value: randomJSON(),
|
||||
}
|
||||
}
|
||||
|
||||
func createDeleteRequest() state.DeleteRequest {
|
||||
return state.DeleteRequest{
|
||||
Key: randomKey(),
|
||||
}
|
||||
}
|
||||
|
||||
func createPostgreSQLWithFake(t *testing.T) (*PostgreSQL, *fakeDBaccess) {
|
||||
pgs := createPostgreSQL(t)
|
||||
fake := pgs.dbaccess.(*fakeDBaccess)
|
||||
return pgs, fake
|
||||
}
|
||||
|
||||
func createPostgreSQL(t *testing.T) *PostgreSQL {
|
||||
logger := logger.NewLogger("test")
|
||||
|
||||
dba := &fakeDBaccess{
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
pgs := newPostgreSQLStateStore(logger, dba)
|
||||
assert.NotNil(t, pgs)
|
||||
|
||||
metadata := &state.Metadata{
|
||||
Properties: map[string]string{connectionStringKey: fakeConnectionString},
|
||||
}
|
||||
|
||||
err := pgs.Init(*metadata)
|
||||
|
||||
assert.Nil(t, err)
|
||||
assert.NotNil(t, pgs.dbaccess)
|
||||
|
||||
return pgs
|
||||
}
|
||||
Loading…
Reference in New Issue