Skip to content
Open
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)
}

Check warning on line 44 in kv/errors.go

View check run for this annotation

probelabs / Visor: security

security Issue

Error messages in `StoreUnavailableError` include the underlying provider error. If these errors are propagated to external systems or logs with insufficient access controls, they could reveal sensitive internal architecture details, such as network addresses or stack traces from the provider's SDK.
Raw output
Consider redacting sensitive information from errors that may leave the service boundary. A common pattern is to have a detailed internal error for logging and a generic, sanitized error for external responses. For `StoreUnavailableError`, consider logging the full `e.Err` internally but not including its string representation in the error message returned to the caller.

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