From 08e9908d2705c4f492da69ad0b1d2374ec394cf7 Mon Sep 17 00:00:00 2001 From: Will Assis Date: Thu, 13 Nov 2025 16:55:09 -0300 Subject: [PATCH] setup settingstore --- pkg/storage/unified/resource/settingstore.go | 95 ++++++++++++ .../unified/resource/settingstore_test.go | 142 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 pkg/storage/unified/resource/settingstore.go create mode 100644 pkg/storage/unified/resource/settingstore_test.go diff --git a/pkg/storage/unified/resource/settingstore.go b/pkg/storage/unified/resource/settingstore.go new file mode 100644 index 00000000000..f394ff6808a --- /dev/null +++ b/pkg/storage/unified/resource/settingstore.go @@ -0,0 +1,95 @@ +package resource + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/apimachinery/validation" +) + +const ( + settingsSection = "unified/settings" +) + +type settingStore struct { + kv KV +} + +type SettingKey struct { + Namespace string + Group string + Resource string +} + +func (k SettingKey) String() string { + return fmt.Sprintf("%s/%s/%s", k.Group, k.Resource, k.Namespace) +} + +func (k SettingKey) Validate() error { + if k.Namespace == "" { + return NewValidationError("namespace", k.Namespace, ErrNamespaceRequired) + } + if err := validation.IsValidGroup(k.Group); err != nil { + return NewValidationError("group", k.Group, err[0]) + } + if err := validation.IsValidResource(k.Resource); err != nil { + return NewValidationError("resource", k.Resource, err[0]) + } + return nil +} + +type Setting struct { + Namespace string `json:"namespace"` + Group string `json:"group"` + Resource string `json:"resource"` + LastImportTime time.Time `json:"lastImportTime"` +} + +func newSettingStore(kv KV) *settingStore { + return &settingStore{ + kv: kv, + } +} + +func (d *settingStore) Get(ctx context.Context, key SettingKey) (Setting, error) { + if err := key.Validate(); err != nil { + return Setting{}, fmt.Errorf("invalid setting key: %w", err) + } + + reader, err := d.kv.Get(ctx, settingsSection, key.String()) + if err != nil { + return Setting{}, err + } + defer func() { _ = reader.Close() }() + var setting Setting + if err = json.NewDecoder(reader).Decode(&setting); err != nil { + return Setting{}, err + } + return setting, nil +} + +func (d *settingStore) Save(ctx context.Context, setting Setting) error { + settingKey := SettingKey{ + Namespace: setting.Namespace, + Group: setting.Group, + Resource: setting.Resource, + } + + if err := settingKey.Validate(); err != nil { + return fmt.Errorf("invalid settingKey key: %w", err) + } + + writer, err := d.kv.Save(ctx, settingsSection, settingKey.String()) + if err != nil { + return err + } + encoder := json.NewEncoder(writer) + if err := encoder.Encode(setting); err != nil { + _ = writer.Close() + return err + } + + return writer.Close() +} diff --git a/pkg/storage/unified/resource/settingstore_test.go b/pkg/storage/unified/resource/settingstore_test.go new file mode 100644 index 00000000000..91363bec703 --- /dev/null +++ b/pkg/storage/unified/resource/settingstore_test.go @@ -0,0 +1,142 @@ +package resource + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func setupTestSettingStore(t *testing.T) *settingStore { + db := setupTestBadgerDB(t) + t.Cleanup(func() { + err := db.Close() + require.NoError(t, err) + }) + kv := NewBadgerKV(db) + return newSettingStore(kv) +} + +func TestNewSettingStore(t *testing.T) { + store := setupTestSettingStore(t) + assert.NotNil(t, store.kv) +} + +func TestSettingStore_SettingKey_String(t *testing.T) { + tests := []struct { + name string + settingKey SettingKey + expected string + }{ + { + name: "basic event key", + settingKey: SettingKey{ + Namespace: "default", + Group: "apps", + Resource: "resource", + }, + expected: "apps/resource/default", + }, + { + name: "empty namespace", + settingKey: SettingKey{ + Namespace: "", + Group: "apps", + Resource: "resource", + }, + expected: "apps/resource/", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.settingKey.String() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestSettingStore_SettingKey_Validate(t *testing.T) { + tests := []struct { + name string + key SettingKey + error error + }{ + { + name: "valid key", + key: SettingKey{ + Namespace: "default", + Group: "apps", + Resource: "resource", + }, + error: nil, + }, + { + name: "empty namespace", + key: SettingKey{ + Namespace: "", + Group: "apps", + Resource: "resource", + }, + error: errors.New("namespace '' is invalid: namespace is required"), + }, + { + name: "empty group", + key: SettingKey{ + Namespace: "default", + Group: "", + Resource: "resource", + }, + error: errors.New("group '' is invalid: group is too short"), + }, + { + name: "empty resource", + key: SettingKey{ + Namespace: "default", + Group: "apps", + Resource: "", + }, + error: errors.New("resource '' is invalid: resource is too short"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.key.Validate() + + if tt.error == nil { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } + }) + } +} + +func TestSettingStore_Save_Get(t *testing.T) { + ctx := context.Background() + store := setupTestSettingStore(t) + + setting := Setting{ + Namespace: "default", + Group: "apps", + Resource: "resource", + LastImportTime: time.Now().Truncate(time.Microsecond), + } + + err := store.Save(ctx, setting) + require.NoError(t, err) + + settingKey := SettingKey{ + Namespace: "default", + Group: "apps", + Resource: "resource", + } + + retrievedSetting, err := store.Get(ctx, settingKey) + require.NoError(t, err) + assert.Equal(t, setting, retrievedSetting) +}