Validation: Move validation into apimachinery package (#111736)

This commit is contained in:
Ryan McKinley
2025-09-30 12:59:33 +00:00
committed by GitHub
parent d8fd872ad3
commit cfbf64c3fd
13 changed files with 379 additions and 133 deletions
+3
View File
@@ -100,6 +100,9 @@ func (s *Storage) prepareObjectForStorage(ctx context.Context, newObject runtime
if obj.GetFolder() != "" && !s.opts.EnableFolderSupport {
return v, apierrors.NewBadRequest(fmt.Sprintf("folders are not supported for: %s", s.gr.String()))
}
if s.opts.MaximumNameLength > 0 && len(obj.GetName()) > s.opts.MaximumNameLength {
return v, apierrors.NewBadRequest(fmt.Sprintf("name exceeds maximum length (%d)", s.opts.MaximumNameLength))
}
v.grantPermissions = obj.GetAnnotation(utils.AnnoKeyGrantPermissions)
if v.grantPermissions != "" {
+13 -3
View File
@@ -3,6 +3,7 @@ package apistore
import (
"context"
"math/rand/v2"
"strings"
"testing"
"time"
@@ -35,6 +36,7 @@ func TestPrepareObjectForStorage(t *testing.T) {
opts: StorageOptions{
EnableFolderSupport: true,
LargeObjectSupport: nil,
MaximumNameLength: 100,
},
}
@@ -49,10 +51,18 @@ func TestPrepareObjectForStorage(t *testing.T) {
})
t.Run("Error on missing name", func(t *testing.T) {
dashboard := dashv1.Dashboard{}
_, err := s.prepareObjectForStorage(ctx, dashboard.DeepCopyObject())
dashboard := &dashv1.Dashboard{}
_, err := s.prepareObjectForStorage(ctx, dashboard)
require.Error(t, err)
require.Contains(t, err.Error(), "missing name")
require.ErrorContains(t, err, "missing name")
})
t.Run("name is too long", func(t *testing.T) {
dashboard := &dashv1.Dashboard{}
dashboard.Name = strings.Repeat("a", 120)
_, err := s.prepareObjectForStorage(ctx, dashboard)
require.Error(t, err)
require.ErrorContains(t, err, "name exceeds maximum length")
})
t.Run("Error on non-empty resource version", func(t *testing.T) {
+3
View File
@@ -59,6 +59,9 @@ type StorageOptions struct {
// Allow writing objects with metadata.annotations[grafana.app/folder]
EnableFolderSupport bool
// Some resources should not allow the absolute maximum (254 characters)
MaximumNameLength int
// Add internalID label when missing
RequireDeprecatedInternalID bool
+9 -8
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"strings"
"github.com/grafana/grafana/pkg/apimachinery/validation"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
)
@@ -17,17 +18,17 @@ func verifyRequestKey(key *resourcepb.ResourceKey) *resourcepb.ErrorResult {
if key.Resource == "" {
return NewBadRequestError("request key is missing resource")
}
if err := validateName(key.Name); err != nil {
return NewBadRequestError(fmt.Sprintf("name '%s' is invalid: '%s'", key.Name, err))
if err := validation.IsValidNamespace(key.Namespace); err != nil {
return NewBadRequestError(err[0])
}
if err := validateNamespace(key.Namespace); err != nil {
return NewBadRequestError(fmt.Sprintf("namespace '%s' is invalid: '%s'", key.Namespace, err))
if err := validation.IsValidGroup(key.Group); err != nil {
return NewBadRequestError(err[0])
}
if err := validateGroup(key.Group); err != nil {
return NewBadRequestError(fmt.Sprintf("group '%s' is invalid: '%s'", key.Group, err))
if err := validation.IsValidateResource(key.Resource); err != nil {
return NewBadRequestError(err[0])
}
if err := validateResource(key.Resource); err != nil {
return NewBadRequestError(fmt.Sprintf("resource '%s' is invalid: '%s'", key.Resource, err))
if err := validation.IsValidGrafanaName(key.Name); err != nil {
return NewBadRequestError(err[0])
}
return nil
}
+1 -1
View File
@@ -81,7 +81,7 @@ func TestVerifyRequestKey(t *testing.T) {
invalidNamespace := "(((((default"
invalidName := " " // only spaces
namespaceTooLong := strings.Repeat("a", MaxNameLength+1)
namespaceTooLong := strings.Repeat("a", 61)
nameTooLong := strings.Repeat("a", 300)
tests := []struct {
+3 -2
View File
@@ -24,6 +24,7 @@ import (
"github.com/grafana/dskit/ring"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apimachinery/validation"
secrets "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"github.com/grafana/grafana/pkg/util/scheduler"
@@ -523,8 +524,8 @@ func (s *server) newEvent(ctx context.Context, user claims.AuthInfo, key *resour
return nil, NewBadRequestError(
fmt.Sprintf("key/name do not match (key: %s, name: %s)", key.Name, obj.GetName()))
}
if err := validateName(obj.GetName()); err != nil {
return nil, err
if errs := validation.IsValidGrafanaName(obj.GetName()); err != nil {
return nil, NewBadRequestError(errs[0])
}
// For folder moves, we need to check permissions on both folders
+1 -1
View File
@@ -365,7 +365,7 @@ func TestSimpleServer(t *testing.T) {
invalidQualifiedNames := []string{
"", // empty
strings.Repeat("1", MaxNameLength+1), // too long
strings.Repeat("1", 260), // too long
" ", // only spaces
"f8cc010c.ee72.4681;89d2+d46e1bd47d33", // invalid chars
}
@@ -1,85 +0,0 @@
package resource
import (
"fmt"
"regexp"
"github.com/grafana/grafana/pkg/storage/unified/resourcepb"
"k8s.io/apimachinery/pkg/util/validation"
)
const MaxNameLength = 253
const MaxNamespaceLength = 40
const MaxGroupLength = 60
const MaxResourceLength = 40
var validNameCharPattern = `a-zA-Z0-9:\-\_\.`
var validNamePattern = regexp.MustCompile(`^[` + validNameCharPattern + `]*$`).MatchString
func validateName(name string) *resourcepb.ErrorResult {
if len(name) == 0 {
return NewBadRequestError("name is too short")
}
if len(name) > MaxNameLength {
return NewBadRequestError("name is too long")
}
if !validNamePattern(name) {
return NewBadRequestError("name includes invalid characters")
}
// In standard k8s, it must not start with a number
// however that would force us to update many many many existing resources
// so we will be slightly more lenient than standard k8s
return nil
}
func validateNamespace(value string) *resourcepb.ErrorResult {
if len(value) == 0 {
// empty namespace is allowed (means cluster-scoped)
return nil
}
if len(value) > MaxNamespaceLength {
return NewBadRequestError("value is too long")
}
err := validation.IsQualifiedName(value)
if len(err) > 0 {
return NewBadRequestError(fmt.Sprintf("name is not a valid qualified name: %+v", err))
}
return nil
}
func validateGroup(value string) *resourcepb.ErrorResult {
if len(value) == 0 {
return NewBadRequestError("value is too short")
}
if len(value) > MaxGroupLength {
return NewBadRequestError("value is too long")
}
err := validation.IsQualifiedName(value)
if len(err) > 0 {
return NewBadRequestError(fmt.Sprintf("name is not a valid qualified name: %+v", err))
}
return nil
}
func validateResource(value string) *resourcepb.ErrorResult {
if len(value) == 0 {
return NewBadRequestError("value is too short")
}
if len(value) > MaxResourceLength {
return NewBadRequestError("value is too long")
}
err := validation.IsQualifiedName(value)
if len(err) > 0 {
return NewBadRequestError(fmt.Sprintf("name is not a valid qualified name: %+v", err))
}
return nil
}
@@ -1,30 +0,0 @@
package resource
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestNameValidation(t *testing.T) {
require.NotNil(t, validateName("")) // too short
require.NotNil(t, validateName(strings.Repeat("0", 254))) // too long (max 253)
// OK
require.Nil(t, validateName("a"))
require.Nil(t, validateName("hello-world"))
require.Nil(t, validateName("hello.world"))
require.Nil(t, validateName("hello_world"))
require.Nil(t, validateName("hello:world"))
// Bad characters
require.NotNil(t, validateName("hello world"))
require.NotNil(t, validateName("hello!"))
require.NotNil(t, validateName("hello~"))
require.NotNil(t, validateName("hello "))
require.NotNil(t, validateName("hello*"))
require.NotNil(t, validateName("hello+"))
require.NotNil(t, validateName("hello="))
require.NotNil(t, validateName("hello%"))
}
@@ -1029,9 +1029,9 @@ func runTestIntegrationBlobSupport(t *testing.T, backend resource.StorageBackend
t.Run("put and fetch blob", func(t *testing.T) {
key := &resourcepb.ResourceKey{
Namespace: ns,
Group: "g",
Resource: "r",
Name: "n",
Group: "ggg",
Resource: "rrr",
Name: "nnn",
}
b1, err := server.PutBlob(ctx, &resourcepb.PutBlobRequest{
Resource: key,