From 8f596dc6fbf32047b3a12cfa32de7a203bc3c49c Mon Sep 17 00:00:00 2001 From: Maciej Szulik Date: Thu, 7 May 2015 14:47:20 +0200 Subject: [PATCH] Added SecretFileMode for specifying mounted secrets file permissions. --- pkg/api/types.go | 12 ++++++++ pkg/api/v1beta1/conversion.go | 16 ++++++++++ pkg/api/v1beta1/types.go | 12 ++++++++ pkg/api/v1beta2/conversion.go | 16 ++++++++++ pkg/api/v1beta2/types.go | 12 ++++++++ pkg/api/v1beta3/conversion_generated.go | 40 +++++++++++++++++++++++++ pkg/api/v1beta3/types.go | 12 ++++++++ pkg/api/validation/validation.go | 5 ++++ pkg/api/validation/validation_test.go | 4 ++- pkg/volume/secret/secret.go | 36 +++++++++++++--------- pkg/volume/secret/secret_test.go | 15 +++++++++- 11 files changed, 164 insertions(+), 16 deletions(-) diff --git a/pkg/api/types.go b/pkg/api/types.go index c172ca4e8fcf9..6cce639bf443d 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -17,6 +17,8 @@ limitations under the License. package api import ( + "os" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/fields" "github.com/GoogleCloudPlatform/kubernetes/pkg/labels" @@ -450,6 +452,16 @@ type GitRepoVolumeSource struct { type SecretVolumeSource struct { // Name of the secret in the pod's namespace to use SecretName string `json:"secretName"` + // Modes describes access permissions to be applied per Secret's data key while mounting + Modes []SecretFileMode `json:"modes"` +} + +// SecretFileMode sets up access permissions per Secret's data key +type SecretFileMode struct { + // Name is the name of the Secret's data key + Name string `json:"name"` + // Mode is os.FileMode to be applied + Mode os.FileMode `json:"mode"` } // NFSVolumeSource represents an NFS Mount that lasts the lifetime of a pod diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index dba9b5d468951..9e5047fbe597e 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -1574,10 +1574,26 @@ func init() { }, func(in *newer.SecretVolumeSource, out *SecretVolumeSource, s conversion.Scope) error { out.Target.ID = in.SecretName + if in.Modes != nil { + out.Modes = make([]SecretFileMode, len(in.Modes)) + for i := range in.Modes { + if err := s.Convert(&in.Modes[i], &out.Modes[i], 0); err != nil { + return err + } + } + } return nil }, func(in *SecretVolumeSource, out *newer.SecretVolumeSource, s conversion.Scope) error { out.SecretName = in.Target.ID + if in.Modes != nil { + out.Modes = make([]newer.SecretFileMode, len(in.Modes)) + for i := range in.Modes { + if err := s.Convert(&in.Modes[i], &out.Modes[i], 0); err != nil { + return err + } + } + } return nil }, ) diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 951a2f92d7361..0c7e965c6bcea 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -17,6 +17,8 @@ limitations under the License. package v1beta1 import ( + "os" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -363,6 +365,16 @@ type SecretVolumeSource struct { // Reference to a Secret to use. Only the ID field of this reference is used; a // secret can only be used by pods in its namespace. Target ObjectReference `json:"target" description:"target is a reference to a secret"` + // Modes describes access permissions to be applied per Secret's data key while mounting + Modes []SecretFileMode `json:"modes"` +} + +// SecretFileMode sets up access permissions per Secret's data key +type SecretFileMode struct { + // Name is the name of the Secret's data key + Name string `json:"name"` + // Mode is os.FileMode to be applied + Mode os.FileMode `json:"mode"` } // ContainerPort represents a network port in a single container diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index d4b1d59dffba2..0212ffcfba616 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -1490,10 +1490,26 @@ func init() { }, func(in *newer.SecretVolumeSource, out *SecretVolumeSource, s conversion.Scope) error { out.Target.ID = in.SecretName + if in.Modes != nil { + out.Modes = make([]SecretFileMode, len(in.Modes)) + for i := range in.Modes { + if err := s.Convert(&in.Modes[i], &out.Modes[i], 0); err != nil { + return err + } + } + } return nil }, func(in *SecretVolumeSource, out *newer.SecretVolumeSource, s conversion.Scope) error { out.SecretName = in.Target.ID + if in.Modes != nil { + out.Modes = make([]newer.SecretFileMode, len(in.Modes)) + for i := range in.Modes { + if err := s.Convert(&in.Modes[i], &out.Modes[i], 0); err != nil { + return err + } + } + } return nil }, ) diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index a1b3d0d31aa62..58764f7af9236 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -17,6 +17,8 @@ limitations under the License. package v1beta2 import ( + "os" + "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" "github.com/GoogleCloudPlatform/kubernetes/pkg/util" @@ -249,6 +251,16 @@ type SecretVolumeSource struct { // Reference to a Secret to use. Only the ID field of this reference is used; a // secret can only be used by pods in its namespace. Target ObjectReference `json:"target" description:"target is a reference to a secret"` + // Modes describes access permissions to be applied per Secret's data key while mounting + Modes []SecretFileMode `json:"modes"` +} + +// SecretFileMode sets up access permissions per Secret's data key +type SecretFileMode struct { + // Name is the name of the Secret's data key + Name string `json:"name"` + // Mode is os.FileMode to be applied + Mode os.FileMode `json:"mode"` } // Protocol defines network protocols supported for things like conatiner ports. diff --git a/pkg/api/v1beta3/conversion_generated.go b/pkg/api/v1beta3/conversion_generated.go index a1701b4bea8e4..6248cadbb93c7 100644 --- a/pkg/api/v1beta3/conversion_generated.go +++ b/pkg/api/v1beta3/conversion_generated.go @@ -3377,6 +3377,24 @@ func convert_api_Secret_To_v1beta3_Secret(in *newer.Secret, out *Secret, s conve return nil } +func convert_v1beta3_SecretFileMode_To_api_SecretFileMode(in *SecretFileMode, out *newer.SecretFileMode, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*SecretFileMode))(in) + } + out.Name = in.Name + out.Mode = in.Mode + return nil +} + +func convert_api_SecretFileMode_To_v1beta3_SecretFileMode(in *newer.SecretFileMode, out *SecretFileMode, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*newer.SecretFileMode))(in) + } + out.Name = in.Name + out.Mode = in.Mode + return nil +} + func convert_v1beta3_SecretList_To_api_SecretList(in *SecretList, out *newer.SecretList, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*SecretList))(in) @@ -3428,6 +3446,16 @@ func convert_v1beta3_SecretVolumeSource_To_api_SecretVolumeSource(in *SecretVolu defaulting.(func(*SecretVolumeSource))(in) } out.SecretName = in.SecretName + if in.Modes != nil { + out.Modes = make([]newer.SecretFileMode, len(in.Modes)) + for i := range in.Modes { + if err := convert_v1beta3_SecretFileMode_To_api_SecretFileMode(&in.Modes[i], &out.Modes[i], s); err != nil { + return err + } + } + } else { + out.Modes = nil + } return nil } @@ -3436,6 +3464,16 @@ func convert_api_SecretVolumeSource_To_v1beta3_SecretVolumeSource(in *newer.Secr defaulting.(func(*newer.SecretVolumeSource))(in) } out.SecretName = in.SecretName + if in.Modes != nil { + out.Modes = make([]SecretFileMode, len(in.Modes)) + for i := range in.Modes { + if err := convert_api_SecretFileMode_To_v1beta3_SecretFileMode(&in.Modes[i], &out.Modes[i], s); err != nil { + return err + } + } + } else { + out.Modes = nil + } return nil } @@ -4186,6 +4224,7 @@ func init() { convert_api_ResourceQuota_To_v1beta3_ResourceQuota, convert_api_ResourceRequirements_To_v1beta3_ResourceRequirements, convert_api_SELinuxOptions_To_v1beta3_SELinuxOptions, + convert_api_SecretFileMode_To_v1beta3_SecretFileMode, convert_api_SecretList_To_v1beta3_SecretList, convert_api_SecretVolumeSource_To_v1beta3_SecretVolumeSource, convert_api_Secret_To_v1beta3_Secret, @@ -4291,6 +4330,7 @@ func init() { convert_v1beta3_ResourceQuota_To_api_ResourceQuota, convert_v1beta3_ResourceRequirements_To_api_ResourceRequirements, convert_v1beta3_SELinuxOptions_To_api_SELinuxOptions, + convert_v1beta3_SecretFileMode_To_api_SecretFileMode, convert_v1beta3_SecretList_To_api_SecretList, convert_v1beta3_SecretVolumeSource_To_api_SecretVolumeSource, convert_v1beta3_Secret_To_api_Secret, diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index b0ff3fd864789..6be1dc331aba3 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -17,6 +17,8 @@ limitations under the License. package v1beta3 import ( + "os" + "github.com/GoogleCloudPlatform/kubernetes/pkg/api/resource" "github.com/GoogleCloudPlatform/kubernetes/pkg/runtime" "github.com/GoogleCloudPlatform/kubernetes/pkg/types" @@ -455,6 +457,16 @@ type GitRepoVolumeSource struct { type SecretVolumeSource struct { // Name of the secret in the pod's namespace to use SecretName string `json:"secretName" description:"secretName is the name of a secret in the pod's namespace"` + // Modes describes access permissions to be applied per Secret's data key while mounting + Modes []SecretFileMode `json:"modes"` +} + +// SecretFileMode sets up access permissions per Secret's data key +type SecretFileMode struct { + // Name is the name of the Secret's data key + Name string `json:"name"` + // Mode is os.FileMode to be applied + Mode os.FileMode `json:"mode"` } // NFSVolumeSource represents an NFS mount that lasts the lifetime of a pod diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 1dff82c764609..8090299876f01 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -396,6 +396,11 @@ func validateSecretVolumeSource(secretSource *api.SecretVolumeSource) errs.Valid if secretSource.SecretName == "" { allErrs = append(allErrs, errs.NewFieldRequired("secretName")) } + for _, mode := range secretSource.Modes { + if mode.Mode|0444 != 0444 { + allErrs = append(allErrs, errs.NewFieldForbidden("modes.mode", "Not allowed mode set")) + } + } return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 3b8973a5603e3..b14e58ef1a86e 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -519,7 +519,7 @@ func TestValidateVolumes(t *testing.T) { {Name: "awsebs", VolumeSource: api.VolumeSource{AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{"my-PD", "ext4", 1, false}}}, {Name: "gitrepo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{"my-repo", "hashstring"}}}, {Name: "iscsidisk", VolumeSource: api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{"127.0.0.1", "iqn.2015-02.example.com:test", 1, "ext4", false}}}, - {Name: "secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{"my-secret"}}}, + {Name: "secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{"my-secret", []api.SecretFileMode{}}}}, {Name: "glusterfs", VolumeSource: api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{"host1", "path", false}}}, } names, errs := validateVolumes(successCase) @@ -534,6 +534,7 @@ func TestValidateVolumes(t *testing.T) { emptyIQN := api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{"127.0.0.1", "", 1, "ext4", false}} emptyHosts := api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{"", "path", false}} emptyPath := api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{"host", "", false}} + wrongFileMode := api.VolumeSource{Secret: &api.SecretVolumeSource{"my-secret", []api.SecretFileMode{{"data", 0700}}}} errorCases := map[string]struct { V []api.Volume T errors.ValidationErrorType @@ -547,6 +548,7 @@ func TestValidateVolumes(t *testing.T) { "empty iqn": {[]api.Volume{{Name: "badiqn", VolumeSource: emptyIQN}}, errors.ValidationErrorTypeRequired, "[0].source.iscsi.iqn"}, "empty hosts": {[]api.Volume{{Name: "badhost", VolumeSource: emptyHosts}}, errors.ValidationErrorTypeRequired, "[0].source.glusterfs.endpoints"}, "empty path": {[]api.Volume{{Name: "badpath", VolumeSource: emptyPath}}, errors.ValidationErrorTypeRequired, "[0].source.glusterfs.path"}, + "wrong fileMode": {[]api.Volume{{Name: "badfilemode", VolumeSource: wrongFileMode}}, errors.ValidationErrorTypeForbidden, "[0].source.secret.modes.mode"}, } for k, v := range errorCases { _, errs := validateVolumes(v.V) diff --git a/pkg/volume/secret/secret.go b/pkg/volume/secret/secret.go index c41e437ac41b4..e8afb4d0180a6 100644 --- a/pkg/volume/secret/secret.go +++ b/pkg/volume/secret/secret.go @@ -19,6 +19,7 @@ package secret import ( "fmt" "io/ioutil" + "os" "path" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -61,7 +62,7 @@ func (plugin *secretPlugin) NewBuilder(spec *volume.Spec, podRef *api.ObjectRefe } func (plugin *secretPlugin) newBuilderInternal(spec *volume.Spec, podRef *api.ObjectReference, opts volume.VolumeOptions, mounter mount.Interface) (volume.Builder, error) { - return &secretVolume{spec.Name, *podRef, plugin, spec.VolumeSource.Secret.SecretName, &opts, mounter}, nil + return &secretVolume{spec.Name, *podRef, plugin, spec.VolumeSource.Secret, &opts, mounter}, nil } func (plugin *secretPlugin) NewCleaner(volName string, podUID types.UID, mounter mount.Interface) (volume.Cleaner, error) { @@ -69,18 +70,18 @@ func (plugin *secretPlugin) NewCleaner(volName string, podUID types.UID, mounter } func (plugin *secretPlugin) newCleanerInternal(volName string, podUID types.UID, mounter mount.Interface) (volume.Cleaner, error) { - return &secretVolume{volName, api.ObjectReference{UID: podUID}, plugin, "", nil, mounter}, nil + return &secretVolume{volName, api.ObjectReference{UID: podUID}, plugin, nil, nil, mounter}, nil } // secretVolume handles retrieving secrets from the API server // and placing them into the volume on the host. type secretVolume struct { - volName string - podRef api.ObjectReference - plugin *secretPlugin - secretName string - opts *volume.VolumeOptions - mounter mount.Interface + volName string + podRef api.ObjectReference + plugin *secretPlugin + secret *api.SecretVolumeSource + opts *volume.VolumeOptions + mounter mount.Interface } func (sv *secretVolume) SetUp() error { @@ -114,24 +115,31 @@ func (sv *secretVolume) SetUpAt(dir string) error { return fmt.Errorf("Cannot setup secret volume %v because kube client is not configured", sv) } - secret, err := kubeClient.Secrets(sv.podRef.Namespace).Get(sv.secretName) + secret, err := kubeClient.Secrets(sv.podRef.Namespace).Get(sv.secret.SecretName) if err != nil { - glog.Errorf("Couldn't get secret %v/%v", sv.podRef.Namespace, sv.secretName) + glog.Errorf("Couldn't get secret %v/%v", sv.podRef.Namespace, sv.secret.SecretName) return err } else { totalBytes := totalSecretBytes(secret) glog.V(3).Infof("Received secret %v/%v containing (%v) pieces of data, %v total bytes", sv.podRef.Namespace, - sv.secretName, + sv.secret.SecretName, len(secret.Data), totalBytes) } + customFileModes := make(map[string]os.FileMode) + for _, mode := range sv.secret.Modes { + customFileModes[mode.Name] = mode.Mode + } for name, data := range secret.Data { hostFilePath := path.Join(dir, name) - glog.V(3).Infof("Writing secret data %v/%v/%v (%v bytes) to host file %v", sv.podRef.Namespace, sv.secretName, name, len(data), hostFilePath) - err := ioutil.WriteFile(hostFilePath, data, 0444) - if err != nil { + glog.V(3).Infof("Writing secret data %v/%v/%v (%v bytes) to host file %v", sv.podRef.Namespace, sv.secret.SecretName, name, len(data), hostFilePath) + var fileMode os.FileMode = 0444 + if customFileMode, ok := customFileModes[name]; ok { + fileMode = customFileMode + } + if err := ioutil.WriteFile(hostFilePath, data, fileMode); err != nil { glog.Errorf("Error writing secret data to host path: %v, %v", hostFilePath, err) return err } diff --git a/pkg/volume/secret/secret_test.go b/pkg/volume/secret/secret_test.go index 8a37735333dcd..8cd6012b73944 100644 --- a/pkg/volume/secret/secret_test.go +++ b/pkg/volume/secret/secret_test.go @@ -71,6 +71,10 @@ func TestPlugin(t *testing.T) { VolumeSource: api.VolumeSource{ Secret: &api.SecretVolumeSource{ SecretName: testName, + Modes: []api.SecretFileMode{ + {"data-1", 0400}, + {"data-2", 0404}, + }, }, }, } @@ -124,7 +128,7 @@ func TestPlugin(t *testing.T) { for key, value := range secret.Data { secretDataHostPath := path.Join(volumePath, key) - if _, err := os.Stat(secretDataHostPath); err != nil { + if fileInfo, err := os.Stat(secretDataHostPath); err != nil { t.Fatalf("SetUp() failed, couldn't find secret data on disk: %v", secretDataHostPath) } else { actualSecretBytes, err := ioutil.ReadFile(secretDataHostPath) @@ -136,6 +140,15 @@ func TestPlugin(t *testing.T) { if string(value) != actualSecretValue { t.Errorf("Unexpected value; expected %q, got %q", value, actualSecretValue) } + var expectedFileMode os.FileMode = 0444 + for _, customFileMode := range volumeSpec.VolumeSource.Secret.Modes { + if customFileMode.Name == key { + expectedFileMode = customFileMode.Mode + } + } + if fileInfo.Mode() != expectedFileMode { + t.Errorf("Unexpected secret access permissions; expected %v, got %v", expectedFileMode, fileInfo.Mode()) + } } }