mirror of
https://github.com/containers/podman.git
synced 2026-03-13 08:01:19 +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
|
||||
}
|
||||
80
pkg/annotations/validate_test.go
Normal file
80
pkg/annotations/validate_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
Copyright 2017 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package annotations
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/containers/podman/v5/libpod/define"
|
||||
)
|
||||
|
||||
func TestValidateAnnotations(t *testing.T) {
|
||||
successCases := []map[string]string{
|
||||
{"simple": "bar"},
|
||||
{"now-with-dashes": "bar"},
|
||||
{"1-starts-with-num": "bar"},
|
||||
{"1234": "bar"},
|
||||
{"simple/simple": "bar"},
|
||||
{"now-with-dashes/simple": "bar"},
|
||||
{"now-with-dashes/now-with-dashes": "bar"},
|
||||
{"now.with.dots/simple": "bar"},
|
||||
{"now-with.dashes-and.dots/simple": "bar"},
|
||||
{"1-num.2-num/3-num": "bar"},
|
||||
{"1234/5678": "bar"},
|
||||
{"1.2.3.4/5678": "bar"},
|
||||
{"UpperCase123": "bar"},
|
||||
{"a": strings.Repeat("b", define.TotalAnnotationSizeLimitB-1)},
|
||||
{
|
||||
"a": strings.Repeat("b", define.TotalAnnotationSizeLimitB/2-1),
|
||||
"c": strings.Repeat("d", define.TotalAnnotationSizeLimitB/2-1),
|
||||
},
|
||||
}
|
||||
|
||||
for i := range successCases {
|
||||
if err := ValidateAnnotations(successCases[i]); err != nil {
|
||||
t.Errorf("case[%d] expected success, got %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
nameErrorCases := []map[string]string{
|
||||
{"nospecialchars^=@": "bar"},
|
||||
{"cantendwithadash-": "bar"},
|
||||
{"only/one/slash": "bar"},
|
||||
{strings.Repeat("a", 254): "bar"},
|
||||
}
|
||||
|
||||
for i := range nameErrorCases {
|
||||
if err := ValidateAnnotations(nameErrorCases[i]); err == nil {
|
||||
t.Errorf("case[%d]: expected failure", i)
|
||||
}
|
||||
}
|
||||
|
||||
totalSizeErrorCases := []map[string]string{
|
||||
{"a": strings.Repeat("b", define.TotalAnnotationSizeLimitB)},
|
||||
{
|
||||
"a": strings.Repeat("b", define.TotalAnnotationSizeLimitB/2),
|
||||
"c": strings.Repeat("d", define.TotalAnnotationSizeLimitB/2),
|
||||
},
|
||||
}
|
||||
|
||||
for i := range totalSizeErrorCases {
|
||||
if err := ValidateAnnotations(totalSizeErrorCases[i]); err == nil {
|
||||
t.Errorf("case[%d] expected failure", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user