mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
296 lines
7.1 KiB
Go
296 lines
7.1 KiB
Go
// Copyright (C) 2022-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// Package kms provides a unified Key Management Service with support for
|
|
// both embedded (ZapDB) and distributed (PostgreSQL) storage backends.
|
|
package kms
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sync"
|
|
"time"
|
|
|
|
badger "github.com/luxfi/zapdb"
|
|
"github.com/luxfi/zapdb/options"
|
|
)
|
|
|
|
// StorageBackend defines the storage interface for KMS operations.
|
|
type StorageBackend interface {
|
|
// Key operations
|
|
Get(ctx context.Context, key string) ([]byte, error)
|
|
Set(ctx context.Context, key string, value []byte) error
|
|
SetWithTTL(ctx context.Context, key string, value []byte, ttl time.Duration) error
|
|
Delete(ctx context.Context, key string) error
|
|
Exists(ctx context.Context, key string) (bool, error)
|
|
|
|
// Iteration
|
|
List(ctx context.Context, prefix string) ([]string, error)
|
|
Scan(ctx context.Context, prefix string, fn func(key string, value []byte) error) error
|
|
|
|
// Transaction support
|
|
BeginTx(ctx context.Context) (Transaction, error)
|
|
|
|
// Lifecycle
|
|
Close() error
|
|
}
|
|
|
|
// Transaction represents a storage transaction.
|
|
type Transaction interface {
|
|
Get(key string) ([]byte, error)
|
|
Set(key string, value []byte) error
|
|
Delete(key string) error
|
|
Commit() error
|
|
Rollback() error
|
|
}
|
|
|
|
// BadgerStore implements StorageBackend using BadgerDB for embedded storage.
|
|
type BadgerStore struct {
|
|
db *badger.DB
|
|
mu sync.RWMutex
|
|
closed bool
|
|
}
|
|
|
|
// BadgerConfig holds BadgerDB configuration options.
|
|
type BadgerConfig struct {
|
|
Dir string
|
|
InMemory bool
|
|
SyncWrites bool
|
|
Compression bool
|
|
EncryptionKey []byte // 16, 24, or 32 bytes for AES-128, AES-192, AES-256
|
|
}
|
|
|
|
// NewBadgerStore creates a new BadgerDB-backed storage.
|
|
func NewBadgerStore(cfg *BadgerConfig) (*BadgerStore, error) {
|
|
opts := badger.DefaultOptions(cfg.Dir)
|
|
|
|
if cfg.InMemory {
|
|
opts = opts.WithInMemory(true)
|
|
}
|
|
|
|
opts = opts.WithSyncWrites(cfg.SyncWrites)
|
|
|
|
if cfg.Compression {
|
|
opts = opts.WithCompression(options.Snappy)
|
|
}
|
|
|
|
if len(cfg.EncryptionKey) > 0 {
|
|
opts = opts.WithEncryptionKey(cfg.EncryptionKey)
|
|
}
|
|
|
|
// Performance tuning for KMS workloads
|
|
opts = opts.WithNumVersionsToKeep(3)
|
|
opts = opts.WithNumLevelZeroTables(5)
|
|
opts = opts.WithNumLevelZeroTablesStall(15)
|
|
opts = opts.WithValueLogFileSize(64 << 20) // 64MB
|
|
|
|
db, err := badger.Open(opts)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open badger db: %w", err)
|
|
}
|
|
|
|
store := &BadgerStore{db: db}
|
|
|
|
// Start background GC
|
|
go store.runGC()
|
|
|
|
return store, nil
|
|
}
|
|
|
|
// runGC periodically runs garbage collection on the value log.
|
|
func (s *BadgerStore) runGC() {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
s.mu.RLock()
|
|
if s.closed {
|
|
s.mu.RUnlock()
|
|
return
|
|
}
|
|
s.mu.RUnlock()
|
|
|
|
// Run GC until no more garbage to collect
|
|
for {
|
|
err := s.db.RunValueLogGC(0.5)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get retrieves a value by key.
|
|
func (s *BadgerStore) Get(ctx context.Context, key string) ([]byte, error) {
|
|
var value []byte
|
|
err := s.db.View(func(txn *badger.Txn) error {
|
|
item, err := txn.Get([]byte(key))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
value, err = item.ValueCopy(nil)
|
|
return err
|
|
})
|
|
if err == badger.ErrKeyNotFound {
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
return value, err
|
|
}
|
|
|
|
// Set stores a value at the given key.
|
|
func (s *BadgerStore) Set(ctx context.Context, key string, value []byte) error {
|
|
return s.db.Update(func(txn *badger.Txn) error {
|
|
return txn.Set([]byte(key), value)
|
|
})
|
|
}
|
|
|
|
// SetWithTTL stores a value with a time-to-live.
|
|
func (s *BadgerStore) SetWithTTL(ctx context.Context, key string, value []byte, ttl time.Duration) error {
|
|
return s.db.Update(func(txn *badger.Txn) error {
|
|
e := badger.NewEntry([]byte(key), value).WithTTL(ttl)
|
|
return txn.SetEntry(e)
|
|
})
|
|
}
|
|
|
|
// Delete removes a key from storage.
|
|
func (s *BadgerStore) Delete(ctx context.Context, key string) error {
|
|
return s.db.Update(func(txn *badger.Txn) error {
|
|
return txn.Delete([]byte(key))
|
|
})
|
|
}
|
|
|
|
// Exists checks if a key exists.
|
|
func (s *BadgerStore) Exists(ctx context.Context, key string) (bool, error) {
|
|
err := s.db.View(func(txn *badger.Txn) error {
|
|
_, err := txn.Get([]byte(key))
|
|
return err
|
|
})
|
|
if err == badger.ErrKeyNotFound {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// List returns all keys with the given prefix.
|
|
func (s *BadgerStore) List(ctx context.Context, prefix string) ([]string, error) {
|
|
var keys []string
|
|
err := s.db.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.PrefetchValues = false
|
|
opts.Prefix = []byte(prefix)
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek([]byte(prefix)); it.ValidForPrefix([]byte(prefix)); it.Next() {
|
|
keys = append(keys, string(it.Item().Key()))
|
|
}
|
|
return nil
|
|
})
|
|
return keys, err
|
|
}
|
|
|
|
// Scan iterates over all key-value pairs with the given prefix.
|
|
func (s *BadgerStore) Scan(ctx context.Context, prefix string, fn func(key string, value []byte) error) error {
|
|
return s.db.View(func(txn *badger.Txn) error {
|
|
opts := badger.DefaultIteratorOptions
|
|
opts.Prefix = []byte(prefix)
|
|
|
|
it := txn.NewIterator(opts)
|
|
defer it.Close()
|
|
|
|
for it.Seek([]byte(prefix)); it.ValidForPrefix([]byte(prefix)); it.Next() {
|
|
item := it.Item()
|
|
key := string(item.Key())
|
|
|
|
err := item.Value(func(val []byte) error {
|
|
return fn(key, val)
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// badgerTx implements Transaction for BadgerDB.
|
|
type badgerTx struct {
|
|
txn *badger.Txn
|
|
}
|
|
|
|
// BeginTx starts a new transaction.
|
|
func (s *BadgerStore) BeginTx(ctx context.Context) (Transaction, error) {
|
|
return &badgerTx{txn: s.db.NewTransaction(true)}, nil
|
|
}
|
|
|
|
func (t *badgerTx) Get(key string) ([]byte, error) {
|
|
item, err := t.txn.Get([]byte(key))
|
|
if err == badger.ErrKeyNotFound {
|
|
return nil, ErrKeyNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return item.ValueCopy(nil)
|
|
}
|
|
|
|
func (t *badgerTx) Set(key string, value []byte) error {
|
|
return t.txn.Set([]byte(key), value)
|
|
}
|
|
|
|
func (t *badgerTx) Delete(key string) error {
|
|
return t.txn.Delete([]byte(key))
|
|
}
|
|
|
|
func (t *badgerTx) Commit() error {
|
|
return t.txn.Commit()
|
|
}
|
|
|
|
func (t *badgerTx) Rollback() error {
|
|
t.txn.Discard()
|
|
return nil
|
|
}
|
|
|
|
// Close closes the BadgerDB store.
|
|
func (s *BadgerStore) Close() error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.closed = true
|
|
return s.db.Close()
|
|
}
|
|
|
|
// Helper functions for JSON storage
|
|
|
|
// GetJSON retrieves and unmarshals a JSON value.
|
|
func GetJSON[T any](ctx context.Context, store StorageBackend, key string) (*T, error) {
|
|
data, err := store.Get(ctx, key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var v T
|
|
if err := json.Unmarshal(data, &v); err != nil {
|
|
return nil, fmt.Errorf("failed to unmarshal: %w", err)
|
|
}
|
|
return &v, nil
|
|
}
|
|
|
|
// SetJSON marshals and stores a JSON value.
|
|
func SetJSON(ctx context.Context, store StorageBackend, key string, value any) error {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal: %w", err)
|
|
}
|
|
return store.Set(ctx, key, data)
|
|
}
|
|
|
|
// Common errors
|
|
var (
|
|
ErrKeyNotFound = fmt.Errorf("key not found")
|
|
ErrInvalidKey = fmt.Errorf("invalid key")
|
|
)
|