github.com/zntrio/harp/v2@v2.0.9/pkg/kv/etcd3/etcd3.go (about) 1 // Licensed to Elasticsearch B.V. under one or more contributor 2 // license agreements. See the NOTICE file distributed with 3 // this work for additional information regarding copyright 4 // ownership. Elasticsearch B.V. licenses this file to you under 5 // the Apache License, Version 2.0 (the "License"); you may 6 // not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, 12 // software distributed under the License is distributed on an 13 // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 // KIND, either express or implied. See the License for the 15 // specific language governing permissions and limitations 16 // under the License. 17 18 package etcd3 19 20 import ( 21 "bytes" 22 "context" 23 "errors" 24 "fmt" 25 "strings" 26 27 clientv3 "go.etcd.io/etcd/client/v3" 28 "go.uber.org/zap" 29 30 "github.com/zntrio/harp/v2/pkg/kv" 31 "github.com/zntrio/harp/v2/pkg/sdk/log" 32 ) 33 34 const ( 35 // ListBatchSize defines the pagination page size. 36 ListBatchSize = 50 37 ) 38 39 type etcd3Driver struct { 40 client *clientv3.Client 41 } 42 43 func Store(client *clientv3.Client) kv.Store { 44 return &etcd3Driver{ 45 client: client, 46 } 47 } 48 49 // ----------------------------------------------------------------------------- 50 51 func (d *etcd3Driver) Get(ctx context.Context, key string) (*kv.Pair, error) { 52 // Retrieve key value 53 resp, err := d.client.KV.Get(ctx, d.normalize(key), clientv3.WithLimit(1)) 54 if err != nil { 55 return nil, fmt.Errorf("etcd3: unable to retrieve %q key: %w", key, err) 56 } 57 if resp == nil { 58 return nil, fmt.Errorf("etcd3: got nil response for %q", key) 59 } 60 61 // Unpack result 62 if len(resp.Kvs) == 0 { 63 return nil, kv.ErrKeyNotFound 64 } 65 if len(resp.Kvs) > 1 { 66 return nil, fmt.Errorf("etcd3: %q key returned multiple result where only one is expected", key) 67 } 68 69 // No error 70 return &kv.Pair{ 71 Key: string(resp.Kvs[0].Key), 72 Value: resp.Kvs[0].Value, 73 Version: uint64(resp.Kvs[0].Version), 74 }, nil 75 } 76 77 func (d *etcd3Driver) Put(ctx context.Context, key string, value []byte) error { 78 // Prepare a transaction 79 tx := d.client.Txn(ctx) 80 81 // Put a value 82 tx.Then(clientv3.OpPut(key, string(value))) 83 84 // Commit transaction 85 _, err := tx.Commit() 86 if err != nil { 87 return fmt.Errorf("etcd3: unable to put %q value: %w", key, err) 88 } 89 90 // No error 91 return nil 92 } 93 94 func (d *etcd3Driver) Delete(ctx context.Context, key string) error { 95 // Try to delete from store 96 resp, err := d.client.Delete(ctx, d.normalize(key)) 97 if err != nil { 98 return fmt.Errorf("etcd3: unable to delete %q key: %w", key, err) 99 } 100 if resp == nil { 101 return fmt.Errorf("etcd3: got nil response for %q", key) 102 } 103 if resp.Deleted == 0 { 104 return kv.ErrKeyNotFound 105 } 106 107 // No error 108 return nil 109 } 110 111 func (d *etcd3Driver) Exists(ctx context.Context, key string) (bool, error) { 112 _, err := d.Get(ctx, key) 113 if err != nil { 114 if errors.Is(err, kv.ErrKeyNotFound) { 115 return false, nil 116 } 117 return false, fmt.Errorf("etcd3: unable to check key %q existence: %w", key, err) 118 } 119 120 // No error 121 return true, nil 122 } 123 124 func (d *etcd3Driver) List(ctx context.Context, basePath string) ([]*kv.Pair, error) { 125 log.For(ctx).Debug("etcd3: Try to list keys", zap.String("prefix", basePath)) 126 127 var ( 128 results = []*kv.Pair{} 129 lastKey string 130 ) 131 for { 132 // Check if operation is ended 133 if ctx.Err() != nil { 134 return nil, ctx.Err() 135 } 136 137 // Prepare query options 138 opts := []clientv3.OpOption{ 139 clientv3.WithPrefix(), 140 clientv3.WithSort(clientv3.SortByKey, clientv3.SortAscend), 141 clientv3.WithLimit(ListBatchSize), 142 } 143 144 // If lastkey is defined set the cursor 145 if lastKey != "" { 146 opts = append(opts, clientv3.WithFromKey()) 147 basePath = lastKey 148 } 149 150 log.For(ctx).Debug("etcd3: Get all keys", zap.String("key", basePath)) 151 152 // Retrieve key value 153 resp, err := d.client.KV.Get(ctx, d.normalize(basePath), opts...) 154 if err != nil { 155 return nil, fmt.Errorf("etcd3: unable to retrieve %q from base path: %w", basePath, err) 156 } 157 if resp == nil { 158 return nil, fmt.Errorf("etcd3: got nil response for %q", basePath) 159 } 160 161 // Exit on empty result 162 if len(resp.Kvs) == 0 { 163 log.For(ctx).Debug("etcd3: No more result, stop.") 164 break 165 } 166 167 // Unpack values 168 for _, item := range resp.Kvs { 169 log.For(ctx).Debug("etcd3: Unpack result", zap.String("key", string(item.Key))) 170 171 // Skip first if lastKey is defined 172 if lastKey != "" && bytes.Equal(item.Key, []byte(lastKey)) { 173 continue 174 } 175 results = append(results, &kv.Pair{ 176 Key: string(item.Key), 177 Value: item.Value, 178 Version: uint64(item.Version), 179 }) 180 } 181 182 // No need to paginate 183 if len(resp.Kvs) < ListBatchSize { 184 break 185 } 186 187 // Retrieve last key 188 lastKey = string(resp.Kvs[len(resp.Kvs)-1].Key) 189 } 190 191 // Raise keynotfound if no result. 192 if len(results) == 0 { 193 return nil, kv.ErrKeyNotFound 194 } 195 196 // No error 197 return results, nil 198 } 199 200 func (d *etcd3Driver) Close() error { 201 // Skip if client instance is nil 202 if d.client == nil { 203 return nil 204 } 205 206 // Try to close client connection. 207 if err := d.client.Close(); err != nil { 208 return fmt.Errorf("etcd3: unable to close client connection: %w", err) 209 } 210 211 // No error 212 return nil 213 } 214 215 // ----------------------------------------------------------------------------- 216 217 // Normalize the key for usage in Consul. 218 func (d *etcd3Driver) normalize(key string) string { 219 key = kv.Normalize(key) 220 return strings.TrimPrefix(key, "/") 221 }