Skip to content

Commit

Permalink
Merge pull request #21699 from vikas-goel/i21663
Browse files Browse the repository at this point in the history
Comply to Kubernetes specifications for annotation size.
  • Loading branch information
openshift-merge-bot[bot] authored Feb 21, 2024
2 parents 0d1a8c0 + 89b415b commit 70091d5
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 218 deletions.
1 change: 1 addition & 0 deletions cmd/podman/kube/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ func generateFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) {

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

podmanOnlyFlagName := "podman-only"
flags.BoolVar(&generateOptions.PodmanOnly, podmanOnlyFlagName, false, "Add podman-only reserved annotations to the generated YAML file (Cannot be used by Kubernetes)")
Expand Down
9 changes: 6 additions & 3 deletions cmd/podman/kube/play.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/containers/podman/v5/cmd/podman/utils"
"github.com/containers/podman/v5/libpod/define"
"github.com/containers/podman/v5/libpod/shutdown"
"github.com/containers/podman/v5/pkg/annotations"
"github.com/containers/podman/v5/pkg/domain/entities"
"github.com/containers/podman/v5/pkg/errorhandling"
"github.com/containers/podman/v5/pkg/util"
Expand Down Expand Up @@ -171,6 +172,7 @@ func playFlags(cmd *cobra.Command) {

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

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

if err := annotations.ValidateAnnotations(playOptions.Annotations); err != nil {
return err
}

for _, mac := range playOptions.macs {
m, err := net.ParseMAC(mac)
if err != nil {
Expand Down
5 changes: 0 additions & 5 deletions docs/source/markdown/podman-kube-generate.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ Also note that both Deployment and DaemonSet can only have `restartPolicy` set t

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

#### **--no-trunc**

Don't truncate annotations to the Kubernetes maximum length of 63 characters.
Note: enabling this flag means the generated YAML file is not Kubernetes compatible and can only be used with `podman kube play`

#### **--podman-only**

Add podman-only reserved annotations in generated YAML file (Cannot be used by Kubernetes)
Expand Down
4 changes: 0 additions & 4 deletions docs/source/markdown/podman-kube-play.1.md.in
Original file line number Diff line number Diff line change
Expand Up @@ -217,10 +217,6 @@ When no network option is specified and *host* network mode is not configured in

This option conflicts with host added in the Kubernetes YAML.

#### **--no-trunc**

Use annotations that are not truncated to the Kubernetes maximum length of 63 characters

#### **--publish**=*[[ip:][hostPort]:]containerPort[/protocol]*

Define or override a port definition in the YAML file.
Expand Down
4 changes: 2 additions & 2 deletions libpod/define/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,8 @@ const (
// the k8s behavior of waiting for the intialDelaySeconds to be over before updating the status
KubeHealthCheckAnnotation = "io.podman.annotations.kube.health.check"

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

// IsReservedAnnotation returns true if the specified value corresponds to an
Expand Down
45 changes: 17 additions & 28 deletions libpod/kube.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"

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

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

// GenerateForKube takes a slice of libpod containers and generates
// one v1.Pod description
func (p *Pod) GenerateForKube(ctx context.Context, getService, useLongAnnotations, podmanOnly bool) (*v1.Pod, []v1.ServicePort, error) {
func (p *Pod) GenerateForKube(ctx context.Context, getService, podmanOnly bool) (*v1.Pod, []v1.ServicePort, error) {
// Generate the v1.Pod yaml description
var (
ports []v1.ContainerPort
Expand Down Expand Up @@ -95,7 +94,7 @@ func (p *Pod) GenerateForKube(ctx context.Context, getService, useLongAnnotation
hostUsers = infraContainer.IDMappings().HostUIDMapping && infraContainer.IDMappings().HostGIDMapping
infraName = infraContainer.config.Name
}
pod, err := p.podWithContainers(ctx, allContainers, ports, hostNetwork, hostUsers, getService, useLongAnnotations, podmanOnly, infraName)
pod, err := p.podWithContainers(ctx, allContainers, ports, hostNetwork, hostUsers, getService, podmanOnly, infraName)
if err != nil {
return nil, servicePorts, err
}
Expand Down Expand Up @@ -451,16 +450,6 @@ func newServicePortState() servicePortState {
}
}

func truncateKubeAnnotation(str string, useLongAnnotations bool) string {
str = strings.TrimSpace(str)
if useLongAnnotations || utf8.RuneCountInString(str) < define.MaxKubeAnnotation {
return str
}
trunc := string([]rune(str)[:define.MaxKubeAnnotation])
logrus.Warnf("Truncation Annotation: %q to %q: Kubernetes only allows %d characters", str, trunc, define.MaxKubeAnnotation)
return trunc
}

// containerPortsToServicePorts takes a slice of containerports and generates a
// slice of service ports
func (state *servicePortState) containerPortsToServicePorts(containerPorts []v1.ContainerPort) ([]v1.ServicePort, error) {
Expand Down Expand Up @@ -507,7 +496,7 @@ func containersToServicePorts(containers []v1.Container) ([]v1.ServicePort, erro
return sps, nil
}

func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, ports []v1.ContainerPort, hostNetwork, hostUsers, getService, useLongAnnotations, podmanOnly bool, infraName string) (*v1.Pod, error) {
func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, ports []v1.ContainerPort, hostNetwork, hostUsers, getService, podmanOnly bool, infraName string) (*v1.Pod, error) {
deDupPodVolumes := make(map[string]*v1.Volume)
first := true
podContainers := make([]v1.Container, 0, len(containers))
Expand All @@ -529,11 +518,11 @@ func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, po
if !podmanOnly && (define.IsReservedAnnotation(k) || annotations.IsReservedAnnotation(k)) {
continue
}
podAnnotations[fmt.Sprintf("%s/%s", k, removeUnderscores(ctr.Name()))] = truncateKubeAnnotation(v, useLongAnnotations)
podAnnotations[fmt.Sprintf("%s/%s", k, removeUnderscores(ctr.Name()))] = v
}
// Convert auto-update labels into kube annotations
for k, v := range getAutoUpdateAnnotations(ctr.Name(), ctr.Labels(), useLongAnnotations) {
podAnnotations[k] = truncateKubeAnnotation(v, useLongAnnotations)
for k, v := range getAutoUpdateAnnotations(ctr.Name(), ctr.Labels()) {
podAnnotations[k] = v
}
isInit := ctr.IsInitCtr()
// Since hostname is only set at pod level, set the hostname to the hostname of the first container we encounter
Expand All @@ -556,7 +545,7 @@ func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, po
return nil, err
}
for k, v := range annotations {
podAnnotations[define.BindMountPrefix] = truncateKubeAnnotation(k+":"+v, useLongAnnotations)
podAnnotations[define.BindMountPrefix] = k + ":" + v
}
// Since port bindings for the pod are handled by the
// infra container, wipe them here only if we are sharing the net namespace
Expand Down Expand Up @@ -605,7 +594,7 @@ func (p *Pod) podWithContainers(ctx context.Context, containers []*Container, po
// If the infraName is not the podID-infra, that means the user set another infra name using
// --infra-name during pod creation
if infraName != "" && infraName != p.ID()[:12]+"-infra" {
podAnnotations[define.InfraNameAnnotation] = truncateKubeAnnotation(infraName, useLongAnnotations)
podAnnotations[define.InfraNameAnnotation] = infraName
}
}
}
Expand Down Expand Up @@ -674,7 +663,7 @@ func newPodObject(podName string, annotations map[string]string, initCtrs, conta

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

// Convert auto-update labels into kube annotations
for k, v := range getAutoUpdateAnnotations(ctr.Name(), ctr.Labels(), useLongAnnotations) {
kubeAnnotations[k] = truncateKubeAnnotation(v, useLongAnnotations)
for k, v := range getAutoUpdateAnnotations(ctr.Name(), ctr.Labels()) {
kubeAnnotations[k] = v
}

isInit := ctr.IsInitCtr()
Expand Down Expand Up @@ -752,7 +741,7 @@ func simplePodWithV1Containers(ctx context.Context, ctrs []*Container, getServic
return nil, err
}
for k, v := range annotations {
kubeAnnotations[define.BindMountPrefix] = truncateKubeAnnotation(k+":"+v, useLongAnnotations)
kubeAnnotations[define.BindMountPrefix] = k + ":" + v
}
if isInit {
kubeInitCtrs = append(kubeInitCtrs, kubeCtr)
Expand Down Expand Up @@ -1384,7 +1373,7 @@ func removeUnderscores(s string) string {

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

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

Expand Down
124 changes: 124 additions & 0 deletions pkg/annotations/validate.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 70091d5

Please sign in to comment.