mirror of
https://github.com/containers/podman.git
synced 2025-08-06 11:32:07 +08:00
Comply to Kubernetes specifications for annotation size.
An annotation is a pair of key-value. The key has two parts, viz. a name and an optional prefix in DNS format. The limitations on name is 63, prefix 253 chars. The limitation on total size of all key+value pairs combined is 256KB. https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set Fixes: https://github.com/containers/podman/issues/21663 Signed-off-by: Vikas Goel <vikas.goel@gmail.com>
This commit is contained in:
124
pkg/annotations/validate.go
Normal file
124
pkg/annotations/validate.go
Normal file
@ -0,0 +1,124 @@
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
)
|
||||
|
||||
// regexErrorMsg returns a string explanation of a regex validation failure.
|
||||
func regexErrorMsg(msg string, fmt string, examples ...string) string {
|
||||
if len(examples) == 0 {
|
||||
return msg + " (regex used for validation is '" + fmt + "')"
|
||||
}
|
||||
msg += " (e.g. "
|
||||
for i := range examples {
|
||||
if i > 0 {
|
||||
msg += " or "
|
||||
}
|
||||
msg += "'" + examples[i] + "', "
|
||||
}
|
||||
msg += "regex used for validation is '" + fmt + "')"
|
||||
return msg
|
||||
}
|
||||
|
||||
const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
|
||||
const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"
|
||||
const dns1123SubdomainErrorMsg string = "annotations must be formatted as a valid lowercase RFC1123 subdomain of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character"
|
||||
|
||||
// DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123)
|
||||
const DNS1123SubdomainMaxLength int = 253
|
||||
|
||||
var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$")
|
||||
|
||||
// isDNS1123Subdomain tests for a string that conforms to the definition of a
|
||||
// subdomain in DNS (RFC 1123).
|
||||
func isDNS1123Subdomain(value string) error {
|
||||
if len(value) > DNS1123SubdomainMaxLength {
|
||||
return fmt.Errorf("prefix part must be no more than %d characters", DNS1123SubdomainMaxLength)
|
||||
}
|
||||
|
||||
if !dns1123SubdomainRegexp.MatchString(value) {
|
||||
return fmt.Errorf(regexErrorMsg(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com"))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
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 qualifiedNameMaxLength int = 63
|
||||
|
||||
var qualifiedNameRegexp = regexp.MustCompile("^" + qualifiedNameFmt + "$")
|
||||
|
||||
// isQualifiedName tests whether the value passed is what Kubernetes calls a
|
||||
// "qualified name". This is a format used in various places throughout the
|
||||
// system. If the value is not valid, a list of error strings is returned.
|
||||
// Otherwise an empty list (or nil) is returned.
|
||||
func isQualifiedName(value string) error {
|
||||
parts := strings.Split(value, "/")
|
||||
var name string
|
||||
|
||||
switch len(parts) {
|
||||
case 1:
|
||||
name = parts[0]
|
||||
case 2:
|
||||
var prefix string
|
||||
prefix, name = parts[0], parts[1]
|
||||
if len(prefix) == 0 {
|
||||
return fmt.Errorf("prefix part of %s must be non-empty", value)
|
||||
} else if err := isDNS1123Subdomain(prefix); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("a qualified name of %s "+
|
||||
regexErrorMsg(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")+
|
||||
" with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')", value)
|
||||
}
|
||||
|
||||
if len(name) == 0 {
|
||||
return fmt.Errorf("name part of %s must be non-empty", value)
|
||||
} else if len(name) > qualifiedNameMaxLength {
|
||||
return fmt.Errorf("name part of %s must be no more than %d characters", value, qualifiedNameMaxLength)
|
||||
}
|
||||
|
||||
if !qualifiedNameRegexp.MatchString(name) {
|
||||
return fmt.Errorf("name part of %s "+
|
||||
regexErrorMsg(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc"), value)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAnnotationsSize(annotations map[string]string) error {
|
||||
var totalSize int64
|
||||
for k, v := range annotations {
|
||||
totalSize += (int64)(len(k)) + (int64)(len(v))
|
||||
}
|
||||
if totalSize > (int64)(define.TotalAnnotationSizeLimitB) {
|
||||
return fmt.Errorf("annotations size %d is larger than limit %d", totalSize, define.TotalAnnotationSizeLimitB)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateAnnotations validates that a set of annotations are correctly
|
||||
// defined.
|
||||
func ValidateAnnotations(annotations map[string]string) error {
|
||||
for k := range annotations {
|
||||
// The rule is QualifiedName except that case doesn't matter,
|
||||
// so convert to lowercase before checking.
|
||||
if err := isQualifiedName(strings.ToLower(k)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateAnnotationsSize(annotations); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user