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  }