store

package
v0.2.0-alpha.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

README

pkg/k8s/store

Two concrete implementations of types.Store: MemoryStore (holds full resources in memory) and CachedStore (holds index references and fetches on demand). Both are thread-safe and support composite-key indexing with prefix-scan lookups.

For the user-facing framing (when to pick which, template-side List/Fetch/GetSingle semantics) see docs/controller/docs/watching-resources.md. This README covers the Go API.

Interface

Both stores satisfy pkg/k8s/types.Store:

type Store interface {
    Get(keys ...string) ([]any, error)    // exact match, or prefix scan if len(keys) < numKeys
    List() ([]any, error)                 // everything in the store
    Add(resource any, keys []string) error
    Update(resource any, keys []string) error
    Delete(keys ...string) error
    Clear() error
}

The number of keys is fixed at construction time — it comes from the indexBy JSONPath list. Passing fewer keys to Get does a prefix scan that returns every resource whose composite key starts with those values (useful for one-to-many relationships like "all EndpointSlices for service X").

MemoryStore

import "gitlab.com/haproxy-haptic/haptic/pkg/k8s/store"

mem := store.NewMemoryStore(2)          // 2-key composite index (e.g. namespace + name)
mem.Add(ingress, []string{"default", "my-ingress"})
ingresses, _ := mem.Get("default", "my-ingress") // single match
inNs, _      := mem.Get("default")               // prefix scan (all in namespace)
all, _       := mem.List()

Implementation highlights (see pkg/k8s/store/memory.go):

  • Backing data is map[string][]any keyed by the composite-string form of the index. Multiple resources can share a key; Get returns them all.
  • Get with the full key count is an O(1) map lookup that returns the per-bucket slice as-is (zero-copy — see "Immutability Contract"). Per-bucket slices are kept sorted at insert time so reads are deterministic without runtime sorting; partial-prefix scans aggregate matching buckets and sort the result.
  • List rebuilds and sorts the full slice on every call — there's no memoised result. The optimisation is "buckets are pre-sorted, so per-bucket reads are zero-copy", not "the whole list is cached". A consumer that needs a memoised List should cache at its own layer, not inside the store.
  • An RWMutex protects the data map; concurrent readers don't contend.

CachedStore

cached, _ := store.NewCachedStore(&store.CachedStoreConfig{
    NumKeys:  2,
    CacheTTL: 2*time.Minute + 10*time.Second,
    Client:   dynamicClient,
    GVR:      schema.GroupVersionResource{Group: "", Version: "v1", Resource: "secrets"},
    Indexer:  indexer,          // used by the watcher to extract keys on Add
    Logger:   logger,
})
  • Stores only resourceRef tuples (index keys + namespace/name) in memory.
  • Get cache hits return immediately; cache misses call the dynamic client, cache the result with CacheTTL, and return it.
  • The cache is keyed by namespace/name, separate from the index composite key — multiple references can share the same index key while each has its own cache entry.
  • List forces a fetch for every reference. Use it only for small collections or debugging; prefer MemoryStore for templates that iterate everything.
  • The implementation releases the store lock before dispatching API calls so one slow fetch doesn't block other lookups.
  • Projected mode (set by the watcher for every on-demand kind): the informer feeding the store delivers body-stripped objects (only indexBy / field-selector / identity fields survive — see ADR-0012). In this mode Add/Update do not cache the stripped body; the value cache is populated solely by the live API GET (full body), and Update/Delete invalidate any stale entry. This shrinks the informer's resident memory without ever serving a husk to a render read.

In the controller the TTL is auto-derived from dataplane.driftPreventionInterval × 2.2 (see pkg/controller/resourcewatcher/watcher.go) — it's not a user-configurable CRD field. That derivation means a cached entry stays warm for slightly longer than the drift prevention window, so it's already there when the next reconciliation fires.

Error Shape

StoreError wraps every failure with the operation name and the keys involved:

var sErr *store.StoreError
if errors.As(err, &sErr) {
    log.Error("store op failed",
        "op", sErr.Operation,
        "keys", sErr.Keys,
        "cause", sErr.Cause)
}

Common .Cause values: errors.New("at least one key required"), key-count mismatch (passed N keys when the store was built for M), fetcher errors (CachedStore only).

Immutability Contract

