Skip to content
4 changes: 4 additions & 0 deletions .github/workflows/ci-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ jobs:
database: redis
version: 7.0.0
instancetype: TLS
- storagetype: kv
database: none
version: none
instancetype: none

steps:
- name: Generate GitHub App token
Expand Down
8 changes: 8 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,11 @@ tasks:
DB_VERSION: "{{.DB_VERSION}}"
- mkdir -p coverage/temporal
- cp temporal/*.cov coverage/temporal/

test-kv:
desc: "Run tests for kv storage"
cmds:
- task: run-tests
vars:
STORAGE_TYPE: kv
DB: "none"
60 changes: 60 additions & 0 deletions kv/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package kv

import (
"encoding/json"
)

// Config represents the top-level "kv" configuration block in component configs.
// It contains global settings and named store definitions.
//
// Example JSON structure:
//
// {
// "kv": {
// "cache": {"enabled": true, "ttl": "60s"},
// "stores": {
// "vault-prod": {"type": "vault", "required": true, "config": {...}}
// }
// }
// }
type Config struct {
Stores map[string]StoreConfig `json:"stores"`
Cache CacheConfig `json:"cache"`
}

// StoreConfig defines the configuration for a single named KV store instance.
type StoreConfig struct {
// Type specifies which provider factory to use.
Type ProviderType `json:"type"`

// Required determines startup behavior if the store fails to initialize.
Required bool `json:"required"`

// Config contains provider-specific configuration as raw JSON.
// Each provider's factory knows how to parse its own config format.
Config json.RawMessage `json:"config"`
}

// CacheConfig controls the caching behavior for resolved secrets.
type CacheConfig struct {
// Enabled controls whether resolved secrets are cached in memory
Enabled bool `json:"enabled"`

// TTL specifies how long cached values remain valid before refresh.
// Format: Go duration string (e.g., "60s", "5m", "1h")
TTL string `json:"ttl"`

// RefreshBeforeExpiry specifies the threshold before TTL expiration when a background
// refresh is proactively triggered. It must be less than TTL.
// Format: Go duration string (e.g., "10s"). 0s or empty disables background refresh.
RefreshBeforeExpiry string `json:"refresh_before_expiry"`

// NegativeTTLNotFound specifies how long to cache "key not found" errors.
// This is typically longer than transient errors as missing keys rarely resolve quickly.
NegativeTTLNotFound string `json:"negative_ttl_not_found"`

// NegativeTTLTransient specifies how long to cache transient provider errors
// (e.g., network timeouts, service unavailable) to prevent hammering a failing provider.
// This should typically be short to allow quick recovery.
NegativeTTLTransient string `json:"negative_ttl_transient"`
}
48 changes: 48 additions & 0 deletions kv/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package kv

import (
"errors"
"fmt"
)

var (
// ErrStoreNotFound is returned when referencing an unregistered store name.
ErrStoreNotFound = errors.New("store not found")

// ErrContractViolation indicates that an underlying KV provider returned data
// violates the expected API contract (e.g., type assertion failures)
ErrContractViolation = errors.New("provider contract violation")

// ErrStoreClosed is returned when an operation is attempted on closed store or
// provider that has already been shut down via its Close method.
ErrStoreClosed = errors.New("secret store is closed")
)

func NewStoreNotFoundError(storeName string) error {
return fmt.Errorf("store %q: %w", storeName, ErrStoreNotFound)
}

// KeyNotFoundError indicates the store is reachable but the key does not exist.
type KeyNotFoundError struct {
StoreName string
KeyPath string
}

func (e *KeyNotFoundError) Error() string {
return fmt.Sprintf("key %q not found in store %q", e.KeyPath, e.StoreName)
}

// StoreUnavailableError indicates a transient failure reaching the store.
type StoreUnavailableError struct {
StoreName string
KeyPath string
Err error
}

func (e *StoreUnavailableError) Error() string {
return fmt.Sprintf("store %q unavailable when fetching key %q: %v", e.StoreName, e.KeyPath, e.Err)
}

func (e *StoreUnavailableError) Unwrap() error {
return e.Err
}
Loading
Loading