From 289ec02e8b5f3224376b3257adf85c48d1bcca08 Mon Sep 17 00:00:00 2001 From: Tim Allclair Date: Wed, 21 Feb 2024 00:12:12 -0800 Subject: [PATCH] Implement version skew strategy --- pkg/apis/core/validation/validation.go | 61 +++ pkg/apis/core/validation/validation_test.go | 68 ++++ pkg/registry/core/pod/strategy.go | 107 +++++ pkg/registry/core/pod/strategy_test.go | 429 ++++++++++++++++++++ 4 files changed, 665 insertions(+) diff --git a/pkg/apis/core/validation/validation.go b/pkg/apis/core/validation/validation.go index 43583928b48..d5479dfaeca 100644 --- a/pkg/apis/core/validation/validation.go +++ b/pkg/apis/core/validation/validation.go @@ -4729,6 +4729,66 @@ func ValidateAppArmorProfileFormat(profile string) error { return nil } +// validateAppArmorAnnotationsAndFields validates that AppArmor fields and annotations are consistent. +func validateAppArmorAnnotationsAndFields(objectMeta metav1.ObjectMeta, podSpec *core.PodSpec, specPath *field.Path) field.ErrorList { + if podSpec.OS != nil && podSpec.OS.Name == core.Windows { + // Skip consistency check for windows pods. + return nil + } + + allErrs := field.ErrorList{} + + podshelper.VisitContainersWithPath(podSpec, specPath, func(c *core.Container, cFldPath *field.Path) bool { + var field *core.AppArmorProfile + if c.SecurityContext != nil { + field = c.SecurityContext.AppArmorProfile + } + + if field == nil { + return true + } + + key := core.AppArmorContainerAnnotationKeyPrefix + c.Name + if annotation, found := objectMeta.Annotations[key]; found { + apparmorPath := cFldPath.Child("securityContext").Child("appArmorProfile") + err := validateAppArmorAnnotationsAndFieldsMatch(annotation, field, apparmorPath) + if err != nil { + allErrs = append(allErrs, err) + } + } + return true + }) + + return allErrs +} + +func validateAppArmorAnnotationsAndFieldsMatch(annotationValue string, apparmorField *core.AppArmorProfile, fldPath *field.Path) *field.Error { + if apparmorField == nil { + return nil + } + + switch apparmorField.Type { + case core.AppArmorProfileTypeUnconfined: + if annotationValue != core.AppArmorProfileNameUnconfined { + return field.Forbidden(fldPath.Child("type"), "apparmor type in annotation and field must match") + } + + case core.AppArmorProfileTypeRuntimeDefault: + if annotationValue != core.AppArmorProfileRuntimeDefault { + return field.Forbidden(fldPath.Child("type"), "apparmor type in annotation and field must match") + } + + case core.AppArmorProfileTypeLocalhost: + if !strings.HasPrefix(annotationValue, core.AppArmorProfileLocalhostPrefix) { + return field.Forbidden(fldPath.Child("type"), "apparmor type in annotation and field must match") + } else if apparmorField.LocalhostProfile == nil || strings.TrimPrefix(annotationValue, core.AppArmorProfileLocalhostPrefix) != *apparmorField.LocalhostProfile { + return field.Forbidden(fldPath.Child("localhostProfile"), "apparmor profile in annotation and field must match") + } + } + + return nil +} + func podSpecHasContainer(spec *core.PodSpec, containerName string) bool { var hasContainer bool podshelper.VisitContainersWithPath(spec, field.NewPath("spec"), func(c *core.Container, _ *field.Path) bool { @@ -4883,6 +4943,7 @@ func ValidatePodCreate(pod *core.Pod, opts PodValidationOptions) field.ErrorList allErrs = append(allErrs, field.Forbidden(fldPath.Child("nodeName"), "cannot be set until all schedulingGates have been cleared")) } allErrs = append(allErrs, validateSeccompAnnotationsAndFields(pod.ObjectMeta, &pod.Spec, fldPath)...) + allErrs = append(allErrs, validateAppArmorAnnotationsAndFields(pod.ObjectMeta, &pod.Spec, fldPath)...) return allErrs } diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index 4c6bd0366f7..d87015678d5 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -10425,6 +10425,27 @@ func TestValidatePod(t *testing.T) { DNSPolicy: core.DNSDefault, }, }, + "matching AppArmor fields and annotations": { + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + Annotations: map[string]string{ + core.AppArmorContainerAnnotationKeyPrefix + "ctr": core.AppArmorProfileLocalhostPrefix + "foo", + }, + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", + SecurityContext: &core.SecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeLocalhost, + LocalhostProfile: ptr.To("foo"), + }, + }, + }}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + }, + }, "syntactically valid sysctls": { ObjectMeta: metav1.ObjectMeta{ Name: "123", @@ -12118,6 +12139,53 @@ func TestValidatePod(t *testing.T) { }, }, }, + "mismatched AppArmor field and annotation types": { + expectedError: "Forbidden: apparmor type in annotation and field must match", + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + Annotations: map[string]string{ + core.AppArmorContainerAnnotationKeyPrefix + "ctr": core.AppArmorProfileRuntimeDefault, + }, + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", + SecurityContext: &core.SecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeUnconfined, + }, + }, + }}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + }, + }, + }, + "mismatched AppArmor localhost profiles": { + expectedError: "Forbidden: apparmor profile in annotation and field must match", + spec: core.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "123", + Namespace: "ns", + Annotations: map[string]string{ + core.AppArmorContainerAnnotationKeyPrefix + "ctr": core.AppArmorProfileLocalhostPrefix + "foo", + }, + }, + Spec: core.PodSpec{ + Containers: []core.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File", + SecurityContext: &core.SecurityContext{ + AppArmorProfile: &core.AppArmorProfile{ + Type: core.AppArmorProfileTypeLocalhost, + LocalhostProfile: ptr.To("bar"), + }, + }, + }}, + RestartPolicy: core.RestartPolicyAlways, + DNSPolicy: core.DNSDefault, + }, + }, + }, "invalid extended resource name in container request": { expectedError: "must be a standard resource for containers", spec: core.Pod{ diff --git a/pkg/registry/core/pod/strategy.go b/pkg/registry/core/pod/strategy.go index c72ad557f65..e2c6a204efc 100644 --- a/pkg/registry/core/pod/strategy.go +++ b/pkg/registry/core/pod/strategy.go @@ -91,6 +91,7 @@ func (podStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { applySchedulingGatedCondition(pod) mutatePodAffinity(pod) + applyAppArmorVersionSkew(pod) } // PrepareForUpdate clears fields that are not allowed to be set by end users on update. @@ -758,3 +759,109 @@ func applySchedulingGatedCondition(pod *api.Pod) { Message: "Scheduling is blocked due to non-empty scheduling gates", }) } + +// applyAppArmorVersionSkew implements the version skew behavior described in: +// https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/24-apparmor#version-skew-strategy +func applyAppArmorVersionSkew(pod *api.Pod) { + if pod.Spec.OS != nil && pod.Spec.OS.Name == api.Windows { + return + } + + var podProfile *api.AppArmorProfile + if pod.Spec.SecurityContext != nil { + podProfile = pod.Spec.SecurityContext.AppArmorProfile + } + + // Handle the containers of the pod + podutil.VisitContainers(&pod.Spec, podutil.AllFeatureEnabledContainers(), + func(ctr *api.Container, _ podutil.ContainerType) bool { + // get possible annotation and field + key := api.AppArmorContainerAnnotationKeyPrefix + ctr.Name + annotation, hasAnnotation := pod.Annotations[key] + + field, hasField := (*api.AppArmorProfile)(nil), false + if ctr.SecurityContext != nil && ctr.SecurityContext.AppArmorProfile != nil { + field = ctr.SecurityContext.AppArmorProfile + hasField = true + } + + // sync field and annotation + if !hasAnnotation { + newAnnotation := "" + if hasField { + newAnnotation = appArmorAnnotationForField(field) + } else if podProfile != nil { + newAnnotation = appArmorAnnotationForField(podProfile) + } + + if newAnnotation != "" { + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + pod.Annotations[key] = newAnnotation + } + } else if !hasField { + newField := apparmorFieldForAnnotation(annotation) + + if newField != nil { + if ctr.SecurityContext == nil { + ctr.SecurityContext = &api.SecurityContext{} + } + ctr.SecurityContext.AppArmorProfile = newField + } + } + + return true + }) +} + +// appArmorFieldForAnnotation takes a pod apparmor profile field and returns the +// converted annotation value +func appArmorAnnotationForField(field *api.AppArmorProfile) string { + // If only apparmor fields are specified, add the corresponding annotations. + // This ensures that the fields are enforced even if the node version + // trails the API version + switch field.Type { + case api.AppArmorProfileTypeUnconfined: + return api.AppArmorProfileNameUnconfined + + case api.AppArmorProfileTypeRuntimeDefault: + return api.AppArmorProfileRuntimeDefault + + case api.AppArmorProfileTypeLocalhost: + if field.LocalhostProfile != nil { + return api.AppArmorProfileLocalhostPrefix + *field.LocalhostProfile + } + } + + // we can only reach this code path if the LocalhostProfile is nil but the + // provided field type is AppArmorProfileTypeLocalhost or if an unrecognized + // type is specified + return "" +} + +// apparmorFieldForAnnotation takes a pod annotation and returns the converted +// apparmor profile field. +func apparmorFieldForAnnotation(annotation string) *api.AppArmorProfile { + if annotation == api.AppArmorProfileNameUnconfined { + return &api.AppArmorProfile{Type: api.AppArmorProfileTypeUnconfined} + } + + if annotation == api.AppArmorProfileRuntimeDefault { + return &api.AppArmorProfile{Type: api.AppArmorProfileTypeRuntimeDefault} + } + + if strings.HasPrefix(annotation, api.AppArmorProfileLocalhostPrefix) { + localhostProfile := strings.TrimPrefix(annotation, api.AppArmorProfileLocalhostPrefix) + if localhostProfile != "" { + return &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeLocalhost, + LocalhostProfile: &localhostProfile, + } + } + } + + // we can only reach this code path if the localhostProfile name has a zero + // length or if the annotation has an unrecognized value + return nil +} diff --git a/pkg/registry/core/pod/strategy_test.go b/pkg/registry/core/pod/strategy_test.go index 89b919cc08d..19c8f6a1739 100644 --- a/pkg/registry/core/pod/strategy_test.go +++ b/pkg/registry/core/pod/strategy_test.go @@ -26,6 +26,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" + "github.com/stretchr/testify/assert" apiv1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" @@ -2106,3 +2107,431 @@ func TestPodLifecycleSleepActionEnablement(t *testing.T) { }) } } + +func TestApplyAppArmorVersionSkew(t *testing.T) { + testProfile := "test" + + tests := []struct { + description string + pod *api.Pod + validation func(*testing.T, *api.Pod) + }{{ + description: "Security context nil", + pod: &api.Pod{ + Spec: api.PodSpec{ + InitContainers: []api.Container{{Name: "init"}}, + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Empty(t, pod.Annotations) + assert.Nil(t, pod.Spec.SecurityContext) + }, + }, { + description: "Security context not nil", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{}, + InitContainers: []api.Container{{Name: "init"}}, + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Empty(t, pod.Annotations) + assert.Nil(t, pod.Spec.SecurityContext.AppArmorProfile) + }, + }, { + description: "Field type unconfined and no annotation present", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeUnconfined, + }, + }, + InitContainers: []api.Container{{Name: "init"}}, + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "init": api.AppArmorProfileNameUnconfined, + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileNameUnconfined, + }, pod.Annotations) + }, + }, { + description: "Field type default and no annotation present", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeRuntimeDefault, + }, + }, + InitContainers: []api.Container{{Name: "init"}}, + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "init": api.AppArmorProfileRuntimeDefault, + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileRuntimeDefault, + }, pod.Annotations) + }, + }, { + description: "Field type localhost and no annotation present", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeLocalhost, + LocalhostProfile: &testProfile, + }, + }, + InitContainers: []api.Container{{Name: "init"}}, + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "init": api.AppArmorProfileLocalhostPrefix + testProfile, + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileLocalhostPrefix + testProfile, + }, pod.Annotations) + }, + }, { + description: "Field type localhost but profile is nil", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeLocalhost, + }, + }, + InitContainers: []api.Container{{Name: "init"}}, + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Len(t, pod.Annotations, 0) + }, + }, { + description: "Security context not nil (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{{ + Name: "ctr", + SecurityContext: &api.SecurityContext{}, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Len(t, pod.Annotations, 0) + }, + }, { + description: "Field type RuntimeDefault and no annotation present (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{{ + Name: "ctr", + SecurityContext: &api.SecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeRuntimeDefault, + }, + }, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileRuntimeDefault, + }, pod.Annotations) + assert.Nil(t, pod.Spec.SecurityContext) + assert.Equal(t, api.AppArmorProfileTypeRuntimeDefault, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.Type) + }, + }, { + description: "Field type localhost and no annotation present (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{{ + Name: "ctr", + SecurityContext: &api.SecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeLocalhost, + LocalhostProfile: &testProfile, + }, + }, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileLocalhostPrefix + testProfile, + }, pod.Annotations) + assert.Nil(t, pod.Spec.SecurityContext) + assert.Equal(t, api.AppArmorProfileTypeLocalhost, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.Type) + }, + }, { + description: "Container overrides pod profile", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeRuntimeDefault, + }, + }, + Containers: []api.Container{{ + Name: "ctr", + SecurityContext: &api.SecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeUnconfined, + }, + }, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileNameUnconfined, + }, pod.Annotations) + assert.Equal(t, api.AppArmorProfileTypeRuntimeDefault, pod.Spec.SecurityContext.AppArmorProfile.Type) + assert.Equal(t, api.AppArmorProfileTypeUnconfined, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.Type) + }, + }, { + description: "Multiple containers with fields (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + InitContainers: []api.Container{{ + Name: "init", + SecurityContext: &api.SecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeLocalhost, + LocalhostProfile: &testProfile, + }, + }, + }}, + Containers: []api.Container{{ + Name: "a", + SecurityContext: &api.SecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeUnconfined, + }, + }, + }, { + Name: "b", + }, { + Name: "c", + SecurityContext: &api.SecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeRuntimeDefault, + }, + }, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "init": api.AppArmorProfileLocalhostPrefix + testProfile, + api.AppArmorContainerAnnotationKeyPrefix + "a": api.AppArmorProfileNameUnconfined, + api.AppArmorContainerAnnotationKeyPrefix + "c": api.AppArmorProfileRuntimeDefault, + }, pod.Annotations) + assert.Nil(t, pod.Spec.SecurityContext) + assert.Equal(t, api.AppArmorProfileTypeLocalhost, pod.Spec.InitContainers[0].SecurityContext.AppArmorProfile.Type) + assert.Equal(t, api.AppArmorProfileTypeUnconfined, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.Type) + assert.Nil(t, pod.Spec.Containers[1].SecurityContext) + assert.Equal(t, api.AppArmorProfileTypeRuntimeDefault, pod.Spec.Containers[2].SecurityContext.AppArmorProfile.Type) + }, + }, { + description: "Annotation 'unconfined' and no field present (container)", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileNameUnconfined, + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileNameUnconfined, + }, pod.Annotations) + assert.Equal(t, api.AppArmorProfileTypeUnconfined, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.Type) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.LocalhostProfile) + assert.Nil(t, pod.Spec.SecurityContext) + }, + }, { + description: "Annotation for non-existent container", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "foo-bar": api.AppArmorProfileNameUnconfined, + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "foo-bar": api.AppArmorProfileNameUnconfined, + }, pod.Annotations) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext) + assert.Nil(t, pod.Spec.SecurityContext) + }, + }, { + description: "Annotation 'runtime/default' and no field present (container)", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileRuntimeDefault, + }, + }, + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeUnconfined, + }, + }, + Containers: []api.Container{{ + Name: "ctr", + SecurityContext: &api.SecurityContext{}, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileRuntimeDefault, + }, pod.Annotations) + assert.Equal(t, api.AppArmorProfileTypeRuntimeDefault, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.Type) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.LocalhostProfile) + assert.Equal(t, api.AppArmorProfileTypeUnconfined, pod.Spec.SecurityContext.AppArmorProfile.Type) + }, + }, { + description: "Multiple containers by annotations (container)", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "init": api.AppArmorProfileNameUnconfined, + api.AppArmorContainerAnnotationKeyPrefix + "a": api.AppArmorProfileLocalhostPrefix + testProfile, + api.AppArmorContainerAnnotationKeyPrefix + "c": api.AppArmorProfileRuntimeDefault, + }, + }, + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeRuntimeDefault, + }, + }, + InitContainers: []api.Container{{Name: "init"}}, + Containers: []api.Container{ + {Name: "a"}, + {Name: "b"}, + {Name: "c"}, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "init": api.AppArmorProfileNameUnconfined, + api.AppArmorContainerAnnotationKeyPrefix + "a": api.AppArmorProfileLocalhostPrefix + testProfile, + api.AppArmorContainerAnnotationKeyPrefix + "b": api.AppArmorProfileRuntimeDefault, + api.AppArmorContainerAnnotationKeyPrefix + "c": api.AppArmorProfileRuntimeDefault, + }, pod.Annotations) + assert.Equal(t, api.AppArmorProfileTypeUnconfined, pod.Spec.InitContainers[0].SecurityContext.AppArmorProfile.Type) + assert.Equal(t, api.AppArmorProfileTypeLocalhost, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.Type) + assert.Equal(t, testProfile, *pod.Spec.Containers[0].SecurityContext.AppArmorProfile.LocalhostProfile) + assert.Nil(t, pod.Spec.Containers[1].SecurityContext) + assert.Equal(t, api.AppArmorProfileTypeRuntimeDefault, pod.Spec.Containers[2].SecurityContext.AppArmorProfile.Type) + assert.Equal(t, api.AppArmorProfileTypeRuntimeDefault, pod.Spec.SecurityContext.AppArmorProfile.Type) + }, + }, { + description: "Conflicting field and annotations", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileLocalhostPrefix + testProfile, + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{ + Name: "ctr", + SecurityContext: &api.SecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: api.AppArmorProfileTypeRuntimeDefault, + }, + }, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileLocalhostPrefix + testProfile, + }, pod.Annotations) + assert.Equal(t, api.AppArmorProfileTypeRuntimeDefault, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.Type) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext.AppArmorProfile.LocalhostProfile) + assert.Nil(t, pod.Spec.SecurityContext) + }, + }, { + description: "Invalid annotation value", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": "not-a-real-type", + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": "not-a-real-type", + }, pod.Annotations) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext) + assert.Nil(t, pod.Spec.SecurityContext) + }, + }, { + description: "Invalid field type", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + AppArmorProfile: &api.AppArmorProfile{ + Type: "invalid-type", + }, + }, + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Empty(t, pod.Annotations) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext) + }, + }, { + description: "Ignore annotations on windows", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileRuntimeDefault, + }, + }, + Spec: api.PodSpec{ + OS: &api.PodOS{Name: api.Windows}, + Containers: []api.Container{{Name: "ctr"}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + assert.Equal(t, map[string]string{ + api.AppArmorContainerAnnotationKeyPrefix + "ctr": api.AppArmorProfileRuntimeDefault, + }, pod.Annotations) + assert.Nil(t, pod.Spec.Containers[0].SecurityContext) + }, + }} + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + applyAppArmorVersionSkew(test.pod) + test.validation(t, test.pod) + }) + } +}