Returned slices and resources must not be mutated. Both stores return their internal data directly for performance; a caller mutating the returned value corrupts the store for every subsequent reader. Clone before modifying:

for _, obj := range resources {
    copy := runtime.DeepCopyObject(obj.(runtime.Object))
    // mutate `copy` freely
}

This is enforced by convention, not by type — the Store interface returns any, so the compiler can't help. Watchers and pkg/stores/overlay.go both rely on this contract.

Non-Unique Keys

Indexing by labels (e.g. metadata.labels.kubernetes\\.io/service-name for EndpointSlices) is expected to collide — many slices share a service. Add appends to the slot instead of overwriting, and Get with that key returns every match. Resource-identity equality for Update (and Delete-by-name) is namespace + name only, via extractNamespaceName — UID is not consulted, so a deleted-and-recreated resource looks identical to its predecessor (which is correct: the watcher fires Update, not Delete+Add, on a re-create). Add itself does not dedupe — duplicates are possible if the watcher's delta logic is wrong. That's by design: cheap append, dedupe lives in Update.

Testing

go test ./pkg/k8s/store/...          # unit tests
go test ./pkg/k8s/store/... -race    # race detector

Tests exercise both stores against the same types.Store interface contract — adding a new implementation means satisfying the same test table.

See Also

  • pkg/k8s/typesStore interface definition
  • pkg/k8s/watcherWatcher builds these stores from a SharedInformerFactory
  • pkg/k8s/indexer — JSONPath extraction that produces the composite keys
  • pkg/stores/overlay.go — overlay wrapper used by the webhook dry-run path
  • pkg/k8s/store/CLAUDE.md — developer context (adding storage strategies, cache tuning, thread-safety proofs)

License

Apache-2.0 — see root LICENSE.

Documentation

Overview

Package store provides storage implementations for indexed Kubernetes resources.

This package offers two store types: - Memory store: Fast in-memory storage with complete resources - Cached store: Memory-efficient storage with API-backed retrieval and caching

Index

Constants

View Source
const DefaultMaxCacheSize = 256

DefaultMaxCacheSize is the default maximum number of entries in the LRU cache.

Variables

This section is empty.

Functions

This section is empty.

Types

type CachedStore

type CachedStore struct {
	// contains filtered or unexported fields
}

CachedStore stores only resource references in memory and fetches resources from the Kubernetes API on access. Fetched resources are cached with a TTL.

This reduces memory usage for large resources (e.g., Secrets) at the cost of API latency on cache misses.

Supports non-unique index keys by storing multiple resource references per composite key.

Thread-safe for concurrent access.

func NewCachedStore

func NewCachedStore(cfg *CachedStoreConfig) (*CachedStore, error)

NewCachedStore creates a new API-backed store with caching.

func (*CachedStore) Add

func (s *CachedStore) Add(resource any, keys []string) error

Add inserts a new resource into the store.

func (*CachedStore) Clear

func (s *CachedStore) Clear() error

Clear removes all resources from the store.

func (*CachedStore) Delete

func (s *CachedStore) Delete(keys ...string) error

Delete removes a resource from the store. NOTE: With non-unique index keys, this removes ALL resources matching the provided keys.

func (*CachedStore) Get

func (s *CachedStore) Get(keys ...string) ([]any, error)

Get retrieves all resources matching the provided index keys.

func (*CachedStore) List

func (s *CachedStore) List() ([]any, error)

List returns all resources in the store.

func (*CachedStore) ListCached

func (s *CachedStore) ListCached() ([]any, error)

ListCached returns only resources currently warm in the LRU cache — no API fetches. Used by callers that want to prime a per-render snapshot with whatever's free, without paying for the full store-wide List() fan-out. The slice may be a small subset of what's in the cluster (the LRU is `MaxCacheSize` entries; unaccessed references contribute nothing). Expired entries are skipped.

Callers that need cluster-wide iteration should still call List(), accepting the WARN and per-reference API fetch cost.

func (*CachedStore) Size

func (s *CachedStore) Size() int

Size returns the number of tracked resources in the store.

func (*CachedStore) Update

func (s *CachedStore) Update(resource any, keys []string) error

Update modifies an existing resource or adds it if it doesn't exist.

type CachedStoreConfig

