diff --git a/pkg/registry/core/pod/BUILD b/pkg/registry/core/pod/BUILD index 5d842a6780e..47b3f479410 100644 --- a/pkg/registry/core/pod/BUILD +++ b/pkg/registry/core/pod/BUILD @@ -22,6 +22,7 @@ go_library( "//pkg/features:go_default_library", "//pkg/kubelet/client:go_default_library", "//pkg/proxy/util:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/fields:go_default_library", @@ -49,6 +50,7 @@ go_test( "//pkg/apis/core/install:go_default_library", "//pkg/features:go_default_library", "//pkg/kubelet/client:go_default_library", + "//staging/src/k8s.io/api/core/v1:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/api/resource:go_default_library", "//staging/src/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", @@ -60,6 +62,7 @@ go_test( "//staging/src/k8s.io/apiserver/pkg/util/feature:go_default_library", "//staging/src/k8s.io/client-go/tools/cache:go_default_library", "//staging/src/k8s.io/component-base/featuregate/testing:go_default_library", + "//vendor/github.com/stretchr/testify/require:go_default_library", ], ) diff --git a/pkg/registry/core/pod/strategy.go b/pkg/registry/core/pod/strategy.go index fcc2cd27b90..917101548f5 100644 --- a/pkg/registry/core/pod/strategy.go +++ b/pkg/registry/core/pod/strategy.go @@ -26,6 +26,7 @@ import ( "strings" "time" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -74,6 +75,8 @@ func (podStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) { } podutil.DropDisabledPodFields(pod, nil) + + applySeccompVersionSkew(pod) } // PrepareForUpdate clears fields that are not allowed to be set by end users on update. @@ -569,3 +572,129 @@ func validateContainer(container string, pod *api.Pod) (string, error) { return container, nil } + +// applySeccompVersionSkew implements the version skew behavior described in: +// https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/20190717-seccomp-ga.md#version-skew-strategy +func applySeccompVersionSkew(pod *api.Pod) { + // get possible annotation and field + annotation, hasAnnotation := pod.Annotations[v1.SeccompPodAnnotationKey] + field, hasField := (*api.SeccompProfile)(nil), false + + if pod.Spec.SecurityContext != nil && pod.Spec.SecurityContext.SeccompProfile != nil { + field = pod.Spec.SecurityContext.SeccompProfile + hasField = true + } + + // sync field and annotation + if hasField && !hasAnnotation { + newAnnotation := seccompAnnotationForField(field) + + if newAnnotation != "" { + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + pod.Annotations[v1.SeccompPodAnnotationKey] = newAnnotation + } + } else if hasAnnotation && !hasField { + newField := seccompFieldForAnnotation(annotation) + + if newField != nil { + if pod.Spec.SecurityContext == nil { + pod.Spec.SecurityContext = &api.PodSecurityContext{} + } + pod.Spec.SecurityContext.SeccompProfile = newField + } + } + + // 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.SeccompContainerAnnotationKeyPrefix + ctr.Name + annotation, hasAnnotation := pod.Annotations[key] + + field, hasField := (*api.SeccompProfile)(nil), false + if ctr.SecurityContext != nil && ctr.SecurityContext.SeccompProfile != nil { + field = ctr.SecurityContext.SeccompProfile + hasField = true + } + + // sync field and annotation + if hasField && !hasAnnotation { + newAnnotation := seccompAnnotationForField(field) + + if newAnnotation != "" { + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + pod.Annotations[key] = newAnnotation + } + } else if hasAnnotation && !hasField { + newField := seccompFieldForAnnotation(annotation) + + if newField != nil { + if ctr.SecurityContext == nil { + ctr.SecurityContext = &api.SecurityContext{} + } + ctr.SecurityContext.SeccompProfile = newField + } + } + + return true + }) +} + +// seccompFieldForAnnotation takes a pod seccomp profile field and returns the +// converted annotation value +func seccompAnnotationForField(field *api.SeccompProfile) string { + // If only seccomp 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.SeccompProfileTypeUnconfined: + return v1.SeccompProfileNameUnconfined + + case api.SeccompProfileTypeRuntimeDefault: + return v1.SeccompProfileRuntimeDefault + + case api.SeccompProfileTypeLocalhost: + if field.LocalhostProfile != nil { + return v1.SeccompLocalhostProfileNamePrefix + *field.LocalhostProfile + } + } + + // we can only reach this code path if the LocalhostProfile is nil but the + // provided field type is SeccompProfileTypeLocalhost or if an unrecognized + // type is specified + return "" +} + +// seccompFieldForAnnotation takes a pod annotation and returns the converted +// seccomp profile field. +func seccompFieldForAnnotation(annotation string) *api.SeccompProfile { + // If only seccomp annotations are specified, copy the values into the + // corresponding fields. This ensures that existing applications continue + // to enforce seccomp, and prevents the kubelet from needing to resolve + // annotations & fields. + if annotation == v1.SeccompProfileNameUnconfined { + return &api.SeccompProfile{Type: api.SeccompProfileTypeUnconfined} + } + + if annotation == api.SeccompProfileRuntimeDefault || annotation == api.DeprecatedSeccompProfileDockerDefault { + return &api.SeccompProfile{Type: api.SeccompProfileTypeRuntimeDefault} + } + + if strings.HasPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix) { + localhostProfile := strings.TrimPrefix(annotation, v1.SeccompLocalhostProfileNamePrefix) + if localhostProfile != "" { + return &api.SeccompProfile{ + Type: api.SeccompProfileTypeLocalhost, + 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 a945c1c1af0..f7f0dad477b 100644 --- a/pkg/registry/core/pod/strategy_test.go +++ b/pkg/registry/core/pod/strategy_test.go @@ -24,6 +24,8 @@ import ( "reflect" "testing" + "github.com/stretchr/testify/require" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -696,3 +698,408 @@ func TestPodIndexFunc(t *testing.T) { } } +func TestApplySeccompVersionSkew(t *testing.T) { + const containerName = "container" + testProfile := "test" + + for _, test := range []struct { + description string + pod *api.Pod + validation func(*testing.T, *api.Pod) + }{ + { + description: "Security context nil", + pod: &api.Pod{}, + validation: func(t *testing.T, pod *api.Pod) { + require.NotNil(t, pod) + }, + }, + { + description: "Security context not nil", + pod: &api.Pod{ + Spec: api.PodSpec{SecurityContext: &api.PodSecurityContext{}}, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.NotNil(t, pod) + }, + }, + { + description: "Field type unconfined and no annotation present", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeUnconfined, + }, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Len(t, pod.Annotations, 1) + require.Equal(t, v1.SeccompProfileNameUnconfined, pod.Annotations[api.SeccompPodAnnotationKey]) + }, + }, + { + description: "Field type default and no annotation present", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeRuntimeDefault, + }, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Len(t, pod.Annotations, 1) + require.Equal(t, v1.SeccompProfileRuntimeDefault, pod.Annotations[v1.SeccompPodAnnotationKey]) + }, + }, + { + description: "Field type localhost and no annotation present", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeLocalhost, + LocalhostProfile: &testProfile, + }, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Len(t, pod.Annotations, 1) + require.Equal(t, "localhost/test", pod.Annotations[v1.SeccompPodAnnotationKey]) + }, + }, + { + description: "Field type localhost but profile is nil", + pod: &api.Pod{ + Spec: api.PodSpec{ + SecurityContext: &api.PodSecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeLocalhost, + }, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Len(t, pod.Annotations, 0) + }, + }, + { + description: "Annotation 'unconfined' and no field present", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompProfileNameUnconfined, + }, + }, + Spec: api.PodSpec{}, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeUnconfined, pod.Spec.SecurityContext.SeccompProfile.Type) + require.Nil(t, pod.Spec.SecurityContext.SeccompProfile.LocalhostProfile) + }, + }, + { + description: "Annotation 'runtime/default' and no field present", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompProfileRuntimeDefault, + }, + }, + Spec: api.PodSpec{SecurityContext: &api.PodSecurityContext{}}, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeRuntimeDefault, pod.Spec.SecurityContext.SeccompProfile.Type) + require.Nil(t, pod.Spec.SecurityContext.SeccompProfile.LocalhostProfile) + }, + }, + { + description: "Annotation 'docker/default' and no field present", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompPodAnnotationKey: v1.DeprecatedSeccompProfileDockerDefault, + }, + }, + Spec: api.PodSpec{SecurityContext: &api.PodSecurityContext{}}, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeRuntimeDefault, pod.Spec.SecurityContext.SeccompProfile.Type) + require.Nil(t, pod.Spec.SecurityContext.SeccompProfile.LocalhostProfile) + }, + }, + { + description: "Annotation 'localhost/test' and no field present", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompLocalhostProfileNamePrefix + testProfile, + }, + }, + Spec: api.PodSpec{SecurityContext: &api.PodSecurityContext{}}, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeLocalhost, pod.Spec.SecurityContext.SeccompProfile.Type) + require.Equal(t, testProfile, *pod.Spec.SecurityContext.SeccompProfile.LocalhostProfile) + }, + }, + { + description: "Annotation 'localhost/' has zero length", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompPodAnnotationKey: v1.SeccompLocalhostProfileNamePrefix, + }, + }, + Spec: api.PodSpec{SecurityContext: &api.PodSecurityContext{}}, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Nil(t, pod.Spec.SecurityContext.SeccompProfile) + }, + }, + { + description: "Security context nil (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{{}}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.NotNil(t, pod) + }, + }, + { + description: "Security context not nil (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{{ + SecurityContext: &api.SecurityContext{}, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.NotNil(t, pod) + }, + }, + { + description: "Field type unconfined and no annotation present (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: containerName, + SecurityContext: &api.SecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeUnconfined, + }, + }, + }, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Len(t, pod.Annotations, 1) + require.Equal(t, v1.SeccompProfileNameUnconfined, pod.Annotations[v1.SeccompContainerAnnotationKeyPrefix+containerName]) + }, + }, + { + description: "Field type runtime/default and no annotation present (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: containerName, + SecurityContext: &api.SecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeRuntimeDefault, + }, + }, + }, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Len(t, pod.Annotations, 1) + require.Equal(t, v1.SeccompProfileRuntimeDefault, pod.Annotations[v1.SeccompContainerAnnotationKeyPrefix+containerName]) + }, + }, + { + description: "Field type localhost and no annotation present (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: containerName, + SecurityContext: &api.SecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeLocalhost, + LocalhostProfile: &testProfile, + }, + }, + }, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Len(t, pod.Annotations, 1) + require.Equal(t, "localhost/test", pod.Annotations[v1.SeccompContainerAnnotationKeyPrefix+containerName]) + }, + }, + { + description: "Multiple containers with fields (container)", + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{ + { + Name: containerName + "1", + SecurityContext: &api.SecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeUnconfined, + }, + }, + }, + { + Name: containerName + "2", + }, + { + Name: containerName + "3", + SecurityContext: &api.SecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileTypeRuntimeDefault, + }, + }, + }, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Len(t, pod.Annotations, 2) + require.Equal(t, v1.SeccompProfileNameUnconfined, pod.Annotations[v1.SeccompContainerAnnotationKeyPrefix+containerName+"1"]) + require.Equal(t, v1.SeccompProfileRuntimeDefault, pod.Annotations[v1.SeccompContainerAnnotationKeyPrefix+containerName+"3"]) + }, + }, + { + description: "Annotation 'unconfined' and no field present (container)", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompContainerAnnotationKeyPrefix + containerName: v1.SeccompProfileNameUnconfined, + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{ + Name: containerName, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeUnconfined, pod.Spec.Containers[0].SecurityContext.SeccompProfile.Type) + require.Nil(t, pod.Spec.Containers[0].SecurityContext.SeccompProfile.LocalhostProfile) + }, + }, + { + description: "Annotation 'runtime/default' and no field present (container)", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompContainerAnnotationKeyPrefix + containerName: v1.SeccompProfileRuntimeDefault, + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{ + Name: containerName, + SecurityContext: &api.SecurityContext{}, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeRuntimeDefault, pod.Spec.Containers[0].SecurityContext.SeccompProfile.Type) + require.Nil(t, pod.Spec.Containers[0].SecurityContext.SeccompProfile.LocalhostProfile) + }, + }, + { + description: "Annotation 'docker/default' and no field present (container)", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompContainerAnnotationKeyPrefix + containerName: v1.DeprecatedSeccompProfileDockerDefault, + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{ + Name: containerName, + SecurityContext: &api.SecurityContext{}, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeRuntimeDefault, pod.Spec.Containers[0].SecurityContext.SeccompProfile.Type) + require.Nil(t, pod.Spec.Containers[0].SecurityContext.SeccompProfile.LocalhostProfile) + }, + }, + { + description: "Multiple containers by annotations (container)", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompContainerAnnotationKeyPrefix + containerName + "1": v1.SeccompLocalhostProfileNamePrefix + testProfile, + v1.SeccompContainerAnnotationKeyPrefix + containerName + "3": v1.SeccompProfileRuntimeDefault, + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{ + {Name: containerName + "1"}, + {Name: containerName + "2"}, + {Name: containerName + "3"}, + }, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeLocalhost, pod.Spec.Containers[0].SecurityContext.SeccompProfile.Type) + require.Equal(t, testProfile, *pod.Spec.Containers[0].SecurityContext.SeccompProfile.LocalhostProfile) + require.Equal(t, api.SeccompProfileTypeRuntimeDefault, pod.Spec.Containers[2].SecurityContext.SeccompProfile.Type) + }, + }, + { + description: "Annotation 'localhost/test' and no field present (container)", + pod: &api.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{ + v1.SeccompContainerAnnotationKeyPrefix + containerName: v1.SeccompLocalhostProfileNamePrefix + testProfile, + }, + }, + Spec: api.PodSpec{ + Containers: []api.Container{{ + Name: containerName, + SecurityContext: &api.SecurityContext{}, + }}, + }, + }, + validation: func(t *testing.T, pod *api.Pod) { + require.Equal(t, api.SeccompProfileTypeLocalhost, pod.Spec.Containers[0].SecurityContext.SeccompProfile.Type) + require.Equal(t, testProfile, *pod.Spec.Containers[0].SecurityContext.SeccompProfile.LocalhostProfile) + }, + }, + } { + output := &api.Pod{ + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}}, + } + for i, ctr := range test.pod.Spec.Containers { + output.Spec.Containers = append(output.Spec.Containers, api.Container{}) + if ctr.SecurityContext != nil && ctr.SecurityContext.SeccompProfile != nil { + output.Spec.Containers[i].SecurityContext = &api.SecurityContext{ + SeccompProfile: &api.SeccompProfile{ + Type: api.SeccompProfileType(ctr.SecurityContext.SeccompProfile.Type), + LocalhostProfile: ctr.SecurityContext.SeccompProfile.LocalhostProfile, + }, + } + } + } + applySeccompVersionSkew(test.pod) + test.validation(t, test.pod) + } +}