diff --git a/clientv3/kv.go b/clientv3/kv.go index a481ed9fc31..2a97c865da6 100644 --- a/clientv3/kv.go +++ b/clientv3/kv.go @@ -156,7 +156,7 @@ func (kv *kv) do(ctx context.Context, op Op) (OpResponse, error) { } case tPut: var resp *pb.PutResponse - r := &pb.PutRequest{Key: op.key, Value: op.val, Lease: int64(op.leaseID)} + r := &pb.PutRequest{Key: op.key, Value: op.val, Lease: int64(op.leaseID), PrevKv: op.prevKV} resp, err = kv.remote.Put(ctx, r) if err == nil { return OpResponse{put: (*PutResponse)(resp)}, nil diff --git a/clientv3/op.go b/clientv3/op.go index 8a56b854cd7..58e2c94cc15 100644 --- a/clientv3/op.go +++ b/clientv3/op.go @@ -74,7 +74,7 @@ func (op Op) toRequestOp() *pb.RequestOp { } return &pb.RequestOp{Request: &pb.RequestOp_RequestRange{RequestRange: r}} case tPut: - r := &pb.PutRequest{Key: op.key, Value: op.val, Lease: int64(op.leaseID)} + r := &pb.PutRequest{Key: op.key, Value: op.val, Lease: int64(op.leaseID), PrevKv: op.prevKV} return &pb.RequestOp{Request: &pb.RequestOp_RequestPut{RequestPut: r}} case tDeleteRange: r := &pb.DeleteRangeRequest{Key: op.key, RangeEnd: op.end, PrevKv: op.prevKV} diff --git a/etcdctl/ctlv3/command/printer.go b/etcdctl/ctlv3/command/printer.go index 318ec6e4bd2..b19f952632e 100644 --- a/etcdctl/ctlv3/command/printer.go +++ b/etcdctl/ctlv3/command/printer.go @@ -119,7 +119,12 @@ func (s *simplePrinter) Get(resp v3.GetResponse) { } } -func (s *simplePrinter) Put(r v3.PutResponse) { fmt.Println("OK") } +func (s *simplePrinter) Put(r v3.PutResponse) { + fmt.Println("OK") + if r.PrevKv != nil { + printKV(s.isHex, r.PrevKv) + } +} func (s *simplePrinter) Txn(resp v3.TxnResponse) { if resp.Succeeded { diff --git a/etcdctl/ctlv3/command/put_command.go b/etcdctl/ctlv3/command/put_command.go index 1cffd31401e..0e582d4dd1d 100644 --- a/etcdctl/ctlv3/command/put_command.go +++ b/etcdctl/ctlv3/command/put_command.go @@ -24,7 +24,8 @@ import ( ) var ( - leaseStr string + leaseStr string + putPrevKV bool ) // NewPutCommand returns the cobra command for "put". @@ -49,6 +50,7 @@ will store the content of the file to . Run: putCommandFunc, } cmd.Flags().StringVar(&leaseStr, "lease", "0", "lease ID (in hexadecimal) to attach to the key") + cmd.Flags().BoolVar(&putPrevKV, "prev-kv", false, "return changed key-value pairs") return cmd } @@ -85,6 +87,9 @@ func getPutOp(cmd *cobra.Command, args []string) (string, string, []clientv3.OpO if id != 0 { opts = append(opts, clientv3.WithLease(clientv3.LeaseID(id))) } + if putPrevKV { + opts = append(opts, clientv3.WithPrevKV()) + } return key, value, opts } diff --git a/etcdserver/apply.go b/etcdserver/apply.go index be7213e11be..ed2027c82e2 100644 --- a/etcdserver/apply.go +++ b/etcdserver/apply.go @@ -159,6 +159,22 @@ func (a *applierV3backend) Put(txnID int64, p *pb.PutRequest) (*pb.PutResponse, rev int64 err error ) + + var rr *mvcc.RangeResult + if p.PrevKv { + if txnID != noTxn { + rr, err = a.s.KV().TxnRange(txnID, p.Key, nil, mvcc.RangeOptions{}) + if err != nil { + return nil, err + } + } else { + rr, err = a.s.KV().Range(p.Key, nil, mvcc.RangeOptions{}) + if err != nil { + return nil, err + } + } + } + if txnID != noTxn { rev, err = a.s.KV().TxnPut(txnID, p.Key, p.Value, lease.LeaseID(p.Lease)) if err != nil { @@ -174,6 +190,9 @@ func (a *applierV3backend) Put(txnID int64, p *pb.PutRequest) (*pb.PutResponse, rev = a.s.KV().Put(p.Key, p.Value, leaseID) } resp.Header.Revision = rev + if rr != nil && len(rr.KVs) != 0 { + resp.PrevKv = &rr.KVs[0] + } return resp, nil } diff --git a/etcdserver/apply_auth.go b/etcdserver/apply_auth.go index 0745fbd0699..22a2a84de5a 100644 --- a/etcdserver/apply_auth.go +++ b/etcdserver/apply_auth.go @@ -56,6 +56,9 @@ func (aa *authApplierV3) Put(txnID int64, r *pb.PutRequest) (*pb.PutResponse, er if !aa.as.IsPutPermitted(aa.user, r.Key) { return nil, auth.ErrPermissionDenied } + if r.PrevKv && !aa.as.IsRangePermitted(aa.user, r.Key, nil) { + return nil, auth.ErrPermissionDenied + } return aa.applierV3.Put(txnID, r) }