Merge pull request #30722 from timstclair/aa-validation

Automatic merge from submit-queue

Validate AppArmor annotations in the API server

This is important because it applies the same validation to the annotations that we would eventually want for the AppArmor fields, which will smooth the upgrade path.

/cc @pmorie @pweil- @jfrazelle @vishh
pull/6/head
Kubernetes Submit Queue 2016-08-21 22:55:04 -07:00 committed by GitHub
commit 52b3ef6f9c
7 changed files with 229 additions and 213 deletions

View File

@ -36,6 +36,7 @@ import (
"k8s.io/kubernetes/pkg/api/v1"
"k8s.io/kubernetes/pkg/capabilities"
"k8s.io/kubernetes/pkg/labels"
"k8s.io/kubernetes/pkg/security/apparmor"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/kubernetes/pkg/util/sets"
"k8s.io/kubernetes/pkg/util/validation"
@ -110,7 +111,7 @@ func ValidateDNS1123Subdomain(value string, fldPath *field.Path) field.ErrorList
return allErrs
}
func ValidatePodSpecificAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList {
func ValidatePodSpecificAnnotations(annotations map[string]string, spec *api.PodSpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
if annotations[api.AffinityAnnotationKey] != "" {
allErrs = append(allErrs, ValidateAffinityInPodAnnotations(annotations, fldPath)...)
@ -129,10 +130,36 @@ func ValidatePodSpecificAnnotations(annotations map[string]string, fldPath *fiel
}
allErrs = append(allErrs, ValidateSeccompPodAnnotations(annotations, fldPath)...)
allErrs = append(allErrs, ValidateAppArmorPodAnnotations(annotations, spec, fldPath)...)
return allErrs
}
func ValidatePodSpecificAnnotationUpdates(newPod, oldPod *api.Pod, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
newAnnotations := newPod.Annotations
oldAnnotations := oldPod.Annotations
for k, oldVal := range oldAnnotations {
if newAnnotations[k] == oldVal {
continue // No change.
}
if strings.HasPrefix(k, apparmor.ContainerAnnotationKeyPrefix) {
allErrs = append(allErrs, field.Forbidden(fldPath.Key(k), "may not update AppArmor annotations"))
}
}
// Check for removals.
for k := range newAnnotations {
if _, ok := oldAnnotations[k]; ok {
continue // No change.
}
if strings.HasPrefix(k, apparmor.ContainerAnnotationKeyPrefix) {
allErrs = append(allErrs, field.Forbidden(fldPath.Key(k), "may not remove AppArmor annotations"))
}
}
allErrs = append(allErrs, ValidatePodSpecificAnnotations(newAnnotations, &newPod.Spec, fldPath)...)
return allErrs
}
func ValidateEndpointsSpecificAnnotations(annotations map[string]string, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
// TODO: remove this after we EOL the annotation.
@ -1717,7 +1744,7 @@ func validateTolerations(tolerations []api.Toleration, fldPath *field.Path) fiel
func ValidatePod(pod *api.Pod) field.ErrorList {
fldPath := field.NewPath("metadata")
allErrs := ValidateObjectMeta(&pod.ObjectMeta, true, ValidatePodName, fldPath)
allErrs = append(allErrs, ValidatePodSpecificAnnotations(pod.ObjectMeta.Annotations, fldPath.Child("annotations"))...)
allErrs = append(allErrs, ValidatePodSpecificAnnotations(pod.ObjectMeta.Annotations, &pod.Spec, fldPath.Child("annotations"))...)
allErrs = append(allErrs, ValidatePodSpec(&pod.Spec, field.NewPath("spec"))...)
return allErrs
}
@ -2033,6 +2060,39 @@ func ValidateSeccompPodAnnotations(annotations map[string]string, fldPath *field
return allErrs
}
func ValidateAppArmorPodAnnotations(annotations map[string]string, spec *api.PodSpec, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
for k, p := range annotations {
if !strings.HasPrefix(k, apparmor.ContainerAnnotationKeyPrefix) {
continue
}
containerName := strings.TrimPrefix(k, apparmor.ContainerAnnotationKeyPrefix)
if !podSpecHasContainer(spec, containerName) {
allErrs = append(allErrs, field.Invalid(fldPath.Child(k), containerName, "container not found"))
}
if err := apparmor.ValidateProfileFormat(p); err != nil {
allErrs = append(allErrs, field.Invalid(fldPath.Child(k), p, err.Error()))
}
}
return allErrs
}
func podSpecHasContainer(spec *api.PodSpec, containerName string) bool {
for _, c := range spec.InitContainers {
if c.Name == containerName {
return true
}
}
for _, c := range spec.Containers {
if c.Name == containerName {
return true
}
}
return false
}
// ValidatePodSecurityContext test that the specified PodSecurityContext has valid data.
func ValidatePodSecurityContext(securityContext *api.PodSecurityContext, spec *api.PodSpec, specPath, fldPath *field.Path) field.ErrorList {
allErrs := field.ErrorList{}
@ -2081,7 +2141,7 @@ func ValidateContainerUpdates(newContainers, oldContainers []api.Container, fldP
func ValidatePodUpdate(newPod, oldPod *api.Pod) field.ErrorList {
fldPath := field.NewPath("metadata")
allErrs := ValidateObjectMetaUpdate(&newPod.ObjectMeta, &oldPod.ObjectMeta, fldPath)
allErrs = append(allErrs, ValidatePodSpecificAnnotations(newPod.ObjectMeta.Annotations, fldPath.Child("annotations"))...)
allErrs = append(allErrs, ValidatePodSpecificAnnotationUpdates(newPod, oldPod, fldPath.Child("annotations"))...)
specPath := field.NewPath("spec")
// validate updateable fields:
@ -2473,7 +2533,7 @@ func ValidatePodTemplateSpec(spec *api.PodTemplateSpec, fldPath *field.Path) fie
allErrs := field.ErrorList{}
allErrs = append(allErrs, unversionedvalidation.ValidateLabels(spec.Labels, fldPath.Child("labels"))...)
allErrs = append(allErrs, ValidateAnnotations(spec.Annotations, fldPath.Child("annotations"))...)
allErrs = append(allErrs, ValidatePodSpecificAnnotations(spec.Annotations, fldPath.Child("annotations"))...)
allErrs = append(allErrs, ValidatePodSpecificAnnotations(spec.Annotations, &spec.Spec, fldPath.Child("annotations"))...)
allErrs = append(allErrs, ValidatePodSpec(&spec.Spec, fldPath.Child("spec"))...)
return allErrs
}

View File

@ -29,6 +29,7 @@ import (
"k8s.io/kubernetes/pkg/api/testapi"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/capabilities"
"k8s.io/kubernetes/pkg/security/apparmor"
"k8s.io/kubernetes/pkg/util/intstr"
"k8s.io/kubernetes/pkg/util/sets"
"k8s.io/kubernetes/pkg/util/validation/field"
@ -3186,6 +3187,11 @@ func TestValidatePodSpec(t *testing.T) {
}
func TestValidatePod(t *testing.T) {
validPodSpec := api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
}
successCases := []api.Pod{
{ // Basic fields.
ObjectMeta: api.ObjectMeta{Name: "123", Namespace: "ns"},
@ -3305,11 +3311,7 @@ func TestValidatePod(t *testing.T) {
}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // Serialized pod anti affinity with different Label Operators in affinity requirements in annotations.
ObjectMeta: api.ObjectMeta{
@ -3357,11 +3359,7 @@ func TestValidatePod(t *testing.T) {
}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // populate tolerations equal operator in annotations.
ObjectMeta: api.ObjectMeta{
@ -3377,11 +3375,7 @@ func TestValidatePod(t *testing.T) {
}]`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // populate tolerations exists operator in annotations.
ObjectMeta: api.ObjectMeta{
@ -3396,11 +3390,7 @@ func TestValidatePod(t *testing.T) {
}]`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // empty operator is ok for toleration
ObjectMeta: api.ObjectMeta{
@ -3415,11 +3405,7 @@ func TestValidatePod(t *testing.T) {
}]`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // empty efffect is ok for toleration
ObjectMeta: api.ObjectMeta{
@ -3434,11 +3420,7 @@ func TestValidatePod(t *testing.T) {
}]`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // docker default seccomp profile
ObjectMeta: api.ObjectMeta{
@ -3448,11 +3430,7 @@ func TestValidatePod(t *testing.T) {
api.SeccompPodAnnotationKey: "docker/default",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // unconfined seccomp profile
ObjectMeta: api.ObjectMeta{
@ -3462,11 +3440,7 @@ func TestValidatePod(t *testing.T) {
api.SeccompPodAnnotationKey: "unconfined",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // localhost seccomp profile
ObjectMeta: api.ObjectMeta{
@ -3476,11 +3450,7 @@ func TestValidatePod(t *testing.T) {
api.SeccompPodAnnotationKey: "localhost/foo",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
{ // localhost seccomp profile for a container
ObjectMeta: api.ObjectMeta{
@ -3490,11 +3460,42 @@ func TestValidatePod(t *testing.T) {
api.SeccompContainerAnnotationKeyPrefix + "foo": "localhost/foo",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Spec: validPodSpec,
},
{ // default AppArmor profile for a container
ObjectMeta: api.ObjectMeta{
Name: "123",
Namespace: "ns",
Annotations: map[string]string{
apparmor.ContainerAnnotationKeyPrefix + "ctr": apparmor.ProfileRuntimeDefault,
},
},
Spec: validPodSpec,
},
{ // default AppArmor profile for an init container
ObjectMeta: api.ObjectMeta{
Name: "123",
Namespace: "ns",
Annotations: map[string]string{
apparmor.ContainerAnnotationKeyPrefix + "init-ctr": apparmor.ProfileRuntimeDefault,
},
},
Spec: api.PodSpec{
InitContainers: []api.Container{{Name: "init-ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
},
{ // localhost AppArmor profile for a container
ObjectMeta: api.ObjectMeta{
Name: "123",
Namespace: "ns",
Annotations: map[string]string{
apparmor.ContainerAnnotationKeyPrefix + "ctr": apparmor.ProfileNamePrefix + "foo",
},
},
Spec: validPodSpec,
},
}
for _, pod := range successCases {
@ -3552,11 +3553,7 @@ func TestValidatePod(t *testing.T) {
`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid node selector requirement in node affinity in pod annotations, operator can't be null": {
ObjectMeta: api.ObjectMeta{
@ -3573,11 +3570,7 @@ func TestValidatePod(t *testing.T) {
}}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid preferredSchedulingTerm in node affinity in pod annotations, weight should be in range 1-100": {
ObjectMeta: api.ObjectMeta{
@ -3599,11 +3592,7 @@ func TestValidatePod(t *testing.T) {
]}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid requiredDuringSchedulingIgnoredDuringExecution node selector, nodeSelectorTerms must have at least one term": {
ObjectMeta: api.ObjectMeta{
@ -3618,11 +3607,7 @@ func TestValidatePod(t *testing.T) {
}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid requiredDuringSchedulingIgnoredDuringExecution node selector term, matchExpressions must have at least one node selector requirement": {
ObjectMeta: api.ObjectMeta{
@ -3639,11 +3624,7 @@ func TestValidatePod(t *testing.T) {
}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid weight in preferredDuringSchedulingIgnoredDuringExecution in pod affinity annotations, weight should be in range 1-100": {
ObjectMeta: api.ObjectMeta{
@ -3668,11 +3649,7 @@ func TestValidatePod(t *testing.T) {
}]}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid labelSelector in preferredDuringSchedulingIgnoredDuringExecution in podaffinity annotations, values should be empty if the operator is Exists": {
ObjectMeta: api.ObjectMeta{
@ -3697,11 +3674,7 @@ func TestValidatePod(t *testing.T) {
}]}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid name space in preferredDuringSchedulingIgnoredDuringExecution in podaffinity annotations, name space shouldbe valid": {
ObjectMeta: api.ObjectMeta{
@ -3726,11 +3699,7 @@ func TestValidatePod(t *testing.T) {
}]}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid labelOperator in preferredDuringSchedulingIgnoredDuringExecution in podantiaffinity annotations, labelOperator should be proper": {
ObjectMeta: api.ObjectMeta{
@ -3755,11 +3724,7 @@ func TestValidatePod(t *testing.T) {
}]}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid pod affinity, empty topologyKey is not allowed for hard pod affinity": {
ObjectMeta: api.ObjectMeta{
@ -3784,11 +3749,7 @@ func TestValidatePod(t *testing.T) {
}]}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid pod anti-affinity, empty topologyKey is not allowed for hard pod anti-affinity": {
ObjectMeta: api.ObjectMeta{
@ -3813,11 +3774,7 @@ func TestValidatePod(t *testing.T) {
}]}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid pod anti-affinity, empty topologyKey is not allowed for soft pod affinity": {
ObjectMeta: api.ObjectMeta{
@ -3842,11 +3799,7 @@ func TestValidatePod(t *testing.T) {
}]}}`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid toleration key": {
ObjectMeta: api.ObjectMeta{
@ -3862,11 +3815,7 @@ func TestValidatePod(t *testing.T) {
}]`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"invalid toleration operator": {
ObjectMeta: api.ObjectMeta{
@ -3882,11 +3831,7 @@ func TestValidatePod(t *testing.T) {
}]`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"value must be empty when `operator` is 'Exists'": {
ObjectMeta: api.ObjectMeta{
@ -3902,11 +3847,7 @@ func TestValidatePod(t *testing.T) {
}]`,
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"must be a valid pod seccomp profile": {
ObjectMeta: api.ObjectMeta{
@ -3916,11 +3857,7 @@ func TestValidatePod(t *testing.T) {
api.SeccompPodAnnotationKey: "foo",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"must be a valid container seccomp profile": {
ObjectMeta: api.ObjectMeta{
@ -3930,11 +3867,7 @@ func TestValidatePod(t *testing.T) {
api.SeccompContainerAnnotationKeyPrefix + "foo": "foo",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"must be a non-empty container name in seccomp annotation": {
ObjectMeta: api.ObjectMeta{
@ -3944,11 +3877,7 @@ func TestValidatePod(t *testing.T) {
api.SeccompContainerAnnotationKeyPrefix: "foo",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"must be a non-empty container profile in seccomp annotation": {
ObjectMeta: api.ObjectMeta{
@ -3958,11 +3887,7 @@ func TestValidatePod(t *testing.T) {
api.SeccompContainerAnnotationKeyPrefix + "foo": "",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"must be a relative path in a node-local seccomp profile annotation": {
ObjectMeta: api.ObjectMeta{
@ -3972,11 +3897,7 @@ func TestValidatePod(t *testing.T) {
api.SeccompPodAnnotationKey: "localhost//foo",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
Spec: validPodSpec,
},
"must not start with '../'": {
ObjectMeta: api.ObjectMeta{
@ -3986,11 +3907,44 @@ func TestValidatePod(t *testing.T) {
api.SeccompPodAnnotationKey: "localhost/../foo",
},
},
Spec: api.PodSpec{
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
Spec: validPodSpec,
},
"AppArmor profile must apply to a container": {
ObjectMeta: api.ObjectMeta{
Name: "123",
Namespace: "ns",
Annotations: map[string]string{
apparmor.ContainerAnnotationKeyPrefix + "ctr": apparmor.ProfileRuntimeDefault,
apparmor.ContainerAnnotationKeyPrefix + "init-ctr": apparmor.ProfileRuntimeDefault,
apparmor.ContainerAnnotationKeyPrefix + "fake-ctr": apparmor.ProfileRuntimeDefault,
},
},
Spec: api.PodSpec{
InitContainers: []api.Container{{Name: "init-ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}},
RestartPolicy: api.RestartPolicyAlways,
DNSPolicy: api.DNSClusterFirst,
},
},
"AppArmor profile format must be valid": {
ObjectMeta: api.ObjectMeta{
Name: "123",
Namespace: "ns",
Annotations: map[string]string{
apparmor.ContainerAnnotationKeyPrefix + "ctr": "bad-name",
},
},
Spec: validPodSpec,
},
"only default AppArmor profile may start with runtime/": {
ObjectMeta: api.ObjectMeta{
Name: "123",
Namespace: "ns",
Annotations: map[string]string{
apparmor.ContainerAnnotationKeyPrefix + "ctr": "runtime/foo",
},
},
Spec: validPodSpec,
},
}
for k, v := range errorCases {

View File

@ -57,7 +57,7 @@ func ValidatePodTemplateSpecForPetSet(template *api.PodTemplateSpec, selector la
// allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(template, fldPath)...)
allErrs = append(allErrs, unversionedvalidation.ValidateLabels(template.Labels, fldPath.Child("labels"))...)
allErrs = append(allErrs, apivalidation.ValidateAnnotations(template.Annotations, fldPath.Child("annotations"))...)
allErrs = append(allErrs, apivalidation.ValidatePodSpecificAnnotations(template.Annotations, fldPath.Child("annotations"))...)
allErrs = append(allErrs, apivalidation.ValidatePodSpecificAnnotations(template.Annotations, &template.Spec, fldPath.Child("annotations"))...)
}
return allErrs
}

View File

@ -73,7 +73,6 @@ import (
"k8s.io/kubernetes/pkg/kubelet/util/queue"
"k8s.io/kubernetes/pkg/kubelet/volumemanager"
"k8s.io/kubernetes/pkg/runtime"
"k8s.io/kubernetes/pkg/security/apparmor"
"k8s.io/kubernetes/pkg/securitycontext"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/bandwidth"
@ -601,7 +600,7 @@ func NewMainKubelet(
klet.AddPodSyncLoopHandler(activeDeadlineHandler)
klet.AddPodSyncHandler(activeDeadlineHandler)
klet.AddPodAdmitHandler(apparmor.NewValidator(containerRuntime))
klet.AddPodAdmitHandler(lifecycle.NewAppArmorAdmitHandler(containerRuntime))
// apply functional Option's
for _, opt := range kubeOptions {

View File

@ -30,6 +30,7 @@ import (
kubetypes "k8s.io/kubernetes/pkg/kubelet/types"
"k8s.io/kubernetes/pkg/kubelet/util/format"
"k8s.io/kubernetes/pkg/kubelet/util/ioutils"
"k8s.io/kubernetes/pkg/security/apparmor"
"k8s.io/kubernetes/pkg/types"
"k8s.io/kubernetes/pkg/util/intstr"
)
@ -142,3 +143,25 @@ func getHttpRespBody(resp *http.Response) string {
}
return ""
}
func NewAppArmorAdmitHandler(runtime string) PodAdmitHandler {
return &appArmorAdmitHandler{
Validator: apparmor.NewValidator(runtime),
}
}
type appArmorAdmitHandler struct {
apparmor.Validator
}
func (a *appArmorAdmitHandler) Admit(attrs *PodAdmitAttributes) PodAdmitResult {
err := a.Validate(attrs.Pod)
if err == nil {
return PodAdmitResult{Admit: true}
}
return PodAdmitResult{
Admit: false,
Reason: "AppArmor",
Message: fmt.Sprintf("Cannot enforce AppArmor: %v", err),
}
}

View File

@ -26,7 +26,6 @@ import (
"strings"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
"k8s.io/kubernetes/pkg/util"
)
@ -34,11 +33,12 @@ import (
// Set to true if the wrong build tags are set (see validate_disabled.go).
var isDisabledBuild bool
const (
rejectReason = "AppArmor"
)
// Interface for validating that a pod with with an AppArmor profile can be run by a Node.
type Validator interface {
Validate(pod *api.Pod) error
}
func NewValidator(runtime string) lifecycle.PodAdmitHandler {
func NewValidator(runtime string) Validator {
if err := validateHost(runtime); err != nil {
return &validator{validateHostErr: err}
}
@ -58,21 +58,7 @@ type validator struct {
appArmorFS string
}
// TODO(timstclair): Refactor the PodAdmitInterface to return a (Admit, Reason Message) rather than
// the PodAdmitResult struct so that the interface can be implemented without importing lifecycle.
func (v *validator) Admit(attrs *lifecycle.PodAdmitAttributes) lifecycle.PodAdmitResult {
err := v.validate(attrs.Pod)
if err == nil {
return lifecycle.PodAdmitResult{Admit: true}
}
return lifecycle.PodAdmitResult{
Admit: false,
Reason: rejectReason,
Message: fmt.Sprintf("Cannot enforce AppArmor: %v", err),
}
}
func (v *validator) validate(pod *api.Pod) error {
func (v *validator) Validate(pod *api.Pod) error {
if !isRequired(pod) {
return nil
}
@ -122,18 +108,27 @@ func validateHost(runtime string) error {
// Verify that the profile is valid and loaded.
func validateProfile(profile string, loadedProfiles map[string]bool) error {
if err := ValidateProfileFormat(profile); err != nil {
return err
}
if strings.HasPrefix(profile, ProfileNamePrefix) {
profileName := strings.TrimPrefix(profile, ProfileNamePrefix)
if !loadedProfiles[profileName] {
return fmt.Errorf("profile %q is not loaded", profileName)
}
}
return nil
}
func ValidateProfileFormat(profile string) error {
if profile == "" || profile == ProfileRuntimeDefault {
return nil
}
if !strings.HasPrefix(profile, ProfileNamePrefix) {
return fmt.Errorf("invalid AppArmor profile name: %q", profile)
}
profileName := strings.TrimPrefix(profile, ProfileNamePrefix)
if !loadedProfiles[profileName] {
return fmt.Errorf("profile %q is not loaded", profileName)
}
return nil
}

View File

@ -21,7 +21,6 @@ import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/kubelet/lifecycle"
"github.com/stretchr/testify/assert"
)
@ -37,7 +36,7 @@ func TestGetAppArmorFS(t *testing.T) {
assert.Equal(t, expectedPath, actualPath)
}
func TestAdmitHost(t *testing.T) {
func TestValidateHost(t *testing.T) {
// This test only passes on systems running AppArmor with the default configuration.
// The test should be manually run if modifying the getAppArmorFS function.
t.Skip()
@ -46,7 +45,7 @@ func TestAdmitHost(t *testing.T) {
assert.Error(t, validateHost("rkt"))
}
func TestAdmitProfile(t *testing.T) {
func TestValidateProfile(t *testing.T) {
loadedProfiles := map[string]bool{
"docker-default": true,
"foo-bar": true,
@ -79,7 +78,7 @@ func TestAdmitProfile(t *testing.T) {
}
}
func TestAdmitBadHost(t *testing.T) {
func TestValidateBadHost(t *testing.T) {
hostErr := errors.New("expected host error")
v := &validator{
validateHostErr: hostErr,
@ -95,20 +94,16 @@ func TestAdmitBadHost(t *testing.T) {
}
for _, test := range tests {
result := v.Admit(&lifecycle.PodAdmitAttributes{
Pod: getPodWithProfile(test.profile),
})
err := v.Validate(getPodWithProfile(test.profile))
if test.expectValid {
assert.True(t, result.Admit, "Pod with profile %q should be admitted", test.profile)
assert.NoError(t, err, "Pod with profile %q should be valid", test.profile)
} else {
assert.False(t, result.Admit, "Pod with profile %q should be rejected", test.profile)
assert.Equal(t, rejectReason, result.Reason, "Pod with profile %q", test.profile)
assert.Contains(t, result.Message, hostErr.Error(), "Pod with profile %q", test.profile)
assert.Equal(t, hostErr, err, "Pod with profile %q should trigger a host validation error", test.profile)
}
}
}
func TestAdmitValidHost(t *testing.T) {
func TestValidateValidHost(t *testing.T) {
v := &validator{
appArmorFS: "./testdata/",
}
@ -128,15 +123,11 @@ func TestAdmitValidHost(t *testing.T) {
}
for _, test := range tests {
result := v.Admit(&lifecycle.PodAdmitAttributes{
Pod: getPodWithProfile(test.profile),
})
err := v.Validate(getPodWithProfile(test.profile))
if test.expectValid {
assert.True(t, result.Admit, "Pod with profile %q should be admitted", test.profile)
assert.NoError(t, err, "Pod with profile %q should be valid", test.profile)
} else {
assert.False(t, result.Admit, "Pod with profile %q should be rejected", test.profile)
assert.Equal(t, rejectReason, result.Reason, "Pod with profile %q", test.profile)
assert.NotEmpty(t, result.Message, "Pod with profile %q", test.profile)
assert.Error(t, err, "Pod with profile %q should trigger a validation error", test.profile)
}
}
@ -160,16 +151,10 @@ func TestAdmitValidHost(t *testing.T) {
},
},
}
assert.True(t, v.Admit(&lifecycle.PodAdmitAttributes{Pod: pod}).Admit,
"Multi-container pod should be admitted")
assert.NoError(t, v.Validate(pod), "Multi-container pod should validate")
for k, val := range pod.Annotations {
pod.Annotations[k] = val + "-bad"
result := v.Admit(&lifecycle.PodAdmitAttributes{Pod: pod})
assert.False(t, result.Admit, "Multi-container pod with invalid profile should be rejected")
assert.Equal(t, rejectReason, result.Reason, "Multi-container pod with invalid profile")
assert.NotEmpty(t, result.Message, "Multi-container pod with invalid profile")
assert.Error(t, v.Validate(pod), "Multi-container pod with invalid profile %s:%s", k, pod.Annotations[k])
pod.Annotations[k] = val // Restore.
}
}