diff --git a/ctrl/internal/application/app.go b/ctrl/internal/application/app.go index c20936457b5f138b4dfc959bc5610408ffc55480..efbcbc62a0e24e0757448bf4bbd972483a5d85b9 100644 --- a/ctrl/internal/application/app.go +++ b/ctrl/internal/application/app.go @@ -1,7 +1,9 @@ package application import ( + "code.fbi.h-da.de/danet/costaquanta/ctrl/internal/core/ports" "code.fbi.h-da.de/danet/costaquanta/ctrl/internal/infrastructure/interaction" + "code.fbi.h-da.de/danet/costaquanta/ctrl/internal/infrastructure/store" "code.fbi.h-da.de/danet/costaquanta/libs/logging" sdktrace "go.opentelemetry.io/otel/sdk/trace" "go.uber.org/zap" @@ -11,6 +13,7 @@ type Application struct { tracer *sdktrace.TracerProvider HealthServer *interaction.HealthServer RoutingServer *interaction.RoutingServer + KmsStore ports.KmsStore } func NewApplication(log *zap.Logger, tracer *sdktrace.TracerProvider) *Application { @@ -22,9 +25,16 @@ func NewApplication(log *zap.Logger, tracer *sdktrace.TracerProvider) *Applicati routingTracer := tracer.Tracer("routingServer") routerSrv := interaction.NewRoutingServer(routingLogger, routingTracer) + // Create the in-memory KMS store, as we currently have no other option. + // Can be replaced by a store choice via flags or config file later. + kmsStoreLogger := logging.CreateChildLogger(log, "KmsStore") + kmsStoreTracer := tracer.Tracer("KmsStore") + kmsStore := store.NewInMemoryKmsStore(kmsStoreLogger, kmsStoreTracer) + return &Application{ tracer: tracer, HealthServer: healthSrv, RoutingServer: routerSrv, + KmsStore: kmsStore, } } diff --git a/ctrl/internal/core/model/kms.go b/ctrl/internal/core/model/kms.go new file mode 100644 index 0000000000000000000000000000000000000000..1a7bff23959c3803ccb8747d51ad0c10ee6fb9cc --- /dev/null +++ b/ctrl/internal/core/model/kms.go @@ -0,0 +1,14 @@ +package model + +import "github.com/google/uuid" + +type KMS struct { + Id uuid.UUID + Name string + // Many more fields missing, add when needed. + // Should represent everything defined in the kms config api spec. +} + +func NewKMS(id uuid.UUID, name string) *KMS { + return &KMS{Id: id, Name: name} +} diff --git a/ctrl/internal/core/ports/kms_store.go b/ctrl/internal/core/ports/kms_store.go new file mode 100644 index 0000000000000000000000000000000000000000..895c1ca50cfa92aac3c1826aef8dad9e5e3d1af3 --- /dev/null +++ b/ctrl/internal/core/ports/kms_store.go @@ -0,0 +1,16 @@ +package ports + +import ( + "context" + + "code.fbi.h-da.de/danet/costaquanta/ctrl/internal/core/model" + "github.com/google/uuid" +) + +type KmsStore interface { + Create(context.Context, model.KMS) (model.KMS, error) + Get(context.Context, uuid.UUID) (model.KMS, error) + GetAll(context.Context) ([]model.KMS, error) + Update(context.Context, model.KMS) (model.KMS, error) + Delete(context.Context, uuid.UUID) error +} diff --git a/ctrl/internal/infrastructure/store/in_memory_kms_store.go b/ctrl/internal/infrastructure/store/in_memory_kms_store.go new file mode 100644 index 0000000000000000000000000000000000000000..cd3e80f44fc49cf7f0d1dfdf3c298ef71fa28da6 --- /dev/null +++ b/ctrl/internal/infrastructure/store/in_memory_kms_store.go @@ -0,0 +1,140 @@ +package store + +import ( + "context" + "errors" + "fmt" + "sync" + + "code.fbi.h-da.de/danet/costaquanta/ctrl/internal/core/model" + "github.com/google/uuid" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" +) + +// Implements the KmsStore interface. +type InMemoryKmsStore struct { + // The key is the ID of the KMS, and the value is the KMS object. + // The ID of the element is therefore stored twice, as the key and in the value object. + // This has to be checked by the store implementation, so that no error happens. + dataStore map[uuid.UUID]model.KMS + // Used to lock the dataStore + dsMutex sync.Mutex + + logger *zap.SugaredLogger + tracer trace.Tracer +} + +func NewInMemoryKmsStore(logger *zap.SugaredLogger, tracer trace.Tracer) *InMemoryKmsStore { + inMemoryKmsStore := &InMemoryKmsStore{ + dataStore: make(map[uuid.UUID]model.KMS), + dsMutex: sync.Mutex{}, + logger: logger, + tracer: tracer, + } + + return inMemoryKmsStore +} + +func (s *InMemoryKmsStore) Create(ctx context.Context, inputKms model.KMS) (model.KMS, error) { + _, span := s.tracer.Start(ctx, "Create KMS") + defer span.End() + + s.dsMutex.Lock() + defer s.dsMutex.Unlock() + + if _, kmsAlreadyExists := s.dataStore[inputKms.Id]; kmsAlreadyExists { + s.logger.Errorw("KMS already exists in database", + "kmsId", inputKms.Id.String(), + "kmsName", inputKms.Name) + + errorString := fmt.Sprintf( + "kms with id %s already exists in database", + inputKms.Id.String(), + ) + + return model.KMS{}, errors.New(errorString) + } else { + s.dataStore[inputKms.Id] = inputKms + } + + return s.dataStore[inputKms.Id], nil +} + +func (s *InMemoryKmsStore) Get(ctx context.Context, id uuid.UUID) (model.KMS, error) { + _, span := s.tracer.Start(ctx, "Get KMS") + defer span.End() + + s.dsMutex.Lock() + defer s.dsMutex.Unlock() + + kms, kmsExists := s.dataStore[id] + if !kmsExists { + s.logger.Errorw("KMS not found in database", + "kmsId", id.String()) + + errorString := fmt.Sprintf("no kms found with id %s", id.String()) + + return model.KMS{}, errors.New(errorString) + } + + return kms, nil +} + +func (s *InMemoryKmsStore) GetAll(ctx context.Context) ([]model.KMS, error) { + _, span := s.tracer.Start(ctx, "GetAll KMS") + defer span.End() + + s.dsMutex.Lock() + defer s.dsMutex.Unlock() + + kmsList := make([]model.KMS, 0, len(s.dataStore)) + for _, kms := range s.dataStore { + kmsList = append(kmsList, kms) + } + + return kmsList, nil +} + +func (s *InMemoryKmsStore) Update(ctx context.Context, inputKms model.KMS) (model.KMS, error) { + _, span := s.tracer.Start(ctx, "Update KMS") + defer span.End() + + s.dsMutex.Lock() + defer s.dsMutex.Unlock() + + _, kmsExists := s.dataStore[inputKms.Id] + if !kmsExists { + s.logger.Errorw("KMS not found in database", + "kmsId: ", inputKms.Id.String(), + "kmsName: ", inputKms.Name) + errorString := fmt.Sprintf("no kms found with id %s", inputKms.Id.String()) + + return model.KMS{}, errors.New(errorString) + } + + s.dataStore[inputKms.Id] = inputKms + + return s.dataStore[inputKms.Id], nil +} + +func (s *InMemoryKmsStore) Delete(ctx context.Context, id uuid.UUID) error { + _, span := s.tracer.Start(ctx, "Delete KMS") + defer span.End() + + s.dsMutex.Lock() + defer s.dsMutex.Unlock() + + _, kmsExists := s.dataStore[id] + if !kmsExists { + s.logger.Errorw("KMS not found in database", + "kmsId: ", id.String()) + errorString := fmt.Sprintf("no kms found with id %s", id.String()) + + return errors.New(errorString) + } + + delete(s.dataStore, id) + + return nil +} diff --git a/ctrl/internal/infrastructure/store/in_memory_kms_store_test.go b/ctrl/internal/infrastructure/store/in_memory_kms_store_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7edb39e64ad9cb1fafe04461d1102006bc936bc8 --- /dev/null +++ b/ctrl/internal/infrastructure/store/in_memory_kms_store_test.go @@ -0,0 +1,243 @@ +package store + +import ( + "context" + "testing" + + "code.fbi.h-da.de/danet/costaquanta/ctrl/internal/core/model" + "code.fbi.h-da.de/danet/costaquanta/libs/logging" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel/trace/noop" +) + +func createCtxAndStoreForTest() (*InMemoryKmsStore, context.Context) { + // Create a tracer which does nothing but satisfies the interface + tracer := noop.NewTracerProvider().Tracer("kmsStoreTest") + + // Creates a logger for the tests + parentLogger := logging.CreateProductionLogger("kms") + childLogger := logging.CreateChildLogger(parentLogger, "kmsStoreTest") + + ctx := context.Background() + + return NewInMemoryKmsStore(childLogger, tracer), ctx +} + +func TestInMemoryKmsStoreCreate(t *testing.T) { + t.Parallel() + + t.Run("Create one KMS", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + createdKms, err := store.Create(ctx, kms) + require.NoError(t, err) + assert.Equal(t, kms, createdKms) + assert.Equal(t, kms, store.dataStore[kms.Id]) + }) + + t.Run("Create an already existing KMS should result in error", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + kms1Id := uuid.New() + + kms1 := model.KMS{Id: kms1Id, Name: "Test KMS"} + _, err := store.Create(ctx, kms1) + require.NoError(t, err) + + kms2 := model.KMS{Id: kms1Id, Name: "Test KMS"} + _, err = store.Create(ctx, kms2) + + assert.Equal(t, kms1, store.dataStore[kms1Id]) + assert.Len(t, store.dataStore, 1) + + require.Error(t, err) + }) + + t.Run("Create multiple KMS with different IDs", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + for range 100 { + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + _, err := store.Create(ctx, kms) + require.NoError(t, err) + } + + assert.Len(t, store.dataStore, 100) + }) +} + +func TestInMemoryKmsStoreUpdate(t *testing.T) { + t.Parallel() + + t.Run("Update one KMS", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + + // Update the KMS + kms.Name = "Updated KMS" + updatedKms, err := store.Update(ctx, kms) + require.NoError(t, err) + assert.Equal(t, kms, updatedKms) + + assert.Equal(t, kms, store.dataStore[kms.Id]) + assert.Len(t, store.dataStore, 1) + }) + + t.Run("Update not existing KMS should result in error", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + + kms2 := model.KMS{Id: uuid.New(), Name: "Updated KMS"} + + // Update the KMS which does not exist. + _, err := store.Update(ctx, kms2) + require.Error(t, err) + + assert.Len(t, store.dataStore, 1) + assert.Equal(t, kms, store.dataStore[kms.Id]) + }) +} + +func TestInMemoryKmsStoreGet(t *testing.T) { + t.Parallel() + + t.Run("Get one KMS", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + + returnedKms, err := store.Get(ctx, kms.Id) + require.NoError(t, err) + assert.Equal(t, kms, returnedKms) + + assert.Equal(t, returnedKms, store.dataStore[kms.Id]) + assert.Len(t, store.dataStore, 1) + }) + + t.Run("Get not existing KMS should result in error", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + // Store ist currently empty + _, err := store.Get(ctx, uuid.New()) + require.Error(t, err) + + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + + // Store is not empty, but we request an non existing uuid + _, err = store.Get(ctx, uuid.New()) + require.Error(t, err) + }) +} + +func TestInMemoryKmsStoreGetAll(t *testing.T) { + t.Parallel() + + t.Run("Get all KMS", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + for range 100 { + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + } + + returnedSlice, err := store.GetAll(ctx) + require.NoError(t, err) + assert.Len(t, returnedSlice, 100) + + idMap := make(map[uuid.UUID]bool) + for _, kms := range returnedSlice { + assert.Equal(t, "Test KMS", kms.Name) + _, exists := idMap[kms.Id] + assert.False(t, exists) + idMap[kms.Id] = true + } + }) + + t.Run( + "Get all KMS when none available should result in empty return slice", + func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + returnedSlice, err := store.GetAll(ctx) + require.NoError(t, err) + assert.Empty(t, returnedSlice) + + assert.Empty(t, returnedSlice) + }, + ) +} + +func TestInMemoryKmsStoreDelete(t *testing.T) { + t.Parallel() + + t.Run("Delete one KMS", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + + err := store.Delete(ctx, kms.Id) + require.NoError(t, err) + + assert.Empty(t, store.dataStore) + }) + + t.Run("Delete one KMS should only delete the exact KMS", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + + for range 99 { + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + } + assert.Len(t, store.dataStore, 100) + + err := store.Delete(ctx, kms.Id) + require.NoError(t, err) + + _, err = store.Get(ctx, kms.Id) + require.Error(t, err) + + assert.Len(t, store.dataStore, 99) + }) + + t.Run("Delete not existing KMS should result in error", func(t *testing.T) { + t.Parallel() + store, ctx := createCtxAndStoreForTest() + + // Store ist currently empty + err := store.Delete(ctx, uuid.New()) + require.Error(t, err) + + kms := model.KMS{Id: uuid.New(), Name: "Test KMS"} + store.dataStore[kms.Id] = kms + + // Store is not empty, but we request an non existing uuid + err = store.Delete(ctx, uuid.New()) + require.Error(t, err) + + assert.Equal(t, kms, store.dataStore[kms.Id]) + assert.Len(t, store.dataStore, 1) + }) +} diff --git a/go.mod b/go.mod index e8bb12203594b8d90430f88a78eaf7c600a84d94..8b45236a07e4b85643d6082fa8806895bf8166ff 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,9 @@ go 1.24 require ( github.com/caarlos0/env/v11 v11.3.1 + github.com/google/uuid v1.6.0 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 + github.com/stretchr/testify v1.10.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 go.opentelemetry.io/otel v1.35.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 @@ -18,10 +20,11 @@ require ( require ( github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 // indirect go.opentelemetry.io/otel/metric v1.35.0 // indirect @@ -33,4 +36,5 @@ require ( golang.org/x/text v0.22.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250218202821-56aae31c358a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index db8e9b44ebdb9f554a47ac8c3bd63abf55803013..82ada2ccf3631a33271aa0a0b059a95a72f2bb05 100644 --- a/go.sum +++ b/go.sum @@ -19,8 +19,14 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1 h1:KcFzXwzM/kGhIRHvc8jdix github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.1/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1 h1:e9Rjr40Z98/clHv5Yg79Is0NtosR5LXRvdr7o/6NwbA= github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.1/go.mod h1:tIxuGz/9mpox++sgp9fJjHO0+q1X9/UOWd798aAm22M= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= @@ -65,5 +71,8 @@ google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI= google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/justfile b/justfile index 7f0178e59d003b6487680e649295ab7df5a8cbff..db8ec8798a1574601bd00310bd843bc30e973873 100644 --- a/justfile +++ b/justfile @@ -10,6 +10,9 @@ run-ctrl: build-api: buf generate +test: + go test ./... + lint: docker run --rm -t -v $(pwd):/app -w /app \ --user $(id -u):$(id -g) \