From 463b354c20848a3546a3ef8ee5c65f544887ecb1 Mon Sep 17 00:00:00 2001 From: Dimitar Mirchev Date: Tue, 7 Jan 2025 13:41:13 +0200 Subject: [PATCH 1/3] Make data store generic --- cmd/discovery-server/app/app.go | 9 +- internal/handler/openidmeta/handler.go | 13 +- internal/handler/openidmeta/handler_test.go | 11 +- internal/reconciler/openidmeta/metadata.go | 3 +- .../reconciler/openidmeta/metadata_test.go | 21 +- internal/store/openidmeta/data.go | 29 +++ internal/store/openidmeta/store.go | 90 -------- internal/store/openidmeta/store_test.go | 203 ----------------- internal/store/store.go | 88 ++++++++ ...meta_suite_test.go => store_suite_test.go} | 6 +- internal/store/store_test.go | 212 ++++++++++++++++++ 11 files changed, 363 insertions(+), 322 deletions(-) create mode 100644 internal/store/openidmeta/data.go delete mode 100644 internal/store/openidmeta/store.go delete mode 100644 internal/store/openidmeta/store_test.go create mode 100644 internal/store/store.go rename internal/store/{openidmeta/openidmeta_suite_test.go => store_suite_test.go} (69%) create mode 100644 internal/store/store_test.go diff --git a/cmd/discovery-server/app/app.go b/cmd/discovery-server/app/app.go index d7468b7..15cf329 100644 --- a/cmd/discovery-server/app/app.go +++ b/cmd/discovery-server/app/app.go @@ -40,7 +40,8 @@ import ( "github.com/gardener/gardener-discovery-server/internal/handler/workloadidentity" "github.com/gardener/gardener-discovery-server/internal/metrics" oidreconciler "github.com/gardener/gardener-discovery-server/internal/reconciler/openidmeta" - store "github.com/gardener/gardener-discovery-server/internal/store/openidmeta" + "github.com/gardener/gardener-discovery-server/internal/store" + "github.com/gardener/gardener-discovery-server/internal/store/openidmeta" ) // AppName is the name of the application. @@ -126,15 +127,15 @@ func run(ctx context.Context, log logr.Logger, conf *options.Config) error { return err } - store := store.NewStore() + s := store.MustNewStore(openidmeta.Copy) if err := (&oidreconciler.Reconciler{ ResyncPeriod: conf.Resync.Duration, - Store: store, + Store: s, }).SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to create controller: %w", err) } - h := oidhandler.New(store, log.WithName("oid-meta-handler")) + h := oidhandler.New(s, log.WithName("oid-meta-handler")) mux := http.NewServeMux() const ( diff --git a/internal/handler/openidmeta/handler.go b/internal/handler/openidmeta/handler.go index 466ed76..c6914a7 100644 --- a/internal/handler/openidmeta/handler.go +++ b/internal/handler/openidmeta/handler.go @@ -11,7 +11,8 @@ import ( "github.com/google/uuid" "github.com/gardener/gardener-discovery-server/internal/handler" - store "github.com/gardener/gardener-discovery-server/internal/store/openidmeta" + "github.com/gardener/gardener-discovery-server/internal/store" + "github.com/gardener/gardener-discovery-server/internal/store/openidmeta" ) const ( @@ -28,12 +29,12 @@ var ( // Handler is capable or serving openid discovery documents. type Handler struct { - store store.Reader + store store.Reader[openidmeta.Data] log logr.Logger } // New constructs a new [Handler]. -func New(store store.Reader, log logr.Logger) *Handler { +func New(store store.Reader[openidmeta.Data], log logr.Logger) *Handler { return &Handler{ store: store, log: log, @@ -46,7 +47,7 @@ func (h *Handler) HandleOpenIDConfiguration() http.Handler { log := h.log.WithName("openid-configuration") return handler.SetHSTS( handler.AllowMethods(handleRequest(log, h.store, - func(data store.Data) []byte { return data.Config }, + func(data openidmeta.Data) []byte { return data.Config }, ), log, http.MethodGet, http.MethodHead, ), @@ -59,14 +60,14 @@ func (h *Handler) HandleJWKS() http.Handler { log := h.log.WithName("jwks") return handler.SetHSTS( handler.AllowMethods(handleRequest(log, h.store, - func(data store.Data) []byte { return data.JWKS }, + func(data openidmeta.Data) []byte { return data.JWKS }, ), log, http.MethodGet, http.MethodHead, ), ) } -func handleRequest(log logr.Logger, s store.Reader, getContent func(store.Data) []byte) http.Handler { +func handleRequest(log logr.Logger, s store.Reader[openidmeta.Data], getContent func(openidmeta.Data) []byte) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { shootUID := r.PathValue("shootUID") if _, err := uuid.Parse(shootUID); err != nil { diff --git a/internal/handler/openidmeta/handler_test.go b/internal/handler/openidmeta/handler_test.go index d7c501f..ad99e5d 100644 --- a/internal/handler/openidmeta/handler_test.go +++ b/internal/handler/openidmeta/handler_test.go @@ -14,12 +14,13 @@ import ( "github.com/gardener/gardener-discovery-server/internal/handler" oidhandler "github.com/gardener/gardener-discovery-server/internal/handler/openidmeta" + "github.com/gardener/gardener-discovery-server/internal/store" oidstore "github.com/gardener/gardener-discovery-server/internal/store/openidmeta" ) var _ = Describe("#HttpHandlerOpenIDMeta", func() { var ( - store *oidstore.Store + s *store.Store[oidstore.Data] projectName = "foo" @@ -31,18 +32,18 @@ var _ = Describe("#HttpHandlerOpenIDMeta", func() { ) BeforeEach(func() { - store = oidstore.NewStore() - store.Write(projectName+"--"+uid1, oidstore.Data{ + s = store.MustNewStore(oidstore.Copy) + s.Write(projectName+"--"+uid1, oidstore.Data{ Config: []byte("config1"), JWKS: []byte("jwks1"), }) - store.Write(projectName+"--"+uid2, oidstore.Data{ + s.Write(projectName+"--"+uid2, oidstore.Data{ Config: []byte("config2"), JWKS: []byte("jwks2"), }) log := logzap.New(logzap.WriteTo(GinkgoWriter)) - oidHandler = oidhandler.New(store, log) + oidHandler = oidhandler.New(s, log) mux = http.NewServeMux() mux.Handle("/projects/{projectName}/shoots/{shootUID}/issuer/.well-known/openid-configuration", oidHandler.HandleOpenIDConfiguration()) mux.Handle("/projects/{projectName}/shoots/{shootUID}/issuer/jwks", oidHandler.HandleJWKS()) diff --git a/internal/reconciler/openidmeta/metadata.go b/internal/reconciler/openidmeta/metadata.go index fd35a8a..bbb9be0 100644 --- a/internal/reconciler/openidmeta/metadata.go +++ b/internal/reconciler/openidmeta/metadata.go @@ -20,6 +20,7 @@ import ( logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/gardener/gardener-discovery-server/internal/store" "github.com/gardener/gardener-discovery-server/internal/store/openidmeta" "github.com/gardener/gardener-discovery-server/internal/utils" ) @@ -29,7 +30,7 @@ import ( type Reconciler struct { Client client.Client ResyncPeriod time.Duration - Store openidmeta.Writer + Store store.Writer[openidmeta.Data] } // Reconcile retrieves the public OIDC metadata info from a secret and stores into cache. diff --git a/internal/reconciler/openidmeta/metadata_test.go b/internal/reconciler/openidmeta/metadata_test.go index 4181d56..0ed942b 100644 --- a/internal/reconciler/openidmeta/metadata_test.go +++ b/internal/reconciler/openidmeta/metadata_test.go @@ -31,6 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" oidreconciler "github.com/gardener/gardener-discovery-server/internal/reconciler/openidmeta" + "github.com/gardener/gardener-discovery-server/internal/store" oidstore "github.com/gardener/gardener-discovery-server/internal/store/openidmeta" "github.com/gardener/gardener-discovery-server/internal/utils" ) @@ -39,8 +40,8 @@ var _ = Describe("#ReconcileOpenIDMeta", func() { var ( reconciler *oidreconciler.Reconciler - c client.Client - store *oidstore.Store + c client.Client + s *store.Store[oidstore.Data] shoot *gardencorev1beta1.Shoot project *gardencorev1beta1.Project @@ -53,7 +54,7 @@ var _ = Describe("#ReconcileOpenIDMeta", func() { shootUID = types.UID("7a25a9b8-f7fc-4e1e-a421-31b4deaa3086") resyncPeriod = time.Second - expectStoreEntry = func(store *oidstore.Store, key string, want oidstore.Data) { + expectStoreEntry = func(store *store.Store[oidstore.Data], key string, want oidstore.Data) { got, ok := store.Read(key) Expect(ok).To(BeTrue()) Expect(got).To(Equal(want)) @@ -128,10 +129,10 @@ var _ = Describe("#ReconcileOpenIDMeta", func() { "jwks": jwksBytes, }, } - store = oidstore.NewStore() + s = store.MustNewStore(oidstore.Copy) reconciler = &oidreconciler.Reconciler{ Client: c, - Store: store, + Store: s, ResyncPeriod: resyncPeriod, } secretNamespacedName = client.ObjectKeyFromObject(secret) @@ -146,8 +147,8 @@ var _ = Describe("#ReconcileOpenIDMeta", func() { Expect(err).ToNot(HaveOccurred()) Expect(res).To(Equal(ctrl.Result{RequeueAfter: resyncPeriod})) - Expect(store.Len()).To(Equal(1)) - expectStoreEntry(store, secret.Name, oidstore.Data{ + Expect(s.Len()).To(Equal(1)) + expectStoreEntry(s, secret.Name, oidstore.Data{ Config: []byte(`{"issuer":"https://foo","jwks_uri":"https://foo/jwks"}`), JWKS: expectedJWKSBytes, }) @@ -164,8 +165,8 @@ var _ = Describe("#ReconcileOpenIDMeta", func() { Expect(err).ToNot(HaveOccurred()) Expect(res).To(Equal(ctrl.Result{RequeueAfter: resyncPeriod})) - Expect(store.Len()).To(Equal(1)) - expectStoreEntry(store, secret.Name, oidstore.Data{ + Expect(s.Len()).To(Equal(1)) + expectStoreEntry(s, secret.Name, oidstore.Data{ Config: []byte(`{"issuer":"https://foo","jwks_uri":"https://foo/jwks"}`), JWKS: expectedJWKSBytes, }) @@ -176,7 +177,7 @@ var _ = Describe("#ReconcileOpenIDMeta", func() { Expect(err).ToNot(HaveOccurred()) Expect(res).To(Equal(ctrl.Result{})) - Expect(store.Len()).To(Equal(0)) + Expect(s.Len()).To(Equal(0)) }, Entry("secret is missing", func() { Expect(c.Delete(ctx, secret)).To(Succeed()) diff --git a/internal/store/openidmeta/data.go b/internal/store/openidmeta/data.go new file mode 100644 index 0000000..3d4228d --- /dev/null +++ b/internal/store/openidmeta/data.go @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package openidmeta + +import "github.com/gardener/gardener-discovery-server/internal/store" + +var ( + _ store.Reader[Data] = (*store.Store[Data])(nil) + _ store.Writer[Data] = (*store.Store[Data])(nil) +) + +// Data holds openid discovery metadata. +type Data struct { + Config []byte + JWKS []byte +} + +// Copy returns a deep copy of [Data]. +func Copy(data Data) Data { + out := Data{ + Config: make([]byte, len(data.Config)), + JWKS: make([]byte, len(data.JWKS)), + } + copy(out.Config, data.Config) + copy(out.JWKS, data.JWKS) + return out +} diff --git a/internal/store/openidmeta/store.go b/internal/store/openidmeta/store.go deleted file mode 100644 index b209867..0000000 --- a/internal/store/openidmeta/store.go +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package openidmeta - -import ( - "sync" -) - -var ( - _ Reader = (*Store)(nil) - _ Writer = (*Store)(nil) -) - -// Reader lets the consumer read entries from [Store]. -type Reader interface { - Read(key string) (Data, bool) -} - -// Writer lets the consumer write entries to [Store]. -type Writer interface { - Write(key string, data Data) - Delete(key string) -} - -// Store is a thread safe in-memory store that can be used to -// read and write openid discovery metadata. Mind that the store -// does not perform any validation on the inputs. -type Store struct { - mutex sync.RWMutex - store map[string]Data -} - -// Data holds openid discovery metadata. -type Data struct { - Config []byte - JWKS []byte -} - -// NewStore returns a ready for use [Store]. -func NewStore() *Store { - return &Store{ - store: make(map[string]Data), - } -} - -func copyData(data Data) Data { - out := Data{ - Config: make([]byte, len(data.Config)), - JWKS: make([]byte, len(data.JWKS)), - } - copy(out.Config, data.Config) - copy(out.JWKS, data.JWKS) - return out -} - -// Read retrieves an entry from the [Store]. -func (s *Store) Read(key string) (Data, bool) { - s.mutex.RLock() - defer s.mutex.RUnlock() - data, ok := s.store[key] - if ok { - return copyData(data), ok - } - return Data{}, false -} - -// Write sets and entry to the [Store]. -// If the entry exists it is overwritten. -func (s *Store) Write(key string, data Data) { - d := copyData(data) - s.mutex.Lock() - defer s.mutex.Unlock() - s.store[key] = d -} - -// Delete removes an entry from the [Store]. -func (s *Store) Delete(key string) { - s.mutex.Lock() - defer s.mutex.Unlock() - delete(s.store, key) -} - -// Len returns the number of entries in the [Store]. -func (s *Store) Len() int { - s.mutex.RLock() - defer s.mutex.RUnlock() - return len(s.store) -} diff --git a/internal/store/openidmeta/store_test.go b/internal/store/openidmeta/store_test.go deleted file mode 100644 index 86e2cb1..0000000 --- a/internal/store/openidmeta/store_test.go +++ /dev/null @@ -1,203 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors -// -// SPDX-License-Identifier: Apache-2.0 - -package openidmeta_test - -import ( - "sync" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - "github.com/gardener/gardener-discovery-server/internal/store/openidmeta" -) - -var _ = Describe("Store", func() { - const ( - fooKey string = "foo" - ) - var ( - store *openidmeta.Store - data openidmeta.Data - expectedData openidmeta.Data - assertExpected = func(store *openidmeta.Store, key string, expected openidmeta.Data) { - retrieved, ok := store.Read(key) - Expect(ok).To(BeTrue()) - Expect(retrieved).To(Equal(expected)) - } - assertNotFound = func(store *openidmeta.Store, key string) { - retrieved, ok := store.Read(key) - Expect(ok).To(BeFalse()) - Expect(retrieved).To(Equal(openidmeta.Data{})) - } - ) - BeforeEach(func() { - store = openidmeta.NewStore() - data = openidmeta.Data{ - Config: []byte("config"), - JWKS: []byte("jwks"), - } - expectedData = openidmeta.Data{ - Config: []byte("config"), - JWKS: []byte("jwks"), - } - }) - - It("should be empty", func() { - Expect(store.Len()).To(Equal(0)) - }) - - It("should not find an entry", func() { - store.Write(fooKey, data) - - Expect(store.Len()).To(Equal(1)) - assertNotFound(store, "bar") - }) - - It("should correctly set an entry", func() { - store.Write(fooKey, data) - - Expect(store.Len()).To(Equal(1)) - assertExpected(store, fooKey, expectedData) - }) - - It("should correctly overwrite an entry", func() { - store.Write(fooKey, data) - - Expect(store.Len()).To(Equal(1)) - assertExpected(store, fooKey, expectedData) - - newData := openidmeta.Data{Config: []byte("foo"), JWKS: []byte("bar")} - expectedNewData := openidmeta.Data{Config: []byte("foo"), JWKS: []byte("bar")} - store.Write(fooKey, newData) - - Expect(store.Len()).To(Equal(1)) - assertExpected(store, fooKey, expectedNewData) - }) - - It("should not be able to modify the unredlying entry", func() { - store.Write(fooKey, data) - - Expect(store.Len()).To(Equal(1)) - retrieved, ok := store.Read(fooKey) - - Expect(ok).To(BeTrue()) - Expect(retrieved).To(Equal(expectedData)) - - // modify a single byte - retrieved.Config[0]++ - retrieved.JWKS[0]++ - - assertExpected(store, fooKey, expectedData) - }) - - It("should correctly remove an entry", func() { - store.Write(fooKey, data) - - assertExpected(store, fooKey, expectedData) - - store.Delete(fooKey) - assertNotFound(store, fooKey) - Expect(store.Len()).To(Equal(0)) - }) - - It("should be able to use the store in parallel", func() { - initialEntries := map[string]openidmeta.Data{ - "0": {Config: []byte("0"), JWKS: []byte("0")}, - "1": {Config: []byte("1"), JWKS: []byte("1")}, - "2": {Config: []byte("2"), JWKS: []byte("2")}, - "3": {Config: []byte("3"), JWKS: []byte("3")}, - "4": {Config: []byte("4"), JWKS: []byte("4")}, - "5": {Config: []byte("5"), JWKS: []byte("5")}, - } - - var wg sync.WaitGroup - wg.Add(len(initialEntries)) - for k, e := range initialEntries { - go func() { - store.Write(k, e) - wg.Done() - }() - } - wg.Wait() - - expectedEntries := map[string]openidmeta.Data{ - "0": {Config: []byte("0"), JWKS: []byte("0")}, - "1": {Config: []byte("1"), JWKS: []byte("1")}, - "2": {Config: []byte("2"), JWKS: []byte("2")}, - "3": {Config: []byte("3"), JWKS: []byte("3")}, - "4": {Config: []byte("4"), JWKS: []byte("4")}, - "5": {Config: []byte("5"), JWKS: []byte("5")}, - } - - Expect(store.Len()).To(Equal(len(expectedEntries))) - wg.Add(len(expectedEntries)) - for k, e := range expectedEntries { - go func() { - assertExpected(store, k, e) - wg.Done() - }() - } - wg.Wait() - - modifyEntries := map[string]openidmeta.Data{ - "2": {Config: []byte("22"), JWKS: []byte("22")}, - "3": {Config: []byte("33"), JWKS: []byte("33")}, - "5": {Config: []byte("55"), JWKS: []byte("55")}, - } - - wg.Add(len(modifyEntries)) - for k, e := range modifyEntries { - go func() { - store.Write(k, e) - wg.Done() - }() - } - wg.Wait() - - expectedEntries = map[string]openidmeta.Data{ - "0": {Config: []byte("0"), JWKS: []byte("0")}, - "1": {Config: []byte("1"), JWKS: []byte("1")}, - "2": {Config: []byte("22"), JWKS: []byte("22")}, - "3": {Config: []byte("33"), JWKS: []byte("33")}, - "4": {Config: []byte("4"), JWKS: []byte("4")}, - "5": {Config: []byte("55"), JWKS: []byte("55")}, - } - - Expect(store.Len()).To(Equal(len(expectedEntries))) - wg.Add(len(expectedEntries)) - for k, e := range expectedEntries { - go func() { - assertExpected(store, k, e) - wg.Done() - }() - } - wg.Wait() - - keysToDelete := []string{"0", "1", "5", "111"} - wg.Add(len(keysToDelete)) - for _, k := range keysToDelete { - go func() { - store.Delete(k) - wg.Done() - }() - } - wg.Wait() - - expectedEntries = map[string]openidmeta.Data{ - "2": {Config: []byte("22"), JWKS: []byte("22")}, - "3": {Config: []byte("33"), JWKS: []byte("33")}, - "4": {Config: []byte("4"), JWKS: []byte("4")}, - } - Expect(store.Len()).To(Equal(len(expectedEntries))) - wg.Add(len(expectedEntries)) - for k, e := range expectedEntries { - go func() { - assertExpected(store, k, e) - wg.Done() - }() - } - wg.Wait() - }) -}) diff --git a/internal/store/store.go b/internal/store/store.go new file mode 100644 index 0000000..ec1251d --- /dev/null +++ b/internal/store/store.go @@ -0,0 +1,88 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package store + +import ( + "errors" + "sync" +) + +var ErrNoCopyFunc = errors.New("store: copyFunc must not be nil") + +// Reader lets the consumer read entries from [Store]. +type Reader[T any] interface { + Read(key string) (T, bool) +} + +// Writer lets the consumer write entries to [Store]. +type Writer[T any] interface { + Write(key string, data T) + Delete(key string) +} + +// Store is a thread safe in-memory store that can be used to +// read and write data. Mind that the store +// does not perform any validation on the inputs. +type Store[T any] struct { + mutex sync.RWMutex + store map[string]T + copyFunc func(T) T +} + +// NewStore returns a ready for use [Store]. +func NewStore[T any](copyFunc func(T) T) (*Store[T], error) { + if copyFunc == nil { + return nil, ErrNoCopyFunc + } + return &Store[T]{ + store: make(map[string]T), + copyFunc: copyFunc, + }, nil +} + +// MustNewStore returns a ready for use [Store]. +// It panics if copyFunc is nil. +func MustNewStore[T any](copyFunc func(T) T) *Store[T] { + store, err := NewStore(copyFunc) + if err != nil { + panic(err) + } + return store +} + +// Read retrieves an entry from the [Store]. +func (s *Store[T]) Read(key string) (T, bool) { + s.mutex.RLock() + defer s.mutex.RUnlock() + data, ok := s.store[key] + if ok { + return s.copyFunc(data), ok + } + var t T + return t, false +} + +// Write sets and entry to the [Store]. +// If the entry exists it is overwritten. +func (s *Store[T]) Write(key string, data T) { + d := s.copyFunc(data) + s.mutex.Lock() + defer s.mutex.Unlock() + s.store[key] = d +} + +// Delete removes an entry from the [Store]. +func (s *Store[T]) Delete(key string) { + s.mutex.Lock() + defer s.mutex.Unlock() + delete(s.store, key) +} + +// Len returns the number of entries in the [Store]. +func (s *Store[T]) Len() int { + s.mutex.RLock() + defer s.mutex.RUnlock() + return len(s.store) +} diff --git a/internal/store/openidmeta/openidmeta_suite_test.go b/internal/store/store_suite_test.go similarity index 69% rename from internal/store/openidmeta/openidmeta_suite_test.go rename to internal/store/store_suite_test.go index 612ee11..06bc1da 100644 --- a/internal/store/openidmeta/openidmeta_suite_test.go +++ b/internal/store/store_suite_test.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package openidmeta_test +package store_test import ( "testing" @@ -11,7 +11,7 @@ import ( . "github.com/onsi/gomega" ) -func TestOpenIDMeta(t *testing.T) { +func TestStore(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "OpenID Metadata Store Test Suite") + RunSpecs(t, "Store Test Suite") } diff --git a/internal/store/store_test.go b/internal/store/store_test.go new file mode 100644 index 0000000..5254420 --- /dev/null +++ b/internal/store/store_test.go @@ -0,0 +1,212 @@ +// SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package store_test + +import ( + "sync" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/gardener/gardener-discovery-server/internal/store" +) + +type data struct { + bytes []byte +} + +func copyData(d data) data { + out := data{ + bytes: make([]byte, len(d.bytes)), + } + copy(out.bytes, d.bytes) + return out +} + +var _ = Describe("Store", func() { + const ( + fooKey string = "foo" + ) + var ( + s *store.Store[data] + d data + expectedData data + assertExpected = func(store *store.Store[data], key string, expected data) { + retrieved, ok := store.Read(key) + Expect(ok).To(BeTrue()) + Expect(retrieved).To(Equal(expected)) + } + assertNotFound = func(store *store.Store[data], key string) { + retrieved, ok := store.Read(key) + Expect(ok).To(BeFalse()) + Expect(retrieved).To(Equal(data{})) + } + ) + BeforeEach(func() { + s = store.MustNewStore(copyData) + d = data{ + bytes: []byte("config"), + } + expectedData = data{ + bytes: []byte("config"), + } + }) + + It("should be empty", func() { + Expect(s.Len()).To(Equal(0)) + }) + + It("should not find an entry", func() { + s.Write(fooKey, d) + + Expect(s.Len()).To(Equal(1)) + assertNotFound(s, "bar") + }) + + It("should correctly set an entry", func() { + s.Write(fooKey, d) + + Expect(s.Len()).To(Equal(1)) + assertExpected(s, fooKey, expectedData) + }) + + It("should correctly overwrite an entry", func() { + s.Write(fooKey, d) + + Expect(s.Len()).To(Equal(1)) + assertExpected(s, fooKey, expectedData) + + newData := data{bytes: []byte("foo")} + expectedNewData := data{bytes: []byte("foo")} + s.Write(fooKey, newData) + + Expect(s.Len()).To(Equal(1)) + assertExpected(s, fooKey, expectedNewData) + }) + + It("should not be able to modify the unredlying entry", func() { + s.Write(fooKey, d) + + Expect(s.Len()).To(Equal(1)) + retrieved, ok := s.Read(fooKey) + + Expect(ok).To(BeTrue()) + Expect(retrieved).To(Equal(expectedData)) + + // modify a single byte + retrieved.bytes[0]++ + + assertExpected(s, fooKey, expectedData) + }) + + It("should correctly remove an entry", func() { + s.Write(fooKey, d) + + assertExpected(s, fooKey, expectedData) + + s.Delete(fooKey) + assertNotFound(s, fooKey) + Expect(s.Len()).To(Equal(0)) + }) + + It("should be able to use the store in parallel", func() { + initialEntries := map[string]data{ + "0": {bytes: []byte("0")}, + "1": {bytes: []byte("1")}, + "2": {bytes: []byte("2")}, + "3": {bytes: []byte("3")}, + "4": {bytes: []byte("4")}, + "5": {bytes: []byte("5")}, + } + + var wg sync.WaitGroup + wg.Add(len(initialEntries)) + for k, e := range initialEntries { + go func() { + s.Write(k, e) + wg.Done() + }() + } + wg.Wait() + + expectedEntries := map[string]data{ + "0": {bytes: []byte("0")}, + "1": {bytes: []byte("1")}, + "2": {bytes: []byte("2")}, + "3": {bytes: []byte("3")}, + "4": {bytes: []byte("4")}, + "5": {bytes: []byte("5")}, + } + + Expect(s.Len()).To(Equal(len(expectedEntries))) + wg.Add(len(expectedEntries)) + for k, e := range expectedEntries { + go func() { + assertExpected(s, k, e) + wg.Done() + }() + } + wg.Wait() + + modifyEntries := map[string]data{ + "2": {bytes: []byte("22")}, + "3": {bytes: []byte("33")}, + "5": {bytes: []byte("55")}, + } + + wg.Add(len(modifyEntries)) + for k, e := range modifyEntries { + go func() { + s.Write(k, e) + wg.Done() + }() + } + wg.Wait() + + expectedEntries = map[string]data{ + "0": {bytes: []byte("0")}, + "1": {bytes: []byte("1")}, + "2": {bytes: []byte("22")}, + "3": {bytes: []byte("33")}, + "4": {bytes: []byte("4")}, + "5": {bytes: []byte("55")}, + } + + Expect(s.Len()).To(Equal(len(expectedEntries))) + wg.Add(len(expectedEntries)) + for k, e := range expectedEntries { + go func() { + assertExpected(s, k, e) + wg.Done() + }() + } + wg.Wait() + + keysToDelete := []string{"0", "1", "5", "111"} + wg.Add(len(keysToDelete)) + for _, k := range keysToDelete { + go func() { + s.Delete(k) + wg.Done() + }() + } + wg.Wait() + + expectedEntries = map[string]data{ + "2": {bytes: []byte("22")}, + "3": {bytes: []byte("33")}, + "4": {bytes: []byte("4")}, + } + Expect(s.Len()).To(Equal(len(expectedEntries))) + wg.Add(len(expectedEntries)) + for k, e := range expectedEntries { + go func() { + assertExpected(s, k, e) + wg.Done() + }() + } + wg.Wait() + }) +}) From 8516687d50abb44c894cc0b0f2e18af4a1a5ad7e Mon Sep 17 00:00:00 2001 From: Dimitar Mirchev Date: Wed, 8 Jan 2025 09:43:30 +0200 Subject: [PATCH 2/3] Add doc string --- internal/store/store.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/store/store.go b/internal/store/store.go index ec1251d..5df0c97 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -9,6 +9,7 @@ import ( "sync" ) +// ErrNoCopyFunc is an error indicating that a copyFunc was not passed to [Store]. var ErrNoCopyFunc = errors.New("store: copyFunc must not be nil") // Reader lets the consumer read entries from [Store]. From ee0848e4003fe2788b52f237807398db96dfbd2c Mon Sep 17 00:00:00 2001 From: Dimitar Mirchev Date: Mon, 13 Jan 2025 16:14:31 +0200 Subject: [PATCH 3/3] Update skaffold deps --- skaffold.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/skaffold.yaml b/skaffold.yaml index ff683cb..0d6e390 100644 --- a/skaffold.yaml +++ b/skaffold.yaml @@ -18,6 +18,7 @@ build: - internal/handler/workloadidentity - internal/metrics - internal/reconciler/openidmeta + - internal/store - internal/store/openidmeta - internal/utils - VERSION