Skip to content

Commit 89b415b

Browse files
committed
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: #21663 Signed-off-by: Vikas Goel <vikas.goel@gmail.com>
1 parent 6c7f987 commit 89b415b

13 files changed

Lines changed: 256 additions & 218 deletions

File tree

cmd/podman/kube/generate.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ func generateFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) {
8383

8484
noTruncAnnotationsFlagName := "no-trunc"
8585
flags.BoolVar(&generateOptions.UseLongAnnotations, noTruncAnnotationsFlagName, false, "Don't truncate annotations to Kubernetes length (63 chars)")
86+
_ = flags.MarkHidden(noTruncAnnotationsFlagName)
8687

8788
podmanOnlyFlagName := "podman-only"
8889
flags.BoolVar(&generateOptions.PodmanOnly, podmanOnlyFlagName, false, "Add podman-only reserved annotations to the generated YAML file (Cannot be used by Kubernetes)")

cmd/podman/kube/play.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/containers/podman/v5/cmd/podman/utils"
2323
"github.com/containers/podman/v5/libpod/define"
2424
"github.com/containers/podman/v5/libpod/shutdown"
25+
"github.com/containers/podman/v5/pkg/annotations"
2526
"github.com/containers/podman/v5/pkg/domain/entities"
2627
"github.com/containers/podman/v5/pkg/errorhandling"
2728
"github.com/containers/podman/v5/pkg/util"
@@ -171,6 +172,7 @@ func playFlags(cmd *cobra.Command) {
171172

172173
noTruncFlagName := "no-trunc"
173174
flags.BoolVar(&playOptions.UseLongAnnotations, noTruncFlagName, false, "Use annotations that are not truncated to the Kubernetes maximum length of 63 characters")
175+
_ = flags.MarkHidden(noTruncFlagName)
174176

175177
if !registry.IsRemote() {
176178
certDirFlagName := "cert-dir"
@@ -253,12 +255,13 @@ func play(cmd *cobra.Command, args []string) error {
253255
if playOptions.Annotations == nil {
254256
playOptions.Annotations = make(map[string]string)
255257
}
256-
if len(val) > define.MaxKubeAnnotation && !playOptions.UseLongAnnotations {
257-
return fmt.Errorf("annotation exceeds maximum size, %d, of kubernetes annotation: %s", define.MaxKubeAnnotation, val)
258-
}
259258
playOptions.Annotations[key] = val
260259
}
261260

261+
if err := annotations.ValidateAnnotations(playOptions.Annotations); err != nil {
262+
return err
263+
}
264+
262265
for _, mac := range playOptions.macs {
263266
m, err := net.ParseMAC(mac)
264267
if err != nil {

docs/source/markdown/podman-kube-generate.1.md

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,6 @@ Also note that both Deployment and DaemonSet can only have `restartPolicy` set t
3939

4040
Output to the given file instead of STDOUT. If the file already exists, `kube generate` refuses to replace it and returns an error.
4141

42-
#### **--no-trunc**
43-
44-
Don't truncate annotations to the Kubernetes maximum length of 63 characters.
45-
Note: enabling this flag means the generated YAML file is not Kubernetes compatible and can only be used with `podman kube play`
46-
4742
#### **--podman-only**
4843

4944
Add podman-only reserved annotations in generated YAML file (Cannot be used by Kubernetes)

docs/source/markdown/podman-kube-play.1.md.in

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,10 +217,6 @@ When no network option is specified and *host* network mode is not configured in
217217

218218
This option conflicts with host added in the Kubernetes YAML.
219219

220-
#### **--no-trunc**
221-
222-
Use annotations that are not truncated to the Kubernetes maximum length of 63 characters
223-
224220
#### **--publish**=*[[ip:][hostPort]:]containerPort[/protocol]*
225221

226222
Define or override a port definition in the YAML file.

libpod/define/annotations.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ const (
160160
// the k8s behavior of waiting for the intialDelaySeconds to be over before updating the status
161161
KubeHealthCheckAnnotation = "io.podman.annotations.kube.health.check"
162162

163-
// MaxKubeAnnotation is the max length of annotations allowed by Kubernetes.
164-
MaxKubeAnnotation = 63
163+
// TotalAnnotationSizeLimitB is the max length of annotations allowed by Kubernetes.
164+
TotalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB
165165
)
166166

167167
// IsReservedAnnotation returns true if the specified value corresponds to an

libpod/kube.go

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import (
1313
"strconv"
1414
"strings"
1515
"time"
16-
"unicode/utf8"
1716

1817
"github.com/containers/common/libnetwork/types"
1918
"github.com/containers/common/pkg/config"
@@ -36,14 +35,14 @@ import (
3635

3736
// GenerateForKube takes a slice of libpod containers and generates
3837
// one v1.Pod description that includes just a single container.
39-
func GenerateForKube(ctx context.Context, ctrs []*Container, getService, useLongAnnotations, podmanOnly bool) (*v1.Pod, error) {
38+
func GenerateForKube(ctx context.Context, ctrs []*Container, getService, podmanOnly bool) (*v1.Pod, error) {
4039
// Generate the v1.Pod yaml description
41-
return simplePodWithV1Containers(ctx, ctrs, getService, useLongAnnotations, podmanOnly)
40+
return simplePodWithV1Containers(ctx, ctrs, getService, podmanOnly)
4241
}
4342

4443
// GenerateForKube takes a slice of libpod containers and generates
4544
// one v1.Pod description
46-
func (p *Pod) GenerateForKube(ctx context.Context, getService, useLongAnnotations, podmanOnly bool) (*v1.Pod, []v1.ServicePort, error) {
45+
func (p *Pod) GenerateForKube(ctx context.Context, getService, podmanOnly bool) (*v1.Pod, []v1.ServicePort, error) {
4746
// Generate the v1.Pod yaml description
4847
var (
4948
ports []v1.ContainerPort
@@ -95,7 +94,7 @@ func (p *Pod) GenerateForKube(ctx context.Context, getService, useLongAnnotation
9594
hostUsers = infraContainer.IDMappings().HostUIDMapping && infraContainer.IDMappings().HostGIDMapping
9695
infraName = infraContainer.config.Name
9796
}
98-
pod, err := p.podWithContainers(ctx, allContainers, ports, hostNetwork, hostUsers, getService, useLongAnnotations, podmanOnly, infraName)
97+
pod, err := p.podWithContainers(ctx, allContainers, ports, hostNetwork, hostUsers, getService, podmanOnly, infraName)
9998
if err != nil {
10099
return nil, servicePorts, err
101100
}
@@ -451,16 +450,6 @@ func newServicePortState() servicePortState {
451450
}
452451
}
453452

454-
func truncateKubeAnnotation(str string, useLongAnnotations bool) string {
455-
str = strings.TrimSpace(str)
456-
if useLongAnnotations || utf8.RuneCountInString(str) < define.MaxKubeAnnotation {
457-
return str
458-
}
459-
trunc := string([]rune(str)[:define.MaxKubeAnnotation])
460-
logrus.Warnf("Truncation Annotation: %q to %q: Kubernetes only allows %d characters", str, trunc, define.MaxKubeAnnotation)
461-
return trunc
462-
}
463-
464453
// containerPortsToServicePorts takes a slice of containerports and generates a
465454
// slice of service ports
466455
func (state *servicePortState) containerPortsToServicePorts(containerPorts []v1.ContainerPort) ([]v1.ServicePort, error) {
@@ -507,7 +496,7 @@ func containersToServicePorts(containers []v1.Container) ([]v1.ServicePort, erro
507496
return sps, nil
508497
}
509498

510-
func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, ports []v1.ContainerPort, hostNetwork, hostUsers, getService, useLongAnnotations, podmanOnly bool, infraName string) (*v1.Pod, error) {
499+
func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, ports []v1.ContainerPort, hostNetwork, hostUsers, getService, podmanOnly bool, infraName string) (*v1.Pod, error) {
511500
deDupPodVolumes := make(map[string]*v1.Volume)
512501
first := true
513502
podContainers := make([]v1.Container, 0, len(containers))
@@ -529,11 +518,11 @@ func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, po
529518
if !podmanOnly && (define.IsReservedAnnotation(k) || annotations.IsReservedAnnotation(k)) {
530519
continue
531520
}
532-
podAnnotations[fmt.Sprintf("%s/%s", k, removeUnderscores(ctr.Name()))] = truncateKubeAnnotation(v, useLongAnnotations)
521+
podAnnotations[fmt.Sprintf("%s/%s", k, removeUnderscores(ctr.Name()))] = v
533522
}
534523
// Convert auto-update labels into kube annotations
535-
for k, v := range getAutoUpdateAnnotations(ctr.Name(), ctr.Labels(), useLongAnnotations) {
536-
podAnnotations[k] = truncateKubeAnnotation(v, useLongAnnotations)
524+
for k, v := range getAutoUpdateAnnotations(ctr.Name(), ctr.Labels()) {
525+
podAnnotations[k] = v
537526
}
538527
isInit := ctr.IsInitCtr()
539528
// Since hostname is only set at pod level, set the hostname to the hostname of the first container we encounter
@@ -556,7 +545,7 @@ func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, po
556545
return nil, err
557546
}
558547
for k, v := range annotations {
559-
podAnnotations[define.BindMountPrefix] = truncateKubeAnnotation(k+":"+v, useLongAnnotations)
548+
podAnnotations[define.BindMountPrefix] = k + ":" + v
560549
}
561550
// Since port bindings for the pod are handled by the
562551
// infra container, wipe them here only if we are sharing the net namespace
@@ -605,7 +594,7 @@ func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, po
605594
// If the infraName is not the podID-infra, that means the user set another infra name using
606595
// --infra-name during pod creation
607596
if infraName != "" && infraName != p.ID()[:12]+"-infra" {
608-
podAnnotations[define.InfraNameAnnotation] = truncateKubeAnnotation(infraName, useLongAnnotations)
597+
podAnnotations[define.InfraNameAnnotation] = infraName
609598
}
610599
}
611600
}
@@ -674,7 +663,7 @@ func newPodObject(podName string, annotations map[string]string, initCtrs, conta
674663

675664
// simplePodWithV1Containers is a function used by inspect when kube yaml needs to be generated
676665
// for a single container. we "insert" that container description in a pod.
677-
func simplePodWithV1Containers(ctx context.Context, ctrs []*Container, getService, useLongAnnotations, podmanOnly bool) (*v1.Pod, error) {
666+
func simplePodWithV1Containers(ctx context.Context, ctrs []*Container, getService, podmanOnly bool) (*v1.Pod, error) {
678667
kubeCtrs := make([]v1.Container, 0, len(ctrs))
679668
kubeInitCtrs := []v1.Container{}
680669
kubeVolumes := make([]v1.Volume, 0)
@@ -694,12 +683,12 @@ func simplePodWithV1Containers(ctx context.Context, ctrs []*Container, getServic
694683
if !podmanOnly && (define.IsReservedAnnotation(k) || annotations.IsReservedAnnotation(k)) {
695684
continue
696685
}
697-
kubeAnnotations[fmt.Sprintf("%s/%s", k, removeUnderscores(ctr.Name()))] = truncateKubeAnnotation(v, useLongAnnotations)
686+
kubeAnnotations[fmt.Sprintf("%s/%s", k, removeUnderscores(ctr.Name()))] = v
698687
}
699688

700689
// Convert auto-update labels into kube annotations
701-
for k, v := range getAutoUpdateAnnotations(ctr.Name(), ctr.Labels(), useLongAnnotations) {
702-
kubeAnnotations[k] = truncateKubeAnnotation(v, useLongAnnotations)
690+
for k, v := range getAutoUpdateAnnotations(ctr.Name(), ctr.Labels()) {
691+
kubeAnnotations[k] = v
703692
}
704693

705694
isInit := ctr.IsInitCtr()
@@ -752,7 +741,7 @@ func simplePodWithV1Containers(ctx context.Context, ctrs []*Container, getServic
752741
return nil, err
753742
}
754743
for k, v := range annotations {
755-
kubeAnnotations[define.BindMountPrefix] = truncateKubeAnnotation(k+":"+v, useLongAnnotations)
744+
kubeAnnotations[define.BindMountPrefix] = k + ":" + v
756745
}
757746
if isInit {
758747
kubeInitCtrs = append(kubeInitCtrs, kubeCtr)
@@ -1384,7 +1373,7 @@ func removeUnderscores(s string) string {
13841373

13851374
// getAutoUpdateAnnotations searches for auto-update container labels
13861375
// and returns them as kube annotations
1387-
func getAutoUpdateAnnotations(ctrName string, ctrLabels map[string]string, useLongAnnotations bool) map[string]string {
1376+
func getAutoUpdateAnnotations(ctrName string, ctrLabels map[string]string) map[string]string {
13881377
autoUpdateLabel := "io.containers.autoupdate"
13891378
annotations := make(map[string]string)
13901379

@@ -1394,7 +1383,7 @@ func getAutoUpdateAnnotations(ctrName string, ctrLabels map[string]string, useLo
13941383
// since labels can variate between containers within a pod, they will be
13951384
// identified with the container name when converted into kube annotations
13961385
kc := fmt.Sprintf("%s/%s", k, ctrName)
1397-
annotations[kc] = truncateKubeAnnotation(v, useLongAnnotations)
1386+
annotations[kc] = v
13981387
}
13991388
}
14001389

pkg/annotations/validate.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package annotations
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
8+
"github.com/containers/podman/v5/libpod/define"
9+
)
10+
11+
// regexErrorMsg returns a string explanation of a regex validation failure.
12+
func regexErrorMsg(msg string, fmt string, examples ...string) string {
13+
if len(examples) == 0 {
14+
return msg + " (regex used for validation is '" + fmt + "')"
15+
}
16+
msg += " (e.g. "
17+
for i := range examples {
18+
if i > 0 {
19+
msg += " or "
20+
}
21+
msg += "'" + examples[i] + "', "
22+
}
23+
msg += "regex used for validation is '" + fmt + "')"
24+
return msg
25+
}
26+
27+
const dns1123LabelFmt string = "[a-z0-9]([-a-z0-9]*[a-z0-9])?"
28+
const dns1123SubdomainFmt string = dns1123LabelFmt + "(\\." + dns1123LabelFmt + ")*"
29+
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"
30+
31+
// DNS1123SubdomainMaxLength is a subdomain's max length in DNS (RFC 1123)
32+
const DNS1123SubdomainMaxLength int = 253
33+
34+
var dns1123SubdomainRegexp = regexp.MustCompile("^" + dns1123SubdomainFmt + "$")
35+
36+
// isDNS1123Subdomain tests for a string that conforms to the definition of a
37+
// subdomain in DNS (RFC 1123).
38+
func isDNS1123Subdomain(value string) error {
39+
if len(value) > DNS1123SubdomainMaxLength {
40+
return fmt.Errorf("prefix part must be no more than %d characters", DNS1123SubdomainMaxLength)
41+
}
42+
43+
if !dns1123SubdomainRegexp.MatchString(value) {
44+
return fmt.Errorf(regexErrorMsg(dns1123SubdomainErrorMsg, dns1123SubdomainFmt, "example.com"))
45+
}
46+
47+
return nil
48+
}
49+
50+
const qnameCharFmt string = "[A-Za-z0-9]"
51+
const qnameExtCharFmt string = "[-A-Za-z0-9_.]"
52+
const qualifiedNameFmt string = "(" + qnameCharFmt + qnameExtCharFmt + "*)?" + qnameCharFmt
53+
const qualifiedNameErrMsg string = "must consist of alphanumeric characters, '-', '_' or '.', and must start and end with an alphanumeric character"
54+
const qualifiedNameMaxLength int = 63
55+
56+
var qualifiedNameRegexp = regexp.MustCompile("^" + qualifiedNameFmt + "$")
57+
58+
// isQualifiedName tests whether the value passed is what Kubernetes calls a
59+
// "qualified name". This is a format used in various places throughout the
60+
// system. If the value is not valid, a list of error strings is returned.
61+
// Otherwise an empty list (or nil) is returned.
62+
func isQualifiedName(value string) error {
63+
parts := strings.Split(value, "/")
64+
var name string
65+
66+
switch len(parts) {
67+
case 1:
68+
name = parts[0]
69+
case 2:
70+
var prefix string
71+
prefix, name = parts[0], parts[1]
72+
if len(prefix) == 0 {
73+
return fmt.Errorf("prefix part of %s must be non-empty", value)
74+
} else if err := isDNS1123Subdomain(prefix); err != nil {
75+
return err
76+
}
77+
default:
78+
return fmt.Errorf("a qualified name of %s "+
79+
regexErrorMsg(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc")+
80+
" with an optional DNS subdomain prefix and '/' (e.g. 'example.com/MyName')", value)
81+
}
82+
83+
if len(name) == 0 {
84+
return fmt.Errorf("name part of %s must be non-empty", value)
85+
} else if len(name) > qualifiedNameMaxLength {
86+
return fmt.Errorf("name part of %s must be no more than %d characters", value, qualifiedNameMaxLength)
87+
}
88+
89+
if !qualifiedNameRegexp.MatchString(name) {
90+
return fmt.Errorf("name part of %s "+
91+
regexErrorMsg(qualifiedNameErrMsg, qualifiedNameFmt, "MyName", "my.name", "123-abc"), value)
92+
}
93+
94+
return nil
95+
}
96+
97+
func validateAnnotationsSize(annotations map[string]string) error {
98+
var totalSize int64
99+
for k, v := range annotations {
100+
totalSize += (int64)(len(k)) + (int64)(len(v))
101+
}
102+
if totalSize > (int64)(define.TotalAnnotationSizeLimitB) {
103+
return fmt.Errorf("annotations size %d is larger than limit %d", totalSize, define.TotalAnnotationSizeLimitB)
104+
}
105+
return nil
106+
}
107+
108+
// ValidateAnnotations validates that a set of annotations are correctly
109+
// defined.
110+
func ValidateAnnotations(annotations map[string]string) error {
111+
for k := range annotations {
112+
// The rule is QualifiedName except that case doesn't matter,
113+
// so convert to lowercase before checking.
114+
if err := isQualifiedName(strings.ToLower(k)); err != nil {
115+
return err
116+
}
117+
}
118+
119+
if err := validateAnnotationsSize(annotations); err != nil {
120+
return err
121+
}
122+
123+
return nil
124+
}

0 commit comments

Comments
 (0)