Validation: Move validation into apimachinery package (#111736)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
package validation
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
)
|
||||
|
||||
const maxNameLength = 253
|
||||
const maxNamespaceLength = 40
|
||||
const minNamespaceLength = 3
|
||||
const maxGroupLength = 60
|
||||
const minGroupLength = 3
|
||||
const maxResourceLength = 40
|
||||
const minResourceLength = 3
|
||||
|
||||
const grafanaNameFmt = `^[a-zA-Z0-9:\-\_\.]*$`
|
||||
const grafanaNameErrMsg string = "must consist of alphanumeric characters, '-', '_', ':' or '.'"
|
||||
|
||||
const qnameCharFmt string = "[A-Za-z0-9]"
|
||||
const qnameExtCharFmt string = "[-A-Za-z0-9_.]"
|
||||
const qualifiedNameFmt string = "^(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt + "$"
|
||||
const qualifiedNameErrMsg string = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character"
|
||||
|
||||
const alphaCharFmt string = "[A-Za-z]"
|
||||
const resourceCharFmt string = "[A-Za-z0-9]" // alpha numeric
|
||||
const resourceFmt string = "^" + alphaCharFmt + resourceCharFmt + "*$"
|
||||
const resourceErrMsg string = "must consist of alphanumeric characters"
|
||||
|
||||
var (
|
||||
grafanaNameRegexp = regexp.MustCompile(grafanaNameFmt).MatchString
|
||||
qualifiedNameRegexp = regexp.MustCompile(qualifiedNameFmt).MatchString
|
||||
resourceRegexp = regexp.MustCompile(resourceFmt).MatchString
|
||||
)
|
||||
|
||||
// IsValidGrafanaName checks if the name is a valid to use for a k8s name
|
||||
// Unlike normal k8s name rules, this allows the name to start with a digit
|
||||
// This compromise means existing grafana UIDs are valid k8s names without migration
|
||||
func IsValidGrafanaName(name string) []string {
|
||||
s := len(name)
|
||||
switch {
|
||||
case s == 0:
|
||||
return []string{"name may not be empty"}
|
||||
case s > maxNameLength:
|
||||
return []string{"name is too long"}
|
||||
}
|
||||
|
||||
if !grafanaNameRegexp(name) {
|
||||
return []string{"name " + validation.RegexError(grafanaNameErrMsg, grafanaNameFmt, "MyName", "my.name", "abc-123")}
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// If the value is not valid, a list of error strings is returned.
|
||||
// Otherwise an empty list (or nil) is returned.
|
||||
func IsValidNamespace(namespace string) []string {
|
||||
s := len(namespace)
|
||||
switch {
|
||||
case s == 0:
|
||||
return nil // empty is OK
|
||||
case s > maxNamespaceLength:
|
||||
return []string{"namespace is too long"}
|
||||
case s < minNamespaceLength:
|
||||
return []string{"namespace is too short"}
|
||||
}
|
||||
if !qualifiedNameRegexp(namespace) {
|
||||
return []string{"namespace " + validation.RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "abc-123")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the value is not valid, a list of error strings is returned.
|
||||
// Otherwise an empty list (or nil) is returned.
|
||||
func IsValidGroup(group string) []string {
|
||||
s := len(group)
|
||||
switch {
|
||||
case s > maxGroupLength:
|
||||
return []string{"group is too long"}
|
||||
case s < minGroupLength:
|
||||
return []string{"group is too short"}
|
||||
}
|
||||
if !qualifiedNameRegexp(group) {
|
||||
return []string{"group " + validation.RegexError(qualifiedNameErrMsg, qualifiedNameFmt, "dashboards.grafana.app", "grafana-loki-datasource")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the value is not valid, a list of error strings is returned.
|
||||
// Otherwise an empty list (or nil) is returned.
|
||||
func IsValidateResource(resource string) []string {
|
||||
s := len(resource)
|
||||
switch {
|
||||
case s > maxResourceLength:
|
||||
return []string{"resource is too long"}
|
||||
case s < minResourceLength:
|
||||
return []string{"resource is too short"}
|
||||
}
|
||||
if !resourceRegexp(resource) {
|
||||
return []string{"resource " + validation.RegexError(resourceErrMsg, resourceFmt, "dashboards", "folders")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package validation_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
k8sValidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/validation"
|
||||
)
|
||||
|
||||
func TestValidation(t *testing.T) {
|
||||
// We are not using the out-of-the-box "isQualifiedName" because it allows slashes
|
||||
rsp := k8sValidation.IsQualifiedName("hello/world")
|
||||
require.Nil(t, rsp, "standard qualified name allows a slash")
|
||||
|
||||
t.Run("name", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string // variations that produce the same output
|
||||
expect []string
|
||||
}{{
|
||||
name: "empty",
|
||||
input: []string{""},
|
||||
expect: []string{"name may not be empty"},
|
||||
}, {
|
||||
name: "too long",
|
||||
input: []string{strings.Repeat("0", 254)},
|
||||
expect: []string{"name is too long"},
|
||||
}, {
|
||||
name: "ok",
|
||||
input: []string{
|
||||
"hello",
|
||||
strings.Repeat("0", 253), // very long starts with number
|
||||
"hello-world",
|
||||
"hello.world",
|
||||
"hello_world",
|
||||
"hello:world",
|
||||
"123456", // starts with numbers
|
||||
"aBCDEFG", // with capitals
|
||||
},
|
||||
}, {
|
||||
name: "bad input",
|
||||
expect: []string{
|
||||
"name must consist of alphanumeric characters, '-', '_', ':' or '.' (e.g. 'MyName', or 'my.name', or 'abc-123', regex used for validation is '^[a-zA-Z0-9:\\-\\_\\.]*$')",
|
||||
},
|
||||
input: []string{
|
||||
"hello world",
|
||||
"hello!",
|
||||
"hello~",
|
||||
"hello ",
|
||||
"hello*",
|
||||
"hello+",
|
||||
"hello=",
|
||||
"hello%",
|
||||
"hello/world",
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, input := range tt.input {
|
||||
output := validation.IsValidGrafanaName(input)
|
||||
require.Equal(t, tt.expect, output, "input: %s", input)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("namespace", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string // variations that produce the same output
|
||||
expect []string
|
||||
}{{
|
||||
name: "empty is OK",
|
||||
input: []string{""},
|
||||
}, {
|
||||
name: "too long",
|
||||
input: []string{strings.Repeat("0", 41)},
|
||||
expect: []string{"namespace is too long"},
|
||||
}, {
|
||||
name: "too short",
|
||||
expect: []string{"namespace is too short"},
|
||||
input: []string{"a", "1", "aa"},
|
||||
}, {
|
||||
name: "ok",
|
||||
input: []string{
|
||||
"hello",
|
||||
strings.Repeat("a", 40), // long... alpha
|
||||
"hello-world",
|
||||
"hello.world",
|
||||
"hello_world",
|
||||
"default",
|
||||
"stacks-123456", // ends with a number
|
||||
"org-3", // ends with a number
|
||||
"1234", // just a numbers
|
||||
"aaa",
|
||||
},
|
||||
}, {
|
||||
name: "bad input",
|
||||
expect: []string{
|
||||
"namespace must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'MyName', or 'my.name', or 'abc-123', regex used for validation is '^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$')",
|
||||
},
|
||||
input: []string{
|
||||
"_bad_input", // starts with non-alpha
|
||||
"hello world",
|
||||
"hello!",
|
||||
"hello~",
|
||||
"hello ",
|
||||
"hello*",
|
||||
"hello+",
|
||||
"hello=",
|
||||
"hello%",
|
||||
"hello/world",
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, input := range tt.input {
|
||||
output := validation.IsValidNamespace(input)
|
||||
require.Equal(t, tt.expect, output, "input: %s", input)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("group", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string // variations that produce the same output
|
||||
expect []string
|
||||
}{{
|
||||
name: "too long",
|
||||
expect: []string{"group is too long"},
|
||||
input: []string{strings.Repeat("0", 61)},
|
||||
}, {
|
||||
name: "too short",
|
||||
expect: []string{"group is too short"},
|
||||
input: []string{"a", "1", "aa"},
|
||||
}, {
|
||||
name: "ok",
|
||||
input: []string{
|
||||
"hello",
|
||||
strings.Repeat("a", 60), // long... alpha
|
||||
"dashboards.grafana.app",
|
||||
"prometheus-datasource",
|
||||
"1234", // just a numbers
|
||||
"aaa",
|
||||
},
|
||||
}, {
|
||||
name: "bad input",
|
||||
expect: []string{
|
||||
"group must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character (e.g. 'dashboards.grafana.app', or 'grafana-loki-datasource', regex used for validation is '^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$')",
|
||||
},
|
||||
input: []string{
|
||||
"_bad_input", // starts with non-alpha
|
||||
"hello world",
|
||||
"hello!",
|
||||
"hello~",
|
||||
"hello ",
|
||||
"hello*",
|
||||
"hello+",
|
||||
"hello=",
|
||||
"hello%",
|
||||
"hello/world",
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, input := range tt.input {
|
||||
output := validation.IsValidGroup(input)
|
||||
require.Equal(t, tt.expect, output, "input: %s", input)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("resource", func(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []string // variations that produce the same output
|
||||
expect []string
|
||||
}{{
|
||||
name: "too long",
|
||||
expect: []string{"resource is too long"},
|
||||
input: []string{strings.Repeat("0", 41)},
|
||||
}, {
|
||||
name: "too short",
|
||||
expect: []string{"resource is too short"},
|
||||
input: []string{"a", "1", "aa"},
|
||||
}, {
|
||||
name: "ok",
|
||||
input: []string{
|
||||
"hello",
|
||||
strings.Repeat("a", 40), // long... alpha
|
||||
"dashboards",
|
||||
"folders",
|
||||
"folders123",
|
||||
"aaa",
|
||||
},
|
||||
}, {
|
||||
name: "bad input",
|
||||
expect: []string{
|
||||
"resource must consist of alphanumeric characters (e.g. 'dashboards', or 'folders', regex used for validation is '^[A-Za-z][A-Za-z0-9]*$')",
|
||||
},
|
||||
input: []string{
|
||||
"_bad_input",
|
||||
"hello world",
|
||||
"hello-world",
|
||||
"hello!",
|
||||
"hello~",
|
||||
"hello ",
|
||||
"hello*",
|
||||
"hello+",
|
||||
"hello=",
|
||||
"hello%",
|
||||
"hello/world",
|
||||
},
|
||||
}}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
for _, input := range tt.input {
|
||||
output := validation.IsValidateResource(input)
|
||||
require.Equal(t, tt.expect, output, "input: %s", input)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user