type CachedStoreConfig struct {
	// NumKeys is the number of index keys (must match indexer configuration)
	NumKeys int

	// CacheTTL is the cache entry time-to-live
	CacheTTL time.Duration

	// MaxCacheSize is the maximum number of entries in the LRU cache.
	// When exceeded, the least recently used entry is evicted.
	// Default: 256
	MaxCacheSize int

	// Client is the Kubernetes dynamic client for fetching resources
	Client dynamic.Interface

	// GVR identifies the resource type to fetch
	GVR schema.GroupVersionResource

	// Namespace restricts fetching to a specific namespace (empty = all namespaces)
	Namespace string

	// Indexer processes fetched resources (field filtering)
	Indexer *indexer.Indexer

	// Logger for debug and warning messages (optional, uses slog.Default if nil)
	Logger *slog.Logger

	// Projected indicates the informer feeding this store delivers
	// body-stripped (projected) objects — only identity, indexBy, and
	// fieldSelector fields survive (see pkg/k8s/watcher projection, ADR-0012).
	//
	// In projected mode the store must NOT cache the projected body on
	// Add/Update (a warm render read would otherwise be served a husk).
	// Instead the value cache is populated only by the live API GET in
	// fetchResourceByRef (full body), and Update/Delete invalidate any stale
	// cached body so the next read re-fetches. Off by default; on only for
	// `store: on-demand` (CachedStore) watchers.
	Projected bool
}

CachedStoreConfig configures a CachedStore.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore stores complete Kubernetes resources in memory using nested maps.

This provides O(1) lookup performance at the cost of higher memory usage. Resources are stored with their full specification after field filtering.

Supports non-unique index keys by storing multiple resources per composite key.

Thread-safe for concurrent access.

Immutability Contract

Resources stored in MemoryStore are pre-converted (floats to ints) at storage time and MUST NOT be mutated by callers. The slices returned by Get() are direct references to internal data structures for performance. Callers MUST NOT:

  • Modify elements of returned slices
  • Append to or reslice returned slices
  • Modify fields within returned resources

Note: List() returns a fresh slice copy for thread safety, but the resource objects within are still references to internal data and must not be mutated.

func NewMemoryStore

func NewMemoryStore(numKeys int) *MemoryStore

NewMemoryStore creates a new memory-backed store.

Parameters:

  • numKeys: Number of index keys (must match indexer configuration)

func (*MemoryStore) Add

func (s *MemoryStore) Add(resource any, keys []string) error

Add inserts a new resource into the store. If resources with the same index keys already exist, the new resource is appended. The slice is kept sorted by namespace/name for deterministic Get() results.

func (*MemoryStore) Clear

func (s *MemoryStore) Clear() error

Clear removes all resources from the store.

func (*MemoryStore) Delete

func (s *MemoryStore) Delete(keys ...string) error

Delete removes a resource from the store. NOTE: With non-unique index keys, this method cannot identify which specific resource to delete when multiple resources have the same index keys. It removes ALL resources matching the provided keys. The watcher should call this with the resource's actual namespace+name as the index keys to delete a specific resource.

func (*MemoryStore) Get

func (s *MemoryStore) Get(keys ...string) ([]any, error)

Get retrieves all resources matching the provided index keys.

Returns a direct reference to the internal slice for exact key matches. Callers MUST NOT modify the returned slice or its elements (see Immutability Contract).

For partial key matches, a new slice is constructed from matching entries and sorted for deterministic order.

func (*MemoryStore) List

func (s *MemoryStore) List() ([]any, error)

List returns all resources in the store. Returns a fresh copy of all resources to avoid race conditions.

func (*MemoryStore) Size

func (s *MemoryStore) Size() int

Size returns the number of resources in the store.

func (*MemoryStore) Update

func (s *MemoryStore) Update(resource any, keys []string) error

Update modifies an existing resource or adds it if it doesn't exist. For non-unique index keys, it finds the resource by namespace+name and replaces it. The slice is kept sorted by namespace/name for deterministic Get() results.

type StoreError

type StoreError struct {
	Operation string
	Keys      []string
	Cause     error
}

StoreError represents a generic store operation error.

func (*StoreError) Error

func (e *StoreError) Error() string

func (*StoreError) Unwrap

func (e *StoreError) Unwrap() error

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL