From 568f4c2e631c8d365a1a6c79e10bc18351385301 Mon Sep 17 00:00:00 2001 From: Rodrigo Campos Date: Sun, 10 Jul 2016 21:48:28 -0300 Subject: [PATCH] Add mode permission bits to configmap, secrets and downwardAPI This implements the proposal in: docs/proposals/secret-configmap-downwarapi-file-mode.md Fixes: #28317. The mounttest image is updated so it returns the permissions of the linked file and not the symlink itself. --- pkg/api/testing/fuzzer.go | 48 +- pkg/api/types.go | 28 ++ pkg/api/v1/defaults.go | 21 + pkg/api/v1/defaults_test.go | 66 +++ pkg/api/v1/types.go | 40 ++ pkg/api/validation/validation.go | 26 + pkg/api/validation/validation_test.go | 197 +++++++- pkg/volume/configmap/configmap.go | 28 +- pkg/volume/configmap/configmap_test.go | 95 +++- pkg/volume/downwardapi/downwardapi.go | 27 +- pkg/volume/downwardapi/downwardapi_test.go | 151 ++++++ pkg/volume/secret/secret.go | 29 +- pkg/volume/secret/secret_test.go | 97 +++- pkg/volume/util/atomic_writer.go | 41 +- pkg/volume/util/atomic_writer_test.go | 474 ++++++++++-------- test/e2e/common/configmap.go | 73 ++- test/e2e/common/downwardapi_volume.go | 46 ++ test/e2e/common/secrets.go | 128 ++++- .../serviceaccount/service_account_test.go | 4 +- 19 files changed, 1306 insertions(+), 313 deletions(-) diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 73b54762eab..50b22739a7a 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -251,15 +251,59 @@ func FuzzerFor(t *testing.T, version unversioned.GroupVersion, src rand.Source) policies := []api.RestartPolicy{api.RestartPolicyAlways, api.RestartPolicyNever, api.RestartPolicyOnFailure} *rp = policies[c.Rand.Intn(len(policies))] }, - // Only api.DownwardAPIVolumeFile needs to have a specific func since FieldRef has to be + // api.DownwardAPIVolumeFile needs to have a specific func since FieldRef has to be // defaulted to a version otherwise roundtrip will fail - // For the remaining volume plugins the default fuzzer is enough. func(m *api.DownwardAPIVolumeFile, c fuzz.Continue) { m.Path = c.RandString() versions := []string{"v1"} m.FieldRef = &api.ObjectFieldSelector{} m.FieldRef.APIVersion = versions[c.Rand.Intn(len(versions))] m.FieldRef.FieldPath = c.RandString() + c.Fuzz(m.Mode) + if m.Mode != nil { + *m.Mode &= 0777 + } + }, + func(s *api.SecretVolumeSource, c fuzz.Continue) { + c.FuzzNoCustom(s) // fuzz self without calling this function again + + // DefaultMode should always be set, it has a default + // value and it is expected to be between 0 and 0777 + var mode int32 + c.Fuzz(&mode) + mode &= 0777 + s.DefaultMode = &mode + }, + func(cm *api.ConfigMapVolumeSource, c fuzz.Continue) { + c.FuzzNoCustom(cm) // fuzz self without calling this function again + + // DefaultMode should always be set, it has a default + // value and it is expected to be between 0 and 0777 + var mode int32 + c.Fuzz(&mode) + mode &= 0777 + cm.DefaultMode = &mode + }, + func(d *api.DownwardAPIVolumeSource, c fuzz.Continue) { + c.FuzzNoCustom(d) // fuzz self without calling this function again + + // DefaultMode should always be set, it has a default + // value and it is expected to be between 0 and 0777 + var mode int32 + c.Fuzz(&mode) + mode &= 0777 + d.DefaultMode = &mode + }, + func(k *api.KeyToPath, c fuzz.Continue) { + c.FuzzNoCustom(k) // fuzz self without calling this function again + k.Key = c.RandString() + k.Path = c.RandString() + + // Mode is not mandatory, but if it is set, it should be + // a value between 0 and 0777 + if k.Mode != nil { + *k.Mode &= 0777 + } }, func(vs *api.VolumeSource, c fuzz.Continue) { // Exactly one of the fields must be set. diff --git a/pkg/api/types.go b/pkg/api/types.go index ac1b282bf39..c308c44aa38 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -607,6 +607,12 @@ type SecretVolumeSource struct { // the volume setup will error. Paths must be relative and may not contain // the '..' path or start with '..'. Items []KeyToPath `json:"items,omitempty"` + // Mode bits to use on created files by default. Must be a value between + // 0 and 0777. + // Directories within the path are not affected by this setting. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + DefaultMode *int32 `json:"defaultMode,omitempty"` } // Represents an NFS mount that lasts the lifetime of a pod. @@ -708,6 +714,12 @@ type FlockerVolumeSource struct { type DownwardAPIVolumeSource struct { // Items is a list of DownwardAPIVolume file Items []DownwardAPIVolumeFile `json:"items,omitempty"` + // Mode bits to use on created files by default. Must be a value between + // 0 and 0777. + // Directories within the path are not affected by this setting. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + DefaultMode *int32 `json:"defaultMode,omitempty"` } // Represents a single file containing information from the downward API @@ -719,6 +731,11 @@ type DownwardAPIVolumeFile struct { // Selects a resource of the container: only resources limits and requests // (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. ResourceFieldRef *ResourceFieldSelector `json:"resourceFieldRef,omitempty"` + // Optional: mode bits to use on this file, must be a value between 0 + // and 0777. If not specified, the volume defaultMode will be used. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + Mode *int32 `json:"mode,omitempty"` } // AzureFile represents an Azure File Service mount on the host and bind mount to the pod. @@ -758,6 +775,12 @@ type ConfigMapVolumeSource struct { // the volume setup will error. Paths must be relative and may not contain // the '..' path or start with '..'. Items []KeyToPath `json:"items,omitempty"` + // Mode bits to use on created files by default. Must be a value between + // 0 and 0777. + // Directories within the path are not affected by this setting. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + DefaultMode *int32 `json:"defaultMode,omitempty"` } // Maps a string key to a path within a volume. @@ -770,6 +793,11 @@ type KeyToPath struct { // May not contain the path element '..'. // May not start with the string '..'. Path string `json:"path"` + // Optional: mode bits to use on this file, should be a value between 0 + // and 0777. If not specified, the volume defaultMode will be used. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + Mode *int32 `json:"mode,omitempty"` } // ContainerPort represents a network port in a single container diff --git a/pkg/api/v1/defaults.go b/pkg/api/v1/defaults.go index 2a79ede82d7..15810468b74 100644 --- a/pkg/api/v1/defaults.go +++ b/pkg/api/v1/defaults.go @@ -35,6 +35,9 @@ func addDefaultingFuncs(scheme *runtime.Scheme) error { SetDefaults_Pod, SetDefaults_PodSpec, SetDefaults_Probe, + SetDefaults_SecretVolumeSource, + SetDefaults_ConfigMapVolumeSource, + SetDefaults_DownwardAPIVolumeSource, SetDefaults_Secret, SetDefaults_PersistentVolume, SetDefaults_PersistentVolumeClaim, @@ -174,6 +177,24 @@ func SetDefaults_Probe(obj *Probe) { obj.FailureThreshold = 3 } } +func SetDefaults_SecretVolumeSource(obj *SecretVolumeSource) { + if obj.DefaultMode == nil { + perm := int32(SecretVolumeSourceDefaultMode) + obj.DefaultMode = &perm + } +} +func SetDefaults_ConfigMapVolumeSource(obj *ConfigMapVolumeSource) { + if obj.DefaultMode == nil { + perm := int32(ConfigMapVolumeSourceDefaultMode) + obj.DefaultMode = &perm + } +} +func SetDefaults_DownwardAPIVolumeSource(obj *DownwardAPIVolumeSource) { + if obj.DefaultMode == nil { + perm := int32(DownwardAPIVolumeSourceDefaultMode) + obj.DefaultMode = &perm + } +} func SetDefaults_Secret(obj *Secret) { if obj.Type == "" { obj.Type = SecretTypeOpaque diff --git a/pkg/api/v1/defaults_test.go b/pkg/api/v1/defaults_test.go index 17aeecf5ee2..6987d0f301c 100644 --- a/pkg/api/v1/defaults_test.go +++ b/pkg/api/v1/defaults_test.go @@ -242,6 +242,72 @@ func TestSetDefaultService(t *testing.T) { } } +func TestSetDefaultSecretVolumeSource(t *testing.T) { + s := versioned.PodSpec{} + s.Volumes = []versioned.Volume{ + { + VolumeSource: versioned.VolumeSource{ + Secret: &versioned.SecretVolumeSource{}, + }, + }, + } + pod := &versioned.Pod{ + Spec: s, + } + output := roundTrip(t, runtime.Object(pod)) + pod2 := output.(*versioned.Pod) + defaultMode := pod2.Spec.Volumes[0].VolumeSource.Secret.DefaultMode + expectedMode := versioned.SecretVolumeSourceDefaultMode + + if defaultMode == nil || *defaultMode != expectedMode { + t.Errorf("Expected secret DefaultMode %v, got %v", expectedMode, defaultMode) + } +} + +func TestSetDefaultConfigMapVolumeSource(t *testing.T) { + s := versioned.PodSpec{} + s.Volumes = []versioned.Volume{ + { + VolumeSource: versioned.VolumeSource{ + ConfigMap: &versioned.ConfigMapVolumeSource{}, + }, + }, + } + pod := &versioned.Pod{ + Spec: s, + } + output := roundTrip(t, runtime.Object(pod)) + pod2 := output.(*versioned.Pod) + defaultMode := pod2.Spec.Volumes[0].VolumeSource.ConfigMap.DefaultMode + expectedMode := versioned.ConfigMapVolumeSourceDefaultMode + + if defaultMode == nil || *defaultMode != expectedMode { + t.Errorf("Expected ConfigMap DefaultMode %v, got %v", expectedMode, defaultMode) + } +} + +func TestSetDefaultDownwardAPIVolumeSource(t *testing.T) { + s := versioned.PodSpec{} + s.Volumes = []versioned.Volume{ + { + VolumeSource: versioned.VolumeSource{ + DownwardAPI: &versioned.DownwardAPIVolumeSource{}, + }, + }, + } + pod := &versioned.Pod{ + Spec: s, + } + output := roundTrip(t, runtime.Object(pod)) + pod2 := output.(*versioned.Pod) + defaultMode := pod2.Spec.Volumes[0].VolumeSource.DownwardAPI.DefaultMode + expectedMode := versioned.DownwardAPIVolumeSourceDefaultMode + + if defaultMode == nil || *defaultMode != expectedMode { + t.Errorf("Expected DownwardAPI DefaultMode %v, got %v", expectedMode, defaultMode) + } +} + func TestSetDefaultSecret(t *testing.T) { s := &versioned.Secret{} obj2 := roundTrip(t, runtime.Object(s)) diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 2292bbc3c56..8b2003586b1 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -770,8 +770,18 @@ type SecretVolumeSource struct { // the volume setup will error. Paths must be relative and may not contain // the '..' path or start with '..'. Items []KeyToPath `json:"items,omitempty" protobuf:"bytes,2,rep,name=items"` + // Optional: mode bits to use on created files by default. Must be a + // value between 0 and 0777. Defaults to 0644. + // Directories within the path are not affected by this setting. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + DefaultMode *int32 `json:"defaultMode,omitempty" protobuf:"bytes,3,opt,name=defaultMode"` } +const ( + SecretVolumeSourceDefaultMode int32 = 0644 +) + // Represents an NFS mount that lasts the lifetime of a pod. // NFS volumes do not support ownership management or SELinux relabeling. type NFSVolumeSource struct { @@ -869,8 +879,18 @@ type ConfigMapVolumeSource struct { // the volume setup will error. Paths must be relative and may not contain // the '..' path or start with '..'. Items []KeyToPath `json:"items,omitempty" protobuf:"bytes,2,rep,name=items"` + // Optional: mode bits to use on created files by default. Must be a + // value between 0 and 0777. Defaults to 0644. + // Directories within the path are not affected by this setting. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + DefaultMode *int32 `json:"defaultMode,omitempty" protobuf:"varint,3,opt,name=defaultMode"` } +const ( + ConfigMapVolumeSourceDefaultMode int32 = 0644 +) + // Maps a string key to a path within a volume. type KeyToPath struct { // The key to project. @@ -881,6 +901,11 @@ type KeyToPath struct { // May not contain the path element '..'. // May not start with the string '..'. Path string `json:"path" protobuf:"bytes,2,opt,name=path"` + // Optional: mode bits to use on this file, must be a value between 0 + // and 0777. If not specified, the volume defaultMode will be used. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + Mode *int32 `json:"mode,omitempty" protobuf:"varint,3,opt,name=mode"` } // ContainerPort represents a network port in a single container. @@ -3287,8 +3312,18 @@ type ComponentStatusList struct { type DownwardAPIVolumeSource struct { // Items is a list of downward API volume file Items []DownwardAPIVolumeFile `json:"items,omitempty" protobuf:"bytes,1,rep,name=items"` + // Optional: mode bits to use on created files by default. Must be a + // value between 0 and 0777. Defaults to 0644. + // Directories within the path are not affected by this setting. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + DefaultMode *int32 `json:"defaultMode,omitempty" protobuf:"varint,2,opt,name=defaultMode"` } +const ( + DownwardAPIVolumeSourceDefaultMode int32 = 0644 +) + // DownwardAPIVolumeFile represents information to create the file containing the pod field type DownwardAPIVolumeFile struct { // Required: Path is the relative path name of the file to be created. Must not be absolute or contain the '..' path. Must be utf-8 encoded. The first item of the relative path must not start with '..' @@ -3298,6 +3333,11 @@ type DownwardAPIVolumeFile struct { // Selects a resource of the container: only resources limits and requests // (limits.cpu, limits.memory, requests.cpu and requests.memory) are currently supported. ResourceFieldRef *ResourceFieldSelector `json:"resourceFieldRef,omitempty" protobuf:"bytes,3,opt,name=resourceFieldRef"` + // Optional: mode bits to use on this file, must be a value between 0 + // and 0777. If not specified, the volume defaultMode will be used. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + Mode *int32 `json:"mode,omitempty" protobuf:"varint,4,opt,name=mode"` } // SecurityContext holds security configuration that will be applied to a container. diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index ce206aad215..b365bb4bd05 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -52,6 +52,7 @@ const fieldImmutableErrorMsg string = `field is immutable` const isNotIntegerErrorMsg string = `must be an integer` var pdPartitionErrorMsg string = validation.InclusiveRangeError(1, 255) +var volumeModeErrorMsg string = "must be a number between 0 and 0777 (octal), both inclusive" const totalAnnotationSizeLimitB int = 256 * (1 << 10) // 256 kB @@ -660,6 +661,12 @@ func validateSecretVolumeSource(secretSource *api.SecretVolumeSource, fldPath *f if len(secretSource.SecretName) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("secretName"), "")) } + + secretMode := secretSource.DefaultMode + if secretMode != nil && (*secretMode > 0777 || *secretMode < 0) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *secretMode, volumeModeErrorMsg)) + } + itemsPath := fldPath.Child("items") for i, kp := range secretSource.Items { itemPath := itemsPath.Index(i) @@ -673,6 +680,12 @@ func validateConfigMapVolumeSource(configMapSource *api.ConfigMapVolumeSource, f if len(configMapSource.Name) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) } + + configMapMode := configMapSource.DefaultMode + if configMapMode != nil && (*configMapMode > 0777 || *configMapMode < 0) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *configMapMode, volumeModeErrorMsg)) + } + itemsPath := fldPath.Child("items") for i, kp := range configMapSource.Items { itemPath := itemsPath.Index(i) @@ -690,6 +703,10 @@ func validateKeyToPath(kp *api.KeyToPath, fldPath *field.Path) field.ErrorList { allErrs = append(allErrs, field.Required(fldPath.Child("path"), "")) } allErrs = append(allErrs, validateLocalNonReservedPath(kp.Path, fldPath.Child("path"))...) + if kp.Mode != nil && (*kp.Mode > 0777 || *kp.Mode < 0) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("mode"), *kp.Mode, volumeModeErrorMsg)) + } + return allErrs } @@ -745,6 +762,12 @@ var validDownwardAPIFieldPathExpressions = sets.NewString( func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSource, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} + + downwardAPIMode := downwardAPIVolume.DefaultMode + if downwardAPIMode != nil && (*downwardAPIMode > 0777 || *downwardAPIMode < 0) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultMode"), *downwardAPIMode, volumeModeErrorMsg)) + } + for _, file := range downwardAPIVolume.Items { if len(file.Path) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("path"), "")) @@ -760,6 +783,9 @@ func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSou } else { allErrs = append(allErrs, field.Required(fldPath, "one of fieldRef and resourceFieldRef is required")) } + if file.Mode != nil && (*file.Mode > 0777 || *file.Mode < 0) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("mode"), *file.Mode, volumeModeErrorMsg)) + } } return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 8d0b10b8131..15f6e7a6cf5 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -794,6 +794,10 @@ func TestValidateKeyToPath(t *testing.T) { kp: api.KeyToPath{Key: "k", Path: "p/..p/p../p..p"}, ok: true, }, + { + kp: api.KeyToPath{Key: "k", Path: "p", Mode: newInt32(0644)}, + ok: true, + }, { kp: api.KeyToPath{Key: "", Path: "p"}, ok: false, @@ -824,6 +828,16 @@ func TestValidateKeyToPath(t *testing.T) { ok: false, errtype: field.ErrorTypeInvalid, }, + { + kp: api.KeyToPath{Key: "k", Path: "p", Mode: newInt32(01000)}, + ok: false, + errtype: field.ErrorTypeInvalid, + }, + { + kp: api.KeyToPath{Key: "k", Path: "p", Mode: newInt32(-1)}, + ok: false, + errtype: field.ErrorTypeInvalid, + }, } for i, tc := range testCases { @@ -1129,7 +1143,19 @@ func TestValidateVolumes(t *testing.T) { }, }, { - name: "valid Secret with projection", + name: "valid Secret with defaultMode", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "my-secret", + DefaultMode: newInt32(0644), + }, + }, + }, + }, + { + name: "valid Secret with projection and mode", vol: api.Volume{ Name: "secret", VolumeSource: api.VolumeSource{ @@ -1138,6 +1164,7 @@ func TestValidateVolumes(t *testing.T) { Items: []api.KeyToPath{{ Key: "key", Path: "filename", + Mode: newInt32(0644), }}, }, }, @@ -1200,6 +1227,34 @@ func TestValidateVolumes(t *testing.T) { errtype: field.ErrorTypeInvalid, errfield: "secret.items[0].path", }, + { + name: "secret with invalid positive defaultMode", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "s", + DefaultMode: newInt32(01000), + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "secret.defaultMode", + }, + { + name: "secret with invalid negative defaultMode", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "s", + DefaultMode: newInt32(-1), + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "secret.defaultMode", + }, // ConfigMap { name: "valid ConfigMap", @@ -1215,7 +1270,21 @@ func TestValidateVolumes(t *testing.T) { }, }, { - name: "valid ConfigMap with projection", + name: "valid ConfigMap with defaultMode", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{ + Name: "my-cfgmap", + }, + DefaultMode: newInt32(0644), + }, + }, + }, + }, + { + name: "valid ConfigMap with projection and mode", vol: api.Volume{ Name: "cfgmap", VolumeSource: api.VolumeSource{ @@ -1225,6 +1294,7 @@ func TestValidateVolumes(t *testing.T) { Items: []api.KeyToPath{{ Key: "key", Path: "filename", + Mode: newInt32(0644), }}, }, }, @@ -1288,6 +1358,34 @@ func TestValidateVolumes(t *testing.T) { errtype: field.ErrorTypeInvalid, errfield: "configMap.items[0].path", }, + { + name: "configmap with invalid positive defaultMode", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{Name: "c"}, + DefaultMode: newInt32(01000), + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "configMap.defaultMode", + }, + { + name: "configmap with invalid negative defaultMode", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{Name: "c"}, + DefaultMode: newInt32(-1), + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "configMap.defaultMode", + }, // Glusterfs { name: "valid Glusterfs", @@ -1551,6 +1649,75 @@ func TestValidateVolumes(t *testing.T) { }, }, }, + { + name: "downapi valid defaultMode", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: newInt32(0644), + }, + }, + }, + }, + { + name: "downapi valid item mode", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Mode: newInt32(0644), + Path: "path", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }}, + }, + }, + }, + }, + { + name: "downapi invalid positive item mode", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Mode: newInt32(01000), + Path: "path", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI.mode", + }, + { + name: "downapi invalid negative item mode", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Mode: newInt32(-1), + Path: "path", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI.mode", + }, { name: "downapi empty metatada path", vol: api.Volume{ @@ -1673,6 +1840,32 @@ func TestValidateVolumes(t *testing.T) { errfield: "downwardAPI", errdetail: "fieldRef and resourceFieldRef can not be specified simultaneously", }, + { + name: "downapi invalid positive defaultMode", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: newInt32(01000), + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI.defaultMode", + }, + { + name: "downapi invalid negative defaultMode", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: newInt32(-1), + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI.defaultMode", + }, // FC { name: "valid FC", diff --git a/pkg/volume/configmap/configmap.go b/pkg/volume/configmap/configmap.go index 6451951f765..f2c34f078c0 100644 --- a/pkg/volume/configmap/configmap.go +++ b/pkg/volume/configmap/configmap.go @@ -175,7 +175,7 @@ func (b *configMapVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { len(configMap.Data), totalBytes) - payload, err := makePayload(b.source.Items, configMap) + payload, err := makePayload(b.source.Items, configMap, b.source.DefaultMode) if err != nil { return err } @@ -202,22 +202,36 @@ func (b *configMapVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { return nil } -func makePayload(mappings []api.KeyToPath, configMap *api.ConfigMap) (map[string][]byte, error) { - payload := make(map[string][]byte, len(configMap.Data)) +func makePayload(mappings []api.KeyToPath, configMap *api.ConfigMap, defaultMode *int32) (map[string]volumeutil.FileProjection, error) { + if defaultMode == nil { + return nil, fmt.Errorf("No defaultMode used, not even the default value for it") + } + + payload := make(map[string]volumeutil.FileProjection, len(configMap.Data)) + var fileProjection volumeutil.FileProjection if len(mappings) == 0 { for name, data := range configMap.Data { - payload[name] = []byte(data) + fileProjection.Data = []byte(data) + fileProjection.Mode = *defaultMode + payload[name] = fileProjection } } else { for _, ktp := range mappings { content, ok := configMap.Data[ktp.Key] if !ok { - glog.Errorf("references non-existent config key") - return nil, fmt.Errorf("references non-existent config key") + err_msg := "references non-existent config key" + glog.Errorf(err_msg) + return nil, fmt.Errorf(err_msg) } - payload[ktp.Path] = []byte(content) + fileProjection.Data = []byte(content) + if ktp.Mode != nil { + fileProjection.Mode = *ktp.Mode + } else { + fileProjection.Mode = *defaultMode + } + payload[ktp.Path] = fileProjection } } diff --git a/pkg/volume/configmap/configmap_test.go b/pkg/volume/configmap/configmap_test.go index 6367d3aa735..67432660daf 100644 --- a/pkg/volume/configmap/configmap_test.go +++ b/pkg/volume/configmap/configmap_test.go @@ -36,11 +36,13 @@ import ( ) func TestMakePayload(t *testing.T) { + case_mapping_mode := int32(0400) cases := []struct { name string mappings []api.KeyToPath configMap *api.ConfigMap - payload map[string][]byte + mode int32 + payload map[string]util.FileProjection success bool }{ { @@ -51,9 +53,10 @@ func TestMakePayload(t *testing.T) { "bar": "bar", }, }, - payload: map[string][]byte{ - "foo": []byte("foo"), - "bar": []byte("bar"), + mode: 0644, + payload: map[string]util.FileProjection{ + "foo": {Data: []byte("foo"), Mode: 0644}, + "bar": {Data: []byte("bar"), Mode: 0644}, }, success: true, }, @@ -71,8 +74,9 @@ func TestMakePayload(t *testing.T) { "bar": "bar", }, }, - payload: map[string][]byte{ - "path/to/foo.txt": []byte("foo"), + mode: 0644, + payload: map[string]util.FileProjection{ + "path/to/foo.txt": {Data: []byte("foo"), Mode: 0644}, }, success: true, }, @@ -90,8 +94,9 @@ func TestMakePayload(t *testing.T) { "bar": "bar", }, }, - payload: map[string][]byte{ - "path/to/1/2/3/foo.txt": []byte("foo"), + mode: 0644, + payload: map[string]util.FileProjection{ + "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644}, }, success: true, }, @@ -109,8 +114,9 @@ func TestMakePayload(t *testing.T) { "bar": "bar", }, }, - payload: map[string][]byte{ - "path/to/1/2/3/foo.txt": []byte("foo"), + mode: 0644, + payload: map[string]util.FileProjection{ + "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644}, }, success: true, }, @@ -132,9 +138,10 @@ func TestMakePayload(t *testing.T) { "bar": "bar", }, }, - payload: map[string][]byte{ - "path/to/1/2/3/foo.txt": []byte("foo"), - "another/path/to/the/esteemed/bar.bin": []byte("bar"), + mode: 0644, + payload: map[string]util.FileProjection{ + "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644}, + "another/path/to/the/esteemed/bar.bin": {Data: []byte("bar"), Mode: 0644}, }, success: true, }, @@ -152,12 +159,65 @@ func TestMakePayload(t *testing.T) { "bar": "bar", }, }, + mode: 0644, success: false, }, + { + name: "mapping with Mode", + mappings: []api.KeyToPath{ + { + Key: "foo", + Path: "foo.txt", + Mode: &case_mapping_mode, + }, + { + Key: "bar", + Path: "bar.bin", + Mode: &case_mapping_mode, + }, + }, + configMap: &api.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + mode: 0644, + payload: map[string]util.FileProjection{ + "foo.txt": {Data: []byte("foo"), Mode: case_mapping_mode}, + "bar.bin": {Data: []byte("bar"), Mode: case_mapping_mode}, + }, + success: true, + }, + { + name: "mapping with defaultMode", + mappings: []api.KeyToPath{ + { + Key: "foo", + Path: "foo.txt", + }, + { + Key: "bar", + Path: "bar.bin", + }, + }, + configMap: &api.ConfigMap{ + Data: map[string]string{ + "foo": "foo", + "bar": "bar", + }, + }, + mode: 0644, + payload: map[string]util.FileProjection{ + "foo.txt": {Data: []byte("foo"), Mode: 0644}, + "bar.bin": {Data: []byte("bar"), Mode: 0644}, + }, + success: true, + }, } for _, tc := range cases { - actualPayload, err := makePayload(tc.mappings, tc.configMap) + actualPayload, err := makePayload(tc.mappings, tc.configMap, &tc.mode) if err != nil && tc.success { t.Errorf("%v: unexpected failure making payload: %v", tc.name, err) continue @@ -214,7 +274,7 @@ func TestPlugin(t *testing.T) { testNamespace = "test_configmap_namespace" testName = "test_configmap_name" - volumeSpec = volumeSpec(testVolumeName, testName) + volumeSpec = volumeSpec(testVolumeName, testName, 0644) configMap = configMap(testNamespace, testName) client = fake.NewSimpleClientset(&configMap) pluginMgr = volume.VolumePluginMgr{} @@ -277,7 +337,7 @@ func TestPluginReboot(t *testing.T) { testNamespace = "test_configmap_namespace" testName = "test_configmap_name" - volumeSpec = volumeSpec(testVolumeName, testName) + volumeSpec = volumeSpec(testVolumeName, testName, 0644) configMap = configMap(testNamespace, testName) client = fake.NewSimpleClientset(&configMap) pluginMgr = volume.VolumePluginMgr{} @@ -324,7 +384,7 @@ func TestPluginReboot(t *testing.T) { doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t) } -func volumeSpec(volumeName, configMapName string) *api.Volume { +func volumeSpec(volumeName, configMapName string, defaultMode int32) *api.Volume { return &api.Volume{ Name: volumeName, VolumeSource: api.VolumeSource{ @@ -332,6 +392,7 @@ func volumeSpec(volumeName, configMapName string) *api.Volume { LocalObjectReference: api.LocalObjectReference{ Name: configMapName, }, + DefaultMode: &defaultMode, }, }, } diff --git a/pkg/volume/downwardapi/downwardapi.go b/pkg/volume/downwardapi/downwardapi.go index beddd851aaf..db8ded6ccbb 100644 --- a/pkg/volume/downwardapi/downwardapi.go +++ b/pkg/volume/downwardapi/downwardapi.go @@ -92,6 +92,7 @@ func (plugin *downwardAPIPlugin) NewMounter(spec *volume.Spec, pod *api.Pod, opt } return &downwardAPIVolumeMounter{ downwardAPIVolume: v, + source: *spec.Volume.DownwardAPI, opts: &opts, }, nil } @@ -130,7 +131,8 @@ type downwardAPIVolume struct { // and dumps it in files type downwardAPIVolumeMounter struct { *downwardAPIVolume - opts *volume.VolumeOptions + source api.DownwardAPIVolumeSource + opts *volume.VolumeOptions } // downwardAPIVolumeMounter implements volume.Mounter interface @@ -166,7 +168,7 @@ func (b *downwardAPIVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { return err } - data, err := b.collectData() + data, err := b.collectData(b.source.DefaultMode) if err != nil { glog.Errorf("Error preparing data for downwardAPI volume %v for pod %v/%v: %s", b.volName, b.pod.Namespace, b.pod.Name, err.Error()) return err @@ -193,16 +195,27 @@ func (b *downwardAPIVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { // collectData collects requested downwardAPI in data map. // Map's key is the requested name of file to dump // Map's value is the (sorted) content of the field to be dumped in the file. -func (d *downwardAPIVolume) collectData() (map[string][]byte, error) { +func (d *downwardAPIVolume) collectData(defaultMode *int32) (map[string]volumeutil.FileProjection, error) { + if defaultMode == nil { + return nil, fmt.Errorf("No defaultMode used, not even the default value for it") + } + errlist := []error{} - data := make(map[string][]byte) + data := make(map[string]volumeutil.FileProjection) for _, fileInfo := range d.items { + var fileProjection volumeutil.FileProjection + fPath := path.Clean(fileInfo.Path) + if fileInfo.Mode != nil { + fileProjection.Mode = *fileInfo.Mode + } else { + fileProjection.Mode = *defaultMode + } if fileInfo.FieldRef != nil { if values, err := fieldpath.ExtractFieldPathAsString(d.pod, fileInfo.FieldRef.FieldPath); err != nil { glog.Errorf("Unable to extract field %s: %s", fileInfo.FieldRef.FieldPath, err.Error()) errlist = append(errlist, err) } else { - data[path.Clean(fileInfo.Path)] = []byte(sortLines(values)) + fileProjection.Data = []byte(sortLines(values)) } } else if fileInfo.ResourceFieldRef != nil { containerName := fileInfo.ResourceFieldRef.ContainerName @@ -213,9 +226,11 @@ func (d *downwardAPIVolume) collectData() (map[string][]byte, error) { glog.Errorf("Unable to extract field %s: %s", fileInfo.ResourceFieldRef.Resource, err.Error()) errlist = append(errlist, err) } else { - data[path.Clean(fileInfo.Path)] = []byte(sortLines(values)) + fileProjection.Data = []byte(sortLines(values)) } } + + data[fPath] = fileProjection } return data, utilerrors.NewAggregate(errlist) } diff --git a/pkg/volume/downwardapi/downwardapi_test.go b/pkg/volume/downwardapi/downwardapi_test.go index 931c0c156d4..a9260289362 100644 --- a/pkg/volume/downwardapi/downwardapi_test.go +++ b/pkg/volume/downwardapi/downwardapi_test.go @@ -103,10 +103,12 @@ func TestLabels(t *testing.T) { defer os.RemoveAll(rootDir) pluginMgr.InitPlugins(ProbeVolumePlugins(), host) plugin, err := pluginMgr.FindPluginByName(downwardAPIPluginName) + defaultMode := int32(0644) volumeSpec := &api.Volume{ Name: testVolumeName, VolumeSource: api.VolumeSource{ DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, Items: []api.DownwardAPIVolumeFile{ {Path: "labels", FieldRef: &api.ObjectFieldSelector{ FieldPath: "metadata.labels"}}}}, @@ -167,10 +169,12 @@ func TestAnnotations(t *testing.T) { "a1": "value1", "a2": "value2"} + defaultMode := int32(0644) volumeSpec := &api.Volume{ Name: testVolumeName, VolumeSource: api.VolumeSource{ DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, Items: []api.DownwardAPIVolumeFile{ {Path: "annotations", FieldRef: &api.ObjectFieldSelector{ FieldPath: "metadata.annotations"}}}}, @@ -230,10 +234,12 @@ func TestName(t *testing.T) { testName = "test_metadata_name" ) + defaultMode := int32(0644) volumeSpec := &api.Volume{ Name: testVolumeName, VolumeSource: api.VolumeSource{ DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, Items: []api.DownwardAPIVolumeFile{ {Path: "name_file_name", FieldRef: &api.ObjectFieldSelector{ FieldPath: "metadata.name"}}}}, @@ -293,10 +299,12 @@ func TestNamespace(t *testing.T) { testName = "test_metadata_name" ) + defaultMode := int32(0644) volumeSpec := &api.Volume{ Name: testVolumeName, VolumeSource: api.VolumeSource{ DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, Items: []api.DownwardAPIVolumeFile{ {Path: "namespace_file_name", FieldRef: &api.ObjectFieldSelector{ FieldPath: "metadata.namespace"}}}}, @@ -371,10 +379,12 @@ func TestWriteTwiceNoUpdate(t *testing.T) { defer os.RemoveAll(tmpDir) pluginMgr.InitPlugins(ProbeVolumePlugins(), host) plugin, err := pluginMgr.FindPluginByName(downwardAPIPluginName) + defaultMode := int32(0644) volumeSpec := &api.Volume{ Name: testVolumeName, VolumeSource: api.VolumeSource{ DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, Items: []api.DownwardAPIVolumeFile{ {Path: "labels", FieldRef: &api.ObjectFieldSelector{ FieldPath: "metadata.labels"}}}}, @@ -457,10 +467,12 @@ func TestWriteTwiceWithUpdate(t *testing.T) { defer os.RemoveAll(tmpDir) pluginMgr.InitPlugins(ProbeVolumePlugins(), host) plugin, err := pluginMgr.FindPluginByName(downwardAPIPluginName) + defaultMode := int32(0644) volumeSpec := &api.Volume{ Name: testVolumeName, VolumeSource: api.VolumeSource{ DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, Items: []api.DownwardAPIVolumeFile{ {Path: "labels", FieldRef: &api.ObjectFieldSelector{ FieldPath: "metadata.labels"}}}}, @@ -563,10 +575,12 @@ func TestWriteWithUnixPath(t *testing.T) { defer os.RemoveAll(tmpDir) pluginMgr.InitPlugins(ProbeVolumePlugins(), host) plugin, err := pluginMgr.FindPluginByName(downwardAPIPluginName) + defaultMode := int32(0644) volumeSpec := &api.Volume{ Name: testVolumeName, VolumeSource: api.VolumeSource{ DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, Items: []api.DownwardAPIVolumeFile{ {Path: "this/is/mine/labels", FieldRef: &api.ObjectFieldSelector{ FieldPath: "metadata.labels"}}, @@ -643,10 +657,12 @@ func TestWriteWithUnixPathBadPath(t *testing.T) { t.Errorf("Can't find the plugin by name") } + defaultMode := int32(0644) volumeSpec := &api.Volume{ Name: testVolumeName, VolumeSource: api.VolumeSource{ DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, Items: []api.DownwardAPIVolumeFile{ { Path: "this//labels", @@ -684,3 +700,138 @@ func TestWriteWithUnixPathBadPath(t *testing.T) { t.Errorf("Found `%s` expected %s", data, fieldpath.FormatMap(labels)) } } + +func TestDefaultMode(t *testing.T) { + var ( + testPodUID = types.UID("test_pod_uid") + testVolumeName = "test_name" + testNamespace = "test_metadata_namespace" + testName = "test_metadata_name" + ) + + defaultMode := int32(0644) + volumeSpec := &api.Volume{ + Name: testVolumeName, + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, + Items: []api.DownwardAPIVolumeFile{ + {Path: "name_file_name", FieldRef: &api.ObjectFieldSelector{ + FieldPath: "metadata.name"}}}}, + }, + } + + clientset := fake.NewSimpleClientset(&api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + }, + }) + + pluginMgr := volume.VolumePluginMgr{} + tmpDir, host := newTestHost(t, clientset) + defer os.RemoveAll(tmpDir) + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + plugin, err := pluginMgr.FindPluginByName(downwardAPIPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: testPodUID, Name: testName}} + mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Mounter: %v", err) + } + if mounter == nil { + t.Errorf("Got a nil Mounter") + } + + volumePath := mounter.GetPath() + + err = mounter.SetUp(nil) + if err != nil { + t.Errorf("Failed to setup volume: %v", err) + } + + fileInfo, err := os.Stat(path.Join(volumePath, "name_file_name")) + if err != nil { + t.Errorf(err.Error()) + } + + actualMode := fileInfo.Mode() + expectedMode := os.FileMode(defaultMode) + if actualMode != expectedMode { + t.Errorf("Found mode `%v` expected %v", actualMode, expectedMode) + } + + CleanEverything(plugin, testVolumeName, volumePath, testPodUID, t) +} + +func TestItemMode(t *testing.T) { + var ( + testPodUID = types.UID("test_pod_uid") + testVolumeName = "test_name" + testNamespace = "test_metadata_namespace" + testName = "test_metadata_name" + ) + + defaultMode := int32(0644) + itemMode := int32(0400) + volumeSpec := &api.Volume{ + Name: testVolumeName, + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + DefaultMode: &defaultMode, + Items: []api.DownwardAPIVolumeFile{ + { + Path: "name_file_name", FieldRef: &api.ObjectFieldSelector{FieldPath: "metadata.name"}, + Mode: &itemMode, + }, + }, + }, + }, + } + + clientset := fake.NewSimpleClientset(&api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: testName, + Namespace: testNamespace, + }, + }) + + pluginMgr := volume.VolumePluginMgr{} + tmpDir, host := newTestHost(t, clientset) + defer os.RemoveAll(tmpDir) + pluginMgr.InitPlugins(ProbeVolumePlugins(), host) + plugin, err := pluginMgr.FindPluginByName(downwardAPIPluginName) + if err != nil { + t.Errorf("Can't find the plugin by name") + } + pod := &api.Pod{ObjectMeta: api.ObjectMeta{UID: testPodUID, Name: testName}} + mounter, err := plugin.NewMounter(volume.NewSpecFromVolume(volumeSpec), pod, volume.VolumeOptions{}) + if err != nil { + t.Errorf("Failed to make a new Mounter: %v", err) + } + if mounter == nil { + t.Errorf("Got a nil Mounter") + } + + volumePath := mounter.GetPath() + + err = mounter.SetUp(nil) + if err != nil { + t.Errorf("Failed to setup volume: %v", err) + } + + fileInfo, err := os.Stat(path.Join(volumePath, "name_file_name")) + if err != nil { + t.Errorf(err.Error()) + } + + actualMode := fileInfo.Mode() + expectedMode := os.FileMode(itemMode) + if actualMode != expectedMode { + t.Errorf("Found mode `%v` expected %v", actualMode, expectedMode) + } + + CleanEverything(plugin, testVolumeName, volumePath, testPodUID, t) +} diff --git a/pkg/volume/secret/secret.go b/pkg/volume/secret/secret.go index 9ca6911690a..116d519bce6 100644 --- a/pkg/volume/secret/secret.go +++ b/pkg/volume/secret/secret.go @@ -190,7 +190,7 @@ func (b *secretVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { len(secret.Data), totalBytes) - payload, err := makePayload(b.source.Items, secret) + payload, err := makePayload(b.source.Items, secret, b.source.DefaultMode) if err != nil { return err } @@ -217,25 +217,38 @@ func (b *secretVolumeMounter) SetUpAt(dir string, fsGroup *int64) error { return nil } -func makePayload(mappings []api.KeyToPath, secret *api.Secret) (map[string][]byte, error) { - payload := make(map[string][]byte, len(secret.Data)) +func makePayload(mappings []api.KeyToPath, secret *api.Secret, defaultMode *int32) (map[string]volumeutil.FileProjection, error) { + if defaultMode == nil { + return nil, fmt.Errorf("No defaultMode used, not even the default value for it") + } + + payload := make(map[string]volumeutil.FileProjection, len(secret.Data)) + var fileProjection volumeutil.FileProjection if len(mappings) == 0 { for name, data := range secret.Data { - payload[name] = []byte(data) + fileProjection.Data = []byte(data) + fileProjection.Mode = *defaultMode + payload[name] = fileProjection } } else { for _, ktp := range mappings { content, ok := secret.Data[ktp.Key] if !ok { - glog.Errorf("references non-existent secret key") - return nil, fmt.Errorf("references non-existent secret key") + err_msg := "references non-existent secret key" + glog.Errorf(err_msg) + return nil, fmt.Errorf(err_msg) } - payload[ktp.Path] = []byte(content) + fileProjection.Data = []byte(content) + if ktp.Mode != nil { + fileProjection.Mode = *ktp.Mode + } else { + fileProjection.Mode = *defaultMode + } + payload[ktp.Path] = fileProjection } } - return payload, nil } diff --git a/pkg/volume/secret/secret_test.go b/pkg/volume/secret/secret_test.go index eb8aa6676b6..f84420d69fb 100644 --- a/pkg/volume/secret/secret_test.go +++ b/pkg/volume/secret/secret_test.go @@ -39,11 +39,13 @@ import ( ) func TestMakePayload(t *testing.T) { + case_mapping_mode := int32(0400) cases := []struct { name string mappings []api.KeyToPath secret *api.Secret - payload map[string][]byte + mode int32 + payload map[string]util.FileProjection success bool }{ { @@ -54,9 +56,10 @@ func TestMakePayload(t *testing.T) { "bar": []byte("bar"), }, }, - payload: map[string][]byte{ - "foo": []byte("foo"), - "bar": []byte("bar"), + mode: 0644, + payload: map[string]util.FileProjection{ + "foo": {Data: []byte("foo"), Mode: 0644}, + "bar": {Data: []byte("bar"), Mode: 0644}, }, success: true, }, @@ -74,8 +77,9 @@ func TestMakePayload(t *testing.T) { "bar": []byte("bar"), }, }, - payload: map[string][]byte{ - "path/to/foo.txt": []byte("foo"), + mode: 0644, + payload: map[string]util.FileProjection{ + "path/to/foo.txt": {Data: []byte("foo"), Mode: 0644}, }, success: true, }, @@ -93,8 +97,9 @@ func TestMakePayload(t *testing.T) { "bar": []byte("bar"), }, }, - payload: map[string][]byte{ - "path/to/1/2/3/foo.txt": []byte("foo"), + mode: 0644, + payload: map[string]util.FileProjection{ + "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644}, }, success: true, }, @@ -112,8 +117,9 @@ func TestMakePayload(t *testing.T) { "bar": []byte("bar"), }, }, - payload: map[string][]byte{ - "path/to/1/2/3/foo.txt": []byte("foo"), + mode: 0644, + payload: map[string]util.FileProjection{ + "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644}, }, success: true, }, @@ -135,9 +141,10 @@ func TestMakePayload(t *testing.T) { "bar": []byte("bar"), }, }, - payload: map[string][]byte{ - "path/to/1/2/3/foo.txt": []byte("foo"), - "another/path/to/the/esteemed/bar.bin": []byte("bar"), + mode: 0644, + payload: map[string]util.FileProjection{ + "path/to/1/2/3/foo.txt": {Data: []byte("foo"), Mode: 0644}, + "another/path/to/the/esteemed/bar.bin": {Data: []byte("bar"), Mode: 0644}, }, success: true, }, @@ -155,12 +162,65 @@ func TestMakePayload(t *testing.T) { "bar": []byte("bar"), }, }, + mode: 0644, success: false, }, + { + name: "mapping with Mode", + mappings: []api.KeyToPath{ + { + Key: "foo", + Path: "foo.txt", + Mode: &case_mapping_mode, + }, + { + Key: "bar", + Path: "bar.bin", + Mode: &case_mapping_mode, + }, + }, + secret: &api.Secret{ + Data: map[string][]byte{ + "foo": []byte("foo"), + "bar": []byte("bar"), + }, + }, + mode: 0644, + payload: map[string]util.FileProjection{ + "foo.txt": {Data: []byte("foo"), Mode: case_mapping_mode}, + "bar.bin": {Data: []byte("bar"), Mode: case_mapping_mode}, + }, + success: true, + }, + { + name: "mapping with defaultMode", + mappings: []api.KeyToPath{ + { + Key: "foo", + Path: "foo.txt", + }, + { + Key: "bar", + Path: "bar.bin", + }, + }, + secret: &api.Secret{ + Data: map[string][]byte{ + "foo": []byte("foo"), + "bar": []byte("bar"), + }, + }, + mode: 0644, + payload: map[string]util.FileProjection{ + "foo.txt": {Data: []byte("foo"), Mode: 0644}, + "bar.bin": {Data: []byte("bar"), Mode: 0644}, + }, + success: true, + }, } for _, tc := range cases { - actualPayload, err := makePayload(tc.mappings, tc.secret) + actualPayload, err := makePayload(tc.mappings, tc.secret, &tc.mode) if err != nil && tc.success { t.Errorf("%v: unexpected failure making payload: %v", tc.name, err) continue @@ -217,7 +277,7 @@ func TestPlugin(t *testing.T) { testNamespace = "test_secret_namespace" testName = "test_secret_name" - volumeSpec = volumeSpec(testVolumeName, testName) + volumeSpec = volumeSpec(testVolumeName, testName, 0644) secret = secret(testNamespace, testName) client = fake.NewSimpleClientset(&secret) pluginMgr = volume.VolumePluginMgr{} @@ -290,7 +350,7 @@ func TestPluginReboot(t *testing.T) { testNamespace = "test_secret_namespace" testName = "test_secret_name" - volumeSpec = volumeSpec(testVolumeName, testName) + volumeSpec = volumeSpec(testVolumeName, testName, 0644) secret = secret(testNamespace, testName) client = fake.NewSimpleClientset(&secret) pluginMgr = volume.VolumePluginMgr{} @@ -336,12 +396,13 @@ func TestPluginReboot(t *testing.T) { doTestCleanAndTeardown(plugin, testPodUID, testVolumeName, volumePath, t) } -func volumeSpec(volumeName, secretName string) *api.Volume { +func volumeSpec(volumeName, secretName string, defaultMode int32) *api.Volume { return &api.Volume{ Name: volumeName, VolumeSource: api.VolumeSource{ Secret: &api.SecretVolumeSource{ - SecretName: secretName, + SecretName: secretName, + DefaultMode: &defaultMode, }, }, } diff --git a/pkg/volume/util/atomic_writer.go b/pkg/volume/util/atomic_writer.go index 542378a85d2..05bcdd40753 100644 --- a/pkg/volume/util/atomic_writer.go +++ b/pkg/volume/util/atomic_writer.go @@ -60,9 +60,14 @@ type AtomicWriter struct { logContext string } +type FileProjection struct { + Data []byte + Mode int32 +} + // NewAtomicWriter creates a new AtomicWriter configured to write to the given // target directory, or returns an error if the target directory does not exist. -func NewAtomicWriter(targetDir, logContext string) (*AtomicWriter, error) { +func NewAtomicWriter(targetDir string, logContext string) (*AtomicWriter, error) { _, err := os.Stat(targetDir) if os.IsNotExist(err) { return nil, err @@ -113,7 +118,7 @@ const ( // 9.  The new data directory symlink is renamed to the data directory; rename is atomic // 10. Old paths are removed from the user-visible portion of the target directory // 11.  The previous timestamped directory is removed, if it exists -func (w *AtomicWriter) Write(payload map[string][]byte) error { +func (w *AtomicWriter) Write(payload map[string]FileProjection) error { // (1) cleanPayload, err := validatePayload(payload) if err != nil { @@ -203,8 +208,8 @@ func (w *AtomicWriter) Write(payload map[string][]byte) error { } // validatePayload returns an error if any path in the payload returns a copy of the payload with the paths cleaned. -func validatePayload(payload map[string][]byte) (map[string][]byte, error) { - cleanPayload := make(map[string][]byte) +func validatePayload(payload map[string]FileProjection) (map[string]FileProjection, error) { + cleanPayload := make(map[string]FileProjection) for k, content := range payload { if err := validatePath(k); err != nil { return nil, err @@ -257,9 +262,9 @@ func validatePath(targetPath string) error { } // shouldWritePayload returns whether the payload should be written to disk. -func (w *AtomicWriter) shouldWritePayload(payload map[string][]byte) (bool, error) { - for userVisiblePath, content := range payload { - shouldWrite, err := w.shouldWriteFile(path.Join(w.targetDir, userVisiblePath), content) +func (w *AtomicWriter) shouldWritePayload(payload map[string]FileProjection) (bool, error) { + for userVisiblePath, fileProjection := range payload { + shouldWrite, err := w.shouldWriteFile(path.Join(w.targetDir, userVisiblePath), fileProjection.Data) if err != nil { return false, err } @@ -290,7 +295,7 @@ func (w *AtomicWriter) shouldWriteFile(path string, content []byte) (bool, error // pathsToRemove walks the user-visible portion of the target directory and // determines which paths should be removed (if any) after the payload is // written to the target directory. -func (w *AtomicWriter) pathsToRemove(payload map[string][]byte) (sets.String, error) { +func (w *AtomicWriter) pathsToRemove(payload map[string]FileProjection) (sets.String, error) { paths := sets.NewString() visitor := func(path string, info os.FileInfo, err error) error { if path == w.targetDir { @@ -355,8 +360,10 @@ func (w *AtomicWriter) newTimestampDir() (string, error) { // writePayloadToDir writes the given payload to the given directory. The // directory must exist. -func (w *AtomicWriter) writePayloadToDir(payload map[string][]byte, dir string) error { - for userVisiblePath, content := range payload { +func (w *AtomicWriter) writePayloadToDir(payload map[string]FileProjection, dir string) error { + for userVisiblePath, fileProjection := range payload { + content := fileProjection.Data + mode := os.FileMode(fileProjection.Mode) fullPath := path.Join(dir, userVisiblePath) baseDir, _ := filepath.Split(fullPath) @@ -366,11 +373,19 @@ func (w *AtomicWriter) writePayloadToDir(payload map[string][]byte, dir string) return err } - err = ioutil.WriteFile(fullPath, content, 0644) + err = ioutil.WriteFile(fullPath, content, mode) if err != nil { - glog.Errorf("%s: unable to write file %s: %v", w.logContext, fullPath, err) + glog.Errorf("%s: unable to write file %s with mode %v: %v", w.logContext, fullPath, mode, err) return err } + // Chmod is needed because ioutil.WriteFile() ends up calling + // open(2) to create the file, so the final mode used is "mode & + // ~umask". But we want to make sure the specified mode is used + // in the file no matter what the umask is. + err = os.Chmod(fullPath, mode) + if err != nil { + glog.Errorf("%s: unable to write file %s with mode %v: %v", w.logContext, fullPath, mode, err) + } } return nil @@ -387,7 +402,7 @@ func (w *AtomicWriter) writePayloadToDir(payload map[string][]byte, dir string) // foo/bar -> ../..data/foo/bar // baz/bar -> ../..data/baz/bar // foo/baz/blah -> ../../..data/foo/baz/blah -func (w *AtomicWriter) createUserVisibleFiles(payload map[string][]byte) error { +func (w *AtomicWriter) createUserVisibleFiles(payload map[string]FileProjection) error { for userVisiblePath := range payload { dir, _ := filepath.Split(userVisiblePath) subDirs := 0 diff --git a/pkg/volume/util/atomic_writer_test.go b/pkg/volume/util/atomic_writer_test.go index dafe86fc873..36777d9b6ca 100644 --- a/pkg/volume/util/atomic_writer_test.go +++ b/pkg/volume/util/atomic_writer_test.go @@ -130,90 +130,90 @@ func TestValidatePath(t *testing.T) { func TestPathsToRemove(t *testing.T) { cases := []struct { name string - payload1 map[string][]byte - payload2 map[string][]byte + payload1 map[string]FileProjection + payload2 map[string]FileProjection expected sets.String }{ { name: "simple", - payload1: map[string][]byte{ - "foo.txt": []byte("foo"), - "bar.txt": []byte("bar"), + payload1: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "bar.txt": {Mode: 0644, Data: []byte("bar")}, }, - payload2: map[string][]byte{ - "foo.txt": []byte("foo"), + payload2: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, }, expected: sets.NewString("bar.txt"), }, { name: "simple 2", - payload1: map[string][]byte{ - "foo.txt": []byte("foo"), - "zip/bar.txt": []byte("zip/bar"), + payload1: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "zip/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")}, }, - payload2: map[string][]byte{ - "foo.txt": []byte("foo"), + payload2: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, }, expected: sets.NewString("zip/bar.txt", "zip"), }, { name: "subdirs 1", - payload1: map[string][]byte{ - "foo.txt": []byte("foo"), - "zip/zap/bar.txt": []byte("zip/bar"), + payload1: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "zip/zap/bar.txt": {Mode: 0644, Data: []byte("zip/bar")}, }, - payload2: map[string][]byte{ - "foo.txt": []byte("foo"), + payload2: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, }, expected: sets.NewString("zip/zap/bar.txt", "zip", "zip/zap"), }, { name: "subdirs 2", - payload1: map[string][]byte{ - "foo.txt": []byte("foo"), - "zip/1/2/3/4/bar.txt": []byte("zip/bar"), + payload1: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "zip/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")}, }, - payload2: map[string][]byte{ - "foo.txt": []byte("foo"), + payload2: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, }, expected: sets.NewString("zip/1/2/3/4/bar.txt", "zip", "zip/1", "zip/1/2", "zip/1/2/3", "zip/1/2/3/4"), }, { name: "subdirs 3", - payload1: map[string][]byte{ - "foo.txt": []byte("foo"), - "zip/1/2/3/4/bar.txt": []byte("zip/bar"), - "zap/a/b/c/bar.txt": []byte("zap/bar"), + payload1: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "zip/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/b}ar")}, + "zap/a/b/c/bar.txt": {Mode: 0644, Data: []byte("zap/bar")}, }, - payload2: map[string][]byte{ - "foo.txt": []byte("foo"), + payload2: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, }, expected: sets.NewString("zip/1/2/3/4/bar.txt", "zip", "zip/1", "zip/1/2", "zip/1/2/3", "zip/1/2/3/4", "zap", "zap/a", "zap/a/b", "zap/a/b/c", "zap/a/b/c/bar.txt"), }, { name: "subdirs 4", - payload1: map[string][]byte{ - "foo.txt": []byte("foo"), - "zap/1/2/3/4/bar.txt": []byte("zip/bar"), - "zap/1/2/c/bar.txt": []byte("zap/bar"), - "zap/1/2/magic.txt": []byte("indigo"), + payload1: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "zap/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/bar")}, + "zap/1/2/c/bar.txt": {Mode: 0644, Data: []byte("zap/bar")}, + "zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")}, }, - payload2: map[string][]byte{ - "foo.txt": []byte("foo"), - "zap/1/2/magic.txt": []byte("indigo"), + payload2: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")}, }, expected: sets.NewString("zap/1/2/3/4/bar.txt", "zap/1/2/3", "zap/1/2/3/4", "zap/1/2/3/4/bar.txt", "zap/1/2/c", "zap/1/2/c/bar.txt"), }, { name: "subdirs 5", - payload1: map[string][]byte{ - "foo.txt": []byte("foo"), - "zap/1/2/3/4/bar.txt": []byte("zip/bar"), - "zap/1/2/c/bar.txt": []byte("zap/bar"), + payload1: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "zap/1/2/3/4/bar.txt": {Mode: 0644, Data: []byte("zip/bar")}, + "zap/1/2/c/bar.txt": {Mode: 0644, Data: []byte("zap/bar")}, }, - payload2: map[string][]byte{ - "foo.txt": []byte("foo"), - "zap/1/2/magic.txt": []byte("indigo"), + payload2: map[string]FileProjection{ + "foo.txt": {Mode: 0644, Data: []byte("foo")}, + "zap/1/2/magic.txt": {Mode: 0644, Data: []byte("indigo")}, }, expected: sets.NewString("zap/1/2/3/4/bar.txt", "zap/1/2/3", "zap/1/2/3/4", "zap/1/2/3/4/bar.txt", "zap/1/2/c", "zap/1/2/c/bar.txt"), }, @@ -263,88 +263,114 @@ IAAAAAAAsDyZDwU=` cases := []struct { name string - payload map[string][]byte + payload map[string]FileProjection success bool }{ { name: "invalid payload 1", - payload: map[string][]byte{ - "foo": []byte("foo"), - "..bar": []byte("bar"), - "binary.bin": mysteryBinaryBytes, + payload: map[string]FileProjection{ + "foo": {Mode: 0644, Data: []byte("foo")}, + "..bar": {Mode: 0644, Data: []byte("bar")}, + "binary.bin": {Mode: 0644, Data: mysteryBinaryBytes}, }, success: false, }, { name: "invalid payload 2", - payload: map[string][]byte{ - "foo/../bar": []byte("foo"), + payload: map[string]FileProjection{ + "foo/../bar": {Mode: 0644, Data: []byte("foo")}, }, success: false, }, { name: "basic 1", - payload: map[string][]byte{ - "foo": []byte("foo"), - "bar": []byte("bar"), + payload: map[string]FileProjection{ + "foo": {Mode: 0644, Data: []byte("foo")}, + "bar": {Mode: 0644, Data: []byte("bar")}, }, success: true, }, { name: "basic 2", - payload: map[string][]byte{ - "binary.bin": mysteryBinaryBytes, - ".binary.bin": mysteryBinaryBytes, + payload: map[string]FileProjection{ + "binary.bin": {Mode: 0644, Data: mysteryBinaryBytes}, + ".binary.bin": {Mode: 0644, Data: mysteryBinaryBytes}, + }, + success: true, + }, + { + name: "basic mode 1", + payload: map[string]FileProjection{ + "foo": {Mode: 0777, Data: []byte("foo")}, + "bar": {Mode: 0400, Data: []byte("bar")}, }, success: true, }, { name: "dotfiles", - payload: map[string][]byte{ - "foo": []byte("foo"), - "bar": []byte("bar"), - ".dotfile": []byte("dotfile"), - ".dotfile.file": []byte("dotfile.file"), + payload: map[string]FileProjection{ + "foo": {Mode: 0644, Data: []byte("foo")}, + "bar": {Mode: 0644, Data: []byte("bar")}, + ".dotfile": {Mode: 0644, Data: []byte("dotfile")}, + ".dotfile.file": {Mode: 0644, Data: []byte("dotfile.file")}, + }, + success: true, + }, + { + name: "dotfiles mode", + payload: map[string]FileProjection{ + "foo": {Mode: 0407, Data: []byte("foo")}, + "bar": {Mode: 0440, Data: []byte("bar")}, + ".dotfile": {Mode: 0777, Data: []byte("dotfile")}, + ".dotfile.file": {Mode: 0666, Data: []byte("dotfile.file")}, }, success: true, }, { name: "subdirectories 1", - payload: map[string][]byte{ - "foo/bar.txt": []byte("foo/bar"), - "bar/zab.txt": []byte("bar/zab.txt"), + payload: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")}, + }, + success: true, + }, + { + name: "subdirectories mode 1", + payload: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0400, Data: []byte("foo/bar")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")}, }, success: true, }, { name: "subdirectories 2", - payload: map[string][]byte{ - "foo//bar.txt": []byte("foo//bar"), - "bar///bar/zab.txt": []byte("bar/../bar/zab.txt"), + payload: map[string]FileProjection{ + "foo//bar.txt": {Mode: 0644, Data: []byte("foo//bar")}, + "bar///bar/zab.txt": {Mode: 0644, Data: []byte("bar/../bar/zab.txt")}, }, success: true, }, { name: "subdirectories 3", - payload: map[string][]byte{ - "foo/bar.txt": []byte("foo/bar"), - "bar/zab.txt": []byte("bar/zab.txt"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar"), - "bar/zib/zab.txt": []byte("bar/zib/zab.txt"), + payload: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")}, + "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt")}, }, success: true, }, { name: "kitchen sink", - payload: map[string][]byte{ - "foo.log": []byte("foo"), - "bar.zap": []byte("bar"), - ".dotfile": []byte("dotfile"), - "foo/bar.txt": []byte("foo/bar"), - "bar/zab.txt": []byte("bar/zab.txt"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar"), - "bar/zib/zab.txt": []byte("bar/zib/zab.txt"), - "1/2/3/4/5/6/7/8/9/10/.dotfile.lib": []byte("1-2-3-dotfile"), + payload: map[string]FileProjection{ + "foo.log": {Mode: 0644, Data: []byte("foo")}, + "bar.zap": {Mode: 0644, Data: []byte("bar")}, + ".dotfile": {Mode: 0644, Data: []byte("dotfile")}, + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")}, + "bar/zib/zab.txt": {Mode: 0400, Data: []byte("bar/zib/zab.txt")}, + "1/2/3/4/5/6/7/8/9/10/.dotfile.lib": {Mode: 0777, Data: []byte("1-2-3-dotfile")}, }, success: true, }, @@ -376,150 +402,150 @@ IAAAAAAAsDyZDwU=` func TestUpdate(t *testing.T) { cases := []struct { name string - first map[string][]byte - next map[string][]byte + first map[string]FileProjection + next map[string]FileProjection shouldWrite bool }{ { name: "update", - first: map[string][]byte{ - "foo": []byte("foo"), - "bar": []byte("bar"), + first: map[string]FileProjection{ + "foo": {Mode: 0644, Data: []byte("foo")}, + "bar": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo": []byte("foo2"), - "bar": []byte("bar2"), + next: map[string]FileProjection{ + "foo": {Mode: 0644, Data: []byte("foo2")}, + "bar": {Mode: 0640, Data: []byte("bar2")}, }, shouldWrite: true, }, { name: "no update", - first: map[string][]byte{ - "foo": []byte("foo"), - "bar": []byte("bar"), + first: map[string]FileProjection{ + "foo": {Mode: 0644, Data: []byte("foo")}, + "bar": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo": []byte("foo"), - "bar": []byte("bar"), + next: map[string]FileProjection{ + "foo": {Mode: 0644, Data: []byte("foo")}, + "bar": {Mode: 0644, Data: []byte("bar")}, }, shouldWrite: false, }, { name: "no update 2", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), + next: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, }, shouldWrite: false, }, { name: "add 1", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), - "blu/zip.txt": []byte("zip"), + next: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, + "blu/zip.txt": {Mode: 0644, Data: []byte("zip")}, }, shouldWrite: true, }, { name: "add 2", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), - "blu/two/2/3/4/5/zip.txt": []byte("zip"), + next: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, + "blu/two/2/3/4/5/zip.txt": {Mode: 0644, Data: []byte("zip")}, }, shouldWrite: true, }, { name: "add 3", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), - "bar/2/3/4/5/zip.txt": []byte("zip"), + next: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, + "bar/2/3/4/5/zip.txt": {Mode: 0644, Data: []byte("zip")}, }, shouldWrite: true, }, { name: "delete 1", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo/bar.txt": []byte("foo"), + next: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, }, shouldWrite: true, }, { name: "delete 2", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/1/2/3/zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/1/2/3/zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo/bar.txt": []byte("foo"), + next: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, }, shouldWrite: true, }, { name: "delete 3", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/1/2/sip.txt": []byte("sip"), - "bar/1/2/3/zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")}, + "bar/1/2/3/zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/1/2/sip.txt": []byte("sip"), + next: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")}, }, shouldWrite: true, }, { name: "delete 4", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/1/2/sip.txt": []byte("sip"), - "bar/1/2/3/4/5/6zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")}, + "bar/1/2/3/4/5/6zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/1/2/sip.txt": []byte("sip"), + next: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")}, }, shouldWrite: true, }, { name: "delete all", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), - "bar/1/2/sip.txt": []byte("sip"), - "bar/1/2/3/4/5/6zab.txt": []byte("bar"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, + "bar/1/2/sip.txt": {Mode: 0644, Data: []byte("sip")}, + "bar/1/2/3/4/5/6zab.txt": {Mode: 0644, Data: []byte("bar")}, }, - next: map[string][]byte{}, + next: map[string]FileProjection{}, shouldWrite: true, }, { name: "add and delete 1", - first: map[string][]byte{ - "foo/bar.txt": []byte("foo"), + first: map[string]FileProjection{ + "foo/bar.txt": {Mode: 0644, Data: []byte("foo")}, }, - next: map[string][]byte{ - "bar/baz.txt": []byte("baz"), + next: map[string]FileProjection{ + "bar/baz.txt": {Mode: 0644, Data: []byte("baz")}, }, shouldWrite: true, }, @@ -563,130 +589,130 @@ func TestUpdate(t *testing.T) { func TestMultipleUpdates(t *testing.T) { cases := []struct { name string - payloads []map[string][]byte + payloads []map[string]FileProjection }{ { name: "update 1", - payloads: []map[string][]byte{ + payloads: []map[string]FileProjection{ { - "foo": []byte("foo"), - "bar": []byte("bar"), + "foo": {Mode: 0644, Data: []byte("foo")}, + "bar": {Mode: 0644, Data: []byte("bar")}, }, { - "foo": []byte("foo2"), - "bar": []byte("bar2"), + "foo": {Mode: 0400, Data: []byte("foo2")}, + "bar": {Mode: 0400, Data: []byte("bar2")}, }, { - "foo": []byte("foo3"), - "bar": []byte("bar3"), + "foo": {Mode: 0600, Data: []byte("foo3")}, + "bar": {Mode: 0600, Data: []byte("bar3")}, }, }, }, { name: "update 2", - payloads: []map[string][]byte{ + payloads: []map[string]FileProjection{ { - "foo/bar.txt": []byte("foo/bar"), - "bar/zab.txt": []byte("bar/zab.txt"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")}, }, { - "foo/bar.txt": []byte("foo/bar2"), - "bar/zab.txt": []byte("bar/zab.txt2"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")}, + "bar/zab.txt": {Mode: 0400, Data: []byte("bar/zab.txt2")}, }, }, }, { name: "clear sentinel", - payloads: []map[string][]byte{ + payloads: []map[string]FileProjection{ { - "foo": []byte("foo"), - "bar": []byte("bar"), + "foo": {Mode: 0644, Data: []byte("foo")}, + "bar": {Mode: 0644, Data: []byte("bar")}, }, { - "foo": []byte("foo2"), - "bar": []byte("bar2"), + "foo": {Mode: 0644, Data: []byte("foo2")}, + "bar": {Mode: 0644, Data: []byte("bar2")}, }, { - "foo": []byte("foo3"), - "bar": []byte("bar3"), + "foo": {Mode: 0644, Data: []byte("foo3")}, + "bar": {Mode: 0644, Data: []byte("bar3")}, }, { - "foo": []byte("foo4"), - "bar": []byte("bar4"), + "foo": {Mode: 0644, Data: []byte("foo4")}, + "bar": {Mode: 0644, Data: []byte("bar4")}, }, }, }, { name: "subdirectories 2", - payloads: []map[string][]byte{ + payloads: []map[string]FileProjection{ { - "foo/bar.txt": []byte("foo/bar"), - "bar/zab.txt": []byte("bar/zab.txt"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar"), - "bar/zib/zab.txt": []byte("bar/zib/zab.txt"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")}, + "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt")}, }, { - "foo/bar.txt": []byte("foo/bar2"), - "bar/zab.txt": []byte("bar/zab.txt2"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar2"), - "bar/zib/zab.txt": []byte("bar/zib/zab.txt2"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")}, + "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt2")}, }, }, }, { name: "add 1", - payloads: []map[string][]byte{ + payloads: []map[string]FileProjection{ { - "foo/bar.txt": []byte("foo/bar"), - "bar//zab.txt": []byte("bar/zab.txt"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar"), - "bar/zib////zib/zab.txt": []byte("bar/zib/zab.txt"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")}, + "bar//zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")}, + "bar/zib////zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt")}, }, { - "foo/bar.txt": []byte("foo/bar2"), - "bar/zab.txt": []byte("bar/zab.txt2"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar2"), - "bar/zib/zab.txt": []byte("bar/zib/zab.txt2"), - "add/new/keys.txt": []byte("addNewKeys"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")}, + "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt2")}, + "add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")}, }, }, }, { name: "add 2", - payloads: []map[string][]byte{ + payloads: []map[string]FileProjection{ { - "foo/bar.txt": []byte("foo/bar2"), - "bar/zab.txt": []byte("bar/zab.txt2"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar2"), - "bar/zib/zab.txt": []byte("bar/zib/zab.txt2"), - "add/new/keys.txt": []byte("addNewKeys"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")}, + "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt2")}, + "add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")}, }, { - "foo/bar.txt": []byte("foo/bar2"), - "bar/zab.txt": []byte("bar/zab.txt2"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar2"), - "bar/zib/zab.txt": []byte("bar/zib/zab.txt2"), - "add/new/keys.txt": []byte("addNewKeys"), - "add/new/keys2.txt": []byte("addNewKeys2"), - "add/new/keys3.txt": []byte("addNewKeys3"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar2")}, + "bar/zib/zab.txt": {Mode: 0644, Data: []byte("bar/zib/zab.txt2")}, + "add/new/keys.txt": {Mode: 0644, Data: []byte("addNewKeys")}, + "add/new/keys2.txt": {Mode: 0644, Data: []byte("addNewKeys2")}, + "add/new/keys3.txt": {Mode: 0644, Data: []byte("addNewKeys3")}, }, }, }, { name: "remove 1", - payloads: []map[string][]byte{ + payloads: []map[string]FileProjection{ { - "foo/bar.txt": []byte("foo/bar"), - "bar//zab.txt": []byte("bar/zab.txt"), - "foo/blaz/bar.txt": []byte("foo/blaz/bar"), - "zip/zap/zup/fop.txt": []byte("zip/zap/zup/fop.txt"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")}, + "bar//zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt")}, + "foo/blaz/bar.txt": {Mode: 0644, Data: []byte("foo/blaz/bar")}, + "zip/zap/zup/fop.txt": {Mode: 0644, Data: []byte("zip/zap/zup/fop.txt")}, }, { - "foo/bar.txt": []byte("foo/bar2"), - "bar/zab.txt": []byte("bar/zab.txt2"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar2")}, + "bar/zab.txt": {Mode: 0644, Data: []byte("bar/zab.txt2")}, }, { - "foo/bar.txt": []byte("foo/bar"), + "foo/bar.txt": {Mode: 0644, Data: []byte("foo/bar")}, }, }, }, @@ -709,9 +735,9 @@ func TestMultipleUpdates(t *testing.T) { } } -func checkVolumeContents(targetDir, tcName string, payload map[string][]byte, t *testing.T) { +func checkVolumeContents(targetDir, tcName string, payload map[string]FileProjection, t *testing.T) { // use filepath.Walk to reconstruct the payload, then deep equal - observedPayload := map[string][]byte{} + observedPayload := make(map[string]FileProjection) visitor := func(path string, info os.FileInfo, err error) error { if info.Mode().IsRegular() || info.IsDir() { return nil @@ -727,7 +753,13 @@ func checkVolumeContents(targetDir, tcName string, payload map[string][]byte, t if err != nil { return err } - observedPayload[relativePath] = content + fileInfo, err := os.Stat(path) + if err != nil { + return err + } + mode := int32(fileInfo.Mode()) + + observedPayload[relativePath] = FileProjection{Data: content, Mode: mode} return nil } @@ -737,7 +769,7 @@ func checkVolumeContents(targetDir, tcName string, payload map[string][]byte, t t.Errorf("%v: unexpected error walking directory: %v", tcName, err) } - cleanPathPayload := make(map[string][]byte, len(payload)) + cleanPathPayload := make(map[string]FileProjection, len(payload)) for k, v := range payload { cleanPathPayload[path.Clean(k)] = v } diff --git a/test/e2e/common/configmap.go b/test/e2e/common/configmap.go index 554120a5144..b0cabaf69a3 100644 --- a/test/e2e/common/configmap.go +++ b/test/e2e/common/configmap.go @@ -18,6 +18,7 @@ package common import ( "fmt" + "os" "time" "k8s.io/kubernetes/pkg/api" @@ -32,27 +33,37 @@ var _ = framework.KubeDescribe("ConfigMap", func() { f := framework.NewDefaultFramework("configmap") It("should be consumable from pods in volume [Conformance]", func() { - doConfigMapE2EWithoutMappings(f, 0, 0) + doConfigMapE2EWithoutMappings(f, 0, 0, nil) + }) + + It("should be consumable from pods in volume with defaultMode set [Conformance]", func() { + defaultMode := int32(0400) + doConfigMapE2EWithoutMappings(f, 0, 0, &defaultMode) }) It("should be consumable from pods in volume as non-root [Conformance]", func() { - doConfigMapE2EWithoutMappings(f, 1000, 0) + doConfigMapE2EWithoutMappings(f, 1000, 0, nil) }) It("should be consumable from pods in volume as non-root with FSGroup [Feature:FSGroup]", func() { - doConfigMapE2EWithoutMappings(f, 1000, 1001) + doConfigMapE2EWithoutMappings(f, 1000, 1001, nil) }) It("should be consumable from pods in volume with mappings [Conformance]", func() { - doConfigMapE2EWithMappings(f, 0, 0) + doConfigMapE2EWithMappings(f, 0, 0, nil) + }) + + It("should be consumable from pods in volume with mappings and Item mode set[Conformance]", func() { + mode := int32(0400) + doConfigMapE2EWithMappings(f, 0, 0, &mode) }) It("should be consumable from pods in volume with mappings as non-root [Conformance]", func() { - doConfigMapE2EWithMappings(f, 1000, 0) + doConfigMapE2EWithMappings(f, 1000, 0, nil) }) It("should be consumable from pods in volume with mappings as non-root with FSGroup [Feature:FSGroup]", func() { - doConfigMapE2EWithMappings(f, 1000, 1001) + doConfigMapE2EWithMappings(f, 1000, 1001, nil) }) It("updates should be reflected in volume [Conformance]", func() { @@ -290,7 +301,7 @@ func newConfigMap(f *framework.Framework, name string) *api.ConfigMap { } } -func doConfigMapE2EWithoutMappings(f *framework.Framework, uid, fsGroup int64) { +func doConfigMapE2EWithoutMappings(f *framework.Framework, uid, fsGroup int64, defaultMode *int32) { var ( name = "configmap-test-volume-" + string(uuid.NewUUID()) volumeName = "configmap-volume" @@ -331,13 +342,14 @@ func doConfigMapE2EWithoutMappings(f *framework.Framework, uid, fsGroup int64) { Containers: []api.Container{ { Name: "configmap-volume-test", - Image: "gcr.io/google_containers/mounttest:0.6", - Args: []string{"--file_content=/etc/configmap-volume/data-1"}, + Image: "gcr.io/google_containers/mounttest:0.7", + Args: []string{ + "--file_content=/etc/configmap-volume/data-1", + "--file_mode=/etc/configmap-volume/data-1"}, VolumeMounts: []api.VolumeMount{ { Name: volumeName, MountPath: volumeMountPath, - ReadOnly: true, }, }, }, @@ -353,14 +365,27 @@ func doConfigMapE2EWithoutMappings(f *framework.Framework, uid, fsGroup int64) { if fsGroup != 0 { pod.Spec.SecurityContext.FSGroup = &fsGroup } + if defaultMode != nil { + pod.Spec.Volumes[0].VolumeSource.ConfigMap.DefaultMode = defaultMode + } else { + mode := int32(0644) + defaultMode = &mode + } - f.TestContainerOutput("consume configMaps", pod, 0, []string{ + // Just check file mode if fsGroup is not set. If fsGroup is set, the + // final mode is adjusted and we are not testing that case. + output := []string{ "content of file \"/etc/configmap-volume/data-1\": value-1", - }) + } + if fsGroup == 0 { + modeString := fmt.Sprintf("%v", os.FileMode(*defaultMode)) + output = append(output, "mode of file \"/etc/configmap-volume/data-1\": "+modeString) + } + f.TestContainerOutput("consume configMaps", pod, 0, output) } -func doConfigMapE2EWithMappings(f *framework.Framework, uid, fsGroup int64) { +func doConfigMapE2EWithMappings(f *framework.Framework, uid, fsGroup int64, itemMode *int32) { var ( name = "configmap-test-volume-map-" + string(uuid.NewUUID()) volumeName = "configmap-volume" @@ -407,8 +432,9 @@ func doConfigMapE2EWithMappings(f *framework.Framework, uid, fsGroup int64) { Containers: []api.Container{ { Name: "configmap-volume-test", - Image: "gcr.io/google_containers/mounttest:0.6", - Args: []string{"--file_content=/etc/configmap-volume/path/to/data-2"}, + Image: "gcr.io/google_containers/mounttest:0.7", + Args: []string{"--file_content=/etc/configmap-volume/path/to/data-2", + "--file_mode=/etc/configmap-volume/path/to/data-2"}, VolumeMounts: []api.VolumeMount{ { Name: volumeName, @@ -429,8 +455,21 @@ func doConfigMapE2EWithMappings(f *framework.Framework, uid, fsGroup int64) { if fsGroup != 0 { pod.Spec.SecurityContext.FSGroup = &fsGroup } + if itemMode != nil { + pod.Spec.Volumes[0].VolumeSource.ConfigMap.Items[0].Mode = itemMode + } else { + mode := int32(0644) + itemMode = &mode + } - f.TestContainerOutput("consume configMaps", pod, 0, []string{ + // Just check file mode if fsGroup is not set. If fsGroup is set, the + // final mode is adjusted and we are not testing that case. + output := []string{ "content of file \"/etc/configmap-volume/path/to/data-2\": value-2", - }) + } + if fsGroup == 0 { + modeString := fmt.Sprintf("%v", os.FileMode(*itemMode)) + output = append(output, "mode of file \"/etc/configmap-volume/path/to/data-2\": "+modeString) + } + f.TestContainerOutput("consume configMaps", pod, 0, output) } diff --git a/test/e2e/common/downwardapi_volume.go b/test/e2e/common/downwardapi_volume.go index b24c7c8cb63..cdc72aae0f0 100644 --- a/test/e2e/common/downwardapi_volume.go +++ b/test/e2e/common/downwardapi_volume.go @@ -47,6 +47,26 @@ var _ = framework.KubeDescribe("Downward API volume", func() { }) }) + It("should set DefaultMode on files [Conformance]", func() { + podName := "downwardapi-volume-" + string(uuid.NewUUID()) + defaultMode := int32(0400) + pod := downwardAPIVolumePodForModeTest(podName, "/etc/podname", nil, &defaultMode) + + f.TestContainerOutput("downward API volume plugin", pod, 0, []string{ + "mode of file \"/etc/podname\": -r--------", + }) + }) + + It("should set mode on item file [Conformance]", func() { + podName := "downwardapi-volume-" + string(uuid.NewUUID()) + mode := int32(0400) + pod := downwardAPIVolumePodForModeTest(podName, "/etc/podname", &mode, nil) + + f.TestContainerOutput("downward API volume plugin", pod, 0, []string{ + "mode of file \"/etc/podname\": -r--------", + }) + }) + It("should provide podname as non-root with fsgroup [Feature:FSGroup]", func() { podName := "metadata-volume-" + string(uuid.NewUUID()) uid := int64(1001) @@ -179,6 +199,32 @@ var _ = framework.KubeDescribe("Downward API volume", func() { }) +func downwardAPIVolumePodForModeTest(name, filePath string, itemMode, defaultMode *int32) *api.Pod { + pod := downwardAPIVolumeBasePod(name, nil, nil) + + pod.Spec.Containers = []api.Container{ + { + Name: "client-container", + Image: "gcr.io/google_containers/mounttest:0.7", + Command: []string{"/mt", "--file_mode=" + filePath}, + VolumeMounts: []api.VolumeMount{ + { + Name: "podinfo", + MountPath: "/etc", + }, + }, + }, + } + if itemMode != nil { + pod.Spec.Volumes[0].VolumeSource.DownwardAPI.Items[0].Mode = itemMode + } + if defaultMode != nil { + pod.Spec.Volumes[0].VolumeSource.DownwardAPI.DefaultMode = defaultMode + } + + return pod +} + func downwardAPIVolumePodForSimpleTest(name string, filePath string) *api.Pod { pod := downwardAPIVolumeBasePod(name, nil, nil) diff --git a/test/e2e/common/secrets.go b/test/e2e/common/secrets.go index e542d07eae0..399fed45b93 100644 --- a/test/e2e/common/secrets.go +++ b/test/e2e/common/secrets.go @@ -73,7 +73,6 @@ var _ = framework.KubeDescribe("Secrets", func() { { Name: volumeName, MountPath: volumeMountPath, - ReadOnly: true, }, }, }, @@ -88,6 +87,133 @@ var _ = framework.KubeDescribe("Secrets", func() { }) }) + It("should be consumable from pods in volume with defaultMode set [Conformance]", func() { + name := "secret-test-defaultmode-" + string(uuid.NewUUID()) + volumeName := "secret-volume" + volumeMountPath := "/etc/secret-volume" + secret := secretForTest(f.Namespace.Name, name) + + By(fmt.Sprintf("Creating secret with name %s", secret.Name)) + defer func() { + By("Cleaning up the secret") + if err := f.Client.Secrets(f.Namespace.Name).Delete(secret.Name); err != nil { + framework.Failf("unable to delete secret %v: %v", secret.Name, err) + } + }() + var err error + if secret, err = f.Client.Secrets(f.Namespace.Name).Create(secret); err != nil { + framework.Failf("unable to create test secret %s: %v", secret.Name, err) + } + + defaultMode := int32(0400) + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "pod-secrets-" + string(uuid.NewUUID()), + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + { + Name: volumeName, + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: name, + DefaultMode: &defaultMode, + }, + }, + }, + }, + Containers: []api.Container{ + { + Name: "secret-volume-test", + Image: "gcr.io/google_containers/mounttest:0.7", + Args: []string{ + "--file_content=/etc/secret-volume/data-1", + "--file_mode=/etc/secret-volume/data-1"}, + VolumeMounts: []api.VolumeMount{ + { + Name: volumeName, + MountPath: volumeMountPath, + ReadOnly: true, + }, + }, + }, + }, + RestartPolicy: api.RestartPolicyNever, + }, + } + + f.TestContainerOutput("consume secrets", pod, 0, []string{ + "content of file \"/etc/secret-volume/data-1\": value-1", + "mode of file \"/etc/secret-volume/data-1\": -r--------", + }) + }) + + It("should be consumable from pods in volume with Mode set in the item [Conformance]", func() { + name := "secret-test-itemmode-" + string(uuid.NewUUID()) + volumeName := "secret-volume" + volumeMountPath := "/etc/secret-volume" + secret := secretForTest(f.Namespace.Name, name) + + By(fmt.Sprintf("Creating secret with name %s", secret.Name)) + defer func() { + By("Cleaning up the secret") + if err := f.Client.Secrets(f.Namespace.Name).Delete(secret.Name); err != nil { + framework.Failf("unable to delete secret %v: %v", secret.Name, err) + } + }() + var err error + if secret, err = f.Client.Secrets(f.Namespace.Name).Create(secret); err != nil { + framework.Failf("unable to create test secret %s: %v", secret.Name, err) + } + + mode := int32(0400) + pod := &api.Pod{ + ObjectMeta: api.ObjectMeta{ + Name: "pod-secrets-" + string(uuid.NewUUID()), + }, + Spec: api.PodSpec{ + Volumes: []api.Volume{ + { + Name: volumeName, + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: name, + Items: []api.KeyToPath{ + { + Key: "data-1", + Path: "data-1", + Mode: &mode, + }, + }, + }, + }, + }, + }, + Containers: []api.Container{ + { + Name: "secret-volume-test", + Image: "gcr.io/google_containers/mounttest:0.7", + Args: []string{ + "--file_content=/etc/secret-volume/data-1", + "--file_mode=/etc/secret-volume/data-1"}, + VolumeMounts: []api.VolumeMount{ + { + Name: volumeName, + MountPath: volumeMountPath, + }, + }, + }, + }, + RestartPolicy: api.RestartPolicyNever, + }, + } + + f.TestContainerOutput("consume secrets", pod, 0, []string{ + "content of file \"/etc/secret-volume/data-1\": value-1", + "mode of file \"/etc/secret-volume/data-1\": -r--------", + }) + }) + It("should be consumable in multiple volumes in a pod [Conformance]", func() { // This test ensures that the same secret can be mounted in multiple // volumes in the same pod. This test case exists to prevent diff --git a/test/integration/serviceaccount/service_account_test.go b/test/integration/serviceaccount/service_account_test.go index 553f40438c2..9121b976993 100644 --- a/test/integration/serviceaccount/service_account_test.go +++ b/test/integration/serviceaccount/service_account_test.go @@ -237,12 +237,14 @@ func TestServiceAccountTokenAutoMount(t *testing.T) { } // Pod we expect to get created + defaultMode := int32(0644) expectedServiceAccount := serviceaccountadmission.DefaultServiceAccountName expectedVolumes := append(protoPod.Spec.Volumes, api.Volume{ Name: defaultTokenName, VolumeSource: api.VolumeSource{ Secret: &api.SecretVolumeSource{ - SecretName: defaultTokenName, + SecretName: defaultTokenName, + DefaultMode: &defaultMode, }, }, })