diff --git a/adapter/dynamodb.go b/adapter/dynamodb.go new file mode 100644 index 0000000..3cd103b --- /dev/null +++ b/adapter/dynamodb.go @@ -0,0 +1,250 @@ +package adapter + +import ( + "context" + "encoding/json" + "io" + "net" + "net/http" + "strings" + "time" + + "github.com/bootjp/elastickv/kv" + "github.com/bootjp/elastickv/store" + "github.com/cockroachdb/errors" +) + +const ( + targetPrefix = "DynamoDB_20120810." + putItemTarget = targetPrefix + "PutItem" + getItemTarget = targetPrefix + "GetItem" + updateItemTarget = targetPrefix + "UpdateItem" + transactWriteItemsTarget = targetPrefix + "TransactWriteItems" +) + +const updateSplitCount = 2 + +type DynamoDBServer struct { + listen net.Listener + store store.ScanStore + coordinator kv.Coordinator + dynamoTranscoder *dynamodbTranscoder + httpServer *http.Server +} + +func NewDynamoDBServer(listen net.Listener, st store.ScanStore, coordinate *kv.Coordinate) *DynamoDBServer { + return &DynamoDBServer{ + listen: listen, + store: st, + coordinator: coordinate, + dynamoTranscoder: newDynamoDBTranscoder(), + } +} + +func (d *DynamoDBServer) Run() error { + mux := http.NewServeMux() + mux.HandleFunc("/", d.handle) + d.httpServer = &http.Server{Handler: mux, ReadHeaderTimeout: time.Second} + if err := d.httpServer.Serve(d.listen); err != nil && !errors.Is(err, http.ErrServerClosed) { + return errors.WithStack(err) + } + return nil +} + +func (d *DynamoDBServer) Stop() { + if d.httpServer != nil { + _ = d.httpServer.Shutdown(context.Background()) + } +} + +func (d *DynamoDBServer) handle(w http.ResponseWriter, r *http.Request) { + target := r.Header.Get("X-Amz-Target") + switch target { + case putItemTarget: + d.putItem(w, r) + case getItemTarget: + d.getItem(w, r) + case updateItemTarget: + d.updateItem(w, r) + case transactWriteItemsTarget: + d.transactWriteItems(w, r) + default: + http.Error(w, "unsupported operation", http.StatusBadRequest) + } +} + +func (d *DynamoDBServer) putItem(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + reqs, err := d.dynamoTranscoder.PutItemToRequest(body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if _, err = d.coordinator.Dispatch(reqs); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/x-amz-json-1.0") + _, _ = w.Write([]byte("{}")) +} + +type getItemInput struct { + TableName string `json:"TableName"` + Key map[string]attributeValue `json:"Key"` +} + +func (d *DynamoDBServer) getItem(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + var in getItemInput + if err := json.Unmarshal(body, &in); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + keyAttr, ok := in.Key["key"] + if !ok { + http.Error(w, "missing key", http.StatusBadRequest) + return + } + v, err := d.store.Get(r.Context(), []byte(keyAttr.S)) + if err != nil { + if errors.Is(err, store.ErrKeyNotFound) { + w.Header().Set("Content-Type", "application/x-amz-json-1.0") + _, _ = w.Write([]byte("{}")) + return + } + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + resp := map[string]map[string]attributeValue{ + "Item": { + "key": {S: keyAttr.S}, + "value": {S: string(v)}, + }, + } + out, err := json.Marshal(resp) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/x-amz-json-1.0") + _, _ = w.Write(out) +} + +type updateItemInput struct { + TableName string `json:"TableName"` + Key map[string]attributeValue `json:"Key"` + UpdateExpression string `json:"UpdateExpression"` + ConditionExpression string `json:"ConditionExpression"` + ExpressionAttributeNames map[string]string `json:"ExpressionAttributeNames"` + ExpressionAttributeValues map[string]attributeValue `json:"ExpressionAttributeValues"` +} + +func (d *DynamoDBServer) updateItem(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + var in updateItemInput + if err := json.Unmarshal(body, &in); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + keyAttr, ok := in.Key["key"] + if !ok { + http.Error(w, "missing key", http.StatusBadRequest) + return + } + key := []byte(keyAttr.S) + + if err := d.validateCondition(r.Context(), in.ConditionExpression, in.ExpressionAttributeNames, key); err != nil { + w.Header().Set("x-amzn-ErrorType", "ConditionalCheckFailedException") + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + updExpr := replaceNames(in.UpdateExpression, in.ExpressionAttributeNames) + parts := strings.SplitN(updExpr, "=", updateSplitCount) + if len(parts) != updateSplitCount { + http.Error(w, "invalid update expression", http.StatusBadRequest) + return + } + valPlaceholder := strings.TrimSpace(parts[1]) + valAttr, ok := in.ExpressionAttributeValues[valPlaceholder] + if !ok { + http.Error(w, "missing value attribute", http.StatusBadRequest) + return + } + + elem := &kv.Elem[kv.OP]{ + Op: kv.Put, + Key: key, + Value: []byte(valAttr.S), + } + req := &kv.OperationGroup[kv.OP]{ + IsTxn: false, + Elems: []*kv.Elem[kv.OP]{elem}, + } + if _, err = d.coordinator.Dispatch(req); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/x-amz-json-1.0") + _, _ = w.Write([]byte("{}")) +} + +func (d *DynamoDBServer) transactWriteItems(w http.ResponseWriter, r *http.Request) { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + reqs, err := d.dynamoTranscoder.TransactWriteItemsToRequest(body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if _, err = d.coordinator.Dispatch(reqs); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + w.Header().Set("Content-Type", "application/x-amz-json-1.0") + _, _ = w.Write([]byte("{}")) +} + +func replaceNames(expr string, names map[string]string) string { + for k, v := range names { + expr = strings.ReplaceAll(expr, k, v) + } + return expr +} + +func (d *DynamoDBServer) validateCondition(ctx context.Context, expr string, names map[string]string, key []byte) error { + expr = replaceNames(expr, names) + if expr == "" { + return nil + } + exists, err := d.store.Exists(ctx, key) + if err != nil { + return errors.WithStack(err) + } + switch { + case strings.HasPrefix(expr, "attribute_exists("): + if !exists { + return errors.New("conditional check failed") + } + case strings.HasPrefix(expr, "attribute_not_exists("): + if exists { + return errors.New("conditional check failed") + } + } + return nil +} diff --git a/adapter/dynamodb_test.go b/adapter/dynamodb_test.go new file mode 100644 index 0000000..f73f0c6 --- /dev/null +++ b/adapter/dynamodb_test.go @@ -0,0 +1,173 @@ +package adapter + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/dynamodb" + "github.com/stretchr/testify/assert" +) + +func TestDynamoDB_PutItem_GetItem(t *testing.T) { + t.Parallel() + nodes, _, _ := createNode(t, 1) + defer shutdown(nodes) + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("us-west-2"), + Endpoint: aws.String("http://" + nodes[0].dynamoAddress), + Credentials: credentials.NewStaticCredentials("dummy", "dummy", ""), + DisableSSL: aws.Bool(true), + }) + assert.NoError(t, err) + + client := dynamodb.New(sess) + + _, err = client.PutItemWithContext(context.Background(), &dynamodb.PutItemInput{ + TableName: aws.String("t"), + Item: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("test")}, + "value": {S: aws.String("v")}, + }, + }) + assert.NoError(t, err) + + out, err := client.GetItemWithContext(context.Background(), &dynamodb.GetItemInput{ + TableName: aws.String("t"), + Key: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("test")}, + }, + }) + assert.NoError(t, err) + assert.Equal(t, "test", aws.StringValue(out.Item["key"].S)) + assert.Equal(t, "v", aws.StringValue(out.Item["value"].S)) +} + +func TestDynamoDB_TransactWriteItems(t *testing.T) { + t.Parallel() + nodes, _, _ := createNode(t, 1) + defer shutdown(nodes) + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("us-west-2"), + Endpoint: aws.String("http://" + nodes[0].dynamoAddress), + Credentials: credentials.NewStaticCredentials("dummy", "dummy", ""), + DisableSSL: aws.Bool(true), + }) + assert.NoError(t, err) + + client := dynamodb.New(sess) + + _, err = client.TransactWriteItemsWithContext(context.Background(), &dynamodb.TransactWriteItemsInput{ + TransactItems: []*dynamodb.TransactWriteItem{ + { + Put: &dynamodb.Put{ + TableName: aws.String("t"), + Item: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("k1")}, + "value": {S: aws.String("v1")}, + }, + }, + }, + { + Put: &dynamodb.Put{ + TableName: aws.String("t"), + Item: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("k2")}, + "value": {S: aws.String("v2")}, + }, + }, + }, + }, + }) + assert.NoError(t, err) + + out1, err := client.GetItemWithContext(context.Background(), &dynamodb.GetItemInput{ + TableName: aws.String("t"), + Key: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("k1")}, + }, + }) + assert.NoError(t, err) + assert.Equal(t, "v1", aws.StringValue(out1.Item["value"].S)) + + out2, err := client.GetItemWithContext(context.Background(), &dynamodb.GetItemInput{ + TableName: aws.String("t"), + Key: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("k2")}, + }, + }) + assert.NoError(t, err) + assert.Equal(t, "v2", aws.StringValue(out2.Item["value"].S)) +} + +func TestDynamoDB_UpdateItem_Condition(t *testing.T) { + t.Parallel() + nodes, _, _ := createNode(t, 1) + defer shutdown(nodes) + + sess, err := session.NewSession(&aws.Config{ + Region: aws.String("us-west-2"), + Endpoint: aws.String("http://" + nodes[0].dynamoAddress), + Credentials: credentials.NewStaticCredentials("dummy", "dummy", ""), + DisableSSL: aws.Bool(true), + }) + assert.NoError(t, err) + + client := dynamodb.New(sess) + + _, err = client.PutItemWithContext(context.Background(), &dynamodb.PutItemInput{ + TableName: aws.String("t"), + Item: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("test")}, + "value": {S: aws.String("v1")}, + }, + }) + assert.NoError(t, err) + + _, err = client.UpdateItemWithContext(context.Background(), &dynamodb.UpdateItemInput{ + TableName: aws.String("t"), + Key: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("test")}, + }, + UpdateExpression: aws.String("SET #v = :val"), + ConditionExpression: aws.String("attribute_exists(#k)"), + ExpressionAttributeNames: map[string]*string{ + "#v": aws.String("value"), + "#k": aws.String("key"), + }, + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":val": {S: aws.String("v2")}, + }, + }) + assert.NoError(t, err) + + out, err := client.GetItemWithContext(context.Background(), &dynamodb.GetItemInput{ + TableName: aws.String("t"), + Key: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("test")}, + }, + }) + assert.NoError(t, err) + assert.Equal(t, "v2", aws.StringValue(out.Item["value"].S)) + + _, err = client.UpdateItemWithContext(context.Background(), &dynamodb.UpdateItemInput{ + TableName: aws.String("t"), + Key: map[string]*dynamodb.AttributeValue{ + "key": {S: aws.String("test")}, + }, + UpdateExpression: aws.String("SET #v = :val"), + ConditionExpression: aws.String("attribute_not_exists(#k)"), + ExpressionAttributeNames: map[string]*string{ + "#v": aws.String("value"), + "#k": aws.String("key"), + }, + ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{ + ":val": {S: aws.String("v3")}, + }, + }) + assert.Error(t, err) +} diff --git a/adapter/dynamodb_transcoder.go b/adapter/dynamodb_transcoder.go new file mode 100644 index 0000000..3b6734a --- /dev/null +++ b/adapter/dynamodb_transcoder.go @@ -0,0 +1,88 @@ +package adapter + +import ( + "encoding/json" + + "github.com/bootjp/elastickv/kv" + "github.com/cockroachdb/errors" +) + +type dynamodbTranscoder struct{} + +func newDynamoDBTranscoder() *dynamodbTranscoder { // create new transcoder + return &dynamodbTranscoder{} +} + +type attributeValue struct { + S string `json:"S"` +} + +type putItemInput struct { + TableName string `json:"TableName"` + Item map[string]attributeValue `json:"Item"` +} + +type transactWriteItemsInput struct { + TransactItems []transactWriteItem `json:"TransactItems"` +} + +type transactWriteItem struct { + Put *putItemInput `json:"Put,omitempty"` +} + +func (t *dynamodbTranscoder) PutItemToRequest(b []byte) (*kv.OperationGroup[kv.OP], error) { + var in putItemInput + if err := json.Unmarshal(b, &in); err != nil { + return nil, errors.WithStack(err) + } + keyAttr, ok := in.Item["key"] + if !ok { + return nil, errors.New("missing key attribute") + } + valAttr, ok := in.Item["value"] + if !ok { + return nil, errors.New("missing value attribute") + } + return &kv.OperationGroup[kv.OP]{ + IsTxn: false, + Elems: []*kv.Elem[kv.OP]{ + { + Op: kv.Put, + Key: []byte(keyAttr.S), + Value: []byte(valAttr.S), + }, + }, + }, nil +} + +func (t *dynamodbTranscoder) TransactWriteItemsToRequest(b []byte) (*kv.OperationGroup[kv.OP], error) { + var in transactWriteItemsInput + if err := json.Unmarshal(b, &in); err != nil { + return nil, errors.WithStack(err) + } + + var elems []*kv.Elem[kv.OP] + for _, item := range in.TransactItems { + if item.Put == nil { + return nil, errors.New("unsupported transact item") + } + keyAttr, ok := item.Put.Item["key"] + if !ok { + return nil, errors.New("missing key attribute") + } + valAttr, ok := item.Put.Item["value"] + if !ok { + return nil, errors.New("missing value attribute") + } + elems = append(elems, &kv.Elem[kv.OP]{ + Op: kv.Put, + Key: []byte(keyAttr.S), + Value: []byte(valAttr.S), + }) + } + + return &kv.OperationGroup[kv.OP]{ + IsTxn: true, + Elems: elems, + }, nil +} diff --git a/adapter/test_util.go b/adapter/test_util.go index 8c2abb9..95ea22c 100644 --- a/adapter/test_util.go +++ b/adapter/test_util.go @@ -28,6 +28,9 @@ func shutdown(nodes []Node) { for _, n := range nodes { n.grpcServer.Stop() n.redisServer.Stop() + if n.dynamoServer != nil { + n.dynamoServer.Stop() + } if n.raft != nil { n.raft.Shutdown() } @@ -40,12 +43,14 @@ func shutdown(nodes []Node) { } type portsAdress struct { - grpc int - raft int - redis int - grpcAddress string - raftAddress string - redisAddress string + grpc int + raft int + redis int + dynamo int + grpcAddress string + raftAddress string + redisAddress string + dynamoAddress string } const ( @@ -53,19 +58,21 @@ const ( grpcPort = 50000 raftPort = 50000 - redisPort = 63790 + redisPort = 63790 + dynamoPort = 28000 ) var mu sync.Mutex var portGrpc atomic.Int32 var portRaft atomic.Int32 var portRedis atomic.Int32 +var portDynamo atomic.Int32 func init() { portGrpc.Store(raftPort) portRaft.Store(grpcPort) portRedis.Store(redisPort) - + portDynamo.Store(dynamoPort) } func portAssigner() portsAdress { @@ -74,35 +81,42 @@ func portAssigner() portsAdress { gp := portGrpc.Add(1) rp := portRaft.Add(1) rd := portRedis.Add(1) + dn := portDynamo.Add(1) return portsAdress{ - grpc: int(gp), - raft: int(rp), - redis: int(rd), - grpcAddress: net.JoinHostPort("localhost", strconv.Itoa(int(gp))), - raftAddress: net.JoinHostPort("localhost", strconv.Itoa(int(rp))), - redisAddress: net.JoinHostPort("localhost", strconv.Itoa(int(rd))), + grpc: int(gp), + raft: int(rp), + redis: int(rd), + dynamo: int(dn), + grpcAddress: net.JoinHostPort("localhost", strconv.Itoa(int(gp))), + raftAddress: net.JoinHostPort("localhost", strconv.Itoa(int(rp))), + redisAddress: net.JoinHostPort("localhost", strconv.Itoa(int(rd))), + dynamoAddress: net.JoinHostPort("localhost", strconv.Itoa(int(dn))), } } type Node struct { - grpcAddress string - raftAddress string - redisAddress string - grpcServer *grpc.Server - redisServer *RedisServer - raft *raft.Raft - tm *transport.Manager + grpcAddress string + raftAddress string + redisAddress string + dynamoAddress string + grpcServer *grpc.Server + redisServer *RedisServer + dynamoServer *DynamoDBServer + raft *raft.Raft + tm *transport.Manager } -func newNode(grpcAddress, raftAddress, redisAddress string, r *raft.Raft, tm *transport.Manager, grpcs *grpc.Server, rd *RedisServer) Node { +func newNode(grpcAddress, raftAddress, redisAddress, dynamoAddress string, r *raft.Raft, tm *transport.Manager, grpcs *grpc.Server, rd *RedisServer, ds *DynamoDBServer) Node { return Node{ - grpcAddress: grpcAddress, - raftAddress: raftAddress, - redisAddress: redisAddress, - grpcServer: grpcs, - redisServer: rd, - raft: r, - tm: tm, + grpcAddress: grpcAddress, + raftAddress: raftAddress, + redisAddress: redisAddress, + dynamoAddress: dynamoAddress, + grpcServer: grpcs, + redisServer: rd, + dynamoServer: ds, + raft: r, + tm: tm, } } @@ -183,16 +197,24 @@ func createNode(t *testing.T, n int) ([]Node, []string, []string) { assert.NoError(t, rd.Run()) }() + dl, err := lc.Listen(ctx, "tcp", port.dynamoAddress) + assert.NoError(t, err) + ds := NewDynamoDBServer(dl, st, coordinator) + go func() { + assert.NoError(t, ds.Run()) + }() + nodes = append(nodes, newNode( port.grpcAddress, port.raftAddress, port.redisAddress, + port.dynamoAddress, r, tm, s, rd, + ds, )) - } d := &net.Dialer{Timeout: time.Second} @@ -208,6 +230,11 @@ func createNode(t *testing.T, n int) ([]Node, []string, []string) { return false } _ = conn.Close() + conn, err = d.DialContext(ctx, "tcp", n.dynamoAddress) + if err != nil { + return false + } + _ = conn.Close() return true }, waitTimeout, waitInterval) } diff --git a/go.mod b/go.mod index 392e85b..434ecef 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/Jille/raft-grpc-leader-rpc v1.1.0 github.com/Jille/raft-grpc-transport v1.6.1 github.com/Jille/raftadmin v1.2.1 + github.com/aws/aws-sdk-go v1.55.7 github.com/cockroachdb/errors v1.12.0 github.com/emirpasic/gods v1.18.1 github.com/hashicorp/go-hclog v1.6.3 @@ -43,6 +44,7 @@ require ( github.com/hashicorp/go-msgpack/v2 v2.1.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index cef21bf..0c0d3fd 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kd github.com/armon/go-metrics v0.0.0-20190430140413-ec5e00d3c878/go.mod h1:3AMJUQhVx52RsWOnlkpikZr01T/yAVN2gn0861vByNg= github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA= github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= +github.com/aws/aws-sdk-go v1.55.7 h1:UJrkFq7es5CShfBwlWAC8DA077vp8PyVbQd3lqLiztE= +github.com/aws/aws-sdk-go v1.55.7/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= @@ -159,6 +161,10 @@ github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702 h1:RLKEcCuKc github.com/hashicorp/raft-boltdb v0.0.0-20230125174641-2a8082862702/go.mod h1:nTakvJ4XYq45UXtn0DbwR4aU9ZdjlnIenpbs6Cd+FM0= github.com/hashicorp/raft-boltdb/v2 v2.3.1 h1:ackhdCNPKblmOhjEU9+4lHSJYFkJd6Jqyvj6eW9pwkc= github.com/hashicorp/raft-boltdb/v2 v2.3.1/go.mod h1:n4S+g43dXF1tqDT+yzcXHhXM6y7MrlUd3TTwGRcUvQE= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= +github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= @@ -473,7 +479,9 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=