uses new US naming validation for kv store validation
CodeQL checks / Detect whether code changed (push) Waiting to run Details
CodeQL checks / Analyze (actions) (push) Blocked by required conditions Details
CodeQL checks / Analyze (go) (push) Blocked by required conditions Details
CodeQL checks / Analyze (javascript) (push) Blocked by required conditions Details

This commit is contained in:
Owen Smallwood 2025-10-06 17:03:26 -06:00
parent 1bc485c0ca
commit 8d59f964c0
4 changed files with 247 additions and 271 deletions

View File

@ -2,15 +2,16 @@ package resource
import (
"context"
"errors"
"fmt"
"io"
"iter"
"math"
"regexp"
"strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/apimachinery/validation"
gocache "github.com/patrickmn/go-cache"
)
@ -54,21 +55,6 @@ type GroupResource struct {
Resource string
}
var (
// validNameRegex validates that a name contains only lowercase alphanumeric characters, '-' or '.'
// and starts and ends with an alphanumeric character
validNameRegex = regexp.MustCompile(`^[a-z0-9]([a-z0-9.-]*[a-z0-9])?$`)
// k8sRegex validates Kubernetes qualified name format
// must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character
// all future Grafana UIDs will need to conform to this
k8sRegex = regexp.MustCompile(`^[A-Za-z0-9][-A-Za-z0-9_.]*[A-Za-z0-9]$`)
// legacyNameRegex validates legacy UIDs: letters, numbers, dashes, underscores
// this matches the shortids that legacy Grafana used to generate uids
legacyNameRegex = regexp.MustCompile(`^[a-zA-Z0-9\-_]+$`)
)
func (k DataKey) String() string {
return fmt.Sprintf("%s/%s/%s/%s/%d~%s~%s", k.Group, k.Resource, k.Namespace, k.Name, k.ResourceVersion, k.Action, k.Folder)
}
@ -78,42 +64,35 @@ func (k DataKey) Equals(other DataKey) bool {
}
func (k DataKey) Validate() error {
if k.Group == "" {
return fmt.Errorf("group is required")
}
if k.Resource == "" {
return fmt.Errorf("resource is required")
}
if k.Namespace == "" {
return fmt.Errorf("namespace is required")
}
if k.Name == "" {
return fmt.Errorf("name is required")
return NewValidationError("namespace", k.Namespace, ErrNamespaceRequired)
}
if k.ResourceVersion <= 0 {
return fmt.Errorf("resource version must be positive")
return NewValidationError("resourceVersion", fmt.Sprintf("%d", k.ResourceVersion), ErrResourceVersionInvalid)
}
if k.Action == "" {
return fmt.Errorf("action is required")
return NewValidationError("action", string(k.Action), ErrActionRequired)
}
// Validate naming conventions for all required fields
if !validNameRegex.MatchString(k.Namespace) {
return fmt.Errorf("namespace '%s' is invalid", k.Namespace)
if err := validation.IsValidNamespace(k.Namespace); err != nil {
return NewValidationError("namespace", k.Namespace, err[0])
}
if !validNameRegex.MatchString(k.Group) {
return fmt.Errorf("group '%s' is invalid", k.Group)
if err := validation.IsValidGroup(k.Group); err != nil {
return NewValidationError("group", k.Group, err[0])
}
if !validNameRegex.MatchString(k.Resource) {
return fmt.Errorf("resource '%s' is invalid", k.Resource)
if err := validation.IsValidateResource(k.Resource); err != nil {
return NewValidationError("resource", k.Resource, err[0])
}
if !k8sRegex.MatchString(k.Name) && !legacyNameRegex.MatchString(k.Name) {
return fmt.Errorf("name '%s' is invalid, must match k8s qualified name format or Grafana shortid format", k.Name)
if err := validation.IsValidGrafanaName(k.Name); err != nil {
return NewValidationError("name", k.Name, err[0])
}
// Validate folder field if provided (optional field)
if k.Folder != "" && !validNameRegex.MatchString(k.Folder) {
return fmt.Errorf("folder '%s' is invalid", k.Folder)
if k.Folder != "" {
if err := validation.IsValidGrafanaName(k.Folder); err != nil {
return NewValidationError("folder", k.Folder, err[0])
}
}
// Validate action is one of the valid values
@ -133,27 +112,21 @@ type ListRequestKey struct {
}
func (k ListRequestKey) Validate() error {
if k.Group == "" {
return fmt.Errorf("group is required")
}
if k.Resource == "" {
return fmt.Errorf("resource is required")
}
if k.Namespace == "" && k.Name != "" {
return fmt.Errorf("name must be empty when namespace is empty")
return errors.New(ErrNameMustBeEmptyWhenNamespaceEmpty)
}
if k.Namespace != "" && !validNameRegex.MatchString(k.Namespace) {
return fmt.Errorf("namespace '%s' is invalid", k.Namespace)
if k.Namespace != "" {
if err := validation.IsValidNamespace(k.Namespace); err != nil {
return NewValidationError("namespace", k.Namespace, err[0])
}
}
if !validNameRegex.MatchString(k.Group) {
return fmt.Errorf("group '%s' is invalid", k.Group)
if err := validation.IsValidGroup(k.Group); err != nil {
return NewValidationError("group", k.Group, err[0])
}
if !validNameRegex.MatchString(k.Resource) {
return fmt.Errorf("resource '%s' is invalid", k.Resource)
}
if k.Name != "" && !k8sRegex.MatchString(k.Name) && !legacyNameRegex.MatchString(k.Name) {
return fmt.Errorf("name '%s' is invalid", k.Name)
if err := validation.IsValidateResource(k.Resource); err != nil {
return NewValidationError("resource", k.Resource, err[0])
}
return nil
}
@ -177,31 +150,20 @@ type GetRequestKey struct {
// Validate validates the get request key
func (k GetRequestKey) Validate() error {
if k.Group == "" {
return fmt.Errorf("group is required")
}
if k.Resource == "" {
return fmt.Errorf("resource is required")
}
if k.Namespace == "" {
return fmt.Errorf("namespace is required")
return errors.New(ErrNamespaceRequired)
}
if k.Name == "" {
return fmt.Errorf("name is required")
if err := validation.IsValidNamespace(k.Namespace); err != nil {
return NewValidationError("namespace", k.Namespace, err[0])
}
// Validate naming conventions
if !validNameRegex.MatchString(k.Namespace) {
return fmt.Errorf("namespace '%s' is invalid", k.Namespace)
if err := validation.IsValidGroup(k.Group); err != nil {
return NewValidationError("group", k.Group, err[0])
}
if !validNameRegex.MatchString(k.Group) {
return fmt.Errorf("group '%s' is invalid", k.Group)
if err := validation.IsValidateResource(k.Resource); err != nil {
return NewValidationError("resource", k.Resource, err[0])
}
if !validNameRegex.MatchString(k.Resource) {
return fmt.Errorf("resource '%s' is invalid", k.Resource)
}
if !validNameRegex.MatchString(k.Name) {
return fmt.Errorf("name '%s' is invalid", k.Name)
if err := validation.IsValidGrafanaName(k.Name); err != nil {
return NewValidationError("name", k.Name, err[0])
}
return nil

View File

@ -3,6 +3,7 @@ package resource
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"testing"
@ -86,6 +87,7 @@ func TestDataKey_Validate(t *testing.T) {
key DataKey
expectError bool
errorMsg string
errorField string
}{
{
name: "valid key with created action",
@ -99,6 +101,18 @@ func TestDataKey_Validate(t *testing.T) {
},
expectError: false,
},
{
name: "valid - underscore in namespace",
key: DataKey{
Namespace: "test_namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid key with updated action",
key: DataKey{
@ -123,18 +137,6 @@ func TestDataKey_Validate(t *testing.T) {
},
expectError: false,
},
{
name: "valid key with dots and dashes",
key: DataKey{
Namespace: "test.namespace-with-dashes",
Group: "test.group-123",
Resource: "test-resource.v1",
Name: "test-name.with.dots",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - name ends with dash",
key: DataKey{
@ -148,11 +150,11 @@ func TestDataKey_Validate(t *testing.T) {
expectError: false,
},
{
name: "valid key with single character names",
name: "valid key with minimum character lengths",
key: DataKey{
Namespace: "a",
Group: "b",
Resource: "c",
Namespace: "abc",
Group: "bcd",
Resource: "cde",
Name: "d",
ResourceVersion: rv,
Action: DataActionCreated,
@ -183,6 +185,42 @@ func TestDataKey_Validate(t *testing.T) {
},
expectError: false,
},
{
name: "valid - uppercase in namespace",
key: DataKey{
Namespace: "Test-Namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - uppercase in group",
key: DataKey{
Namespace: "test-namespace",
Group: "Test-Group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
{
name: "valid - uppercase in resource",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "Test-Resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: false,
},
// Invalid cases - empty fields
{
name: "invalid - empty namespace",
@ -195,7 +233,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: DataActionCreated,
},
expectError: true,
errorMsg: "namespace is required",
errorMsg: ErrNamespaceRequired,
},
{
name: "invalid - empty group",
@ -208,7 +246,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: DataActionCreated,
},
expectError: true,
errorMsg: "group is required",
errorField: "group",
},
{
name: "invalid - empty resource",
@ -221,7 +259,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: DataActionCreated,
},
expectError: true,
errorMsg: "resource is required",
errorField: "resource",
},
{
name: "invalid - empty name",
@ -234,7 +272,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: DataActionCreated,
},
expectError: true,
errorMsg: "name is required",
errorField: "name",
},
{
name: "invalid - empty action",
@ -247,7 +285,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: "",
},
expectError: true,
errorMsg: "action is required",
errorMsg: ErrActionRequired,
},
{
name: "invalid - all fields empty",
@ -260,61 +298,21 @@ func TestDataKey_Validate(t *testing.T) {
Action: "",
},
expectError: true,
errorMsg: "group is required",
},
// Invalid cases - uppercase characters
{
name: "invalid - uppercase in namespace",
key: DataKey{
Namespace: "Test-Namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "namespace 'Test-Namespace' is invalid",
},
{
name: "invalid - uppercase in group",
key: DataKey{
Namespace: "test-namespace",
Group: "Test-Group",
Resource: "test-resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "group 'Test-Group' is invalid",
},
{
name: "invalid - uppercase in resource",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "Test-Resource",
Name: "test-name",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "resource 'Test-Resource' is invalid",
errorField: "namespace",
},
// Invalid cases - invalid characters
{
name: "invalid - underscore in namespace",
name: "invalid - key with dots and dashes",
key: DataKey{
Namespace: "test_namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
Namespace: "test.namespace-with-dashes",
Group: "test.group-123",
Resource: "test-resource.v1",
Name: "test-name.with.dots",
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "namespace 'test_namespace' is invalid",
errorField: "namespace",
},
{
name: "invalid - space in group",
@ -415,7 +413,6 @@ func TestDataKey_Validate(t *testing.T) {
},
expectError: false,
},
// Invalid name cases
{
name: "valid - name starts with dash (legacy format)",
key: DataKey{
@ -441,7 +438,7 @@ func TestDataKey_Validate(t *testing.T) {
expectError: false,
},
{
name: "invalid - name starts with dot",
name: "valid - name starts with dot",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
@ -450,11 +447,10 @@ func TestDataKey_Validate(t *testing.T) {
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "name '.test-name' is invalid, must match k8s qualified name format or Grafana shortid format",
expectError: false,
},
{
name: "invalid - name ends with dot",
name: "valid - name ends with dot",
key: DataKey{
Namespace: "test-namespace",
Group: "test-group",
@ -463,8 +459,7 @@ func TestDataKey_Validate(t *testing.T) {
ResourceVersion: rv,
Action: DataActionCreated,
},
expectError: true,
errorMsg: "name 'test-name.' is invalid, must match k8s qualified name format or Grafana shortid format",
expectError: false,
},
{
name: "valid - name starts with underscore (legacy format)",
@ -490,6 +485,7 @@ func TestDataKey_Validate(t *testing.T) {
},
expectError: false,
},
// Invalid name cases
{
name: "invalid - name with slash",
key: DataKey{
@ -501,7 +497,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: DataActionCreated,
},
expectError: true,
errorMsg: "name 'test/name' is invalid, must match k8s qualified name format or Grafana shortid format",
errorField: "name",
},
{
name: "invalid - name with spaces",
@ -514,7 +510,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: DataActionCreated,
},
expectError: true,
errorMsg: "name 'test name' is invalid, must match k8s qualified name format or Grafana shortid format",
errorField: "name",
},
{
name: "invalid - name with special characters",
@ -527,7 +523,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: DataActionCreated,
},
expectError: true,
errorMsg: "name 'test@name#with$special' is invalid, must match k8s qualified name format or Grafana shortid format",
errorField: "name",
},
{
name: "invalid - empty name",
@ -540,7 +536,7 @@ func TestDataKey_Validate(t *testing.T) {
Action: DataActionCreated,
},
expectError: true,
errorMsg: "name cannot be empty",
errorField: "name",
},
// Invalid cases - start/end with invalid characters
{
@ -606,6 +602,10 @@ func TestDataKey_Validate(t *testing.T) {
if tt.errorMsg != "" {
require.Contains(t, err.Error(), tt.errorMsg)
}
var validationErr *ValidationError
if errors.Is(err, validationErr) && tt.errorField != "" {
require.Equal(t, tt.errorField, validationErr.Field)
}
} else {
require.NoError(t, err)
}
@ -1159,7 +1159,7 @@ func TestDataStore_ValidationEnforced(t *testing.T) {
// Create an invalid key
invalidKey := DataKey{
Namespace: "Invalid-Namespace", // uppercase is invalid
Namespace: "Invalid-Namespace-$$$",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
@ -1173,21 +1173,27 @@ func TestDataStore_ValidationEnforced(t *testing.T) {
_, err := ds.Get(ctx, invalidKey)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
require.Contains(t, err.Error(), "namespace 'Invalid-Namespace' is invalid")
var validationErr ValidationError
require.True(t, errors.As(err, &validationErr))
require.Equal(t, "namespace", validationErr.Field)
})
t.Run("Save with invalid key returns validation error", func(t *testing.T) {
err := ds.Save(ctx, invalidKey, testValue)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
require.Contains(t, err.Error(), "namespace 'Invalid-Namespace' is invalid")
var validationErr ValidationError
require.True(t, errors.As(err, &validationErr))
require.Equal(t, "namespace", validationErr.Field)
})
t.Run("Delete with invalid key returns validation error", func(t *testing.T) {
err := ds.Delete(ctx, invalidKey)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid data key")
require.Contains(t, err.Error(), "namespace 'Invalid-Namespace' is invalid")
var validationErr ValidationError
require.True(t, errors.As(err, &validationErr))
require.Equal(t, "namespace", validationErr.Field)
})
// Test another type of invalid key
@ -1228,6 +1234,7 @@ func TestListRequestKey_Validate(t *testing.T) {
key ListRequestKey
expectError bool
errorMsg string
errorField string
}{
{
name: "valid - all fields provided",
@ -1239,6 +1246,45 @@ func TestListRequestKey_Validate(t *testing.T) {
},
expectError: false,
},
{
name: "valid - uppercase in namespace",
key: ListRequestKey{
Namespace: "Test-Namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expectError: false,
},
{
name: "valid - uppercase in group and resource",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "Test-Group",
Resource: "test-resource",
Name: "test-name",
},
expectError: false,
},
{
name: "valid - uppercase in resource",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "Test-Resource",
},
expectError: false,
},
{
name: "valid - underscore in namespace",
key: ListRequestKey{
Namespace: "test_namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expectError: false,
},
{
name: "valid - only group and resource",
key: ListRequestKey{
@ -1260,7 +1306,7 @@ func TestListRequestKey_Validate(t *testing.T) {
name: "invalid - all empty",
key: ListRequestKey{},
expectError: true,
errorMsg: "group is required",
errorField: "namespace",
},
{
name: "valid - legacy grafana uid 1",
@ -1309,7 +1355,7 @@ func TestListRequestKey_Validate(t *testing.T) {
Group: "test-group",
},
expectError: true,
errorMsg: "resource is required",
errorField: "resource",
},
{
name: "invalid - name without namespace",
@ -1319,7 +1365,7 @@ func TestListRequestKey_Validate(t *testing.T) {
Group: "test-group",
},
expectError: true,
errorMsg: "name must be empty when namespace is empty",
errorMsg: ErrNameMustBeEmptyWhenNamespaceEmpty,
},
{
name: "invalid - name without group and resource",
@ -1328,52 +1374,9 @@ func TestListRequestKey_Validate(t *testing.T) {
Name: "test-name",
},
expectError: true,
errorMsg: "group is required",
errorField: "group",
},
// Invalid naming cases
{
name: "invalid - uppercase in namespace",
key: ListRequestKey{
Namespace: "Test-Namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expectError: true,
errorMsg: "namespace 'Test-Namespace' is invalid",
},
{
name: "invalid - uppercase in group and resource",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "Test-Group",
Resource: "test-resource",
Name: "test-name",
},
expectError: true,
errorMsg: "group 'Test-Group' is invalid",
},
{
name: "invalid - uppercase in resource",
key: ListRequestKey{
Namespace: "test-namespace",
Group: "test-group",
Resource: "Test-Resource",
},
expectError: true,
errorMsg: "resource 'Test-Resource' is invalid",
},
{
name: "invalid - underscore in namespace",
key: ListRequestKey{
Namespace: "test_namespace",
Group: "test-group",
Resource: "test-resource",
Name: "test-name",
},
expectError: true,
errorMsg: "namespace 'test_namespace' is invalid",
},
{
name: "invalid - starts with dash",
key: ListRequestKey{
@ -2512,10 +2515,11 @@ func TestDataKey_SameResource(t *testing.T) {
func TestGetRequestKey_Validate(t *testing.T) {
tests := []struct {
name string
key GetRequestKey
expectErr bool
wantError string
name string
key GetRequestKey
expectErr bool
wantError string
errorField string
}{
{
name: "valid key",
@ -2537,6 +2541,16 @@ func TestGetRequestKey_Validate(t *testing.T) {
},
expectErr: false,
},
{
name: "valid grafana name - ends with dot",
key: GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: ".123_hello",
},
expectErr: false,
},
{
name: "missing group",
key: GetRequestKey{
@ -2544,8 +2558,8 @@ func TestGetRequestKey_Validate(t *testing.T) {
Namespace: "default",
Name: "test-resource",
},
expectErr: true,
wantError: "group is required",
expectErr: true,
errorField: "group",
},
{
name: "missing resource",
@ -2554,8 +2568,8 @@ func TestGetRequestKey_Validate(t *testing.T) {
Namespace: "default",
Name: "test-resource",
},
expectErr: true,
wantError: "resource is required",
expectErr: true,
errorField: "resource",
},
{
name: "missing namespace",
@ -2564,8 +2578,8 @@ func TestGetRequestKey_Validate(t *testing.T) {
Resource: "resources",
Name: "test-resource",
},
expectErr: true,
wantError: "namespace is required",
expectErr: true,
errorField: "namespace",
},
{
name: "missing name",
@ -2574,30 +2588,19 @@ func TestGetRequestKey_Validate(t *testing.T) {
Resource: "resources",
Namespace: "default",
},
expectErr: true,
wantError: "name is required",
expectErr: true,
errorField: "name",
},
{
name: "invalid namespace - uppercase",
name: "invalid group - underscore at start",
key: GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "Default",
Name: "test-resource",
},
expectErr: true,
wantError: "namespace 'Default' is invalid",
},
{
name: "invalid group - underscore",
key: GetRequestKey{
Group: "apps_v1",
Group: "_apps_v1",
Resource: "resources",
Namespace: "default",
Name: "test-resource",
},
expectErr: true,
wantError: "group 'apps_v1' is invalid",
expectErr: true,
errorField: "group",
},
{
name: "invalid resource - starts with dash",
@ -2607,19 +2610,8 @@ func TestGetRequestKey_Validate(t *testing.T) {
Namespace: "default",
Name: "test-resource",
},
expectErr: true,
wantError: "resource '-resources' is invalid",
},
{
name: "invalid name - ends with dot",
key: GetRequestKey{
Group: "apps",
Resource: "resources",
Namespace: "default",
Name: "test-resource.",
},
expectErr: true,
wantError: "name 'test-resource.' is invalid",
expectErr: true,
errorField: "resource",
},
}
@ -2631,6 +2623,10 @@ func TestGetRequestKey_Validate(t *testing.T) {
if tt.wantError != "" {
require.Contains(t, err.Error(), tt.wantError)
}
var validationErr *ValidationError
if errors.Is(err, validationErr) && tt.errorField != "" {
require.Equal(t, tt.errorField, validationErr.Field)
}
} else {
require.NoError(t, err)
}

View File

@ -2,6 +2,7 @@ package resource
import (
"errors"
"fmt"
"net/http"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
@ -199,3 +200,25 @@ func HandleQueueError[T any](err error, makeResp func(*resourcepb.ErrorResult) *
}
return makeResp(AsErrorResult(err)), nil
}
var (
ErrNamespaceRequired = "namespace is required"
ErrResourceVersionInvalid = "resource version must be positive"
ErrActionRequired = "action is required"
ErrActionInvalid = "action is invalid: must be one of 'created', 'updated', or 'deleted'"
ErrNameMustBeEmptyWhenNamespaceEmpty = "name must be empty when namespace is empty"
)
type ValidationError struct {
Field string
Value string
Msg string
}
func (e ValidationError) Error() string {
return fmt.Sprintf("%s '%s' is invalid: %s", e.Field, e.Value, e.Msg)
}
func NewValidationError(field, value, msg string) error {
return ValidationError{Field: field, Value: value, Msg: msg}
}

View File

@ -3,6 +3,7 @@ package resource
import (
"context"
"encoding/json"
"errors"
"fmt"
"iter"
"strconv"
@ -10,6 +11,7 @@ import (
"time"
"github.com/bwmarrin/snowflake"
"github.com/grafana/grafana/pkg/apimachinery/validation"
)
const (
@ -37,46 +39,38 @@ func (k EventKey) String() string {
func (k EventKey) Validate() error {
if k.Namespace == "" {
return fmt.Errorf("namespace cannot be empty")
}
if k.Group == "" {
return fmt.Errorf("group cannot be empty")
}
if k.Resource == "" {
return fmt.Errorf("resource cannot be empty")
}
if k.Name == "" {
return fmt.Errorf("name cannot be empty")
return NewValidationError("namespace", k.Namespace, ErrNamespaceRequired)
}
if k.ResourceVersion < 0 {
return fmt.Errorf("resource version must be non-negative")
return errors.New(ErrResourceVersionInvalid)
}
if k.Action == "" {
return fmt.Errorf("action cannot be empty")
return NewValidationError("action", string(k.Action), ErrActionRequired)
}
if k.Folder != "" && !validNameRegex.MatchString(k.Folder) {
return fmt.Errorf("folder '%s' is invalid", k.Folder)
// Validate each field against the naming rules
// Validate naming conventions for all required fields
if err := validation.IsValidNamespace(k.Namespace); err != nil {
return NewValidationError("namespace", k.Namespace, err[0])
}
// Validate each field against the naming rules (reusing the regex from datastore.go)
if !validNameRegex.MatchString(k.Namespace) {
return fmt.Errorf("namespace '%s' is invalid", k.Namespace)
if err := validation.IsValidGroup(k.Group); err != nil {
return NewValidationError("group", k.Group, err[0])
}
if !validNameRegex.MatchString(k.Group) {
return fmt.Errorf("group '%s' is invalid", k.Group)
if err := validation.IsValidateResource(k.Resource); err != nil {
return NewValidationError("resource", k.Resource, err[0])
}
if !validNameRegex.MatchString(k.Resource) {
return fmt.Errorf("resource '%s' is invalid", k.Resource)
if err := validation.IsValidGrafanaName(k.Name); err != nil {
return NewValidationError("name", k.Name, err[0])
}
if !validNameRegex.MatchString(k.Name) {
return fmt.Errorf("name '%s' is invalid", k.Name)
}
if k.Folder != "" && !validNameRegex.MatchString(k.Folder) {
return fmt.Errorf("folder '%s' is invalid", k.Folder)
if k.Folder != "" {
if err := validation.IsValidGrafanaName(k.Folder); err != nil {
return NewValidationError("folder", k.Folder, err[0])
}
}
switch k.Action {
case DataActionCreated, DataActionUpdated, DataActionDeleted:
default:
return fmt.Errorf("action '%s' is invalid: must be one of 'created', 'updated', or 'deleted'", k.Action)
return NewValidationError("action", string(k.Action), ErrActionInvalid)
}
return nil
@ -148,6 +142,7 @@ func (n *eventStore) Save(ctx context.Context, event Event) error {
Name: event.Name,
ResourceVersion: event.ResourceVersion,
Action: event.Action,
//TODO why isnt folder part of the key?
}
if err := eventKey.Validate(); err != nil {