Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
# Hermetic IAM auth unit tests (root-level iamauth/ pkgs) — no infra.
# These live outside ./temporal and ./persistent, so without this row
# they run in no CI job at all.
Expand Down
8 changes: 8 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ tasks:
- 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"

test-iamauth:
desc: "Run hermetic IAM auth unit tests (provider selection + GCP token minting against fake endpoints; no external infra)"
cmds:
Expand Down
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