From 982bf19c205045259cf778ce4fe99df849daa20d Mon Sep 17 00:00:00 2001 From: Paul Weil Date: Tue, 5 May 2015 12:37:23 -0400 Subject: [PATCH] security context initial implementation - squash --- cluster/aws/config-default.sh | 2 +- cluster/azure/config-default.sh | 2 +- cluster/gce/config-default.sh | 2 +- cluster/gce/config-test.sh | 2 +- cluster/vagrant/config-default.sh | 2 +- cmd/kube-apiserver/app/plugins.go | 1 + hack/local-up-cluster.sh | 2 +- pkg/api/testing/fuzzer.go | 11 + pkg/api/types.go | 40 +- pkg/api/v1/conversion.go | 30 +- pkg/api/v1/conversion_test.go | 59 + pkg/api/v1/defaults.go | 44 + pkg/api/v1/defaults_test.go | 101 ++ pkg/api/v1/types.go | 46 +- pkg/api/v1beta1/conversion.go | 32 +- pkg/api/v1beta1/conversion_test.go | 60 + pkg/api/v1beta1/defaults.go | 42 + pkg/api/v1beta1/defaults_test.go | 103 ++ pkg/api/v1beta1/types.go | 46 +- pkg/api/v1beta2/conversion.go | 32 +- pkg/api/v1beta2/conversion_test.go | 60 + pkg/api/v1beta2/defaults.go | 42 + pkg/api/v1beta2/defaults_test.go | 103 ++ pkg/api/v1beta2/types.go | 46 +- pkg/api/v1beta3/conversion.go | 1006 ++++++++--------- pkg/api/v1beta3/conversion_test.go | 60 + pkg/api/v1beta3/defaults.go | 43 + pkg/api/v1beta3/defaults_test.go | 101 ++ pkg/api/v1beta3/types.go | 46 +- pkg/api/validation/validation.go | 26 +- pkg/api/validation/validation_test.go | 90 +- pkg/controller/replication_controller_test.go | 2 + pkg/kubelet/config/common_test.go | 3 + pkg/kubelet/config/config_test.go | 10 +- pkg/kubelet/config/file_test.go | 17 +- pkg/kubelet/config/http_test.go | 25 +- pkg/kubelet/dockertools/manager.go | 27 +- pkg/kubelet/rkt/rkt_linux.go | 24 +- pkg/registry/pod/etcd/etcd_test.go | 11 +- pkg/securitycontext/doc.go | 18 + pkg/securitycontext/fake.go | 45 + pkg/securitycontext/provider.go | 97 ++ pkg/securitycontext/provider_test.go | 181 +++ pkg/securitycontext/types.go | 45 + pkg/securitycontext/util.go | 43 + .../securitycontext/scdeny/admission.go | 70 ++ .../securitycontext/scdeny/admission_test.go | 65 ++ 47 files changed, 2359 insertions(+), 606 deletions(-) create mode 100644 pkg/securitycontext/doc.go create mode 100644 pkg/securitycontext/fake.go create mode 100644 pkg/securitycontext/provider.go create mode 100644 pkg/securitycontext/provider_test.go create mode 100644 pkg/securitycontext/types.go create mode 100644 pkg/securitycontext/util.go create mode 100644 plugin/pkg/admission/securitycontext/scdeny/admission.go create mode 100644 plugin/pkg/admission/securitycontext/scdeny/admission_test.go diff --git a/cluster/aws/config-default.sh b/cluster/aws/config-default.sh index 8ef0d3e769..5d16c72b2f 100644 --- a/cluster/aws/config-default.sh +++ b/cluster/aws/config-default.sh @@ -72,4 +72,4 @@ DNS_DOMAIN="kubernetes.local" DNS_REPLICAS=1 # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota diff --git a/cluster/azure/config-default.sh b/cluster/azure/config-default.sh index 411fd45945..8b8f91f061 100644 --- a/cluster/azure/config-default.sh +++ b/cluster/azure/config-default.sh @@ -49,4 +49,4 @@ ELASTICSEARCH_LOGGING_REPLICAS=1 ENABLE_CLUSTER_MONITORING="${KUBE_ENABLE_CLUSTER_MONITORING:-true}" # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota diff --git a/cluster/gce/config-default.sh b/cluster/gce/config-default.sh index 1466aa96e0..e1ba5ebb10 100755 --- a/cluster/gce/config-default.sh +++ b/cluster/gce/config-default.sh @@ -111,4 +111,4 @@ DNS_DOMAIN="kubernetes.local" DNS_REPLICAS=1 # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota, diff --git a/cluster/gce/config-test.sh b/cluster/gce/config-test.sh index 5b6b20c8c5..c637dcc6bc 100755 --- a/cluster/gce/config-test.sh +++ b/cluster/gce/config-test.sh @@ -70,4 +70,4 @@ DNS_SERVER_IP="10.0.0.10" DNS_DOMAIN="kubernetes.local" DNS_REPLICAS=1 -ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,ResourceQuota +ADMISSION_CONTROL=NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota diff --git a/cluster/vagrant/config-default.sh b/cluster/vagrant/config-default.sh index af1e96c202..2653cbca19 100755 --- a/cluster/vagrant/config-default.sh +++ b/cluster/vagrant/config-default.sh @@ -50,7 +50,7 @@ MASTER_USER=vagrant MASTER_PASSWD=vagrant # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota # Optional: Install node monitoring. ENABLE_NODE_MONITORING=true diff --git a/cmd/kube-apiserver/app/plugins.go b/cmd/kube-apiserver/app/plugins.go index 9221b13e0b..5a9bf03021 100644 --- a/cmd/kube-apiserver/app/plugins.go +++ b/cmd/kube-apiserver/app/plugins.go @@ -36,4 +36,5 @@ import ( _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/exists" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/namespace/lifecycle" _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/resourcequota" + _ "github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/admission/securitycontext/scdeny" ) diff --git a/hack/local-up-cluster.sh b/hack/local-up-cluster.sh index e45ab54425..8a3a6bdaf6 100755 --- a/hack/local-up-cluster.sh +++ b/hack/local-up-cluster.sh @@ -116,7 +116,7 @@ echo "Starting etcd" kube::etcd::start # Admission Controllers to invoke prior to persisting objects in cluster -ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,ResourceQuota +ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota APISERVER_LOG=/tmp/kube-apiserver.log sudo -E "${GO_OUT}/kube-apiserver" \ diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 0e53b2c33e..49a27c7778 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -204,6 +204,17 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { ev.ValueFrom.FieldRef.FieldPath = c.RandString() } }, + func(sc *api.SecurityContext, c fuzz.Continue) { + c.FuzzNoCustom(sc) // fuzz self without calling this function again + priv := c.RandBool() + sc.Privileged = &priv + sc.Capabilities = &api.Capabilities{ + Add: make([]api.CapabilityType, 0), + Drop: make([]api.CapabilityType, 0), + } + c.Fuzz(&sc.Capabilities.Add) + c.Fuzz(&sc.Capabilities.Drop) + }, func(e *api.Event, c fuzz.Continue) { c.FuzzNoCustom(e) // fuzz self without calling this function again // Fix event count to 1, otherwise, if a v1beta1 or v1beta2 event has a count set arbitrarily, it's count is ignored diff --git a/pkg/api/types.go b/pkg/api/types.go index 7328b459d2..3ed8f06d82 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -623,12 +623,10 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty"` // Required. TerminationMessagePath string `json:"terminationMessagePath,omitempty"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty"` // Required: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty"` + // Optional: SecurityContext defines the security options the pod should be run with + SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"` } // Handler defines a specific action that should be taken @@ -1876,3 +1874,37 @@ type ComponentStatusList struct { Items []ComponentStatus `json:"items"` } + +// SecurityContext holds security configuration that will be applied to a container. SecurityContext +// contains duplication of some existing fields from the Container resource. These duplicate fields +// will be populated based on the Container configuration if they are not set. Defining them on +// both the Container AND the SecurityContext will result in an error. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"` + + // Run the container in privileged mode + Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"` + + // RunAsUser is the UID to run the entrypoint of the container process. + RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"` +} + +// SELinuxOptions are the labels to be applied to the container. +type SELinuxOptions struct { + // SELinux user label + User string `json:"user,omitempty" description:"the user label to apply to the container"` + + // SELinux role label + Role string `json:"role,omitempty" description:"the role label to apply to the container"` + + // SELinux type label + Type string `json:"type,omitempty" description:"the type label to apply to the container"` + + // SELinux level label. + Level string `json:"level,omitempty" description:"the level label to apply to the container"` +} diff --git a/pkg/api/v1/conversion.go b/pkg/api/v1/conversion.go index 10573633b1..08e455fabe 100644 --- a/pkg/api/v1/conversion.go +++ b/pkg/api/v1/conversion.go @@ -18,6 +18,7 @@ package v1 import ( "fmt" + "reflect" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" @@ -237,9 +238,22 @@ func init() { return err } out.TerminationMessagePath = in.TerminationMessagePath - out.Privileged = in.Privileged out.ImagePullPolicy = newer.PullPolicy(in.ImagePullPolicy) - if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + + if in.SecurityContext != nil { + if in.SecurityContext.Capabilities != nil { + if !reflect.DeepEqual(in.SecurityContext.Capabilities.Add, in.Capabilities.Add) || + !reflect.DeepEqual(in.SecurityContext.Capabilities.Drop, in.Capabilities.Drop) { + return fmt.Errorf("container capability settings do not match security context settings, cannot convert") + } + } + if in.SecurityContext.Privileged != nil { + if in.Privileged != *in.SecurityContext.Privileged { + return fmt.Errorf("container privileged settings do not match security context settings, cannot convert") + } + } + } + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { return err } return nil @@ -297,11 +311,19 @@ func init() { return err } out.TerminationMessagePath = in.TerminationMessagePath - out.Privileged = in.Privileged out.ImagePullPolicy = PullPolicy(in.ImagePullPolicy) - if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { return err } + + // now that we've converted set the container field from security context + if out.SecurityContext != nil && out.SecurityContext.Privileged != nil { + out.Privileged = *out.SecurityContext.Privileged + } + // now that we've converted set the container field from security context + if out.SecurityContext != nil && out.SecurityContext.Capabilities != nil { + out.Capabilities = *out.SecurityContext.Capabilities + } return nil }, func(in *ContainerPort, out *newer.ContainerPort, s conversion.Scope) error { diff --git a/pkg/api/v1/conversion_test.go b/pkg/api/v1/conversion_test.go index 3722d84d7d..33c1ff02ff 100644 --- a/pkg/api/v1/conversion_test.go +++ b/pkg/api/v1/conversion_test.go @@ -45,3 +45,62 @@ func TestNodeConversion(t *testing.T) { t.Fatalf("unexpected error: %v", err) } } + +func TestBadSecurityContextConversion(t *testing.T) { + priv := false + testCases := map[string]struct { + c *current.Container + err string + }{ + // this use case must use true for the container and false for the sc. Otherwise the defaulter + // will assume privileged was left undefined (since it is the default value) and copy the + // sc setting upwards + "mismatched privileged": { + c: ¤t.Container{ + Privileged: true, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + }, + }, + err: "container privileged settings do not match security context settings, cannot convert", + }, + "mismatched caps add": { + c: ¤t.Container{ + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"bar"}, + }, + }, + }, + err: "container capability settings do not match security context settings, cannot convert", + }, + "mismatched caps drop": { + c: ¤t.Container{ + Capabilities: current.Capabilities{ + Drop: []current.CapabilityType{"foo"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + err: "container capability settings do not match security context settings, cannot convert", + }, + } + + for k, v := range testCases { + got := newer.Container{} + err := newer.Scheme.Convert(v.c, &got) + if err == nil { + t.Errorf("expected error for case %s but got none", k) + } else { + if err.Error() != v.err { + t.Errorf("unexpected error for case %s. Expected: %s but got: %s", k, v.err, err.Error()) + } + } + } +} diff --git a/pkg/api/v1/defaults.go b/pkg/api/v1/defaults.go index bb542aaed3..443e910101 100644 --- a/pkg/api/v1/defaults.go +++ b/pkg/api/v1/defaults.go @@ -19,6 +19,8 @@ package v1 import ( "strings" + "github.com/golang/glog" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" ) @@ -66,6 +68,7 @@ func init() { if obj.TerminationMessagePath == "" { obj.TerminationMessagePath = TerminationMessagePathDefault } + defaultSecurityContext(obj) }, func(obj *ServiceSpec) { if obj.SessionAffinity == "" { @@ -156,3 +159,44 @@ func defaultHostNetworkPorts(containers *[]Container) { } } } + +// defaultSecurityContext performs the downward and upward merges of a pod definition +func defaultSecurityContext(container *Container) { + if container.SecurityContext == nil { + glog.V(4).Infof("creating security context for container %s", container.Name) + container.SecurityContext = &SecurityContext{} + } + // if there are no capabilities defined on the SecurityContext then copy the container settings + if container.SecurityContext.Capabilities == nil { + glog.V(4).Infof("downward merge of container.Capabilities for container %s", container.Name) + container.SecurityContext.Capabilities = &container.Capabilities + } else { + // if there are capabilities defined on the security context and the container setting is + // empty then assume that it was left off the pod definition and ensure that the container + // settings match the security context settings (checked by the convert functions). If + // there are settings in both then don't touch it, the converter will error if they don't + // match + if len(container.Capabilities.Add) == 0 { + glog.V(4).Infof("upward merge of container.Capabilities.Add for container %s", container.Name) + container.Capabilities.Add = container.SecurityContext.Capabilities.Add + } + if len(container.Capabilities.Drop) == 0 { + glog.V(4).Infof("upward merge of container.Capabilities.Drop for container %s", container.Name) + container.Capabilities.Drop = container.SecurityContext.Capabilities.Drop + } + } + // if there are no privileged settings on the security context then copy the container settings + if container.SecurityContext.Privileged == nil { + glog.V(4).Infof("downward merge of container.Privileged for container %s", container.Name) + container.SecurityContext.Privileged = &container.Privileged + } else { + // we don't have a good way to know if container.Privileged was set or just defaulted to false + // so the best we can do here is check if the securityContext is set to true and the + // container is set to false and assume that the Privileged field was left off the container + // definition and not an intentional mismatch + if *container.SecurityContext.Privileged && !container.Privileged { + glog.V(4).Infof("upward merge of container.Privileged for container %s", container.Name) + container.Privileged = *container.SecurityContext.Privileged + } + } +} diff --git a/pkg/api/v1/defaults_test.go b/pkg/api/v1/defaults_test.go index 09667ef841..4b2a4fd30a 100644 --- a/pkg/api/v1/defaults_test.go +++ b/pkg/api/v1/defaults_test.go @@ -349,3 +349,104 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) { t.Errorf("Expected default APIVersion v1, got: %v", apiVersion) } } + +func TestSetDefaultSecurityContext(t *testing.T) { + priv := false + privTrue := true + testCases := map[string]struct { + c current.Container + }{ + "downward defaulting caps": { + c: current.Container{ + Privileged: false, + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + }, + }, + }, + "downward defaulting priv": { + c: current.Container{ + Privileged: false, + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + }, + "upward defaulting caps": { + c: current.Container{ + Privileged: false, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"biz"}, + Drop: []current.CapabilityType{"baz"}, + }, + }, + }, + }, + "upward defaulting priv": { + c: current.Container{ + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Privileged: &privTrue, + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + }, + } + + pod := ¤t.Pod{ + Spec: current.PodSpec{}, + } + + for k, v := range testCases { + pod.Spec.Containers = []current.Container{v.c} + obj := roundTrip(t, runtime.Object(pod)) + defaultedPod := obj.(*current.Pod) + c := defaultedPod.Spec.Containers[0] + if isEqual, issues := areSecurityContextAndContainerEqual(&c); !isEqual { + t.Errorf("test case %s expected the security context to have the same values as the container but found %#v", k, issues) + } + } +} + +func areSecurityContextAndContainerEqual(c *current.Container) (bool, []string) { + issues := make([]string, 0) + equal := true + + if c.SecurityContext == nil || c.SecurityContext.Privileged == nil || c.SecurityContext.Capabilities == nil { + equal = false + issues = append(issues, "Expected non nil settings for SecurityContext") + return equal, issues + } + if *c.SecurityContext.Privileged != c.Privileged { + equal = false + issues = append(issues, "The defaulted SecurityContext.Privileged value did not match the container value") + } + if !reflect.DeepEqual(c.Capabilities.Add, c.Capabilities.Add) { + equal = false + issues = append(issues, "The defaulted SecurityContext.Capabilities.Add did not match the container settings") + } + if !reflect.DeepEqual(c.Capabilities.Drop, c.Capabilities.Drop) { + equal = false + issues = append(issues, "The defaulted SecurityContext.Capabilities.Drop did not match the container settings") + } + return equal, issues +} diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index cbde94d06b..6c694a946e 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -636,12 +636,14 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` + // Deprecated - see SecurityContext. Optional: Default to false. + Privileged bool `json:"privileged,omitempty" description:"hether or not the container is granted privileged status; defaults to false; cannot be updated; deprecated; See SecurityContext"` // Optional: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` + // Deprecated - see SecurityContext. Optional: Capabilities for container. + Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated; deprecated; See SecurityContext"` + // Optional: SecurityContext defines the security options the pod should be run with + SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"` } // Handler defines a specific action that should be taken @@ -1735,3 +1737,39 @@ type ComponentStatusList struct { Items []ComponentStatus `json:"items" description:"list of component status objects"` } + +// SecurityContext holds security configuration that will be applied to a container. SecurityContext +// contains duplication of some existing fields from the Container resource. These duplicate fields +// will be populated based on the Container configuration if they are not set. Defining them on +// both the Container AND the SecurityContext will result in an error. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + // Must match Container.Capabilities or be unset. Will be defaulted to Container.Capabilities if left unset + Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"` + + // Run the container in privileged mode + // Must match Container.Privileged or be unset. Will be defaulted to Container.Privileged if left unset + Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"` + + // RunAsUser is the UID to run the entrypoint of the container process. + RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"` +} + +// SELinuxOptions are the labels to be applied to the container +type SELinuxOptions struct { + // SELinux user label + User string `json:"user,omitempty" description:"the user label to apply to the container"` + + // SELinux role label + Role string `json:"role,omitempty" description:"the role label to apply to the container"` + + // SELinux type label + Type string `json:"type,omitempty" description:"the type label to apply to the container"` + + // SELinux level label. + Level string `json:"level,omitempty" description:"the level label to apply to the container"` +} diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 2aff508c53..dba9b5d468 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -19,6 +19,7 @@ package v1beta1 import ( "fmt" "net" + "reflect" "strconv" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -579,15 +580,20 @@ func init() { if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil { return err } - if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil { - return err - } if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil { return err } - if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { return err } + // now that we've converted set the container field from security context + if out.SecurityContext != nil && out.SecurityContext.Privileged != nil { + out.Privileged = *out.SecurityContext.Privileged + } + // now that we've converted set the container field from security context + if out.SecurityContext != nil && out.SecurityContext.Capabilities != nil { + out.Capabilities = *out.SecurityContext.Capabilities + } return nil }, // Internal API does not support CPU to be specified via an explicit field. @@ -665,13 +671,23 @@ func init() { if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil { return err } - if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil { - return err - } if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil { return err } - if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + if in.SecurityContext != nil { + if in.SecurityContext.Capabilities != nil { + if !reflect.DeepEqual(in.SecurityContext.Capabilities.Add, in.Capabilities.Add) || + !reflect.DeepEqual(in.SecurityContext.Capabilities.Drop, in.Capabilities.Drop) { + return fmt.Errorf("container capability settings do not match security context settings, cannot convert") + } + } + if in.SecurityContext.Privileged != nil { + if in.Privileged != *in.SecurityContext.Privileged { + return fmt.Errorf("container privileged settings do not match security context settings, cannot convert") + } + } + } + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { return err } return nil diff --git a/pkg/api/v1beta1/conversion_test.go b/pkg/api/v1beta1/conversion_test.go index d4997ad032..d8d21ea7c5 100644 --- a/pkg/api/v1beta1/conversion_test.go +++ b/pkg/api/v1beta1/conversion_test.go @@ -749,3 +749,63 @@ func TestSecretVolumeSourceConversion(t *testing.T) { t.Errorf("Expected %v; got %v", given, got2) } } + +func TestBadSecurityContextConversion(t *testing.T) { + priv := false + testCases := map[string]struct { + c *current.Container + err string + }{ + // this use case must use true for the container and false for the sc. Otherwise the defaulter + // will assume privileged was left undefined (since it is the default value) and copy the + // sc setting upwards + "mismatched privileged": { + c: ¤t.Container{ + Privileged: true, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + }, + }, + err: "container privileged settings do not match security context settings, cannot convert", + }, + "mismatched caps add": { + c: ¤t.Container{ + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"bar"}, + }, + }, + }, + err: "container capability settings do not match security context settings, cannot convert", + }, + "mismatched caps drop": { + c: ¤t.Container{ + Capabilities: current.Capabilities{ + Drop: []current.CapabilityType{"foo"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + err: "container capability settings do not match security context settings, cannot convert", + }, + } + + for k, v := range testCases { + got := newer.Container{} + err := Convert(v.c, &got) + if err == nil { + t.Errorf("expected error for case %s but got none", k) + } else { + if err.Error() != v.err { + t.Errorf("unexpected error for case %s. Expected: %s but got: %s", k, v.err, err.Error()) + } + } + } + +} diff --git a/pkg/api/v1beta1/defaults.go b/pkg/api/v1beta1/defaults.go index 1dc6b71a7d..f217c694bf 100644 --- a/pkg/api/v1beta1/defaults.go +++ b/pkg/api/v1beta1/defaults.go @@ -62,6 +62,7 @@ func init() { if obj.TerminationMessagePath == "" { obj.TerminationMessagePath = TerminationMessagePathDefault } + defaultSecurityContext(obj) }, func(obj *RestartPolicy) { if util.AllPtrFieldsNil(obj) { @@ -194,3 +195,44 @@ func defaultHostNetworkPorts(containers *[]Container) { } } } + +// defaultSecurityContext performs the downward and upward merges of a pod definition +func defaultSecurityContext(container *Container) { + if container.SecurityContext == nil { + glog.V(4).Infof("creating security context for container %s", container.Name) + container.SecurityContext = &SecurityContext{} + } + // if there are no capabilities defined on the SecurityContext then copy the container settings + if container.SecurityContext.Capabilities == nil { + glog.V(4).Infof("downward merge of container.Capabilities for container %s", container.Name) + container.SecurityContext.Capabilities = &container.Capabilities + } else { + // if there are capabilities defined on the security context and the container setting is + // empty then assume that it was left off the pod definition and ensure that the container + // settings match the security context settings (checked by the convert functions). If + // there are settings in both then don't touch it, the converter will error if they don't + // match + if len(container.Capabilities.Add) == 0 { + glog.V(4).Infof("upward merge of container.Capabilities.Add for container %s", container.Name) + container.Capabilities.Add = container.SecurityContext.Capabilities.Add + } + if len(container.Capabilities.Drop) == 0 { + glog.V(4).Infof("upward merge of container.Capabilities.Drop for container %s", container.Name) + container.Capabilities.Drop = container.SecurityContext.Capabilities.Drop + } + } + // if there are no privileged settings on the security context then copy the container settings + if container.SecurityContext.Privileged == nil { + glog.V(4).Infof("downward merge of container.Privileged for container %s", container.Name) + container.SecurityContext.Privileged = &container.Privileged + } else { + // we don't have a good way to know if container.Privileged was set or just defaulted to false + // so the best we can do here is check if the securityContext is set to true and the + // container is set to false and assume that the Privileged field was left off the container + // definition and not an intentional mismatch + if *container.SecurityContext.Privileged && !container.Privileged { + glog.V(4).Infof("upward merge of container.Privileged for container %s", container.Name) + container.Privileged = *container.SecurityContext.Privileged + } + } +} diff --git a/pkg/api/v1beta1/defaults_test.go b/pkg/api/v1beta1/defaults_test.go index 988a87d8a0..7a83c93411 100644 --- a/pkg/api/v1beta1/defaults_test.go +++ b/pkg/api/v1beta1/defaults_test.go @@ -340,3 +340,106 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) { t.Errorf("Expected default APIVersion v1beta1, got: %v", apiVersion) } } + +func TestSetDefaultSecurityContext(t *testing.T) { + priv := false + privTrue := true + testCases := map[string]struct { + c current.Container + }{ + "downward defaulting caps": { + c: current.Container{ + Privileged: false, + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + }, + }, + }, + "downward defaulting priv": { + c: current.Container{ + Privileged: false, + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + }, + "upward defaulting caps": { + c: current.Container{ + Privileged: false, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"biz"}, + Drop: []current.CapabilityType{"baz"}, + }, + }, + }, + }, + "upward defaulting priv": { + c: current.Container{ + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Privileged: &privTrue, + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + }, + } + + pod := ¤t.Pod{ + DesiredState: current.PodState{ + Manifest: current.ContainerManifest{}, + }, + } + + for k, v := range testCases { + pod.DesiredState.Manifest.Containers = []current.Container{v.c} + obj := roundTrip(t, runtime.Object(pod)) + defaultedPod := obj.(*current.Pod) + c := defaultedPod.DesiredState.Manifest.Containers[0] + if isEqual, issues := areSecurityContextAndContainerEqual(&c); !isEqual { + t.Errorf("test case %s expected the security context to have the same values as the container but found %#v", k, issues) + } + } +} + +func areSecurityContextAndContainerEqual(c *current.Container) (bool, []string) { + issues := make([]string, 0) + equal := true + + if c.SecurityContext == nil || c.SecurityContext.Privileged == nil || c.SecurityContext.Capabilities == nil { + equal = false + issues = append(issues, "Expected non nil settings for SecurityContext") + return equal, issues + } + if *c.SecurityContext.Privileged != c.Privileged { + equal = false + issues = append(issues, "The defaulted SecurityContext.Privileged value did not match the container value") + } + if !reflect.DeepEqual(c.Capabilities.Add, c.Capabilities.Add) { + equal = false + issues = append(issues, "The defaulted SecurityContext.Capabilities.Add did not match the container settings") + } + if !reflect.DeepEqual(c.Capabilities.Drop, c.Capabilities.Drop) { + equal = false + issues = append(issues, "The defaulted SecurityContext.Capabilities.Drop did not match the container settings") + } + return equal, issues +} diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 35e550e0f9..5a10af2b30 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -525,12 +525,14 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` + // Deprecated - see SecurityContext. Optional: Default to false. + Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated; deprecated; See SecurityContext"` // Optional: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` + // Deprecated - see SecurityContext. Optional: Capabilities for container. + Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated; deprecated; See SecurityContext"` + // Optional: SecurityContext defines the security options the pod should be run with + SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"` } // Handler defines a specific action that should be taken @@ -1655,3 +1657,39 @@ type ComponentStatusList struct { Items []ComponentStatus `json:"items" description:"list of component status objects"` } + +// SecurityContext holds security configuration that will be applied to a container. SecurityContext +// contains duplication of some existing fields from the Container resource. These duplicate fields +// will be populated based on the Container configuration if they are not set. Defining them on +// both the Container AND the SecurityContext will result in an error. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + // Must match Container.Capabilities or be unset. Will be defaulted to Container.Capabilities if left unset + Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"` + + // Run the container in privileged mode + // Must match Container.Privileged or be unset. Will be defaulted to Container.Privileged if left unset + Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"` + + // RunAsUser is the UID to run the entrypoint of the container process. + RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"` +} + +// SELinuxOptions are the labels to be applied to the container. +type SELinuxOptions struct { + // SELinux user label + User string `json:"user,omitempty" description:"the user label to apply to the container"` + + // SELinux role label + Role string `json:"role,omitempty" description:"the role label to apply to the container"` + + // SELinux type label + Type string `json:"type,omitempty" description:"the type label to apply to the container"` + + // SELinux level label. + Level string `json:"level,omitempty" description:"the level label to apply to the container"` +} diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index 63ccdd3079..090bc3f87e 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -19,6 +19,7 @@ package v1beta2 import ( "fmt" "net" + "reflect" "strconv" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -357,15 +358,20 @@ func init() { if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil { return err } - if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil { - return err - } if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil { return err } - if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { return err } + // now that we've converted set the container field from security context + if out.SecurityContext != nil && out.SecurityContext.Privileged != nil { + out.Privileged = *out.SecurityContext.Privileged + } + // now that we've converted set the container field from security context + if out.SecurityContext != nil && out.SecurityContext.Capabilities != nil { + out.Capabilities = *out.SecurityContext.Capabilities + } return nil }, // Internal API does not support CPU to be specified via an explicit field. @@ -445,13 +451,23 @@ func init() { if err := s.Convert(&in.TerminationMessagePath, &out.TerminationMessagePath, 0); err != nil { return err } - if err := s.Convert(&in.Privileged, &out.Privileged, 0); err != nil { - return err - } if err := s.Convert(&in.ImagePullPolicy, &out.ImagePullPolicy, 0); err != nil { return err } - if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { + if in.SecurityContext != nil { + if in.SecurityContext.Capabilities != nil { + if !reflect.DeepEqual(in.SecurityContext.Capabilities.Add, in.Capabilities.Add) || + !reflect.DeepEqual(in.SecurityContext.Capabilities.Drop, in.Capabilities.Drop) { + return fmt.Errorf("container capability settings do not match security context settings, cannot convert") + } + } + if in.SecurityContext.Privileged != nil { + if in.Privileged != *in.SecurityContext.Privileged { + return fmt.Errorf("container privileged settings do not match security context settings, cannot convert") + } + } + } + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { return err } return nil diff --git a/pkg/api/v1beta2/conversion_test.go b/pkg/api/v1beta2/conversion_test.go index c482f830bf..df61dcaa70 100644 --- a/pkg/api/v1beta2/conversion_test.go +++ b/pkg/api/v1beta2/conversion_test.go @@ -564,3 +564,63 @@ func TestSecretVolumeSourceConversion(t *testing.T) { t.Errorf("Expected %v; got %v", given, got2) } } + +func TestBadSecurityContextConversion(t *testing.T) { + priv := false + testCases := map[string]struct { + c *current.Container + err string + }{ + // this use case must use true for the container and false for the sc. Otherwise the defaulter + // will assume privileged was left undefined (since it is the default value) and copy the + // sc setting upwards + "mismatched privileged": { + c: ¤t.Container{ + Privileged: true, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + }, + }, + err: "container privileged settings do not match security context settings, cannot convert", + }, + "mismatched caps add": { + c: ¤t.Container{ + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"bar"}, + }, + }, + }, + err: "container capability settings do not match security context settings, cannot convert", + }, + "mismatched caps drop": { + c: ¤t.Container{ + Capabilities: current.Capabilities{ + Drop: []current.CapabilityType{"foo"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + err: "container capability settings do not match security context settings, cannot convert", + }, + } + + for k, v := range testCases { + got := newer.Container{} + err := newer.Scheme.Convert(v.c, &got) + if err == nil { + t.Errorf("expected error for case %s but got none", k) + } else { + if err.Error() != v.err { + t.Errorf("unexpected error for case %s. Expected: %s but got: %s", k, v.err, err.Error()) + } + } + } + +} diff --git a/pkg/api/v1beta2/defaults.go b/pkg/api/v1beta2/defaults.go index 10cebbef20..53c3ab0a89 100644 --- a/pkg/api/v1beta2/defaults.go +++ b/pkg/api/v1beta2/defaults.go @@ -63,6 +63,7 @@ func init() { if obj.TerminationMessagePath == "" { obj.TerminationMessagePath = TerminationMessagePathDefault } + defaultSecurityContext(obj) }, func(obj *RestartPolicy) { if util.AllPtrFieldsNil(obj) { @@ -195,3 +196,44 @@ func defaultHostNetworkPorts(containers *[]Container) { } } } + +// defaultSecurityContext performs the downward and upward merges of a pod definition +func defaultSecurityContext(container *Container) { + if container.SecurityContext == nil { + glog.V(4).Infof("creating security context for container %s", container.Name) + container.SecurityContext = &SecurityContext{} + } + // if there are no capabilities defined on the SecurityContext then copy the container settings + if container.SecurityContext.Capabilities == nil { + glog.V(4).Infof("downward merge of container.Capabilities for container %s", container.Name) + container.SecurityContext.Capabilities = &container.Capabilities + } else { + // if there are capabilities defined on the security context and the container setting is + // empty then assume that it was left off the pod definition and ensure that the container + // settings match the security context settings (checked by the convert functions). If + // there are settings in both then don't touch it, the converter will error if they don't + // match + if len(container.Capabilities.Add) == 0 { + glog.V(4).Infof("upward merge of container.Capabilities.Add for container %s", container.Name) + container.Capabilities.Add = container.SecurityContext.Capabilities.Add + } + if len(container.Capabilities.Drop) == 0 { + glog.V(4).Infof("upward merge of container.Capabilities.Drop for container %s", container.Name) + container.Capabilities.Drop = container.SecurityContext.Capabilities.Drop + } + } + // if there are no privileged settings on the security context then copy the container settings + if container.SecurityContext.Privileged == nil { + glog.V(4).Infof("downward merge of container.Privileged for container %s", container.Name) + container.SecurityContext.Privileged = &container.Privileged + } else { + // we don't have a good way to know if container.Privileged was set or just defaulted to false + // so the best we can do here is check if the securityContext is set to true and the + // container is set to false and assume that the Privileged field was left off the container + // definition and not an intentional mismatch + if *container.SecurityContext.Privileged && !container.Privileged { + glog.V(4).Infof("upward merge of container.Privileged for container %s", container.Name) + container.Privileged = *container.SecurityContext.Privileged + } + } +} diff --git a/pkg/api/v1beta2/defaults_test.go b/pkg/api/v1beta2/defaults_test.go index 10e7ae2532..78af77ac44 100644 --- a/pkg/api/v1beta2/defaults_test.go +++ b/pkg/api/v1beta2/defaults_test.go @@ -339,3 +339,106 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) { t.Errorf("Expected default APIVersion v1beta2, got: %v", apiVersion) } } + +func TestSetDefaultSecurityContext(t *testing.T) { + priv := false + privTrue := true + testCases := map[string]struct { + c current.Container + }{ + "downward defaulting caps": { + c: current.Container{ + Privileged: false, + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + }, + }, + }, + "downward defaulting priv": { + c: current.Container{ + Privileged: false, + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + }, + "upward defaulting caps": { + c: current.Container{ + Privileged: false, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"biz"}, + Drop: []current.CapabilityType{"baz"}, + }, + }, + }, + }, + "upward defaulting priv": { + c: current.Container{ + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Privileged: &privTrue, + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + }, + } + + pod := ¤t.Pod{ + DesiredState: current.PodState{ + Manifest: current.ContainerManifest{}, + }, + } + + for k, v := range testCases { + pod.DesiredState.Manifest.Containers = []current.Container{v.c} + obj := roundTrip(t, runtime.Object(pod)) + defaultedPod := obj.(*current.Pod) + c := defaultedPod.DesiredState.Manifest.Containers[0] + if isEqual, issues := areSecurityContextAndContainerEqual(&c); !isEqual { + t.Errorf("test case %s expected the security context to have the same values as the container but found %#v", k, issues) + } + } +} + +func areSecurityContextAndContainerEqual(c *current.Container) (bool, []string) { + issues := make([]string, 0) + equal := true + + if c.SecurityContext == nil || c.SecurityContext.Privileged == nil || c.SecurityContext.Capabilities == nil { + equal = false + issues = append(issues, "Expected non nil settings for SecurityContext") + return equal, issues + } + if *c.SecurityContext.Privileged != c.Privileged { + equal = false + issues = append(issues, "The defaulted SecurityContext.Privileged value did not match the container value") + } + if !reflect.DeepEqual(c.Capabilities.Add, c.Capabilities.Add) { + equal = false + issues = append(issues, "The defaulted SecurityContext.Capabilities.Add did not match the container settings") + } + if !reflect.DeepEqual(c.Capabilities.Drop, c.Capabilities.Drop) { + equal = false + issues = append(issues, "The defaulted SecurityContext.Capabilities.Drop did not match the container settings") + } + return equal, issues +} diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 50a849bbac..5459104494 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -513,12 +513,14 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` + // Deprecated - see SecurityContext. Optional: Default to false. + Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated; deprecated; See SecurityContext"` // Optional: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` + // Deprecated - see SecurityContext. Optional: Capabilities for container. + Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated; deprecated; See SecurityContext"` + // Optional: SecurityContext defines the security options the pod should be run with + SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"` } const ( @@ -1717,3 +1719,39 @@ type ComponentStatusList struct { Items []ComponentStatus `json:"items" description:"list of component status objects"` } + +// SecurityContext holds security configuration that will be applied to a container. SecurityContext +// contains duplication of some existing fields from the Container resource. These duplicate fields +// will be populated based on the Container configuration if they are not set. Defining them on +// both the Container AND the SecurityContext will result in an error. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + // Must match Container.Capabilities or be unset. Will be defaulted to Container.Capabilities if left unset + Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"` + + // Run the container in privileged mode + // Must match Container.Privileged or be unset. Will be defaulted to Container.Privileged if left unset + Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"` + + // RunAsUser is the UID to run the entrypoint of the container process. + RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"` +} + +// SELinuxOptions are the labels to be applied to the container. +type SELinuxOptions struct { + // SELinux user label + User string `json:"user,omitempty" description:"the user label to apply to the container"` + + // SELinux role label + Role string `json:"role,omitempty" description:"the role label to apply to the container"` + + // SELinux type label + Type string `json:"type,omitempty" description:"the type label to apply to the container"` + + // SELinux level label. + Level string `json:"level,omitempty" description:"the level label to apply to the container"` +} diff --git a/pkg/api/v1beta3/conversion.go b/pkg/api/v1beta3/conversion.go index b78c0b53ce..52d160eb42 100644 --- a/pkg/api/v1beta3/conversion.go +++ b/pkg/api/v1beta3/conversion.go @@ -18,6 +18,7 @@ package v1beta3 import ( "fmt" + "reflect" newer "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" @@ -67,38 +68,6 @@ func convert_api_Binding_To_v1beta3_Binding(in *newer.Binding, out *Binding, s c return nil } -func convert_v1beta3_Capabilities_To_api_Capabilities(in *Capabilities, out *newer.Capabilities, s conversion.Scope) error { - if in.Add != nil { - out.Add = make([]newer.CapabilityType, len(in.Add)) - for i := range in.Add { - out.Add[i] = newer.CapabilityType(in.Add[i]) - } - } - if in.Drop != nil { - out.Drop = make([]newer.CapabilityType, len(in.Drop)) - for i := range in.Drop { - out.Drop[i] = newer.CapabilityType(in.Drop[i]) - } - } - return nil -} - -func convert_api_Capabilities_To_v1beta3_Capabilities(in *newer.Capabilities, out *Capabilities, s conversion.Scope) error { - if in.Add != nil { - out.Add = make([]CapabilityType, len(in.Add)) - for i := range in.Add { - out.Add[i] = CapabilityType(in.Add[i]) - } - } - if in.Drop != nil { - out.Drop = make([]CapabilityType, len(in.Drop)) - for i := range in.Drop { - out.Drop[i] = CapabilityType(in.Drop[i]) - } - } - return nil -} - func convert_v1beta3_ComponentCondition_To_api_ComponentCondition(in *ComponentCondition, out *newer.ComponentCondition, s conversion.Scope) error { out.Type = newer.ComponentConditionType(in.Type) out.Status = newer.ConditionStatus(in.Status) @@ -187,128 +156,6 @@ func convert_api_ComponentStatusList_To_v1beta3_ComponentStatusList(in *newer.Co return nil } -func convert_v1beta3_Container_To_api_Container(in *Container, out *newer.Container, s conversion.Scope) error { - out.Name = in.Name - out.Image = in.Image - if in.Command != nil { - out.Command = make([]string, len(in.Command)) - for i := range in.Command { - out.Command[i] = in.Command[i] - } - } - if in.Args != nil { - out.Args = make([]string, len(in.Args)) - for i := range in.Args { - out.Args[i] = in.Args[i] - } - } - out.WorkingDir = in.WorkingDir - if in.Ports != nil { - out.Ports = make([]newer.ContainerPort, len(in.Ports)) - for i := range in.Ports { - if err := s.Convert(&in.Ports[i], &out.Ports[i], 0); err != nil { - return err - } - } - } - if in.Env != nil { - out.Env = make([]newer.EnvVar, len(in.Env)) - for i := range in.Env { - if err := s.Convert(&in.Env[i], &out.Env[i], 0); err != nil { - return err - } - } - } - if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { - return err - } - if in.VolumeMounts != nil { - out.VolumeMounts = make([]newer.VolumeMount, len(in.VolumeMounts)) - for i := range in.VolumeMounts { - if err := s.Convert(&in.VolumeMounts[i], &out.VolumeMounts[i], 0); err != nil { - return err - } - } - } - if err := s.Convert(&in.LivenessProbe, &out.LivenessProbe, 0); err != nil { - return err - } - if err := s.Convert(&in.ReadinessProbe, &out.ReadinessProbe, 0); err != nil { - return err - } - if err := s.Convert(&in.Lifecycle, &out.Lifecycle, 0); err != nil { - return err - } - out.TerminationMessagePath = in.TerminationMessagePath - out.Privileged = in.Privileged - out.ImagePullPolicy = newer.PullPolicy(in.ImagePullPolicy) - if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { - return err - } - return nil -} - -func convert_api_Container_To_v1beta3_Container(in *newer.Container, out *Container, s conversion.Scope) error { - out.Name = in.Name - out.Image = in.Image - if in.Command != nil { - out.Command = make([]string, len(in.Command)) - for i := range in.Command { - out.Command[i] = in.Command[i] - } - } - if in.Args != nil { - out.Args = make([]string, len(in.Args)) - for i := range in.Args { - out.Args[i] = in.Args[i] - } - } - out.WorkingDir = in.WorkingDir - if in.Ports != nil { - out.Ports = make([]ContainerPort, len(in.Ports)) - for i := range in.Ports { - if err := s.Convert(&in.Ports[i], &out.Ports[i], 0); err != nil { - return err - } - } - } - if in.Env != nil { - out.Env = make([]EnvVar, len(in.Env)) - for i := range in.Env { - if err := s.Convert(&in.Env[i], &out.Env[i], 0); err != nil { - return err - } - } - } - if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { - return err - } - if in.VolumeMounts != nil { - out.VolumeMounts = make([]VolumeMount, len(in.VolumeMounts)) - for i := range in.VolumeMounts { - if err := s.Convert(&in.VolumeMounts[i], &out.VolumeMounts[i], 0); err != nil { - return err - } - } - } - if err := s.Convert(&in.LivenessProbe, &out.LivenessProbe, 0); err != nil { - return err - } - if err := s.Convert(&in.ReadinessProbe, &out.ReadinessProbe, 0); err != nil { - return err - } - if err := s.Convert(&in.Lifecycle, &out.Lifecycle, 0); err != nil { - return err - } - out.TerminationMessagePath = in.TerminationMessagePath - out.Privileged = in.Privileged - out.ImagePullPolicy = PullPolicy(in.ImagePullPolicy) - if err := s.Convert(&in.Capabilities, &out.Capabilities, 0); err != nil { - return err - } - return nil -} - func convert_v1beta3_ContainerPort_To_api_ContainerPort(in *ContainerPort, out *newer.ContainerPort, s conversion.Scope) error { out.Name = in.Name out.HostPort = in.HostPort @@ -1797,38 +1644,6 @@ func convert_api_PersistentVolumeStatus_To_v1beta3_PersistentVolumeStatus(in *ne return nil } -func convert_v1beta3_Pod_To_api_Pod(in *Pod, out *newer.Pod, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { - return err - } - return nil -} - -func convert_api_Pod_To_v1beta3_Pod(in *newer.Pod, out *Pod, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { - return err - } - return nil -} - func convert_v1beta3_PodCondition_To_api_PodCondition(in *PodCondition, out *newer.PodCondition, s conversion.Scope) error { out.Type = newer.PodConditionType(in.Type) out.Status = newer.ConditionStatus(in.Status) @@ -1867,42 +1682,6 @@ func convert_api_PodExecOptions_To_v1beta3_PodExecOptions(in *newer.PodExecOptio return nil } -func convert_v1beta3_PodList_To_api_PodList(in *PodList, out *newer.PodList, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { - return err - } - if in.Items != nil { - out.Items = make([]newer.Pod, len(in.Items)) - for i := range in.Items { - if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { - return err - } - } - } - return nil -} - -func convert_api_PodList_To_v1beta3_PodList(in *newer.PodList, out *PodList, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { - return err - } - if in.Items != nil { - out.Items = make([]Pod, len(in.Items)) - for i := range in.Items { - if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { - return err - } - } - } - return nil -} - func convert_v1beta3_PodLogOptions_To_api_PodLogOptions(in *PodLogOptions, out *newer.PodLogOptions, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -1937,74 +1716,6 @@ func convert_api_PodProxyOptions_To_v1beta3_PodProxyOptions(in *newer.PodProxyOp return nil } -func convert_v1beta3_PodSpec_To_api_PodSpec(in *PodSpec, out *newer.PodSpec, s conversion.Scope) error { - if in.Volumes != nil { - out.Volumes = make([]newer.Volume, len(in.Volumes)) - for i := range in.Volumes { - if err := s.Convert(&in.Volumes[i], &out.Volumes[i], 0); err != nil { - return err - } - } - } - if in.Containers != nil { - out.Containers = make([]newer.Container, len(in.Containers)) - for i := range in.Containers { - if err := s.Convert(&in.Containers[i], &out.Containers[i], 0); err != nil { - return err - } - } - } - out.RestartPolicy = newer.RestartPolicy(in.RestartPolicy) - if in.TerminationGracePeriodSeconds != nil { - out.TerminationGracePeriodSeconds = new(int64) - *out.TerminationGracePeriodSeconds = *in.TerminationGracePeriodSeconds - } - out.DNSPolicy = newer.DNSPolicy(in.DNSPolicy) - if in.NodeSelector != nil { - out.NodeSelector = make(map[string]string) - for key, val := range in.NodeSelector { - out.NodeSelector[key] = val - } - } - out.Host = in.Host - out.HostNetwork = in.HostNetwork - return nil -} - -func convert_api_PodSpec_To_v1beta3_PodSpec(in *newer.PodSpec, out *PodSpec, s conversion.Scope) error { - if in.Volumes != nil { - out.Volumes = make([]Volume, len(in.Volumes)) - for i := range in.Volumes { - if err := s.Convert(&in.Volumes[i], &out.Volumes[i], 0); err != nil { - return err - } - } - } - if in.Containers != nil { - out.Containers = make([]Container, len(in.Containers)) - for i := range in.Containers { - if err := s.Convert(&in.Containers[i], &out.Containers[i], 0); err != nil { - return err - } - } - } - out.RestartPolicy = RestartPolicy(in.RestartPolicy) - if in.TerminationGracePeriodSeconds != nil { - out.TerminationGracePeriodSeconds = new(int64) - *out.TerminationGracePeriodSeconds = *in.TerminationGracePeriodSeconds - } - out.DNSPolicy = DNSPolicy(in.DNSPolicy) - if in.NodeSelector != nil { - out.NodeSelector = make(map[string]string) - for key, val := range in.NodeSelector { - out.NodeSelector[key] = val - } - } - out.Host = in.Host - out.HostNetwork = in.HostNetwork - return nil -} - func convert_v1beta3_PodStatus_To_api_PodStatus(in *PodStatus, out *newer.PodStatus, s conversion.Scope) error { out.Phase = newer.PodPhase(in.Phase) if in.Conditions != nil { @@ -2079,88 +1790,6 @@ func convert_api_PodStatusResult_To_v1beta3_PodStatusResult(in *newer.PodStatusR return nil } -func convert_v1beta3_PodTemplate_To_api_PodTemplate(in *PodTemplate, out *newer.PodTemplate, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Template, &out.Template, 0); err != nil { - return err - } - return nil -} - -func convert_api_PodTemplate_To_v1beta3_PodTemplate(in *newer.PodTemplate, out *PodTemplate, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Template, &out.Template, 0); err != nil { - return err - } - return nil -} - -func convert_v1beta3_PodTemplateList_To_api_PodTemplateList(in *PodTemplateList, out *newer.PodTemplateList, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { - return err - } - if in.Items != nil { - out.Items = make([]newer.PodTemplate, len(in.Items)) - for i := range in.Items { - if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { - return err - } - } - } - return nil -} - -func convert_api_PodTemplateList_To_v1beta3_PodTemplateList(in *newer.PodTemplateList, out *PodTemplateList, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { - return err - } - if in.Items != nil { - out.Items = make([]PodTemplate, len(in.Items)) - for i := range in.Items { - if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { - return err - } - } - } - return nil -} - -func convert_v1beta3_PodTemplateSpec_To_api_PodTemplateSpec(in *PodTemplateSpec, out *newer.PodTemplateSpec, s conversion.Scope) error { - if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { - return err - } - return nil -} - -func convert_api_PodTemplateSpec_To_v1beta3_PodTemplateSpec(in *newer.PodTemplateSpec, out *PodTemplateSpec, s conversion.Scope) error { - if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { - return err - } - return nil -} - func convert_v1beta3_Probe_To_api_Probe(in *Probe, out *newer.Probe, s conversion.Scope) error { if err := s.Convert(&in.Handler, &out.Handler, 0); err != nil { return err @@ -2179,118 +1808,6 @@ func convert_api_Probe_To_v1beta3_Probe(in *newer.Probe, out *Probe, s conversio return nil } -func convert_v1beta3_ReplicationController_To_api_ReplicationController(in *ReplicationController, out *newer.ReplicationController, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { - return err - } - return nil -} - -func convert_api_ReplicationController_To_v1beta3_ReplicationController(in *newer.ReplicationController, out *ReplicationController, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { - return err - } - if err := s.Convert(&in.Status, &out.Status, 0); err != nil { - return err - } - return nil -} - -func convert_v1beta3_ReplicationControllerList_To_api_ReplicationControllerList(in *ReplicationControllerList, out *newer.ReplicationControllerList, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { - return err - } - if in.Items != nil { - out.Items = make([]newer.ReplicationController, len(in.Items)) - for i := range in.Items { - if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { - return err - } - } - } - return nil -} - -func convert_api_ReplicationControllerList_To_v1beta3_ReplicationControllerList(in *newer.ReplicationControllerList, out *ReplicationControllerList, s conversion.Scope) error { - if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { - return err - } - if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { - return err - } - if in.Items != nil { - out.Items = make([]ReplicationController, len(in.Items)) - for i := range in.Items { - if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { - return err - } - } - } - return nil -} - -func convert_v1beta3_ReplicationControllerSpec_To_api_ReplicationControllerSpec(in *ReplicationControllerSpec, out *newer.ReplicationControllerSpec, s conversion.Scope) error { - out.Replicas = in.Replicas - if in.Selector != nil { - out.Selector = make(map[string]string) - for key, val := range in.Selector { - out.Selector[key] = val - } - } - if err := s.Convert(&in.TemplateRef, &out.TemplateRef, 0); err != nil { - return err - } - if err := s.Convert(&in.Template, &out.Template, 0); err != nil { - return err - } - return nil -} - -func convert_api_ReplicationControllerSpec_To_v1beta3_ReplicationControllerSpec(in *newer.ReplicationControllerSpec, out *ReplicationControllerSpec, s conversion.Scope) error { - out.Replicas = in.Replicas - if in.Selector != nil { - out.Selector = make(map[string]string) - for key, val := range in.Selector { - out.Selector[key] = val - } - } - if err := s.Convert(&in.TemplateRef, &out.TemplateRef, 0); err != nil { - return err - } - if err := s.Convert(&in.Template, &out.Template, 0); err != nil { - return err - } - return nil -} - -func convert_v1beta3_ReplicationControllerStatus_To_api_ReplicationControllerStatus(in *ReplicationControllerStatus, out *newer.ReplicationControllerStatus, s conversion.Scope) error { - out.Replicas = in.Replicas - return nil -} - -func convert_api_ReplicationControllerStatus_To_v1beta3_ReplicationControllerStatus(in *newer.ReplicationControllerStatus, out *ReplicationControllerStatus, s conversion.Scope) error { - out.Replicas = in.Replicas - return nil -} - func convert_v1beta3_ResourceQuota_To_api_ResourceQuota(in *ResourceQuota, out *newer.ResourceQuota, s conversion.Scope) error { if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { return err @@ -2929,7 +2446,6 @@ func init() { err := newer.Scheme.AddGeneratedConversionFuncs( convert_api_AWSElasticBlockStoreVolumeSource_To_v1beta3_AWSElasticBlockStoreVolumeSource, convert_api_Binding_To_v1beta3_Binding, - convert_api_Capabilities_To_v1beta3_Capabilities, convert_api_ComponentCondition_To_v1beta3_ComponentCondition, convert_api_ComponentStatusList_To_v1beta3_ComponentStatusList, convert_api_ComponentStatus_To_v1beta3_ComponentStatus, @@ -2939,7 +2455,6 @@ func init() { convert_api_ContainerStateWaiting_To_v1beta3_ContainerStateWaiting, convert_api_ContainerState_To_v1beta3_ContainerState, convert_api_ContainerStatus_To_v1beta3_ContainerStatus, - convert_api_Container_To_v1beta3_Container, convert_api_DeleteOptions_To_v1beta3_DeleteOptions, convert_api_EmptyDirVolumeSource_To_v1beta3_EmptyDirVolumeSource, convert_api_EndpointAddress_To_v1beta3_EndpointAddress, @@ -2993,21 +2508,11 @@ func init() { convert_api_PersistentVolume_To_v1beta3_PersistentVolume, convert_api_PodCondition_To_v1beta3_PodCondition, convert_api_PodExecOptions_To_v1beta3_PodExecOptions, - convert_api_PodList_To_v1beta3_PodList, convert_api_PodLogOptions_To_v1beta3_PodLogOptions, convert_api_PodProxyOptions_To_v1beta3_PodProxyOptions, - convert_api_PodSpec_To_v1beta3_PodSpec, convert_api_PodStatusResult_To_v1beta3_PodStatusResult, convert_api_PodStatus_To_v1beta3_PodStatus, - convert_api_PodTemplateList_To_v1beta3_PodTemplateList, - convert_api_PodTemplateSpec_To_v1beta3_PodTemplateSpec, - convert_api_PodTemplate_To_v1beta3_PodTemplate, - convert_api_Pod_To_v1beta3_Pod, convert_api_Probe_To_v1beta3_Probe, - convert_api_ReplicationControllerList_To_v1beta3_ReplicationControllerList, - convert_api_ReplicationControllerSpec_To_v1beta3_ReplicationControllerSpec, - convert_api_ReplicationControllerStatus_To_v1beta3_ReplicationControllerStatus, - convert_api_ReplicationController_To_v1beta3_ReplicationController, convert_api_ResourceQuotaList_To_v1beta3_ResourceQuotaList, convert_api_ResourceQuotaSpec_To_v1beta3_ResourceQuotaSpec, convert_api_ResourceQuotaStatus_To_v1beta3_ResourceQuotaStatus, @@ -3031,7 +2536,6 @@ func init() { convert_api_Volume_To_v1beta3_Volume, convert_v1beta3_AWSElasticBlockStoreVolumeSource_To_api_AWSElasticBlockStoreVolumeSource, convert_v1beta3_Binding_To_api_Binding, - convert_v1beta3_Capabilities_To_api_Capabilities, convert_v1beta3_ComponentCondition_To_api_ComponentCondition, convert_v1beta3_ComponentStatusList_To_api_ComponentStatusList, convert_v1beta3_ComponentStatus_To_api_ComponentStatus, @@ -3041,7 +2545,6 @@ func init() { convert_v1beta3_ContainerStateWaiting_To_api_ContainerStateWaiting, convert_v1beta3_ContainerState_To_api_ContainerState, convert_v1beta3_ContainerStatus_To_api_ContainerStatus, - convert_v1beta3_Container_To_api_Container, convert_v1beta3_DeleteOptions_To_api_DeleteOptions, convert_v1beta3_EmptyDirVolumeSource_To_api_EmptyDirVolumeSource, convert_v1beta3_EndpointAddress_To_api_EndpointAddress, @@ -3095,21 +2598,11 @@ func init() { convert_v1beta3_PersistentVolume_To_api_PersistentVolume, convert_v1beta3_PodCondition_To_api_PodCondition, convert_v1beta3_PodExecOptions_To_api_PodExecOptions, - convert_v1beta3_PodList_To_api_PodList, convert_v1beta3_PodLogOptions_To_api_PodLogOptions, convert_v1beta3_PodProxyOptions_To_api_PodProxyOptions, - convert_v1beta3_PodSpec_To_api_PodSpec, convert_v1beta3_PodStatusResult_To_api_PodStatusResult, convert_v1beta3_PodStatus_To_api_PodStatus, - convert_v1beta3_PodTemplateList_To_api_PodTemplateList, - convert_v1beta3_PodTemplateSpec_To_api_PodTemplateSpec, - convert_v1beta3_PodTemplate_To_api_PodTemplate, - convert_v1beta3_Pod_To_api_Pod, convert_v1beta3_Probe_To_api_Probe, - convert_v1beta3_ReplicationControllerList_To_api_ReplicationControllerList, - convert_v1beta3_ReplicationControllerSpec_To_api_ReplicationControllerSpec, - convert_v1beta3_ReplicationControllerStatus_To_api_ReplicationControllerStatus, - convert_v1beta3_ReplicationController_To_api_ReplicationController, convert_v1beta3_ResourceQuotaList_To_api_ResourceQuotaList, convert_v1beta3_ResourceQuotaSpec_To_api_ResourceQuotaSpec, convert_v1beta3_ResourceQuotaStatus_To_api_ResourceQuotaStatus, @@ -3133,6 +2626,32 @@ func init() { convert_v1beta3_Volume_To_api_Volume, ) + // Add non-generated conversion functions + newer.Scheme.AddConversionFuncs( + convert_v1beta3_Pod_To_api_Pod, + convert_api_Pod_To_v1beta3_Pod, + convert_v1beta3_ReplicationController_To_api_ReplicationController, + convert_api_ReplicationController_To_v1beta3_ReplicationController, + convert_v1beta3_ReplicationControllerList_To_api_ReplicationControllerList, + convert_api_ReplicationControllerList_To_v1beta3_ReplicationControllerList, + convert_v1beta3_PodList_To_api_PodList, + convert_api_PodList_To_v1beta3_PodList, + convert_v1beta3_PodTemplate_To_api_PodTemplate, + convert_api_PodTemplate_To_v1beta3_PodTemplate, + convert_v1beta3_PodTemplateList_To_api_PodTemplateList, + convert_api_PodTemplateList_To_v1beta3_PodTemplateList, + convert_v1beta3_PodSpec_To_api_PodSpec, + convert_api_PodSpec_To_v1beta3_PodSpec, + convert_v1beta3_PodTemplateSpec_To_api_PodTemplateSpec, + convert_api_PodTemplateSpec_To_v1beta3_PodTemplateSpec, + convert_v1beta3_ReplicationControllerSpec_To_api_ReplicationControllerSpec, + convert_api_ReplicationControllerSpec_To_v1beta3_ReplicationControllerSpec, + convert_v1beta3_ReplicationControllerStatus_To_api_ReplicationControllerStatus, + convert_api_ReplicationControllerStatus_To_v1beta3_ReplicationControllerStatus, + convert_v1beta3_Container_To_api_Container, + convert_api_Container_To_v1beta3_Container, + ) + // Add field conversion funcs. err = newer.Scheme.AddFieldLabelConversionFunc("v1beta3", "Pod", func(label, value string) (string, string, error) { @@ -3227,3 +2746,474 @@ func init() { panic(err) } } + +func convert_v1beta3_Pod_To_api_Pod(in *Pod, out *newer.Pod, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil +} + +func convert_api_Pod_To_v1beta3_Pod(in *newer.Pod, out *Pod, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil +} + +func convert_v1beta3_ReplicationController_To_api_ReplicationController(in *ReplicationController, out *newer.ReplicationController, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil +} + +func convert_api_ReplicationController_To_v1beta3_ReplicationController(in *newer.ReplicationController, out *ReplicationController, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { + return err + } + if err := s.Convert(&in.Status, &out.Status, 0); err != nil { + return err + } + return nil +} + +func convert_v1beta3_ReplicationControllerList_To_api_ReplicationControllerList(in *ReplicationControllerList, out *newer.ReplicationControllerList, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]newer.ReplicationController, len(in.Items)) + for i := range in.Items { + if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { + return err + } + } + } + return nil +} + +func convert_api_ReplicationControllerList_To_v1beta3_ReplicationControllerList(in *newer.ReplicationControllerList, out *ReplicationControllerList, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]ReplicationController, len(in.Items)) + for i := range in.Items { + if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { + return err + } + } + } + return nil +} + +func convert_v1beta3_PodList_To_api_PodList(in *PodList, out *newer.PodList, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]newer.Pod, len(in.Items)) + for i := range in.Items { + if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { + return err + } + } + } + return nil +} + +func convert_api_PodList_To_v1beta3_PodList(in *newer.PodList, out *PodList, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]Pod, len(in.Items)) + for i := range in.Items { + if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { + return err + } + } + } + return nil +} + +func convert_v1beta3_PodTemplate_To_api_PodTemplate(in *PodTemplate, out *newer.PodTemplate, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Template, &out.Template, 0); err != nil { + return err + } + return nil +} + +func convert_api_PodTemplate_To_v1beta3_PodTemplate(in *newer.PodTemplate, out *PodTemplate, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Template, &out.Template, 0); err != nil { + return err + } + return nil +} + +func convert_v1beta3_PodTemplateList_To_api_PodTemplateList(in *PodTemplateList, out *newer.PodTemplateList, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]newer.PodTemplate, len(in.Items)) + for i := range in.Items { + if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { + return err + } + } + } + return nil +} + +func convert_api_PodTemplateList_To_v1beta3_PodTemplateList(in *newer.PodTemplateList, out *PodTemplateList, s conversion.Scope) error { + if err := s.Convert(&in.TypeMeta, &out.TypeMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.ListMeta, &out.ListMeta, 0); err != nil { + return err + } + if in.Items != nil { + out.Items = make([]PodTemplate, len(in.Items)) + for i := range in.Items { + if err := s.Convert(&in.Items[i], &out.Items[i], 0); err != nil { + return err + } + } + } + return nil +} + +func convert_v1beta3_PodSpec_To_api_PodSpec(in *PodSpec, out *newer.PodSpec, s conversion.Scope) error { + if in.Volumes != nil { + out.Volumes = make([]newer.Volume, len(in.Volumes)) + for i := range in.Volumes { + if err := s.Convert(&in.Volumes[i], &out.Volumes[i], 0); err != nil { + return err + } + } + } + if in.Containers != nil { + out.Containers = make([]newer.Container, len(in.Containers)) + for i := range in.Containers { + if err := s.Convert(&in.Containers[i], &out.Containers[i], 0); err != nil { + return err + } + } + } + out.RestartPolicy = newer.RestartPolicy(in.RestartPolicy) + if in.TerminationGracePeriodSeconds != nil { + out.TerminationGracePeriodSeconds = new(int64) + *out.TerminationGracePeriodSeconds = *in.TerminationGracePeriodSeconds + } + out.DNSPolicy = newer.DNSPolicy(in.DNSPolicy) + if in.NodeSelector != nil { + out.NodeSelector = make(map[string]string) + for key, val := range in.NodeSelector { + out.NodeSelector[key] = val + } + } + out.Host = in.Host + out.HostNetwork = in.HostNetwork + return nil +} + +func convert_api_PodSpec_To_v1beta3_PodSpec(in *newer.PodSpec, out *PodSpec, s conversion.Scope) error { + if in.Volumes != nil { + out.Volumes = make([]Volume, len(in.Volumes)) + for i := range in.Volumes { + if err := s.Convert(&in.Volumes[i], &out.Volumes[i], 0); err != nil { + return err + } + } + } + if in.Containers != nil { + out.Containers = make([]Container, len(in.Containers)) + for i := range in.Containers { + if err := s.Convert(&in.Containers[i], &out.Containers[i], 0); err != nil { + return err + } + } + } + out.RestartPolicy = RestartPolicy(in.RestartPolicy) + if in.TerminationGracePeriodSeconds != nil { + out.TerminationGracePeriodSeconds = new(int64) + *out.TerminationGracePeriodSeconds = *in.TerminationGracePeriodSeconds + } + out.DNSPolicy = DNSPolicy(in.DNSPolicy) + if in.NodeSelector != nil { + out.NodeSelector = make(map[string]string) + for key, val := range in.NodeSelector { + out.NodeSelector[key] = val + } + } + out.Host = in.Host + out.HostNetwork = in.HostNetwork + return nil +} + +func convert_v1beta3_PodTemplateSpec_To_api_PodTemplateSpec(in *PodTemplateSpec, out *newer.PodTemplateSpec, s conversion.Scope) error { + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { + return err + } + return nil +} + +func convert_api_PodTemplateSpec_To_v1beta3_PodTemplateSpec(in *newer.PodTemplateSpec, out *PodTemplateSpec, s conversion.Scope) error { + if err := s.Convert(&in.ObjectMeta, &out.ObjectMeta, 0); err != nil { + return err + } + if err := s.Convert(&in.Spec, &out.Spec, 0); err != nil { + return err + } + return nil +} + +func convert_v1beta3_ReplicationControllerSpec_To_api_ReplicationControllerSpec(in *ReplicationControllerSpec, out *newer.ReplicationControllerSpec, s conversion.Scope) error { + out.Replicas = in.Replicas + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } + if err := s.Convert(&in.TemplateRef, &out.TemplateRef, 0); err != nil { + return err + } + if err := s.Convert(&in.Template, &out.Template, 0); err != nil { + return err + } + return nil +} + +func convert_api_ReplicationControllerSpec_To_v1beta3_ReplicationControllerSpec(in *newer.ReplicationControllerSpec, out *ReplicationControllerSpec, s conversion.Scope) error { + out.Replicas = in.Replicas + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } + if err := s.Convert(&in.TemplateRef, &out.TemplateRef, 0); err != nil { + return err + } + if err := s.Convert(&in.Template, &out.Template, 0); err != nil { + return err + } + return nil +} + +func convert_v1beta3_ReplicationControllerStatus_To_api_ReplicationControllerStatus(in *ReplicationControllerStatus, out *newer.ReplicationControllerStatus, s conversion.Scope) error { + out.Replicas = in.Replicas + return nil +} + +func convert_api_ReplicationControllerStatus_To_v1beta3_ReplicationControllerStatus(in *newer.ReplicationControllerStatus, out *ReplicationControllerStatus, s conversion.Scope) error { + out.Replicas = in.Replicas + return nil +} + +func convert_v1beta3_Container_To_api_Container(in *Container, out *newer.Container, s conversion.Scope) error { + out.Name = in.Name + out.Image = in.Image + if in.Command != nil { + out.Command = make([]string, len(in.Command)) + for i := range in.Command { + out.Command[i] = in.Command[i] + } + } + if in.Args != nil { + out.Args = make([]string, len(in.Args)) + for i := range in.Args { + out.Args[i] = in.Args[i] + } + } + out.WorkingDir = in.WorkingDir + if in.Ports != nil { + out.Ports = make([]newer.ContainerPort, len(in.Ports)) + for i := range in.Ports { + if err := s.Convert(&in.Ports[i], &out.Ports[i], 0); err != nil { + return err + } + } + } + if in.Env != nil { + out.Env = make([]newer.EnvVar, len(in.Env)) + for i := range in.Env { + if err := s.Convert(&in.Env[i], &out.Env[i], 0); err != nil { + return err + } + } + } + if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { + return err + } + if in.VolumeMounts != nil { + out.VolumeMounts = make([]newer.VolumeMount, len(in.VolumeMounts)) + for i := range in.VolumeMounts { + if err := s.Convert(&in.VolumeMounts[i], &out.VolumeMounts[i], 0); err != nil { + return err + } + } + } + if err := s.Convert(&in.LivenessProbe, &out.LivenessProbe, 0); err != nil { + return err + } + if err := s.Convert(&in.ReadinessProbe, &out.ReadinessProbe, 0); err != nil { + return err + } + if err := s.Convert(&in.Lifecycle, &out.Lifecycle, 0); err != nil { + return err + } + out.TerminationMessagePath = in.TerminationMessagePath + out.ImagePullPolicy = newer.PullPolicy(in.ImagePullPolicy) + if in.SecurityContext != nil { + if in.SecurityContext.Capabilities != nil { + if !reflect.DeepEqual(in.SecurityContext.Capabilities.Add, in.Capabilities.Add) || + !reflect.DeepEqual(in.SecurityContext.Capabilities.Drop, in.Capabilities.Drop) { + return fmt.Errorf("container capability settings do not match security context settings, cannot convert") + } + } + if in.SecurityContext.Privileged != nil { + if in.Privileged != *in.SecurityContext.Privileged { + return fmt.Errorf("container privileged settings do not match security context settings, cannot convert") + } + } + } + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { + return err + } + return nil +} + +func convert_api_Container_To_v1beta3_Container(in *newer.Container, out *Container, s conversion.Scope) error { + out.Name = in.Name + out.Image = in.Image + if in.Command != nil { + out.Command = make([]string, len(in.Command)) + for i := range in.Command { + out.Command[i] = in.Command[i] + } + } + if in.Args != nil { + out.Args = make([]string, len(in.Args)) + for i := range in.Args { + out.Args[i] = in.Args[i] + } + } + out.WorkingDir = in.WorkingDir + if in.Ports != nil { + out.Ports = make([]ContainerPort, len(in.Ports)) + for i := range in.Ports { + if err := s.Convert(&in.Ports[i], &out.Ports[i], 0); err != nil { + return err + } + } + } + if in.Env != nil { + out.Env = make([]EnvVar, len(in.Env)) + for i := range in.Env { + if err := s.Convert(&in.Env[i], &out.Env[i], 0); err != nil { + return err + } + } + } + if err := s.Convert(&in.Resources, &out.Resources, 0); err != nil { + return err + } + if in.VolumeMounts != nil { + out.VolumeMounts = make([]VolumeMount, len(in.VolumeMounts)) + for i := range in.VolumeMounts { + if err := s.Convert(&in.VolumeMounts[i], &out.VolumeMounts[i], 0); err != nil { + return err + } + } + } + if err := s.Convert(&in.LivenessProbe, &out.LivenessProbe, 0); err != nil { + return err + } + if err := s.Convert(&in.ReadinessProbe, &out.ReadinessProbe, 0); err != nil { + return err + } + if err := s.Convert(&in.Lifecycle, &out.Lifecycle, 0); err != nil { + return err + } + out.TerminationMessagePath = in.TerminationMessagePath + out.ImagePullPolicy = PullPolicy(in.ImagePullPolicy) + if err := s.Convert(&in.SecurityContext, &out.SecurityContext, 0); err != nil { + return err + } + // now that we've converted set the container field from security context + if out.SecurityContext != nil && out.SecurityContext.Privileged != nil { + out.Privileged = *out.SecurityContext.Privileged + } + // now that we've converted set the container field from security context + if out.SecurityContext != nil && out.SecurityContext.Capabilities != nil { + out.Capabilities = *out.SecurityContext.Capabilities + } + return nil +} diff --git a/pkg/api/v1beta3/conversion_test.go b/pkg/api/v1beta3/conversion_test.go index ac1c2962f9..497c1a7c8c 100644 --- a/pkg/api/v1beta3/conversion_test.go +++ b/pkg/api/v1beta3/conversion_test.go @@ -45,3 +45,63 @@ func TestNodeConversion(t *testing.T) { t.Fatalf("unexpected error: %v", err) } } + +func TestBadSecurityContextConversion(t *testing.T) { + priv := false + testCases := map[string]struct { + c *current.Container + err string + }{ + // this use case must use true for the container and false for the sc. Otherwise the defaulter + // will assume privileged was left undefined (since it is the default value) and copy the + // sc setting upwards + "mismatched privileged": { + c: ¤t.Container{ + Privileged: true, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + }, + }, + err: "container privileged settings do not match security context settings, cannot convert", + }, + "mismatched caps add": { + c: ¤t.Container{ + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"bar"}, + }, + }, + }, + err: "container capability settings do not match security context settings, cannot convert", + }, + "mismatched caps drop": { + c: ¤t.Container{ + Capabilities: current.Capabilities{ + Drop: []current.CapabilityType{"foo"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + err: "container capability settings do not match security context settings, cannot convert", + }, + } + + for k, v := range testCases { + got := newer.Container{} + err := newer.Scheme.Convert(v.c, &got) + if err == nil { + t.Errorf("expected error for case %s but got none", k) + } else { + if err.Error() != v.err { + t.Errorf("unexpected error for case %s. Expected: %s but got: %s", k, v.err, err.Error()) + } + } + } + +} diff --git a/pkg/api/v1beta3/defaults.go b/pkg/api/v1beta3/defaults.go index fd3491093a..0eb7d7d099 100644 --- a/pkg/api/v1beta3/defaults.go +++ b/pkg/api/v1beta3/defaults.go @@ -21,6 +21,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" + "github.com/golang/glog" ) func init() { @@ -66,6 +67,7 @@ func init() { if obj.TerminationMessagePath == "" { obj.TerminationMessagePath = TerminationMessagePathDefault } + defaultSecurityContext(obj) }, func(obj *ServiceSpec) { if obj.SessionAffinity == "" { @@ -156,3 +158,44 @@ func defaultHostNetworkPorts(containers *[]Container) { } } } + +// defaultSecurityContext performs the downward and upward merges of a pod definition +func defaultSecurityContext(container *Container) { + if container.SecurityContext == nil { + glog.V(4).Infof("creating security context for container %s", container.Name) + container.SecurityContext = &SecurityContext{} + } + // if there are no capabilities defined on the SecurityContext then copy the container settings + if container.SecurityContext.Capabilities == nil { + glog.V(4).Infof("downward merge of container.Capabilities for container %s", container.Name) + container.SecurityContext.Capabilities = &container.Capabilities + } else { + // if there are capabilities defined on the security context and the container setting is + // empty then assume that it was left off the pod definition and ensure that the container + // settings match the security context settings (checked by the convert functions). If + // there are settings in both then don't touch it, the converter will error if they don't + // match + if len(container.Capabilities.Add) == 0 { + glog.V(4).Infof("upward merge of container.Capabilities.Add for container %s", container.Name) + container.Capabilities.Add = container.SecurityContext.Capabilities.Add + } + if len(container.Capabilities.Drop) == 0 { + glog.V(4).Infof("upward merge of container.Capabilities.Drop for container %s", container.Name) + container.Capabilities.Drop = container.SecurityContext.Capabilities.Drop + } + } + // if there are no privileged settings on the security context then copy the container settings + if container.SecurityContext.Privileged == nil { + glog.V(4).Infof("downward merge of container.Privileged for container %s", container.Name) + container.SecurityContext.Privileged = &container.Privileged + } else { + // we don't have a good way to know if container.Privileged was set or just defaulted to false + // so the best we can do here is check if the securityContext is set to true and the + // container is set to false and assume that the Privileged field was left off the container + // definition and not an intentional mismatch + if *container.SecurityContext.Privileged && !container.Privileged { + glog.V(4).Infof("upward merge of container.Privileged for container %s", container.Name) + container.Privileged = *container.SecurityContext.Privileged + } + } +} diff --git a/pkg/api/v1beta3/defaults_test.go b/pkg/api/v1beta3/defaults_test.go index 4bd9f652e2..dca59583ec 100644 --- a/pkg/api/v1beta3/defaults_test.go +++ b/pkg/api/v1beta3/defaults_test.go @@ -349,3 +349,104 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) { t.Errorf("Expected default APIVersion v1beta3, got: %v", apiVersion) } } + +func TestSetDefaultSecurityContext(t *testing.T) { + priv := false + privTrue := true + testCases := map[string]struct { + c current.Container + }{ + "downward defaulting caps": { + c: current.Container{ + Privileged: false, + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + }, + }, + }, + "downward defaulting priv": { + c: current.Container{ + Privileged: false, + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + }, + "upward defaulting caps": { + c: current.Container{ + Privileged: false, + SecurityContext: ¤t.SecurityContext{ + Privileged: &priv, + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"biz"}, + Drop: []current.CapabilityType{"baz"}, + }, + }, + }, + }, + "upward defaulting priv": { + c: current.Container{ + Capabilities: current.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + SecurityContext: ¤t.SecurityContext{ + Privileged: &privTrue, + Capabilities: ¤t.Capabilities{ + Add: []current.CapabilityType{"foo"}, + Drop: []current.CapabilityType{"bar"}, + }, + }, + }, + }, + } + + pod := ¤t.Pod{ + Spec: current.PodSpec{}, + } + + for k, v := range testCases { + pod.Spec.Containers = []current.Container{v.c} + obj := roundTrip(t, runtime.Object(pod)) + defaultedPod := obj.(*current.Pod) + c := defaultedPod.Spec.Containers[0] + if isEqual, issues := areSecurityContextAndContainerEqual(&c); !isEqual { + t.Errorf("test case %s expected the security context to have the same values as the container but found %#v", k, issues) + } + } +} + +func areSecurityContextAndContainerEqual(c *current.Container) (bool, []string) { + issues := make([]string, 0) + equal := true + + if c.SecurityContext == nil || c.SecurityContext.Privileged == nil || c.SecurityContext.Capabilities == nil { + equal = false + issues = append(issues, "Expected non nil settings for SecurityContext") + return equal, issues + } + if *c.SecurityContext.Privileged != c.Privileged { + equal = false + issues = append(issues, "The defaulted SecurityContext.Privileged value did not match the container value") + } + if !reflect.DeepEqual(c.Capabilities.Add, c.Capabilities.Add) { + equal = false + issues = append(issues, "The defaulted SecurityContext.Capabilities.Add did not match the container settings") + } + if !reflect.DeepEqual(c.Capabilities.Drop, c.Capabilities.Drop) { + equal = false + issues = append(issues, "The defaulted SecurityContext.Capabilities.Drop did not match the container settings") + } + return equal, issues +} diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index e5b9af7c75..8c1a5851a0 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -636,12 +636,14 @@ type Container struct { Lifecycle *Lifecycle `json:"lifecycle,omitempty" description:"actions that the management system should take in response to container lifecycle events; cannot be updated"` // Optional: Defaults to /dev/termination-log TerminationMessagePath string `json:"terminationMessagePath,omitempty" description:"path at which the file to which the container's termination message will be written is mounted into the container's filesystem; message written is intended to be brief final status, such as an assertion failure message; defaults to /dev/termination-log; cannot be updated"` - // Optional: Default to false. - Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated"` + // Deprecated - see SecurityContext. Optional: Default to false. + Privileged bool `json:"privileged,omitempty" description:"whether or not the container is granted privileged status; defaults to false; cannot be updated; deprecated; See SecurityContext."` // Optional: Policy for pulling images for this container ImagePullPolicy PullPolicy `json:"imagePullPolicy" description:"image pull policy; one of PullAlways, PullNever, PullIfNotPresent; defaults to PullAlways if :latest tag is specified, or PullIfNotPresent otherwise; cannot be updated"` - // Optional: Capabilities for container. - Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated"` + // Deprecated - see SecurityContext. Optional: Capabilities for container. + Capabilities Capabilities `json:"capabilities,omitempty" description:"capabilities for container; cannot be updated; deprecated; See SecurityContext."` + // Optional: SecurityContext defines the security options the pod should be run with + SecurityContext *SecurityContext `json:"securityContext,omitempty" description:"security options the pod should run with"` } // Handler defines a specific action that should be taken @@ -1735,3 +1737,39 @@ type ComponentStatusList struct { Items []ComponentStatus `json:"items" description:"list of component status objects"` } + +// SecurityContext holds security configuration that will be applied to a container. SecurityContext +// contains duplication of some existing fields from the Container resource. These duplicate fields +// will be populated based on the Container configuration if they are not set. Defining them on +// both the Container AND the SecurityContext will result in an error. +type SecurityContext struct { + // Capabilities are the capabilities to add/drop when running the container + // Must match Container.Capabilities or be unset. Will be defaulted to Container.Capabilities if left unset + Capabilities *Capabilities `json:"capabilities,omitempty" description:"the linux capabilites that should be added or removed"` + + // Run the container in privileged mode + // Must match Container.Privileged or be unset. Will be defaulted to Container.Privileged if left unset + Privileged *bool `json:"privileged,omitempty" description:"run the container in privileged mode"` + + // SELinuxOptions are the labels to be applied to the container + // and volumes + SELinuxOptions *SELinuxOptions `json:"seLinuxOptions,omitempty" description:"options that control the SELinux labels applied"` + + // RunAsUser is the UID to run the entrypoint of the container process. + RunAsUser *int64 `json:"runAsUser,omitempty" description:"the user id that runs the first process in the container"` +} + +// SELinuxOptions are the labels to be applied to the container. +type SELinuxOptions struct { + // SELinux user label + User string `json:"user,omitempty" description:"the user label to apply to the container"` + + // SELinux role label + Role string `json:"role,omitempty" description:"the role label to apply to the container"` + + // SELinux type label + Type string `json:"type,omitempty" description:"the type label to apply to the container"` + + // SELinux level label. + Level string `json:"level,omitempty" description:"the level label to apply to the container"` +} diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 519ecb05ad..1dff82c764 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -776,15 +776,12 @@ func validateContainers(containers []api.Container, volumes util.StringSet) errs allNames := util.StringSet{} for i, ctr := range containers { cErrs := errs.ValidationErrorList{} - capabilities := capabilities.Get() if len(ctr.Name) == 0 { cErrs = append(cErrs, errs.NewFieldRequired("name")) } else if !util.IsDNS1123Label(ctr.Name) { cErrs = append(cErrs, errs.NewFieldInvalid("name", ctr.Name, dns1123LabelErrorMsg)) } else if allNames.Has(ctr.Name) { cErrs = append(cErrs, errs.NewFieldDuplicate("name", ctr.Name)) - } else if ctr.Privileged && !capabilities.AllowPrivileged { - cErrs = append(cErrs, errs.NewFieldForbidden("privileged", ctr.Privileged)) } else { allNames.Insert(ctr.Name) } @@ -801,6 +798,7 @@ func validateContainers(containers []api.Container, volumes util.StringSet) errs cErrs = append(cErrs, validateVolumeMounts(ctr.VolumeMounts, volumes).Prefix("volumeMounts")...) cErrs = append(cErrs, validatePullPolicy(&ctr).Prefix("pullPolicy")...) cErrs = append(cErrs, ValidateResourceRequirements(&ctr.Resources).Prefix("resources")...) + cErrs = append(cErrs, ValidateSecurityContext(ctr.SecurityContext).Prefix("securityContext")...) allErrs = append(allErrs, cErrs.PrefixIndex(i)...) } // Check for colliding ports across all containers. @@ -1481,3 +1479,25 @@ func ValidateEndpointsUpdate(oldEndpoints, newEndpoints *api.Endpoints) errs.Val allErrs = append(allErrs, validateEndpointSubsets(newEndpoints.Subsets).Prefix("subsets")...) return allErrs } + +// ValidateSecurityContext ensure the security context contains valid settings +func ValidateSecurityContext(sc *api.SecurityContext) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + //this should only be true for testing since SecurityContext is defaulted by the api + if sc == nil { + return allErrs + } + + if sc.Privileged != nil { + if *sc.Privileged && !capabilities.Get().AllowPrivileged { + allErrs = append(allErrs, errs.NewFieldForbidden("privileged", sc.Privileged)) + } + } + + if sc.RunAsUser != nil { + if *sc.RunAsUser < 0 { + allErrs = append(allErrs, errs.NewFieldInvalid("runAsUser", *sc.RunAsUser, "runAsUser cannot be negative")) + } + } + return allErrs +} diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index eb3aac8d87..3b8973a560 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -901,7 +901,7 @@ func TestValidateContainers(t *testing.T) { }, ImagePullPolicy: "IfNotPresent", }, - {Name: "abc-1234", Image: "image", Privileged: true, ImagePullPolicy: "IfNotPresent"}, + {Name: "abc-1234", Image: "image", ImagePullPolicy: "IfNotPresent", SecurityContext: fakeValidSecurityContext(true)}, } if errs := validateContainers(successCase, volumes); len(errs) != 0 { t.Errorf("expected success: %v", errs) @@ -1015,7 +1015,7 @@ func TestValidateContainers(t *testing.T) { }, }, "privilege disabled": { - {Name: "abc", Image: "image", Privileged: true}, + {Name: "abc", Image: "image", SecurityContext: fakeValidSecurityContext(true)}, }, "invalid compute resource": { { @@ -3180,3 +3180,89 @@ func TestValidateEndpoints(t *testing.T) { } } } + +func TestValidateSecurityContext(t *testing.T) { + priv := false + var runAsUser int64 = 1 + fullValidSC := func() *api.SecurityContext { + return &api.SecurityContext{ + Privileged: &priv, + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"foo"}, + Drop: []api.CapabilityType{"bar"}, + }, + SELinuxOptions: &api.SELinuxOptions{ + User: "user", + Role: "role", + Type: "type", + Level: "level", + }, + RunAsUser: &runAsUser, + } + } + + //setup data + allSettings := fullValidSC() + noCaps := fullValidSC() + noCaps.Capabilities = nil + + noSELinux := fullValidSC() + noSELinux.SELinuxOptions = nil + + noPrivRequest := fullValidSC() + noPrivRequest.Privileged = nil + + noRunAsUser := fullValidSC() + noRunAsUser.RunAsUser = nil + + successCases := map[string]struct { + sc *api.SecurityContext + }{ + "all settings": {allSettings}, + "no capabilities": {noCaps}, + "no selinux": {noSELinux}, + "no priv request": {noPrivRequest}, + "no run as user": {noRunAsUser}, + } + for k, v := range successCases { + if errs := ValidateSecurityContext(v.sc); len(errs) != 0 { + t.Errorf("Expected success for %s, got %v", k, errs) + } + } + + privRequestWithGlobalDeny := fullValidSC() + requestPrivileged := true + privRequestWithGlobalDeny.Privileged = &requestPrivileged + + negativeRunAsUser := fullValidSC() + var negativeUser int64 = -1 + negativeRunAsUser.RunAsUser = &negativeUser + + errorCases := map[string]struct { + sc *api.SecurityContext + errorType fielderrors.ValidationErrorType + errorDetail string + }{ + "request privileged when capabilities forbids": { + sc: privRequestWithGlobalDeny, + errorType: "FieldValueForbidden", + errorDetail: "", + }, + "negative RunAsUser": { + sc: negativeRunAsUser, + errorType: "FieldValueInvalid", + errorDetail: "runAsUser cannot be negative", + }, + } + for k, v := range errorCases { + if errs := ValidateSecurityContext(v.sc); len(errs) == 0 || errs[0].(*errors.ValidationError).Type != v.errorType || errs[0].(*errors.ValidationError).Detail != v.errorDetail { + t.Errorf("Expected error type %s with detail %s for %s, got %v", v.errorType, v.errorDetail, k, errs) + } + } +} + +func fakeValidSecurityContext(priv bool) *api.SecurityContext { + return &api.SecurityContext{ + Privileged: &priv, + } +} diff --git a/pkg/controller/replication_controller_test.go b/pkg/controller/replication_controller_test.go index 7667121405..8e7b181f05 100644 --- a/pkg/controller/replication_controller_test.go +++ b/pkg/controller/replication_controller_test.go @@ -35,6 +35,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/wait" "github.com/GoogleCloudPlatform/kubernetes/pkg/watch" @@ -112,6 +113,7 @@ func newReplicationController(replicas int) *api.ReplicationController { Image: "foo/bar", TerminationMessagePath: api.TerminationMessagePathDefault, ImagePullPolicy: api.PullIfNotPresent, + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }, }, RestartPolicy: api.RestartPolicyAlways, diff --git a/pkg/kubelet/config/common_test.go b/pkg/kubelet/config/common_test.go index b34ab9bb48..d311c6b3af 100644 --- a/pkg/kubelet/config/common_test.go +++ b/pkg/kubelet/config/common_test.go @@ -22,6 +22,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/api/testapi" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/ghodss/yaml" ) @@ -46,6 +47,7 @@ func TestDecodeSinglePod(t *testing.T) { Image: "test/image", ImagePullPolicy: "IfNotPresent", TerminationMessagePath: "/dev/termination-log", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }}, }, } @@ -108,6 +110,7 @@ func TestDecodePodList(t *testing.T) { Image: "test/image", ImagePullPolicy: "IfNotPresent", TerminationMessagePath: "/dev/termination-log", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }}, }, } diff --git a/pkg/kubelet/config/config_test.go b/pkg/kubelet/config/config_test.go index f760d80152..9fe1fb860f 100644 --- a/pkg/kubelet/config/config_test.go +++ b/pkg/kubelet/config/config_test.go @@ -23,6 +23,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api" "github.com/GoogleCloudPlatform/kubernetes/pkg/client/record" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" ) @@ -62,7 +63,14 @@ func CreateValidPod(name, namespace, source string) *api.Pod { Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, - Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent"}}, + Containers: []api.Container{ + { + Name: "ctr", + Image: "image", + ImagePullPolicy: "IfNotPresent", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), + }, + }, }, } } diff --git a/pkg/kubelet/config/file_test.go b/pkg/kubelet/config/file_test.go index 7f107f7e26..f649fdcacb 100644 --- a/pkg/kubelet/config/file_test.go +++ b/pkg/kubelet/config/file_test.go @@ -31,6 +31,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" ) @@ -105,7 +106,8 @@ func TestReadContainerManifestFromFile(t *testing.T) { Name: "image", Image: "test/image", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "Always"}}, + ImagePullPolicy: "Always", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, @@ -131,7 +133,8 @@ func TestReadContainerManifestFromFile(t *testing.T) { Name: "image", Image: "test/image", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "Always"}}, + ImagePullPolicy: "Always", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, @@ -182,7 +185,7 @@ func TestReadPodsFromFile(t *testing.T) { Namespace: "mynamespace", }, Spec: api.PodSpec{ - Containers: []api.Container{{Name: "image", Image: "test/image"}}, + Containers: []api.Container{{Name: "image", Image: "test/image", SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }, expected: CreatePodUpdate(kubelet.SET, kubelet.FileSource, &api.Pod{ @@ -200,7 +203,8 @@ func TestReadPodsFromFile(t *testing.T) { Name: "image", Image: "test/image", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "IfNotPresent"}}, + ImagePullPolicy: "IfNotPresent", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, @@ -216,7 +220,7 @@ func TestReadPodsFromFile(t *testing.T) { UID: "12345", }, Spec: api.PodSpec{ - Containers: []api.Container{{Name: "image", Image: "test/image"}}, + Containers: []api.Container{{Name: "image", Image: "test/image", SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }, expected: CreatePodUpdate(kubelet.SET, kubelet.FileSource, &api.Pod{ @@ -234,7 +238,8 @@ func TestReadPodsFromFile(t *testing.T) { Name: "image", Image: "test/image", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "IfNotPresent"}}, + ImagePullPolicy: "IfNotPresent", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, diff --git a/pkg/kubelet/config/http_test.go b/pkg/kubelet/config/http_test.go index bc4ecd0d44..789dd17b9d 100644 --- a/pkg/kubelet/config/http_test.go +++ b/pkg/kubelet/config/http_test.go @@ -28,6 +28,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/api/validation" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/util/errors" ) @@ -151,7 +152,8 @@ func TestExtractManifestFromHTTP(t *testing.T) { Name: "1", Image: "foo", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "Always"}}, + ImagePullPolicy: "Always", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, @@ -177,7 +179,8 @@ func TestExtractManifestFromHTTP(t *testing.T) { Name: "ctr", Image: "image", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "IfNotPresent"}}, + ImagePullPolicy: "IfNotPresent", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, @@ -203,7 +206,8 @@ func TestExtractManifestFromHTTP(t *testing.T) { Name: "1", Image: "foo", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "Always"}}, + ImagePullPolicy: "Always", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, @@ -233,7 +237,8 @@ func TestExtractManifestFromHTTP(t *testing.T) { Name: "1", Image: "foo", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "Always"}}, + ImagePullPolicy: "Always", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }, &api.Pod{ @@ -252,7 +257,8 @@ func TestExtractManifestFromHTTP(t *testing.T) { Name: "1", Image: "foo", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "IfNotPresent"}}, + ImagePullPolicy: "IfNotPresent", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, @@ -344,7 +350,8 @@ func TestExtractPodsFromHTTP(t *testing.T) { Name: "1", Image: "foo", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "Always"}}, + ImagePullPolicy: "Always", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, @@ -396,7 +403,8 @@ func TestExtractPodsFromHTTP(t *testing.T) { Name: "1", Image: "foo", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "Always"}}, + ImagePullPolicy: "Always", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }, &api.Pod{ @@ -415,7 +423,8 @@ func TestExtractPodsFromHTTP(t *testing.T) { Name: "2", Image: "bar", TerminationMessagePath: "/dev/termination-log", - ImagePullPolicy: "IfNotPresent"}}, + ImagePullPolicy: "IfNotPresent", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults()}}, }, }), }, diff --git a/pkg/kubelet/dockertools/manager.go b/pkg/kubelet/dockertools/manager.go index 0244e761ba..eda841f608 100644 --- a/pkg/kubelet/dockertools/manager.go +++ b/pkg/kubelet/dockertools/manager.go @@ -38,6 +38,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/prober" kubeletTypes "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" docker "github.com/fsouza/go-dockerclient" @@ -524,6 +525,8 @@ func (dm *DockerManager) runContainer(pod *api.Pod, container *api.Container, op glog.V(3).Infof("Container %v/%v/%v: setting entrypoint \"%v\" and command \"%v\"", pod.Namespace, pod.Name, container.Name, dockerOpts.Config.Entrypoint, dockerOpts.Config.Cmd) + securityContextProvider := securitycontext.NewSimpleSecurityContextProvider() + securityContextProvider.ModifyContainerConfig(pod, container, dockerOpts.Config) dockerContainer, err := dm.client.CreateContainer(dockerOpts) if err != nil { if ref != nil { @@ -554,22 +557,15 @@ func (dm *DockerManager) runContainer(pod *api.Pod, container *api.Container, op } } - privileged := false - if capabilities.Get().AllowPrivileged { - privileged = container.Privileged - } else if container.Privileged { + if !capabilities.Get().AllowPrivileged && securitycontext.HasPrivilegedRequest(container) { return "", fmt.Errorf("container requested privileged mode, but it is disallowed globally.") } - capAdd, capDrop := makeCapabilites(container.Capabilities.Add, container.Capabilities.Drop) hc := &docker.HostConfig{ PortBindings: portBindings, Binds: opts.Binds, NetworkMode: opts.NetMode, IpcMode: opts.IpcMode, - Privileged: privileged, - CapAdd: capAdd, - CapDrop: capDrop, } if len(opts.DNS) > 0 { hc.DNS = opts.DNS @@ -580,6 +576,7 @@ func (dm *DockerManager) runContainer(pod *api.Pod, container *api.Container, op if len(opts.CgroupParent) > 0 { hc.CgroupParent = opts.CgroupParent } + securityContextProvider.ModifyHostConfig(pod, container, hc) if err = dm.client.StartContainer(dockerContainer.ID, hc); err != nil { if ref != nil { @@ -637,20 +634,6 @@ func makePortsAndBindings(container *api.Container) (map[docker.Port]struct{}, m return exposedPorts, portBindings } -func makeCapabilites(capAdd []api.CapabilityType, capDrop []api.CapabilityType) ([]string, []string) { - var ( - addCaps []string - dropCaps []string - ) - for _, cap := range capAdd { - addCaps = append(addCaps, string(cap)) - } - for _, cap := range capDrop { - dropCaps = append(dropCaps, string(cap)) - } - return addCaps, dropCaps -} - // A helper function to get the KubeletContainerName and hash from a docker // container. func getDockerContainerNameInfo(c *docker.APIContainers) (*KubeletContainerName, uint64, error) { diff --git a/pkg/kubelet/rkt/rkt_linux.go b/pkg/kubelet/rkt/rkt_linux.go index 1bd919f65a..63a40f1fe4 100644 --- a/pkg/kubelet/rkt/rkt_linux.go +++ b/pkg/kubelet/rkt/rkt_linux.go @@ -38,6 +38,7 @@ import ( kubecontainer "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/container" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/prober" "github.com/GoogleCloudPlatform/kubernetes/pkg/probe" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" "github.com/GoogleCloudPlatform/kubernetes/pkg/volume" @@ -187,23 +188,29 @@ func rawValue(value string) *json.RawMessage { } // setIsolators overrides the isolators of the pod manifest if necessary. +// TODO need an apply config in security context for rkt func setIsolators(app *appctypes.App, c *api.Container) error { - if len(c.Capabilities.Add) > 0 || len(c.Capabilities.Drop) > 0 || len(c.Resources.Limits) > 0 || len(c.Resources.Requests) > 0 { + hasCapRequests := securitycontext.HasCapabilitiesRequest(c) + if hasCapRequests || len(c.Resources.Limits) > 0 || len(c.Resources.Requests) > 0 { app.Isolators = []appctypes.Isolator{} } // Retained capabilities/privileged. privileged := false - if capabilities.Get().AllowPrivileged { - privileged = c.Privileged - } else if c.Privileged { - return fmt.Errorf("privileged is disallowed globally") + if !capabilities.Get().AllowPrivileged && securitycontext.HasPrivilegedRequest(c) { + return fmt.Errorf("container requested privileged mode, but it is disallowed globally.") + } else { + if c.SecurityContext != nil && c.SecurityContext.Privileged != nil { + privileged = *c.SecurityContext.Privileged + } } var addCaps string if privileged { addCaps = getAllCapabilities() } else { - addCaps = getCapabilities(c.Capabilities.Add) + if hasCapRequests { + addCaps = getCapabilities(c.SecurityContext.Capabilities.Add) + } } if len(addCaps) > 0 { // TODO(yifan): Replace with constructor, see: @@ -216,7 +223,10 @@ func setIsolators(app *appctypes.App, c *api.Container) error { } // Removed capabilities. - dropCaps := getCapabilities(c.Capabilities.Drop) + var dropCaps string + if hasCapRequests { + dropCaps = getCapabilities(c.SecurityContext.Capabilities.Drop) + } if len(dropCaps) > 0 { // TODO(yifan): Replace with constructor, see: // https://github.com/appc/spec/issues/268 diff --git a/pkg/registry/pod/etcd/etcd_test.go b/pkg/registry/pod/etcd/etcd_test.go index 51e5895886..81ede48d04 100644 --- a/pkg/registry/pod/etcd/etcd_test.go +++ b/pkg/registry/pod/etcd/etcd_test.go @@ -32,6 +32,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" "github.com/GoogleCloudPlatform/kubernetes/pkg/registry/pod" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" + "github.com/GoogleCloudPlatform/kubernetes/pkg/securitycontext" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools" "github.com/GoogleCloudPlatform/kubernetes/pkg/tools/etcdtest" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -68,6 +69,7 @@ func validNewPod() *api.Pod { ImagePullPolicy: api.PullAlways, TerminationMessagePath: api.TerminationMessagePathDefault, + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }, }, }, @@ -1108,8 +1110,9 @@ func TestEtcdUpdateScheduled(t *testing.T) { Host: "machine", Containers: []api.Container{ { - Name: "foobar", - Image: "foo:v1", + Name: "foobar", + Image: "foo:v1", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }, }, }, @@ -1131,6 +1134,7 @@ func TestEtcdUpdateScheduled(t *testing.T) { Image: "foo:v2", ImagePullPolicy: api.PullIfNotPresent, TerminationMessagePath: api.TerminationMessagePathDefault, + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }, }, RestartPolicy: api.RestartPolicyAlways, @@ -1169,7 +1173,8 @@ func TestEtcdUpdateStatus(t *testing.T) { Host: "machine", Containers: []api.Container{ { - Image: "foo:v1", + Image: "foo:v1", + SecurityContext: securitycontext.ValidSecurityContextWithContainerDefaults(), }, }, }, diff --git a/pkg/securitycontext/doc.go b/pkg/securitycontext/doc.go new file mode 100644 index 0000000000..9e9a84ba10 --- /dev/null +++ b/pkg/securitycontext/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package securitycontext contains security context api implementations +package securitycontext diff --git a/pkg/securitycontext/fake.go b/pkg/securitycontext/fake.go new file mode 100644 index 0000000000..02e4d746e6 --- /dev/null +++ b/pkg/securitycontext/fake.go @@ -0,0 +1,45 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package securitycontext + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + docker "github.com/fsouza/go-dockerclient" +) + +// ValidSecurityContextWithContainerDefaults creates a valid security context provider based on +// empty container defaults. Used for testing. +func ValidSecurityContextWithContainerDefaults() *api.SecurityContext { + priv := false + return &api.SecurityContext{ + Capabilities: &api.Capabilities{}, + Privileged: &priv, + } +} + +// NewFakeSecurityContextProvider creates a new, no-op security context provider. +func NewFakeSecurityContextProvider() SecurityContextProvider { + return FakeSecurityContextProvider{} +} + +type FakeSecurityContextProvider struct{} + +func (p FakeSecurityContextProvider) ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) { +} +func (p FakeSecurityContextProvider) ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) { +} diff --git a/pkg/securitycontext/provider.go b/pkg/securitycontext/provider.go new file mode 100644 index 0000000000..0362fcc0f9 --- /dev/null +++ b/pkg/securitycontext/provider.go @@ -0,0 +1,97 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package securitycontext + +import ( + "fmt" + "strconv" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + docker "github.com/fsouza/go-dockerclient" +) + +// NewSimpleSecurityContextProvider creates a new SimpleSecurityContextProvider. +func NewSimpleSecurityContextProvider() SecurityContextProvider { + return SimpleSecurityContextProvider{} +} + +// SimpleSecurityContextProvider is the default implementation of a SecurityContextProvider. +type SimpleSecurityContextProvider struct{} + +// ModifyContainerConfig is called before the Docker createContainer call. +// The security context provider can make changes to the Config with which +// the container is created. +func (p SimpleSecurityContextProvider) ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) { + if container.SecurityContext == nil { + return + } + if container.SecurityContext.RunAsUser != nil { + config.User = strconv.FormatInt(*container.SecurityContext.RunAsUser, 10) + } +} + +// ModifyHostConfig is called before the Docker runContainer call. +// The security context provider can make changes to the HostConfig, affecting +// security options, whether the container is privileged, volume binds, etc. +// An error is returned if it's not possible to secure the container as requested +// with a security context. +func (p SimpleSecurityContextProvider) ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) { + if container.SecurityContext == nil { + return + } + if container.SecurityContext.Privileged != nil { + hostConfig.Privileged = *container.SecurityContext.Privileged + } + + if container.SecurityContext.Capabilities != nil { + add, drop := makeCapabilites(container.SecurityContext.Capabilities.Add, container.SecurityContext.Capabilities.Drop) + hostConfig.CapAdd = add + hostConfig.CapDrop = drop + } + + if container.SecurityContext.SELinuxOptions != nil { + hostConfig.SecurityOpt = modifySecurityOption(hostConfig.SecurityOpt, dockerLabelUser, container.SecurityContext.SELinuxOptions.User) + hostConfig.SecurityOpt = modifySecurityOption(hostConfig.SecurityOpt, dockerLabelRole, container.SecurityContext.SELinuxOptions.Role) + hostConfig.SecurityOpt = modifySecurityOption(hostConfig.SecurityOpt, dockerLabelType, container.SecurityContext.SELinuxOptions.Type) + hostConfig.SecurityOpt = modifySecurityOption(hostConfig.SecurityOpt, dockerLabelLevel, container.SecurityContext.SELinuxOptions.Level) + } +} + +// modifySecurityOption adds the security option of name to the config array with value in the form +// of name:value +func modifySecurityOption(config []string, name, value string) []string { + if len(value) > 0 { + config = append(config, fmt.Sprintf("%s:%s", name, value)) + } + return config +} + +// makeCapabilites creates string slices from CapabilityType slices +func makeCapabilites(capAdd []api.CapabilityType, capDrop []api.CapabilityType) ([]string, []string) { + var ( + addCaps []string + dropCaps []string + ) + for _, cap := range capAdd { + addCaps = append(addCaps, string(cap)) + } + for _, cap := range capDrop { + dropCaps = append(dropCaps, string(cap)) + } + return addCaps, dropCaps +} diff --git a/pkg/securitycontext/provider_test.go b/pkg/securitycontext/provider_test.go new file mode 100644 index 0000000000..609490c7b5 --- /dev/null +++ b/pkg/securitycontext/provider_test.go @@ -0,0 +1,181 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package securitycontext + +import ( + "fmt" + "reflect" + "strconv" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + docker "github.com/fsouza/go-dockerclient" +) + +func TestModifyContainerConfig(t *testing.T) { + var uid int64 = 1 + testCases := map[string]struct { + securityContext *api.SecurityContext + expected *docker.Config + }{ + "modify config, value set for user": { + securityContext: &api.SecurityContext{ + RunAsUser: &uid, + }, + expected: &docker.Config{ + User: strconv.FormatInt(uid, 10), + }, + }, + "modify config, nil user value": { + securityContext: &api.SecurityContext{}, + expected: &docker.Config{}, + }, + } + + provider := NewSimpleSecurityContextProvider() + dummyContainer := &api.Container{} + for k, v := range testCases { + dummyContainer.SecurityContext = v.securityContext + dockerCfg := &docker.Config{} + provider.ModifyContainerConfig(nil, dummyContainer, dockerCfg) + if !reflect.DeepEqual(v.expected, dockerCfg) { + t.Errorf("unexpected modification of docker config for %s. Expected: %#v Got: %#v", k, v.expected, dockerCfg) + } + } +} + +func TestModifyHostConfig(t *testing.T) { + nilPrivSC := fullValidSecurityContext() + nilPrivSC.Privileged = nil + nilPrivHC := fullValidHostConfig() + nilPrivHC.Privileged = false + + nilCapsSC := fullValidSecurityContext() + nilCapsSC.Capabilities = nil + nilCapsHC := fullValidHostConfig() + nilCapsHC.CapAdd = *new([]string) + nilCapsHC.CapDrop = *new([]string) + + nilSELinuxSC := fullValidSecurityContext() + nilSELinuxSC.SELinuxOptions = nil + nilSELinuxHC := fullValidHostConfig() + nilSELinuxHC.SecurityOpt = *new([]string) + + seLinuxLabelsSC := fullValidSecurityContext() + seLinuxLabelsHC := fullValidHostConfig() + + testCases := map[string]struct { + securityContext *api.SecurityContext + expected *docker.HostConfig + }{ + "full settings": { + securityContext: fullValidSecurityContext(), + expected: fullValidHostConfig(), + }, + "nil privileged": { + securityContext: nilPrivSC, + expected: nilPrivHC, + }, + "nil capabilities": { + securityContext: nilCapsSC, + expected: nilCapsHC, + }, + "nil selinux options": { + securityContext: nilSELinuxSC, + expected: nilSELinuxHC, + }, + "selinux labels": { + securityContext: seLinuxLabelsSC, + expected: seLinuxLabelsHC, + }, + } + + provider := NewSimpleSecurityContextProvider() + dummyContainer := &api.Container{} + for k, v := range testCases { + dummyContainer.SecurityContext = v.securityContext + dockerCfg := &docker.HostConfig{} + provider.ModifyHostConfig(nil, dummyContainer, dockerCfg) + if !reflect.DeepEqual(v.expected, dockerCfg) { + t.Errorf("unexpected modification of host config for %s. Expected: %#v Got: %#v", k, v.expected, dockerCfg) + } + } +} + +func TestModifySecurityOption(t *testing.T) { + testCases := []struct { + name string + config []string + optName string + optVal string + expected []string + }{ + { + name: "Empty val", + config: []string{"a:b", "c:d"}, + optName: "optA", + optVal: "", + expected: []string{"a:b", "c:d"}, + }, + { + name: "Valid", + config: []string{"a:b", "c:d"}, + optName: "e", + optVal: "f", + expected: []string{"a:b", "c:d", "e:f"}, + }, + } + + for _, tc := range testCases { + actual := modifySecurityOption(tc.config, tc.optName, tc.optVal) + if !reflect.DeepEqual(tc.expected, actual) { + t.Errorf("Failed to apply options correctly for tc: %S. Expected: %v but got %v", tc.name, tc.expected, actual) + } + } +} + +func fullValidSecurityContext() *api.SecurityContext { + priv := true + return &api.SecurityContext{ + Privileged: &priv, + Capabilities: &api.Capabilities{ + Add: []api.CapabilityType{"addCapA", "addCapB"}, + Drop: []api.CapabilityType{"dropCapA", "dropCapB"}, + }, + SELinuxOptions: &api.SELinuxOptions{ + User: "user", + Role: "role", + Type: "type", + Level: "level", + }, + } +} + +func fullValidHostConfig() *docker.HostConfig { + return &docker.HostConfig{ + Privileged: true, + CapAdd: []string{"addCapA", "addCapB"}, + CapDrop: []string{"dropCapA", "dropCapB"}, + SecurityOpt: []string{ + fmt.Sprintf("%s:%s", dockerLabelUser, "user"), + fmt.Sprintf("%s:%s", dockerLabelRole, "role"), + fmt.Sprintf("%s:%s", dockerLabelType, "type"), + fmt.Sprintf("%s:%s", dockerLabelLevel, "level"), + }, + } +} diff --git a/pkg/securitycontext/types.go b/pkg/securitycontext/types.go new file mode 100644 index 0000000000..d39f5344b0 --- /dev/null +++ b/pkg/securitycontext/types.go @@ -0,0 +1,45 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package securitycontext + +import ( + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + + docker "github.com/fsouza/go-dockerclient" +) + +type SecurityContextProvider interface { + // ModifyContainerConfig is called before the Docker createContainer call. + // The security context provider can make changes to the Config with which + // the container is created. + ModifyContainerConfig(pod *api.Pod, container *api.Container, config *docker.Config) + + // ModifyHostConfig is called before the Docker runContainer call. + // The security context provider can make changes to the HostConfig, affecting + // security options, whether the container is privileged, volume binds, etc. + // An error is returned if it's not possible to secure the container as requested + // with a security context. + ModifyHostConfig(pod *api.Pod, container *api.Container, hostConfig *docker.HostConfig) +} + +const ( + dockerLabelUser string = "label:user" + dockerLabelRole string = "label:role" + dockerLabelType string = "label:type" + dockerLabelLevel string = "label:level" + dockerLabelDisable string = "label:disable" +) diff --git a/pkg/securitycontext/util.go b/pkg/securitycontext/util.go new file mode 100644 index 0000000000..64bf7e53ec --- /dev/null +++ b/pkg/securitycontext/util.go @@ -0,0 +1,43 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package securitycontext + +import "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + +// HasPrivilegedRequest returns the value of SecurityContext.Privileged, taking into account +// the possibility of nils +func HasPrivilegedRequest(container *api.Container) bool { + if container.SecurityContext == nil { + return false + } + if container.SecurityContext.Privileged == nil { + return false + } + return *container.SecurityContext.Privileged +} + +// HasCapabilitiesRequest returns true if Adds or Drops are defined in the security context +// capabilities, taking into account nils +func HasCapabilitiesRequest(container *api.Container) bool { + if container.SecurityContext == nil { + return false + } + if container.SecurityContext.Capabilities == nil { + return false + } + return len(container.SecurityContext.Capabilities.Add) > 0 || len(container.SecurityContext.Capabilities.Drop) > 0 +} diff --git a/plugin/pkg/admission/securitycontext/scdeny/admission.go b/plugin/pkg/admission/securitycontext/scdeny/admission.go new file mode 100644 index 0000000000..2362ae3fe5 --- /dev/null +++ b/plugin/pkg/admission/securitycontext/scdeny/admission.go @@ -0,0 +1,70 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package scdeny + +import ( + "fmt" + "io" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + apierrors "github.com/GoogleCloudPlatform/kubernetes/pkg/api/errors" + "github.com/GoogleCloudPlatform/kubernetes/pkg/client" +) + +func init() { + admission.RegisterPlugin("SecurityContextDeny", func(client client.Interface, config io.Reader) (admission.Interface, error) { + return NewSecurityContextDeny(client), nil + }) +} + +// plugin contains the client used by the SecurityContextDeny admission controller +type plugin struct { + client client.Interface +} + +// NewSecurityContextDeny creates a new instance of the SecurityContextDeny admission controller +func NewSecurityContextDeny(client client.Interface) admission.Interface { + return &plugin{client} +} + +// Admit will deny any SecurityContext that defines options that were not previously available in the api.Container +// struct (Capabilities and Privileged) +func (p *plugin) Admit(a admission.Attributes) (err error) { + if a.GetOperation() == "DELETE" { + return nil + } + if a.GetResource() != string(api.ResourcePods) { + return nil + } + + pod, ok := a.GetObject().(*api.Pod) + if !ok { + return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted") + } + for _, v := range pod.Spec.Containers { + if v.SecurityContext != nil { + if v.SecurityContext.SELinuxOptions != nil { + return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf("SecurityContext.SELinuxOptions is forbidden")) + } + if v.SecurityContext.RunAsUser != nil { + return apierrors.NewForbidden(a.GetResource(), pod.Name, fmt.Errorf("SecurityContext.RunAsUser is forbidden")) + } + } + } + return nil +} diff --git a/plugin/pkg/admission/securitycontext/scdeny/admission_test.go b/plugin/pkg/admission/securitycontext/scdeny/admission_test.go new file mode 100644 index 0000000000..10c11a1a28 --- /dev/null +++ b/plugin/pkg/admission/securitycontext/scdeny/admission_test.go @@ -0,0 +1,65 @@ +/* +Copyright 2014 The Kubernetes Authors All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package scdeny + +import ( + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/admission" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" +) + +// ensures the SecurityContext is denied if it defines anything more than Caps or Privileged +func TestAdmission(t *testing.T) { + handler := NewSecurityContextDeny(nil) + + var runAsUser int64 = 1 + priv := true + successCases := map[string]*api.SecurityContext{ + "no sc": nil, + "empty sc": {}, + "valid sc": {Privileged: &priv, Capabilities: &api.Capabilities{}}, + } + + pod := api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{ + {}, + }, + }, + } + for k, v := range successCases { + pod.Spec.Containers[0].SecurityContext = v + err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", "foo", string(api.ResourcePods), "ignored")) + if err != nil { + t.Errorf("Unexpected error returned from admission handler for case %s", k) + } + } + + errorCases := map[string]*api.SecurityContext{ + "run as user": {RunAsUser: &runAsUser}, + "se linux optons": {SELinuxOptions: &api.SELinuxOptions{}}, + "mixed settings": {Privileged: &priv, RunAsUser: &runAsUser, SELinuxOptions: &api.SELinuxOptions{}}, + } + for k, v := range errorCases { + pod.Spec.Containers[0].SecurityContext = v + err := handler.Admit(admission.NewAttributesRecord(&pod, "Pod", "foo", string(api.ResourcePods), "ignored")) + if err == nil { + t.Errorf("Expected error returned from admission handler for case %s", k) + } + } +}