Further cleanup of container validation

This commit is contained in:
Lee Verberne 2022-07-28 19:38:59 +02:00
parent 1dc040082c
commit 537e73601d
2 changed files with 408 additions and 146 deletions

View File

@ -2907,6 +2907,8 @@ func validatePullPolicy(policy core.PullPolicy, fldPath *field.Path) field.Error
return allErrors return allErrors
} }
// validateEphemeralContainers is called by pod spec and template validation to validate the list of ephemeral containers.
// Note that this is called for pod template even though ephemeral containers aren't allowed in pod templates.
func validateEphemeralContainers(ephemeralContainers []core.EphemeralContainer, containers, initContainers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { func validateEphemeralContainers(ephemeralContainers []core.EphemeralContainer, containers, initContainers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
@ -2914,40 +2916,40 @@ func validateEphemeralContainers(ephemeralContainers []core.EphemeralContainer,
return allErrs return allErrs
} }
allNames := sets.String{} otherNames, allNames := sets.String{}, sets.String{}
for _, c := range containers { for _, c := range containers {
otherNames.Insert(c.Name)
allNames.Insert(c.Name) allNames.Insert(c.Name)
} }
for _, c := range initContainers { for _, c := range initContainers {
otherNames.Insert(c.Name)
allNames.Insert(c.Name) allNames.Insert(c.Name)
} }
for i, ec := range ephemeralContainers { for i, ec := range ephemeralContainers {
idxPath := fldPath.Index(i) idxPath := fldPath.Index(i)
if ec.TargetContainerName != "" && !allNames.Has(ec.TargetContainerName) {
allErrs = append(allErrs, field.NotFound(idxPath.Child("targetContainerName"), ec.TargetContainerName))
}
if ec.Name == "" {
allErrs = append(allErrs, field.Required(idxPath.Child("name"), "ephemeralContainer requires a name"))
continue
}
c := (*core.Container)(&ec.EphemeralContainerCommon) c := (*core.Container)(&ec.EphemeralContainerCommon)
allErrs = append(allErrs, validateContainer(c, volumes, idxPath, opts)...) allErrs = append(allErrs, validateContainerCommon(c, volumes, idxPath, opts)...)
// EphemeralContainers don't require the backwards-compatibility distinction between pod/podTemplate validation // Ephemeral containers don't need looser constraints for pod templates, so it's convenient to apply both validations
// here where we've already converted EphemeralContainerCommon to Container.
allErrs = append(allErrs, validateContainerOnlyForPod(c, idxPath)...) allErrs = append(allErrs, validateContainerOnlyForPod(c, idxPath)...)
// Ephemeral containers must have a name unique across all container types.
if allNames.Has(ec.Name) { if allNames.Has(ec.Name) {
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), ec.Name)) allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), ec.Name))
} else { } else {
allNames.Insert(ec.Name) allNames.Insert(ec.Name)
} }
// Ephemeral Containers should not be relied upon for fundamental pod services, so fields such as // The target container name must exist and be non-ephemeral.
if ec.TargetContainerName != "" && !otherNames.Has(ec.TargetContainerName) {
allErrs = append(allErrs, field.NotFound(idxPath.Child("targetContainerName"), ec.TargetContainerName))
}
// Ephemeral containers should not be relied upon for fundamental pod services, so fields such as
// Lifecycle, probes, resources and ports should be disallowed. This is implemented as a list // Lifecycle, probes, resources and ports should be disallowed. This is implemented as a list
// of allowed fields so that new fields will be given consideration prior to inclusion in Ephemeral Containers. // of allowed fields so that new fields will be given consideration prior to inclusion in ephemeral containers.
allErrs = append(allErrs, validateFieldAllowList(ec.EphemeralContainerCommon, allowedEphemeralContainerFields, "cannot be set for an Ephemeral Container", idxPath)...) allErrs = append(allErrs, validateFieldAllowList(ec.EphemeralContainerCommon, allowedEphemeralContainerFields, "cannot be set for an Ephemeral Container", idxPath)...)
// VolumeMount subpaths have the potential to leak resources since they're implemented with bind mounts // VolumeMount subpaths have the potential to leak resources since they're implemented with bind mounts
@ -2989,44 +2991,52 @@ func validateFieldAllowList(value interface{}, allowedFields map[string]bool, er
return allErrs return allErrs
} }
func validateInitContainers(containers []core.Container, otherContainers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { // validateInitContainers is called by pod spec and template validation to validate the list of init containers
func validateInitContainers(containers []core.Container, regularContainers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList {
var allErrs field.ErrorList var allErrs field.ErrorList
allNames := sets.String{} allNames := sets.String{}
for _, ctr := range otherContainers { for _, ctr := range regularContainers {
allNames.Insert(ctr.Name) allNames.Insert(ctr.Name)
} }
for i, ctr := range containers { for i, ctr := range containers {
idxPath := fldPath.Index(i) idxPath := fldPath.Index(i)
// Apply the validation common to all container types
allErrs = append(allErrs, validateContainerCommon(&ctr, volumes, idxPath, opts)...)
// Names must be unique within regular and init containers. Collisions with ephemeral containers
// will be detected by validateEphemeralContainers().
if allNames.Has(ctr.Name) { if allNames.Has(ctr.Name) {
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), ctr.Name)) allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), ctr.Name))
} else if len(ctr.Name) > 0 { } else if len(ctr.Name) > 0 {
allNames.Insert(ctr.Name) allNames.Insert(ctr.Name)
} }
allErrs = append(allErrs, validateContainer(&ctr, volumes, idxPath, opts)...)
// Check for port conflicts in init containers individually since init containers run one-by-one. // Check for port conflicts in init containers individually since init containers run one-by-one.
allErrs = append(allErrs, checkHostPortConflicts([]core.Container{ctr}, fldPath)...) allErrs = append(allErrs, checkHostPortConflicts([]core.Container{ctr}, fldPath)...)
// These fields are disallowed for init containers.
if ctr.Lifecycle != nil { if ctr.Lifecycle != nil {
allErrs = append(allErrs, field.Invalid(idxPath.Child("lifecycle"), ctr.Lifecycle, "must not be set for init containers")) allErrs = append(allErrs, field.Forbidden(idxPath.Child("lifecycle"), "may not be set for init containers"))
} }
if ctr.LivenessProbe != nil { if ctr.LivenessProbe != nil {
allErrs = append(allErrs, field.Invalid(idxPath.Child("livenessProbe"), ctr.LivenessProbe, "must not be set for init containers")) allErrs = append(allErrs, field.Forbidden(idxPath.Child("livenessProbe"), "may not be set for init containers"))
} }
if ctr.ReadinessProbe != nil { if ctr.ReadinessProbe != nil {
allErrs = append(allErrs, field.Invalid(idxPath.Child("readinessProbe"), ctr.ReadinessProbe, "must not be set for init containers")) allErrs = append(allErrs, field.Forbidden(idxPath.Child("readinessProbe"), "may not be set for init containers"))
} }
if ctr.StartupProbe != nil { if ctr.StartupProbe != nil {
allErrs = append(allErrs, field.Invalid(idxPath.Child("startupProbe"), ctr.StartupProbe, "must not be set for init containers")) allErrs = append(allErrs, field.Forbidden(idxPath.Child("startupProbe"), "may not be set for init containers"))
} }
} }
return allErrs return allErrs
} }
func validateContainer(ctr *core.Container, volumes map[string]core.VolumeSource, path *field.Path, opts PodValidationOptions) field.ErrorList { // validateContainerCommon applies validation common to all container types. It's called by regular, init, and ephemeral
// container list validation to require a properly formatted name, image, etc.
func validateContainerCommon(ctr *core.Container, volumes map[string]core.VolumeSource, path *field.Path, opts PodValidationOptions) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
namePath := path.Child("name") namePath := path.Child("name")
@ -3042,37 +3052,21 @@ func validateContainer(ctr *core.Container, volumes map[string]core.VolumeSource
if len(ctr.Image) == 0 { if len(ctr.Image) == 0 {
allErrs = append(allErrs, field.Required(path.Child("image"), "")) allErrs = append(allErrs, field.Required(path.Child("image"), ""))
} }
if ctr.Lifecycle != nil {
allErrs = append(allErrs, validateLifecycle(ctr.Lifecycle, path.Child("lifecycle"))...)
}
allErrs = append(allErrs, validateProbe(ctr.LivenessProbe, path.Child("livenessProbe"))...)
// Readiness-specific validation
if ctr.ReadinessProbe != nil && ctr.ReadinessProbe.TerminationGracePeriodSeconds != nil {
allErrs = append(allErrs, field.Invalid(path.Child("readinessProbe", "terminationGracePeriodSeconds"), ctr.ReadinessProbe.TerminationGracePeriodSeconds, "must not be set for readinessProbes"))
}
allErrs = append(allErrs, validateProbe(ctr.StartupProbe, path.Child("startupProbe"))...)
// Liveness-specific validation
if ctr.LivenessProbe != nil && ctr.LivenessProbe.SuccessThreshold != 1 {
allErrs = append(allErrs, field.Invalid(path.Child("livenessProbe", "successThreshold"), ctr.LivenessProbe.SuccessThreshold, "must be 1"))
}
allErrs = append(allErrs, validateProbe(ctr.StartupProbe, path.Child("startupProbe"))...)
// Startup-specific validation
if ctr.StartupProbe != nil && ctr.StartupProbe.SuccessThreshold != 1 {
allErrs = append(allErrs, field.Invalid(path.Child("startupProbe", "successThreshold"), ctr.StartupProbe.SuccessThreshold, "must be 1"))
}
switch ctr.TerminationMessagePolicy { switch ctr.TerminationMessagePolicy {
case core.TerminationMessageReadFile, core.TerminationMessageFallbackToLogsOnError: case core.TerminationMessageReadFile, core.TerminationMessageFallbackToLogsOnError:
case "": case "":
allErrs = append(allErrs, field.Required(path.Child("terminationMessagePolicy"), "must be 'File' or 'FallbackToLogsOnError'")) allErrs = append(allErrs, field.Required(path.Child("terminationMessagePolicy"), ""))
default: default:
allErrs = append(allErrs, field.Invalid(path.Child("terminationMessagePolicy"), ctr.TerminationMessagePolicy, "must be 'File' or 'FallbackToLogsOnError'")) supported := []string{
string(core.TerminationMessageReadFile),
string(core.TerminationMessageFallbackToLogsOnError),
}
allErrs = append(allErrs, field.NotSupported(path.Child("terminationMessagePolicy"), ctr.TerminationMessagePolicy, supported))
} }
volMounts := GetVolumeMountMap(ctr.VolumeMounts) volMounts := GetVolumeMountMap(ctr.VolumeMounts)
volDevices := GetVolumeDeviceMap(ctr.VolumeDevices) volDevices := GetVolumeDeviceMap(ctr.VolumeDevices)
allErrs = append(allErrs, validateProbe(ctr.ReadinessProbe, path.Child("readinessProbe"))...)
allErrs = append(allErrs, validateContainerPorts(ctr.Ports, path.Child("ports"))...) allErrs = append(allErrs, validateContainerPorts(ctr.Ports, path.Child("ports"))...)
allErrs = append(allErrs, ValidateEnv(ctr.Env, path.Child("env"), opts)...) allErrs = append(allErrs, ValidateEnv(ctr.Env, path.Child("env"), opts)...)
allErrs = append(allErrs, ValidateEnvFrom(ctr.EnvFrom, path.Child("envFrom"))...) allErrs = append(allErrs, ValidateEnvFrom(ctr.EnvFrom, path.Child("envFrom"))...)
@ -3085,6 +3079,7 @@ func validateContainer(ctr *core.Container, volumes map[string]core.VolumeSource
return allErrs return allErrs
} }
// validateContainers is called by pod spec and template validation to validate the list of regular containers.
func validateContainers(containers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList { func validateContainers(containers []core.Container, volumes map[string]core.VolumeSource, fldPath *field.Path, opts PodValidationOptions) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
@ -3094,18 +3089,40 @@ func validateContainers(containers []core.Container, volumes map[string]core.Vol
allNames := sets.String{} allNames := sets.String{}
for i, ctr := range containers { for i, ctr := range containers {
idxPath := fldPath.Index(i) path := fldPath.Index(i)
// Apply validation common to all containers
allErrs = append(allErrs, validateContainerCommon(&ctr, volumes, path, opts)...)
// Container names must be unique within the list of regular containers.
// Collisions with init or ephemeral container names will be detected by the init or ephemeral
// container validation to prevent duplicate error messages.
if allNames.Has(ctr.Name) { if allNames.Has(ctr.Name) {
allErrs = append(allErrs, field.Duplicate(idxPath.Child("name"), ctr.Name)) allErrs = append(allErrs, field.Duplicate(path.Child("name"), ctr.Name))
} else { } else {
allNames.Insert(ctr.Name) allNames.Insert(ctr.Name)
} }
allErrs = append(allErrs, validateContainer(&ctr, volumes, idxPath, opts)...) // These fields are only allowed for regular containers, so only check supported values here.
// Init and ephemeral container validation will return field.Forbidden() for these paths.
if ctr.Lifecycle != nil {
allErrs = append(allErrs, validateLifecycle(ctr.Lifecycle, path.Child("lifecycle"))...)
}
allErrs = append(allErrs, validateProbe(ctr.LivenessProbe, path.Child("livenessProbe"))...)
if ctr.LivenessProbe != nil && ctr.LivenessProbe.SuccessThreshold != 1 {
allErrs = append(allErrs, field.Invalid(path.Child("livenessProbe", "successThreshold"), ctr.LivenessProbe.SuccessThreshold, "must be 1"))
}
allErrs = append(allErrs, validateProbe(ctr.ReadinessProbe, path.Child("readinessProbe"))...)
if ctr.ReadinessProbe != nil && ctr.ReadinessProbe.TerminationGracePeriodSeconds != nil {
allErrs = append(allErrs, field.Invalid(path.Child("readinessProbe", "terminationGracePeriodSeconds"), ctr.ReadinessProbe.TerminationGracePeriodSeconds, "must not be set for readinessProbes"))
}
allErrs = append(allErrs, validateProbe(ctr.StartupProbe, path.Child("startupProbe"))...)
if ctr.StartupProbe != nil && ctr.StartupProbe.SuccessThreshold != 1 {
allErrs = append(allErrs, field.Invalid(path.Child("startupProbe", "successThreshold"), ctr.StartupProbe.SuccessThreshold, "must be 1"))
}
} }
// Check for colliding ports across all containers. // Port conflicts are checked across all containers
allErrs = append(allErrs, checkHostPortConflicts(containers, fldPath)...) allErrs = append(allErrs, checkHostPortConflicts(containers, fldPath)...)
return allErrs return allErrs
@ -3392,7 +3409,8 @@ func ValidateTolerations(tolerations []core.Toleration, fldPath *field.Path) fie
} }
// validateContainersOnlyForPod does additional validation for containers on a pod versus a pod template // validateContainersOnlyForPod does additional validation for containers on a pod versus a pod template
// it only does additive validation of fields not covered in validateContainers // it only does additive validation of fields not covered in validateContainers and is not called for
// ephemeral containers which require a conversion to core.Container.
func validateContainersOnlyForPod(containers []core.Container, fldPath *field.Path) field.ErrorList { func validateContainersOnlyForPod(containers []core.Container, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
for i, ctr := range containers { for i, ctr := range containers {
@ -3401,6 +3419,8 @@ func validateContainersOnlyForPod(containers []core.Container, fldPath *field.Pa
return allErrs return allErrs
} }
// validateContainerOnlyForPod does pod-only (i.e. not pod template) validation for a single container.
// This is called by validateContainersOnlyForPod and validateEphemeralContainers directly.
func validateContainerOnlyForPod(ctr *core.Container, path *field.Path) field.ErrorList { func validateContainerOnlyForPod(ctr *core.Container, path *field.Path) field.ErrorList {
allErrs := field.ErrorList{} allErrs := field.ErrorList{}
if len(ctr.Image) != len(strings.TrimSpace(ctr.Image)) { if len(ctr.Image) != len(strings.TrimSpace(ctr.Image)) {
@ -3451,6 +3471,7 @@ func validatePodMetadataAndSpec(pod *core.Pod, opts PodValidationOptions) field.
allErrs = append(allErrs, validateContainersOnlyForPod(pod.Spec.Containers, specPath.Child("containers"))...) allErrs = append(allErrs, validateContainersOnlyForPod(pod.Spec.Containers, specPath.Child("containers"))...)
allErrs = append(allErrs, validateContainersOnlyForPod(pod.Spec.InitContainers, specPath.Child("initContainers"))...) allErrs = append(allErrs, validateContainersOnlyForPod(pod.Spec.InitContainers, specPath.Child("initContainers"))...)
// validateContainersOnlyForPod() is checked for ephemeral containers by validateEphemeralContainers()
return allErrs return allErrs
} }

View File

@ -21,11 +21,13 @@ import (
"fmt" "fmt"
"math" "math"
"reflect" "reflect"
"runtime"
"strings" "strings"
"testing" "testing"
"time" "time"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@ -51,6 +53,25 @@ const (
envVarNameErrMsg = "a valid environment variable name must consist of" envVarNameErrMsg = "a valid environment variable name must consist of"
) )
func line() string {
_, _, line, ok := runtime.Caller(1)
var s string
if ok {
s = fmt.Sprintf("%d", line)
} else {
s = "<??>"
}
return s
}
func prettyErrorList(errs field.ErrorList) string {
var s string
for _, e := range errs {
s += fmt.Sprintf("\t%s\n", e)
}
return s
}
func newHostPathType(pathType string) *core.HostPathType { func newHostPathType(pathType string) *core.HostPathType {
hostPathType := new(core.HostPathType) hostPathType := new(core.HostPathType)
*hostPathType = core.HostPathType(pathType) *hostPathType = core.HostPathType(pathType)
@ -6714,74 +6735,102 @@ func TestValidateEphemeralContainers(t *testing.T) {
// Failure Cases // Failure Cases
tcs := []struct { tcs := []struct {
title string title, line string
ephemeralContainers []core.EphemeralContainer ephemeralContainers []core.EphemeralContainer
expectedError field.Error expectedErrors field.ErrorList
}{ }{
{ {
"Name Collision with Container.Containers", "Name Collision with Container.Containers",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
}, },
field.Error{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[0].name"}, field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[0].name"}},
}, },
{ {
"Name Collision with Container.InitContainers", "Name Collision with Container.InitContainers",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ictr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "ictr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
}, },
field.Error{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[0].name"}, field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[0].name"}},
}, },
{ {
"Name Collision with EphemeralContainers", "Name Collision with EphemeralContainers",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug1", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
}, },
field.Error{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[1].name"}, field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "ephemeralContainers[1].name"}},
}, },
{ {
"empty Container Container", "empty Container",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{}},
}, },
field.Error{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].name"}, field.ErrorList{
{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].name"},
{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].image"},
{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].terminationMessagePolicy"},
{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].imagePullPolicy"},
},
}, },
{ {
"empty Container Name", "empty Container Name",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
}, },
field.Error{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].name"}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "ephemeralContainers[0].name"}},
}, },
{ {
"whitespace padded image name", "whitespace padded image name",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: " image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: " image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
}, },
field.Error{Type: field.ErrorTypeInvalid, Field: "ephemeralContainers[0].image"}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "ephemeralContainers[0].image"}},
}, },
{ {
"invalid image pull policy", "invalid image pull policy",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "PullThreeTimes", TerminationMessagePolicy: "File"}}, {EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "PullThreeTimes", TerminationMessagePolicy: "File"}},
}, },
field.Error{Type: field.ErrorTypeNotSupported, Field: "ephemeralContainers[0].imagePullPolicy"}, field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "ephemeralContainers[0].imagePullPolicy"}},
}, },
{ {
"TargetContainerName doesn't exist", "TargetContainerName doesn't exist",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
TargetContainerName: "bogus", TargetContainerName: "bogus",
}, },
}, },
field.Error{Type: field.ErrorTypeNotFound, Field: "ephemeralContainers[0].targetContainerName"}, field.ErrorList{{Type: field.ErrorTypeNotFound, Field: "ephemeralContainers[0].targetContainerName"}},
},
{
"Targets an ephemeral container",
line(),
[]core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
},
{
EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debugception", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
TargetContainerName: "debug",
},
},
field.ErrorList{{Type: field.ErrorTypeNotFound, Field: "ephemeralContainers[1].targetContainerName"}},
}, },
{ {
"Container uses disallowed field: Lifecycle", "Container uses disallowed field: Lifecycle",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{ EphemeralContainerCommon: core.EphemeralContainerCommon{
@ -6797,10 +6846,11 @@ func TestValidateEphemeralContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].lifecycle"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].lifecycle"}},
}, },
{ {
"Container uses disallowed field: LivenessProbe", "Container uses disallowed field: LivenessProbe",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{ EphemeralContainerCommon: core.EphemeralContainerCommon{
@ -6817,10 +6867,11 @@ func TestValidateEphemeralContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].livenessProbe"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].livenessProbe"}},
}, },
{ {
"Container uses disallowed field: Ports", "Container uses disallowed field: Ports",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{ EphemeralContainerCommon: core.EphemeralContainerCommon{
@ -6834,10 +6885,11 @@ func TestValidateEphemeralContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].ports"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].ports"}},
}, },
{ {
"Container uses disallowed field: ReadinessProbe", "Container uses disallowed field: ReadinessProbe",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{ EphemeralContainerCommon: core.EphemeralContainerCommon{
@ -6853,10 +6905,11 @@ func TestValidateEphemeralContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].readinessProbe"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].readinessProbe"}},
}, },
{ {
"Container uses disallowed field: StartupProbe", "Container uses disallowed field: StartupProbe",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{ EphemeralContainerCommon: core.EphemeralContainerCommon{
@ -6873,10 +6926,11 @@ func TestValidateEphemeralContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].startupProbe"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].startupProbe"}},
}, },
{ {
"Container uses disallowed field: Resources", "Container uses disallowed field: Resources",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{ EphemeralContainerCommon: core.EphemeralContainerCommon{
@ -6892,10 +6946,11 @@ func TestValidateEphemeralContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].resources"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].resources"}},
}, },
{ {
"Container uses disallowed field: VolumeMount.SubPath", "Container uses disallowed field: VolumeMount.SubPath",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{ EphemeralContainerCommon: core.EphemeralContainerCommon{
@ -6910,10 +6965,11 @@ func TestValidateEphemeralContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].volumeMounts[1].subPath"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].volumeMounts[1].subPath"}},
}, },
{ {
"Container uses disallowed field: VolumeMount.SubPathExpr", "Container uses disallowed field: VolumeMount.SubPathExpr",
line(),
[]core.EphemeralContainer{ []core.EphemeralContainer{
{ {
EphemeralContainerCommon: core.EphemeralContainerCommon{ EphemeralContainerCommon: core.EphemeralContainerCommon{
@ -6928,25 +6984,40 @@ func TestValidateEphemeralContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].volumeMounts[1].subPathExpr"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].volumeMounts[1].subPathExpr"}},
},
{
"Disallowed field with other errors should only return a single Forbidden",
line(),
[]core.EphemeralContainer{
{
EphemeralContainerCommon: core.EphemeralContainerCommon{
Name: "debug",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
Lifecycle: &core.Lifecycle{
PreStop: &core.LifecycleHandler{
Exec: &core.ExecAction{Command: []string{}},
},
},
},
},
},
field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "ephemeralContainers[0].lifecycle"}},
}, },
} }
for _, tc := range tcs { for _, tc := range tcs {
t.Run(tc.title, func(t *testing.T) { t.Run(tc.title+"__@L"+tc.line, func(t *testing.T) {
errs := validateEphemeralContainers(tc.ephemeralContainers, containers, initContainers, vols, field.NewPath("ephemeralContainers"), PodValidationOptions{}) errs := validateEphemeralContainers(tc.ephemeralContainers, containers, initContainers, vols, field.NewPath("ephemeralContainers"), PodValidationOptions{})
if len(errs) == 0 { if len(errs) == 0 {
t.Fatal("expected error but received none") t.Fatal("expected error but received none")
} }
if len(errs) > 1 { if diff := cmp.Diff(tc.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")); diff != "" {
t.Errorf("expected 1 error but received %d: %q", len(errs), errs) t.Errorf("unexpected diff in errors (-want, +got):\n%s", diff)
} t.Errorf("INFO: all errors:\n%s", prettyErrorList(errs))
if errs[0].Type != tc.expectedError.Type {
t.Errorf("expected error type %q but received %q: %q", string(tc.expectedError.Type), string(errs[0].Type), errs)
}
if errs[0].Field != tc.expectedError.Field {
t.Errorf("expected error for field %q but received error for field %q: %q", tc.expectedError.Field, errs[0].Field, errs)
} }
}) })
} }
@ -7226,70 +7297,80 @@ func TestValidateContainers(t *testing.T) {
AllowPrivileged: false, AllowPrivileged: false,
}) })
errorCases := []struct { errorCases := []struct {
title string title, line string
containers []core.Container containers []core.Container
expectedErrors []field.Error expectedErrors field.ErrorList
}{ }{
{ {
"zero-length name", "zero-length name",
line(),
[]core.Container{{Name: "", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, []core.Container{{Name: "", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
[]field.Error{{Type: field.ErrorTypeRequired, Field: "containers[0].name"}}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].name"}},
}, },
{ {
"zero-length-image", "zero-length-image",
line(),
[]core.Container{{Name: "abc", Image: "", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, []core.Container{{Name: "abc", Image: "", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
[]field.Error{{Type: field.ErrorTypeRequired, Field: "containers[0].image"}}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].image"}},
}, },
{ {
"name > 63 characters", "name > 63 characters",
line(),
[]core.Container{{Name: strings.Repeat("a", 64), Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, []core.Container{{Name: strings.Repeat("a", 64), Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].name"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].name"}},
}, },
{ {
"name not a DNS label", "name not a DNS label",
line(),
[]core.Container{{Name: "a.b.c", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, []core.Container{{Name: "a.b.c", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].name"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].name"}},
}, },
{ {
"name not unique", "name not unique",
line(),
[]core.Container{ []core.Container{
{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, {Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, {Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
}, },
[]field.Error{{Type: field.ErrorTypeDuplicate, Field: "containers[1].name"}}, field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "containers[1].name"}},
}, },
{ {
"zero-length image", "zero-length image",
line(),
[]core.Container{{Name: "abc", Image: "", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}, []core.Container{{Name: "abc", Image: "", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}},
[]field.Error{{Type: field.ErrorTypeRequired, Field: "containers[0].image"}}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].image"}},
}, },
{ {
"host port not unique", "host port not unique",
line(),
[]core.Container{ []core.Container{
{Name: "abc", Image: "image", Ports: []core.ContainerPort{{ContainerPort: 80, HostPort: 80, Protocol: "TCP"}}, {Name: "abc", Image: "image", Ports: []core.ContainerPort{{ContainerPort: 80, HostPort: 80, Protocol: "TCP"}},
ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
{Name: "def", Image: "image", Ports: []core.ContainerPort{{ContainerPort: 81, HostPort: 80, Protocol: "TCP"}}, {Name: "def", Image: "image", Ports: []core.ContainerPort{{ContainerPort: 81, HostPort: 80, Protocol: "TCP"}},
ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
}, },
[]field.Error{{Type: field.ErrorTypeDuplicate, Field: "containers[1].ports[0].hostPort"}}, field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "containers[1].ports[0].hostPort"}},
}, },
{ {
"invalid env var name", "invalid env var name",
line(),
[]core.Container{ []core.Container{
{Name: "abc", Image: "image", Env: []core.EnvVar{{Name: "ev!1"}}, ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, {Name: "abc", Image: "image", Env: []core.EnvVar{{Name: "ev!1"}}, ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].env[0].name"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].env[0].name"}},
}, },
{ {
"unknown volume name", "unknown volume name",
line(),
[]core.Container{ []core.Container{
{Name: "abc", Image: "image", VolumeMounts: []core.VolumeMount{{Name: "anything", MountPath: "/foo"}}, {Name: "abc", Image: "image", VolumeMounts: []core.VolumeMount{{Name: "anything", MountPath: "/foo"}},
ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"},
}, },
[]field.Error{{Type: field.ErrorTypeNotFound, Field: "containers[0].volumeMounts[0].name"}}, field.ErrorList{{Type: field.ErrorTypeNotFound, Field: "containers[0].volumeMounts[0].name"}},
}, },
{ {
"invalid lifecycle, no exec command.", "invalid lifecycle, no exec command.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7303,10 +7384,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop.exec.command"}}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop.exec.command"}},
}, },
{ {
"invalid lifecycle, no http path.", "invalid lifecycle, no http path.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7323,10 +7405,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop.httpGet.path"}}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop.httpGet.path"}},
}, },
{ {
"invalid lifecycle, no http port.", "invalid lifecycle, no http port.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7343,10 +7426,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.httpGet.port"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.httpGet.port"}},
}, },
{ {
"invalid lifecycle, no http scheme.", "invalid lifecycle, no http scheme.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7363,10 +7447,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeNotSupported, Field: "containers[0].lifecycle.preStop.httpGet.scheme"}}, field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "containers[0].lifecycle.preStop.httpGet.scheme"}},
}, },
{ {
"invalid lifecycle, no tcp socket port.", "invalid lifecycle, no tcp socket port.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7380,10 +7465,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.tcpSocket.port"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.tcpSocket.port"}},
}, },
{ {
"invalid lifecycle, zero tcp socket port.", "invalid lifecycle, zero tcp socket port.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7399,10 +7485,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.tcpSocket.port"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].lifecycle.preStop.tcpSocket.port"}},
}, },
{ {
"invalid lifecycle, no action.", "invalid lifecycle, no action.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7414,10 +7501,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop"}}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].lifecycle.preStop"}},
}, },
{ {
"invalid readiness probe, terminationGracePeriodSeconds set.", "invalid readiness probe, terminationGracePeriodSeconds set.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7434,10 +7522,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.terminationGracePeriodSeconds"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.terminationGracePeriodSeconds"}},
}, },
{ {
"invalid liveness probe, no tcp socket port.", "invalid liveness probe, no tcp socket port.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "live-123", Name: "live-123",
@ -7452,10 +7541,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.tcpSocket.port"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.tcpSocket.port"}},
}, },
{ {
"invalid liveness probe, no action.", "invalid liveness probe, no action.",
line(),
[]core.Container{ []core.Container{
{ {
Name: "live-123", Name: "live-123",
@ -7468,10 +7558,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeRequired, Field: "containers[0].livenessProbe"}}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].livenessProbe"}},
}, },
{ {
"invalid liveness probe, successThreshold != 1", "invalid liveness probe, successThreshold != 1",
line(),
[]core.Container{ []core.Container{
{ {
Name: "live-123", Name: "live-123",
@ -7488,10 +7579,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.successThreshold"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.successThreshold"}},
}, },
{ {
"invalid startup probe, successThreshold != 1", "invalid startup probe, successThreshold != 1",
line(),
[]core.Container{ []core.Container{
{ {
Name: "startup-123", Name: "startup-123",
@ -7508,10 +7600,116 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.successThreshold"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.successThreshold"}},
},
{
"invalid liveness probe, negative numbers",
line(),
[]core.Container{
{
Name: "live-123",
Image: "image",
LivenessProbe: &core.Probe{
ProbeHandler: core.ProbeHandler{
TCPSocket: &core.TCPSocketAction{
Port: intstr.FromInt(80),
},
},
InitialDelaySeconds: -1,
TimeoutSeconds: -1,
PeriodSeconds: -1,
SuccessThreshold: -1,
FailureThreshold: -1,
TerminationGracePeriodSeconds: utilpointer.Int64Ptr(-1),
},
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
},
field.ErrorList{
{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.initialDelaySeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.timeoutSeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.periodSeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.successThreshold"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.failureThreshold"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.terminationGracePeriodSeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].livenessProbe.successThreshold"},
},
},
{
"invalid readiness probe, negative numbers",
line(),
[]core.Container{
{
Name: "ready-123",
Image: "image",
ReadinessProbe: &core.Probe{
ProbeHandler: core.ProbeHandler{
TCPSocket: &core.TCPSocketAction{
Port: intstr.FromInt(80),
},
},
InitialDelaySeconds: -1,
TimeoutSeconds: -1,
PeriodSeconds: -1,
SuccessThreshold: -1,
FailureThreshold: -1,
TerminationGracePeriodSeconds: utilpointer.Int64Ptr(-1),
},
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
},
field.ErrorList{
{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.initialDelaySeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.timeoutSeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.periodSeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.successThreshold"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.failureThreshold"},
// terminationGracePeriodSeconds returns multiple validation errors here:
// containers[0].readinessProbe.terminationGracePeriodSeconds: Invalid value: -1: must be greater than 0
{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.terminationGracePeriodSeconds"},
// containers[0].readinessProbe.terminationGracePeriodSeconds: Invalid value: -1: must not be set for readinessProbes
{Type: field.ErrorTypeInvalid, Field: "containers[0].readinessProbe.terminationGracePeriodSeconds"},
},
},
{
"invalid startup probe, negative numbers",
line(),
[]core.Container{
{
Name: "startup-123",
Image: "image",
StartupProbe: &core.Probe{
ProbeHandler: core.ProbeHandler{
TCPSocket: &core.TCPSocketAction{
Port: intstr.FromInt(80),
},
},
InitialDelaySeconds: -1,
TimeoutSeconds: -1,
PeriodSeconds: -1,
SuccessThreshold: -1,
FailureThreshold: -1,
TerminationGracePeriodSeconds: utilpointer.Int64Ptr(-1),
},
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
},
field.ErrorList{
{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.initialDelaySeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.timeoutSeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.periodSeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.successThreshold"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.failureThreshold"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.terminationGracePeriodSeconds"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].startupProbe.successThreshold"},
},
}, },
{ {
"invalid message termination policy", "invalid message termination policy",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7520,10 +7718,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "Unknown", TerminationMessagePolicy: "Unknown",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].terminationMessagePolicy"}}, field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "containers[0].terminationMessagePolicy"}},
}, },
{ {
"empty message termination policy", "empty message termination policy",
line(),
[]core.Container{ []core.Container{
{ {
Name: "life-123", Name: "life-123",
@ -7532,10 +7731,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "", TerminationMessagePolicy: "",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeRequired, Field: "containers[0].terminationMessagePolicy"}}, field.ErrorList{{Type: field.ErrorTypeRequired, Field: "containers[0].terminationMessagePolicy"}},
}, },
{ {
"privilege disabled", "privilege disabled",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc", Name: "abc",
@ -7545,10 +7745,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeForbidden, Field: "containers[0].securityContext.privileged"}}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "containers[0].securityContext.privileged"}},
}, },
{ {
"invalid compute resource", "invalid compute resource",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc-123", Name: "abc-123",
@ -7562,13 +7763,14 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{ field.ErrorList{
{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[disk]"}, {Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[disk]"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[disk]"}, {Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[disk]"},
}, },
}, },
{ {
"Resource CPU invalid", "Resource CPU invalid",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc-123", Name: "abc-123",
@ -7580,10 +7782,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[cpu]"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[cpu]"}},
}, },
{ {
"Resource Requests CPU invalid", "Resource Requests CPU invalid",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc-123", Name: "abc-123",
@ -7595,10 +7798,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests[cpu]"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests[cpu]"}},
}, },
{ {
"Resource Memory invalid", "Resource Memory invalid",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc-123", Name: "abc-123",
@ -7610,10 +7814,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[memory]"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[memory]"}},
}, },
{ {
"Request limit simple invalid", "Request limit simple invalid",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc-123", Name: "abc-123",
@ -7626,10 +7831,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}},
}, },
{ {
"Invalid storage limit request", "Invalid storage limit request",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc-123", Name: "abc-123",
@ -7643,13 +7849,14 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{ field.ErrorList{
{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[attachable-volumes-aws-ebs]"}, {Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[attachable-volumes-aws-ebs]"},
{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[attachable-volumes-aws-ebs]"}, {Type: field.ErrorTypeInvalid, Field: "containers[0].resources.limits[attachable-volumes-aws-ebs]"},
}, },
}, },
{ {
"CPU request limit multiple invalid", "CPU request limit multiple invalid",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc-123", Name: "abc-123",
@ -7662,10 +7869,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}},
}, },
{ {
"Memory request limit multiple invalid", "Memory request limit multiple invalid",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc-123", Name: "abc-123",
@ -7678,10 +7886,11 @@ func TestValidateContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].resources.requests"}},
}, },
{ {
"Invalid env from", "Invalid env from",
line(),
[]core.Container{ []core.Container{
{ {
Name: "env-from-source", Name: "env-from-source",
@ -7699,27 +7908,19 @@ func TestValidateContainers(t *testing.T) {
}, },
}, },
}, },
[]field.Error{{Type: field.ErrorTypeInvalid, Field: "containers[0].envFrom[0].configMapRef.name"}}, field.ErrorList{{Type: field.ErrorTypeInvalid, Field: "containers[0].envFrom[0].configMapRef.name"}},
}, },
} }
for _, tc := range errorCases { for _, tc := range errorCases {
t.Run(tc.title, func(t *testing.T) { t.Run(tc.title+"__@L"+tc.line, func(t *testing.T) {
errs := validateContainers(tc.containers, volumeDevices, field.NewPath("containers"), PodValidationOptions{}) errs := validateContainers(tc.containers, volumeDevices, field.NewPath("containers"), PodValidationOptions{})
if len(errs) == 0 { if len(errs) == 0 {
t.Fatalf("expected error but received none") t.Fatal("expected error but received none")
} }
if len(errs) != len(tc.expectedErrors) { if diff := cmp.Diff(tc.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail")); diff != "" {
t.Fatalf("unexpected number of errors (got %d, want %d) in validation result %q", len(errs), len(tc.expectedErrors), errs) t.Errorf("unexpected diff in errors (-want, +got):\n%s", diff)
} t.Errorf("INFO: all errors:\n%s", prettyErrorList(errs))
for i, err := range errs {
if err.Type != tc.expectedErrors[i].Type {
t.Errorf("returned error %d of %d: got error type %q but wanted %q: %q", i+1, len(errs), string(err.Type), string(tc.expectedErrors[i].Type), err)
}
if errs[i].Field != tc.expectedErrors[i].Field {
t.Errorf("returned error %d of %d: got error for field %q but wanted error for field %q: %q", i+1, len(errs), err.Field, tc.expectedErrors[i].Field, err)
}
} }
}) })
} }
@ -7770,12 +7971,26 @@ func TestValidateInitContainers(t *testing.T) {
AllowPrivileged: false, AllowPrivileged: false,
}) })
errorCases := []struct { errorCases := []struct {
title string title, line string
initContainers []core.Container initContainers []core.Container
expectedError field.Error expectedErrors field.ErrorList
}{ }{
{
"empty name",
line(),
[]core.Container{
{
Name: "",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
},
},
field.ErrorList{{Type: field.ErrorTypeRequired, Field: "initContainers[0].name", BadValue: ""}},
},
{ {
"name collision with regular container", "name collision with regular container",
line(),
[]core.Container{ []core.Container{
{ {
Name: "app", Name: "app",
@ -7784,22 +7999,24 @@ func TestValidateInitContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
field.Error{Type: field.ErrorTypeDuplicate, Field: "initContainers[0].name"}, field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "initContainers[0].name", BadValue: "app"}},
}, },
{ {
"invalid termination message policy", "invalid termination message policy",
line(),
[]core.Container{ []core.Container{
{ {
Name: "init", Name: "init",
Image: "image", Image: "image",
ImagePullPolicy: "IfNotPresent", ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "INVALID_POLICY_NAME", TerminationMessagePolicy: "Unknown",
}, },
}, },
field.Error{Type: field.ErrorTypeInvalid, Field: "initContainers[0].terminationMessagePolicy"}, field.ErrorList{{Type: field.ErrorTypeNotSupported, Field: "initContainers[0].terminationMessagePolicy", BadValue: core.TerminationMessagePolicy("Unknown")}},
}, },
{ {
"duplicate names", "duplicate names",
line(),
[]core.Container{ []core.Container{
{ {
Name: "init", Name: "init",
@ -7814,10 +8031,11 @@ func TestValidateInitContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
field.Error{Type: field.ErrorTypeDuplicate, Field: "initContainers[1].name"}, field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "initContainers[1].name", BadValue: "init"}},
}, },
{ {
"duplicate ports", "duplicate ports",
line(),
[]core.Container{ []core.Container{
{ {
Name: "abc", Name: "abc",
@ -7834,10 +8052,11 @@ func TestValidateInitContainers(t *testing.T) {
TerminationMessagePolicy: "File", TerminationMessagePolicy: "File",
}, },
}, },
field.Error{Type: field.ErrorTypeDuplicate, Field: "initContainers[0].ports[1].hostPort"}, field.ErrorList{{Type: field.ErrorTypeDuplicate, Field: "initContainers[0].ports[1].hostPort", BadValue: "TCP//8080"}},
}, },
{ {
"uses disallowed field: Lifecycle", "uses disallowed field: Lifecycle",
line(),
[]core.Container{ []core.Container{
{ {
Name: "debug", Name: "debug",
@ -7851,10 +8070,11 @@ func TestValidateInitContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeInvalid, Field: "initContainers[0].lifecycle"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].lifecycle", BadValue: ""}},
}, },
{ {
"uses disallowed field: LivenessProbe", "uses disallowed field: LivenessProbe",
line(),
[]core.Container{ []core.Container{
{ {
Name: "debug", Name: "debug",
@ -7869,10 +8089,11 @@ func TestValidateInitContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeInvalid, Field: "initContainers[0].livenessProbe"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].livenessProbe", BadValue: ""}},
}, },
{ {
"uses disallowed field: ReadinessProbe", "uses disallowed field: ReadinessProbe",
line(),
[]core.Container{ []core.Container{
{ {
Name: "debug", Name: "debug",
@ -7886,10 +8107,11 @@ func TestValidateInitContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeInvalid, Field: "initContainers[0].readinessProbe"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].readinessProbe", BadValue: ""}},
}, },
{ {
"Container uses disallowed field: StartupProbe", "Container uses disallowed field: StartupProbe",
line(),
[]core.Container{ []core.Container{
{ {
Name: "debug", Name: "debug",
@ -7904,24 +8126,43 @@ func TestValidateInitContainers(t *testing.T) {
}, },
}, },
}, },
field.Error{Type: field.ErrorTypeInvalid, Field: "initContainers[0].startupProbe"}, field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].startupProbe", BadValue: ""}},
},
{
"Disallowed field with other errors should only return a single Forbidden",
line(),
[]core.Container{
{
Name: "debug",
Image: "image",
ImagePullPolicy: "IfNotPresent",
TerminationMessagePolicy: "File",
StartupProbe: &core.Probe{
ProbeHandler: core.ProbeHandler{
TCPSocket: &core.TCPSocketAction{Port: intstr.FromInt(80)},
},
InitialDelaySeconds: -1,
TimeoutSeconds: -1,
PeriodSeconds: -1,
SuccessThreshold: -1,
FailureThreshold: -1,
TerminationGracePeriodSeconds: utilpointer.Int64Ptr(-1),
},
},
},
field.ErrorList{{Type: field.ErrorTypeForbidden, Field: "initContainers[0].startupProbe", BadValue: ""}},
}, },
} }
for _, tc := range errorCases { for _, tc := range errorCases {
t.Run(tc.title, func(t *testing.T) { t.Run(tc.title+"__@L"+tc.line, func(t *testing.T) {
errs := validateInitContainers(tc.initContainers, containers, volumeDevices, field.NewPath("initContainers"), PodValidationOptions{}) errs := validateInitContainers(tc.initContainers, containers, volumeDevices, field.NewPath("initContainers"), PodValidationOptions{})
if len(errs) == 0 { if len(errs) == 0 {
t.Fatal("expected error but received none") t.Fatal("expected error but received none")
} }
if len(errs) > 1 { if diff := cmp.Diff(tc.expectedErrors, errs, cmpopts.IgnoreFields(field.Error{}, "Detail")); diff != "" {
t.Errorf("expected 1 error but received %d: %q", len(errs), errs) t.Errorf("unexpected diff in errors (-want, +got):\n%s", diff)
} t.Errorf("INFO: all errors:\n%s", prettyErrorList(errs))
if errs[0].Type != tc.expectedError.Type {
t.Errorf("expected error type %q but received %q: %q", string(tc.expectedError.Type), string(errs[0].Type), errs)
}
if errs[0].Field != tc.expectedError.Field {
t.Errorf("expected error for field %q but received error for field %q: %q", tc.expectedError.Field, errs[0].Field, errs)
} }
}) })
} }