From 9f2e13acca984997c5d7028d6fcfb3508abcbef4 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Fri, 29 Jul 2016 22:41:20 -0700 Subject: [PATCH 1/3] Validate that projected files do not contain .. This was checked in the kubelet, but not at the API. --- pkg/api/validation/validation.go | 70 +++++++++-------- pkg/api/validation/validation_test.go | 104 +++++++++++++++++++++++++- 2 files changed, 140 insertions(+), 34 deletions(-) diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index e08bb1e5d60..3f6c9567667 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -598,7 +598,7 @@ func validateGitRepoVolumeSource(gitRepo *api.GitRepoVolumeSource, fldPath *fiel allErrs = append(allErrs, field.Required(fldPath.Child("repository"), "")) } - pathErrs := validateVolumeSourcePath(gitRepo.Directory, fldPath.Child("directory")) + pathErrs := validateLocalDescendingPath(gitRepo.Directory, fldPath.Child("directory")) allErrs = append(allErrs, pathErrs...) return allErrs } @@ -660,6 +660,11 @@ func validateSecretVolumeSource(secretSource *api.SecretVolumeSource, fldPath *f if len(secretSource.SecretName) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("secretName"), "")) } + itemsPath := fldPath.Child("items") + for i, kp := range secretSource.Items { + itemPath := itemsPath.Index(i) + allErrs = append(allErrs, validateKeyToPath(&kp, itemPath)...) + } return allErrs } @@ -668,6 +673,23 @@ func validateConfigMapVolumeSource(configMapSource *api.ConfigMapVolumeSource, f if len(configMapSource.Name) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("name"), "")) } + itemsPath := fldPath.Child("items") + for i, kp := range configMapSource.Items { + itemPath := itemsPath.Index(i) + allErrs = append(allErrs, validateKeyToPath(&kp, itemPath)...) + } + return allErrs +} + +func validateKeyToPath(kp *api.KeyToPath, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if len(kp.Key) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("key"), "")) + } + if len(kp.Path) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("path"), "")) + } + allErrs = append(allErrs, validateLocalNonReservedPath(kp.Path, fldPath.Child("path"))...) return allErrs } @@ -723,7 +745,7 @@ func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSou if len(downwardAPIVolumeFile.Path) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("path"), "")) } - allErrs = append(allErrs, validateVolumeSourcePath(downwardAPIVolumeFile.Path, fldPath.Child("path"))...) + allErrs = append(allErrs, validateLocalNonReservedPath(downwardAPIVolumeFile.Path, fldPath.Child("path"))...) if downwardAPIVolumeFile.FieldRef != nil { allErrs = append(allErrs, validateObjectFieldSelector(downwardAPIVolumeFile.FieldRef, &validDownwardAPIFieldPathExpressions, fldPath.Child("fieldRef"))...) if downwardAPIVolumeFile.ResourceFieldRef != nil { @@ -740,44 +762,32 @@ func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSou // This validate will make sure targetPath: // 1. is not abs path -// 2. does not start with '../' -// 3. does not contain '/../' -// 4. does not end with '/..' -func validateSubPath(targetPath string, fldPath *field.Path) field.ErrorList { +// 2. does not have any element which is ".." +func validateLocalDescendingPath(targetPath string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} if path.IsAbs(targetPath) { allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must be a relative path")) } - if strings.HasPrefix(targetPath, "../") { - allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not start with '../'")) - } - if strings.Contains(targetPath, "/../") { - allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not contain '/../'")) - } - if strings.HasSuffix(targetPath, "/..") { - allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not end with '/..'")) + + // TODO: this assumes the OS of apiserver & nodes are the same + parts := strings.Split(targetPath, string(os.PathSeparator)) + for _, item := range parts { + if item == ".." { + allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not contain '..'")) + } } return allErrs } // This validate will make sure targetPath: // 1. is not abs path -// 2. does not contain '..' +// 2. does not contain any '..' elements // 3. does not start with '..' -func validateVolumeSourcePath(targetPath string, fldPath *field.Path) field.ErrorList { +func validateLocalNonReservedPath(targetPath string, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - if path.IsAbs(targetPath) { - allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must be a relative path")) - } - // TODO assume OS of api server & nodes are the same for now - items := strings.Split(targetPath, string(os.PathSeparator)) - - for _, item := range items { - if item == ".." { - allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not contain '..'")) - } - } - if strings.HasPrefix(items[0], "..") && len(items[0]) > 2 { + allErrs = append(allErrs, validateLocalDescendingPath(targetPath, fldPath)...) + // Don't report this error if the check for .. elements already caught it. + if strings.HasPrefix(targetPath, "..") && !strings.HasPrefix(targetPath, "../") { allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not start with '..'")) } return allErrs @@ -1262,7 +1272,7 @@ func validateVolumeMounts(mounts []api.VolumeMount, volumes sets.String, fldPath } mountpoints.Insert(mnt.MountPath) if len(mnt.SubPath) > 0 { - allErrs = append(allErrs, validateSubPath(mnt.SubPath, fldPath.Child("subPath"))...) + allErrs = append(allErrs, validateLocalDescendingPath(mnt.SubPath, fldPath.Child("subPath"))...) } } return allErrs @@ -1918,7 +1928,7 @@ func validateSeccompProfile(p string, fldPath *field.Path) field.ErrorList { return nil } if strings.HasPrefix(p, "localhost/") { - return validateSubPath(strings.TrimPrefix(p, "localhost/"), fldPath) + return validateLocalDescendingPath(strings.TrimPrefix(p, "localhost/"), fldPath) } return field.ErrorList{field.Invalid(fldPath, p, "must be a valid seccomp profile")} } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index d3fe378ec84..c403897a024 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -776,6 +776,72 @@ func TestValidatePersistentVolumeClaimUpdate(t *testing.T) { } } +func TestValidateKeyToPath(t *testing.T) { + testCases := []struct { + kp api.KeyToPath + ok bool + errtype field.ErrorType + }{ + { + kp: api.KeyToPath{Key: "k", Path: "p"}, + ok: true, + }, + { + kp: api.KeyToPath{Key: "k", Path: "p/p/p/p"}, + ok: true, + }, + { + kp: api.KeyToPath{Key: "k", Path: "p/..p/p../p..p"}, + ok: true, + }, + { + kp: api.KeyToPath{Key: "", Path: "p"}, + ok: false, + errtype: field.ErrorTypeRequired, + }, + { + kp: api.KeyToPath{Key: "k", Path: ""}, + ok: false, + errtype: field.ErrorTypeRequired, + }, + { + kp: api.KeyToPath{Key: "k", Path: "..p"}, + ok: false, + errtype: field.ErrorTypeInvalid, + }, + { + kp: api.KeyToPath{Key: "k", Path: "../p"}, + ok: false, + errtype: field.ErrorTypeInvalid, + }, + { + kp: api.KeyToPath{Key: "k", Path: "p/../p"}, + ok: false, + errtype: field.ErrorTypeInvalid, + }, + { + kp: api.KeyToPath{Key: "k", Path: "p/.."}, + ok: false, + errtype: field.ErrorTypeInvalid, + }, + } + + for i, tc := range testCases { + errs := validateKeyToPath(&tc.kp, field.NewPath("field")) + if tc.ok && len(errs) > 0 { + t.Errorf("[%d] unexpected errors: %v", i, errs) + } else if !tc.ok && len(errs) == 0 { + t.Errorf("[%d] expected error type %v", i, tc.errtype) + } else if len(errs) > 1 { + t.Errorf("[%d] expected only one error, got %d", i, len(errs)) + } else if !tc.ok { + if errs[0].Type != tc.errtype { + t.Errorf("[%d] expected error type %v, got %v", i, tc.errtype, errs[0].Type) + } + } + } +} + func TestValidateVolumes(t *testing.T) { lun := int32(1) successCase := []api.Volume{ @@ -787,8 +853,14 @@ func TestValidateVolumes(t *testing.T) { {Name: "awsebs", VolumeSource: api.VolumeSource{AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{VolumeID: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}, {Name: "gitrepo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "my-repo", Revision: "hashstring", Directory: "target"}}}, {Name: "gitrepodot", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "my-repo", Directory: "."}}}, + {Name: "gitrepodotdotfoo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "my-repo", Directory: "..foo"}}}, {Name: "iscsidisk", VolumeSource: api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{TargetPortal: "127.0.0.1", IQN: "iqn.2015-02.example.com:test", Lun: 1, FSType: "ext4", ReadOnly: false}}}, - {Name: "secret", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "my-secret"}}}, + {Name: "secret1", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "my-secret"}}}, + {Name: "secret2", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "my-secret", Items: []api.KeyToPath{{Key: "key", Path: "filename"}}}}}, + {Name: "secret3", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "my-secret", Items: []api.KeyToPath{{Key: "key", Path: "dir/filename"}}}}}, + {Name: "cfgmap1", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "my-cfgmap"}}}}, + {Name: "cfgmap2", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "my-cfgmap"}, Items: []api.KeyToPath{{Key: "key", Path: "filename"}}}}}, + {Name: "cfgmap3", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "my-cfgmap"}, Items: []api.KeyToPath{{Key: "key", Path: "dir/filename"}}}}}, {Name: "glusterfs", VolumeSource: api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{EndpointsName: "host1", Path: "path", ReadOnly: false}}}, {Name: "flocker", VolumeSource: api.VolumeSource{Flocker: &api.FlockerVolumeSource{DatasetName: "datasetName"}}}, {Name: "rbd", VolumeSource: api.VolumeSource{RBD: &api.RBDVolumeSource{CephMonitors: []string{"foo"}, RBDImage: "bar", FSType: "ext4"}}}, @@ -840,19 +912,23 @@ func TestValidateVolumes(t *testing.T) { if len(errs) != 0 { t.Errorf("expected success: %v", errs) } - if len(names) != len(successCase) || !names.HasAll("abc", "123", "abc-123", "empty", "gcepd", "gitrepo", "secret", "iscsidisk", "cinder", "cephfs", "flexvolume", "fc") { + if len(names) != len(successCase) || !names.HasAll("abc", "123", "abc-123", "empty", "gcepd", "gitrepo", "secret1", "secret2", "secret3", "cfgmap1", "cfgmap2", "cfgmap3", "iscsidisk", "cinder", "cephfs", "flexvolume", "fc") { t.Errorf("wrong names result: %v", names) } emptyVS := api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}} emptyPortal := api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{TargetPortal: "", IQN: "iqn.2015-02.example.com:test", Lun: 1, FSType: "ext4", ReadOnly: false}} emptyIQN := api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{TargetPortal: "127.0.0.1", IQN: "", Lun: 1, FSType: "ext4", ReadOnly: false}} + secretMissingPath := api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "s", Items: []api.KeyToPath{{Key: "key", Path: ""}}}} + secretDotDot := api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "s", Items: []api.KeyToPath{{Key: "key", Path: "../foo"}}}} + cfgmapMissingPath := api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "c"}, Items: []api.KeyToPath{{Key: "key", Path: ""}}}} + cfgmapDotDot := api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "c"}, Items: []api.KeyToPath{{Key: "key", Path: "../foo"}}}} emptyHosts := api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{EndpointsName: "", Path: "path", ReadOnly: false}} emptyPath := api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{EndpointsName: "host", Path: "", ReadOnly: false}} emptyName := api.VolumeSource{Flocker: &api.FlockerVolumeSource{DatasetName: ""}} emptyMon := api.VolumeSource{RBD: &api.RBDVolumeSource{CephMonitors: []string{}, RBDImage: "bar", FSType: "ext4"}} emptyImage := api.VolumeSource{RBD: &api.RBDVolumeSource{CephMonitors: []string{"foo"}, RBDImage: "", FSType: "ext4"}} emptyCephFSMon := api.VolumeSource{CephFS: &api.CephFSVolumeSource{Monitors: []string{}}} - startsWithDots := api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "foo", Directory: "..dots/bar"}} + startsWithDots := api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "foo", Directory: "../dots/bar"}} containsDots := api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "foo", Directory: "dots/../bar"}} absPath := api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "foo", Directory: "/abstarget"}} emptyPathName := api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{Items: []api.DownwardAPIVolumeFile{{Path: "", @@ -929,6 +1005,26 @@ func TestValidateVolumes(t *testing.T) { field.ErrorTypeRequired, "iscsi.iqn", "", }, + "secret with missing path": { + []api.Volume{{Name: "secret-dot-dot", VolumeSource: secretMissingPath}}, + field.ErrorTypeRequired, + "secret.items[0]", "", + }, + "secret with ..": { + []api.Volume{{Name: "secret-dot-dot", VolumeSource: secretDotDot}}, + field.ErrorTypeInvalid, + "secret.items[0]", "", + }, + "configmap with missing path": { + []api.Volume{{Name: "cfgmap-dot-dot", VolumeSource: cfgmapMissingPath}}, + field.ErrorTypeRequired, + "configMap.items[0]", "", + }, + "configmap with ..": { + []api.Volume{{Name: "cfgmap-dot-dot", VolumeSource: cfgmapDotDot}}, + field.ErrorTypeInvalid, + "configMap.items[0]", "", + }, "empty hosts": { []api.Volume{{Name: "badhost", VolumeSource: emptyHosts}}, field.ErrorTypeRequired, @@ -1002,7 +1098,7 @@ func TestValidateVolumes(t *testing.T) { "starts with '..'": { []api.Volume{{Name: "badprefix", VolumeSource: startsWithDots}}, field.ErrorTypeInvalid, - "gitRepo.directory", `must not start with '..'`, + "gitRepo.directory", `must not contain '..'`, }, "contains '..'": { []api.Volume{{Name: "containsdots", VolumeSource: containsDots}}, From 54e92bbc49a003431192f29857f45d7e62381b88 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Fri, 29 Jul 2016 23:07:44 -0700 Subject: [PATCH 2/3] minor rename for readability --- pkg/api/validation/validation.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 3f6c9567667..94e74e6a09b 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -737,22 +737,26 @@ func validateFlockerVolumeSource(flocker *api.FlockerVolumeSource, fldPath *fiel return allErrs } -var validDownwardAPIFieldPathExpressions = sets.NewString("metadata.name", "metadata.namespace", "metadata.labels", "metadata.annotations") +var validDownwardAPIFieldPathExpressions = sets.NewString( + "metadata.name", + "metadata.namespace", + "metadata.labels", + "metadata.annotations") func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSource, fldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} - for _, downwardAPIVolumeFile := range downwardAPIVolume.Items { - if len(downwardAPIVolumeFile.Path) == 0 { + for _, file := range downwardAPIVolume.Items { + if len(file.Path) == 0 { allErrs = append(allErrs, field.Required(fldPath.Child("path"), "")) } - allErrs = append(allErrs, validateLocalNonReservedPath(downwardAPIVolumeFile.Path, fldPath.Child("path"))...) - if downwardAPIVolumeFile.FieldRef != nil { - allErrs = append(allErrs, validateObjectFieldSelector(downwardAPIVolumeFile.FieldRef, &validDownwardAPIFieldPathExpressions, fldPath.Child("fieldRef"))...) - if downwardAPIVolumeFile.ResourceFieldRef != nil { + allErrs = append(allErrs, validateLocalNonReservedPath(file.Path, fldPath.Child("path"))...) + if file.FieldRef != nil { + allErrs = append(allErrs, validateObjectFieldSelector(file.FieldRef, &validDownwardAPIFieldPathExpressions, fldPath.Child("fieldRef"))...) + if file.ResourceFieldRef != nil { allErrs = append(allErrs, field.Invalid(fldPath, "resource", "fieldRef and resourceFieldRef can not be specified simultaneously")) } - } else if downwardAPIVolumeFile.ResourceFieldRef != nil { - allErrs = append(allErrs, validateContainerResourceFieldSelector(downwardAPIVolumeFile.ResourceFieldRef, &validContainerResourceFieldPathExpressions, fldPath.Child("resourceFieldRef"), true)...) + } else if file.ResourceFieldRef != nil { + allErrs = append(allErrs, validateContainerResourceFieldSelector(file.ResourceFieldRef, &validContainerResourceFieldPathExpressions, fldPath.Child("resourceFieldRef"), true)...) } else { allErrs = append(allErrs, field.Required(fldPath, "one of fieldRef and resourceFieldRef is required")) } From ef4bccf63e82891e7035cbc5657aa9b10a0dca43 Mon Sep 17 00:00:00 2001 From: Tim Hockin Date: Fri, 29 Jul 2016 23:52:25 -0700 Subject: [PATCH 3/3] Clean up the ugliest unit test ever This volume-validation test was a disaster. Better now, if longer to scroll-through. --- pkg/api/validation/validation.go | 1 + pkg/api/validation/validation_test.go | 1200 +++++++++++++++++++------ 2 files changed, 936 insertions(+), 265 deletions(-) diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 94e74e6a09b..daf59737c91 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -778,6 +778,7 @@ func validateLocalDescendingPath(targetPath string, fldPath *field.Path) field.E for _, item := range parts { if item == ".." { allErrs = append(allErrs, field.Invalid(fldPath, targetPath, "must not contain '..'")) + break // even for `../../..`, one error is sufficient to make the point } } return allErrs diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index c403897a024..99a1fe00381 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -842,308 +842,978 @@ func TestValidateKeyToPath(t *testing.T) { } } +// helper +func newInt32(val int) *int32 { + p := new(int32) + *p = int32(val) + return p +} + +// This test is a little too top-to-bottom. Ideally we would test each volume +// type on its own, but we want to also make sure that the logic works through +// the one-of wrapper, so we just do it all in one place. func TestValidateVolumes(t *testing.T) { - lun := int32(1) - successCase := []api.Volume{ - {Name: "abc", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/mnt/path1"}}}, - {Name: "123", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/mnt/path2"}}}, - {Name: "abc-123", VolumeSource: api.VolumeSource{HostPath: &api.HostPathVolumeSource{Path: "/mnt/path3"}}}, - {Name: "empty", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, - {Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}, - {Name: "awsebs", VolumeSource: api.VolumeSource{AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{VolumeID: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}, - {Name: "gitrepo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "my-repo", Revision: "hashstring", Directory: "target"}}}, - {Name: "gitrepodot", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "my-repo", Directory: "."}}}, - {Name: "gitrepodotdotfoo", VolumeSource: api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "my-repo", Directory: "..foo"}}}, - {Name: "iscsidisk", VolumeSource: api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{TargetPortal: "127.0.0.1", IQN: "iqn.2015-02.example.com:test", Lun: 1, FSType: "ext4", ReadOnly: false}}}, - {Name: "secret1", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "my-secret"}}}, - {Name: "secret2", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "my-secret", Items: []api.KeyToPath{{Key: "key", Path: "filename"}}}}}, - {Name: "secret3", VolumeSource: api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "my-secret", Items: []api.KeyToPath{{Key: "key", Path: "dir/filename"}}}}}, - {Name: "cfgmap1", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "my-cfgmap"}}}}, - {Name: "cfgmap2", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "my-cfgmap"}, Items: []api.KeyToPath{{Key: "key", Path: "filename"}}}}}, - {Name: "cfgmap3", VolumeSource: api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "my-cfgmap"}, Items: []api.KeyToPath{{Key: "key", Path: "dir/filename"}}}}}, - {Name: "glusterfs", VolumeSource: api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{EndpointsName: "host1", Path: "path", ReadOnly: false}}}, - {Name: "flocker", VolumeSource: api.VolumeSource{Flocker: &api.FlockerVolumeSource{DatasetName: "datasetName"}}}, - {Name: "rbd", VolumeSource: api.VolumeSource{RBD: &api.RBDVolumeSource{CephMonitors: []string{"foo"}, RBDImage: "bar", FSType: "ext4"}}}, - {Name: "cinder", VolumeSource: api.VolumeSource{Cinder: &api.CinderVolumeSource{VolumeID: "29ea5088-4f60-4757-962e-dba678767887", FSType: "ext4", ReadOnly: false}}}, - {Name: "cephfs", VolumeSource: api.VolumeSource{CephFS: &api.CephFSVolumeSource{Monitors: []string{"foo"}}}}, - {Name: "downwardapi", VolumeSource: api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{Items: []api.DownwardAPIVolumeFile{ - {Path: "labels", FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}, - {Path: "annotations", FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.annotations"}}, - {Path: "namespace", FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.namespace"}}, - {Path: "name", FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.name"}}, - {Path: "path/withslash/andslash", FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}, - {Path: "path/./withdot", FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}, - {Path: "path/with..dot", FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}, - {Path: "second-level-dirent-can-have/..dot", FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}, - {Path: "cpu_limit", ResourceFieldRef: &api.ResourceFieldSelector{ - ContainerName: "test-container", - Resource: "limits.cpu"}}, - {Path: "cpu_request", ResourceFieldRef: &api.ResourceFieldSelector{ - ContainerName: "test-container", - Resource: "requests.cpu"}}, - {Path: "memory_limit", ResourceFieldRef: &api.ResourceFieldSelector{ - ContainerName: "test-container", - Resource: "limits.memory"}}, - {Path: "memory_request", ResourceFieldRef: &api.ResourceFieldSelector{ - ContainerName: "test-container", - Resource: "requests.memory"}}, - }}}}, - {Name: "fc", VolumeSource: api.VolumeSource{FC: &api.FCVolumeSource{TargetWWNs: []string{"some_wwn"}, Lun: &lun, FSType: "ext4", ReadOnly: false}}}, - {Name: "flexvolume", VolumeSource: api.VolumeSource{FlexVolume: &api.FlexVolumeSource{Driver: "kubernetes.io/blue", FSType: "ext4"}}}, - {Name: "azure", VolumeSource: api.VolumeSource{AzureFile: &api.AzureFileVolumeSource{SecretName: "key", ShareName: "share", ReadOnly: false}}}, - } - names, errs := validateVolumes(successCase, field.NewPath("field")) - if len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - if len(names) != len(successCase) || !names.HasAll("abc", "123", "abc-123", "empty", "gcepd", "gitrepo", "secret1", "secret2", "secret3", "cfgmap1", "cfgmap2", "cfgmap3", "iscsidisk", "cinder", "cephfs", "flexvolume", "fc") { - t.Errorf("wrong names result: %v", names) - } - emptyVS := api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}} - emptyPortal := api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{TargetPortal: "", IQN: "iqn.2015-02.example.com:test", Lun: 1, FSType: "ext4", ReadOnly: false}} - emptyIQN := api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{TargetPortal: "127.0.0.1", IQN: "", Lun: 1, FSType: "ext4", ReadOnly: false}} - secretMissingPath := api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "s", Items: []api.KeyToPath{{Key: "key", Path: ""}}}} - secretDotDot := api.VolumeSource{Secret: &api.SecretVolumeSource{SecretName: "s", Items: []api.KeyToPath{{Key: "key", Path: "../foo"}}}} - cfgmapMissingPath := api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "c"}, Items: []api.KeyToPath{{Key: "key", Path: ""}}}} - cfgmapDotDot := api.VolumeSource{ConfigMap: &api.ConfigMapVolumeSource{LocalObjectReference: api.LocalObjectReference{Name: "c"}, Items: []api.KeyToPath{{Key: "key", Path: "../foo"}}}} - emptyHosts := api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{EndpointsName: "", Path: "path", ReadOnly: false}} - emptyPath := api.VolumeSource{Glusterfs: &api.GlusterfsVolumeSource{EndpointsName: "host", Path: "", ReadOnly: false}} - emptyName := api.VolumeSource{Flocker: &api.FlockerVolumeSource{DatasetName: ""}} - emptyMon := api.VolumeSource{RBD: &api.RBDVolumeSource{CephMonitors: []string{}, RBDImage: "bar", FSType: "ext4"}} - emptyImage := api.VolumeSource{RBD: &api.RBDVolumeSource{CephMonitors: []string{"foo"}, RBDImage: "", FSType: "ext4"}} - emptyCephFSMon := api.VolumeSource{CephFS: &api.CephFSVolumeSource{Monitors: []string{}}} - startsWithDots := api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "foo", Directory: "../dots/bar"}} - containsDots := api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "foo", Directory: "dots/../bar"}} - absPath := api.VolumeSource{GitRepo: &api.GitRepoVolumeSource{Repository: "foo", Directory: "/abstarget"}} - emptyPathName := api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{Items: []api.DownwardAPIVolumeFile{{Path: "", - FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}}, - }} - absolutePathName := api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{Items: []api.DownwardAPIVolumeFile{{Path: "/absolutepath", - FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}}, - }} - dotDotInPath := api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{Items: []api.DownwardAPIVolumeFile{{Path: "../../passwd", - FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}}, - }} - dotDotPathName := api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{Items: []api.DownwardAPIVolumeFile{{Path: "..badFileName", - FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}}, - }} - dotDotFirstLevelDirent := api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{Items: []api.DownwardAPIVolumeFile{{Path: "..badDirName/goodFileName", - FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}}}, - }} - fieldRefandResourceFieldRef := api.VolumeSource{DownwardAPI: &api.DownwardAPIVolumeSource{Items: []api.DownwardAPIVolumeFile{{Path: "test", - FieldRef: &api.ObjectFieldSelector{ - APIVersion: "v1", - FieldPath: "metadata.labels"}, - ResourceFieldRef: &api.ResourceFieldSelector{ - ContainerName: "test-container", - Resource: "requests.memory"}}}, - }} - zeroWWN := api.VolumeSource{FC: &api.FCVolumeSource{TargetWWNs: []string{}, Lun: &lun, FSType: "ext4", ReadOnly: false}} - emptyLun := api.VolumeSource{FC: &api.FCVolumeSource{TargetWWNs: []string{"wwn"}, Lun: nil, FSType: "ext4", ReadOnly: false}} - slashInName := api.VolumeSource{Flocker: &api.FlockerVolumeSource{DatasetName: "foo/bar"}} - emptyAzureSecret := api.VolumeSource{AzureFile: &api.AzureFileVolumeSource{SecretName: "", ShareName: "share", ReadOnly: false}} - emptyAzureShare := api.VolumeSource{AzureFile: &api.AzureFileVolumeSource{SecretName: "name", ShareName: "", ReadOnly: false}} - errorCases := map[string]struct { - V []api.Volume - T field.ErrorType - F string - D string + testCases := []struct { + name string + vol api.Volume + errtype field.ErrorType + errfield string + errdetail string }{ - "zero-length name": { - []api.Volume{{Name: "", VolumeSource: emptyVS}}, - field.ErrorTypeRequired, - "name", "", + // EmptyDir and basic volume names + { + name: "valid alpha name", + vol: api.Volume{ + Name: "empty", + VolumeSource: api.VolumeSource{ + EmptyDir: &api.EmptyDirVolumeSource{}, + }, + }, }, - "name > 63 characters": { - []api.Volume{{Name: strings.Repeat("a", 64), VolumeSource: emptyVS}}, - field.ErrorTypeInvalid, - "name", "must be no more than", + { + name: "valid num name", + vol: api.Volume{ + Name: "123", + VolumeSource: api.VolumeSource{ + EmptyDir: &api.EmptyDirVolumeSource{}, + }, + }, }, - "name not a DNS label": { - []api.Volume{{Name: "a.b.c", VolumeSource: emptyVS}}, - field.ErrorTypeInvalid, - "name", "must match the regex", + { + name: "valid alphanum name", + vol: api.Volume{ + Name: "empty-123", + VolumeSource: api.VolumeSource{ + EmptyDir: &api.EmptyDirVolumeSource{}, + }, + }, }, - "name not unique": { - []api.Volume{{Name: "abc", VolumeSource: emptyVS}, {Name: "abc", VolumeSource: emptyVS}}, - field.ErrorTypeDuplicate, - "[1].name", "", + { + name: "valid numalpha name", + vol: api.Volume{ + Name: "123-empty", + VolumeSource: api.VolumeSource{ + EmptyDir: &api.EmptyDirVolumeSource{}, + }, + }, }, - "empty portal": { - []api.Volume{{Name: "badportal", VolumeSource: emptyPortal}}, - field.ErrorTypeRequired, - "iscsi.targetPortal", "", + { + name: "zero-length name", + vol: api.Volume{ + Name: "", + VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}, + }, + errtype: field.ErrorTypeRequired, + errfield: "name", }, - "empty iqn": { - []api.Volume{{Name: "badiqn", VolumeSource: emptyIQN}}, - field.ErrorTypeRequired, - "iscsi.iqn", "", + { + name: "name > 63 characters", + vol: api.Volume{ + Name: strings.Repeat("a", 64), + VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}, + }, + errtype: field.ErrorTypeInvalid, + errfield: "name", + errdetail: "must be no more than", }, - "secret with missing path": { - []api.Volume{{Name: "secret-dot-dot", VolumeSource: secretMissingPath}}, - field.ErrorTypeRequired, - "secret.items[0]", "", + { + name: "name not a DNS label", + vol: api.Volume{ + Name: "a.b.c", + VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}, + }, + errtype: field.ErrorTypeInvalid, + errfield: "name", + errdetail: "must match the regex", }, - "secret with ..": { - []api.Volume{{Name: "secret-dot-dot", VolumeSource: secretDotDot}}, - field.ErrorTypeInvalid, - "secret.items[0]", "", + // More than one source field specified. + { + name: "more than one source", + vol: api.Volume{ + Name: "dups", + VolumeSource: api.VolumeSource{ + EmptyDir: &api.EmptyDirVolumeSource{}, + HostPath: &api.HostPathVolumeSource{ + Path: "/mnt/path", + }, + }, + }, + errtype: field.ErrorTypeForbidden, + errfield: "hostPath", + errdetail: "may not specify more than 1 volume", }, - "configmap with missing path": { - []api.Volume{{Name: "cfgmap-dot-dot", VolumeSource: cfgmapMissingPath}}, - field.ErrorTypeRequired, - "configMap.items[0]", "", + // HostPath + { + name: "valid HostPath", + vol: api.Volume{ + Name: "hostpath", + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/mnt/path", + }, + }, + }, }, - "configmap with ..": { - []api.Volume{{Name: "cfgmap-dot-dot", VolumeSource: cfgmapDotDot}}, - field.ErrorTypeInvalid, - "configMap.items[0]", "", + // GcePersistentDisk + { + name: "valid GcePersistentDisk", + vol: api.Volume{ + Name: "gce-pd", + VolumeSource: api.VolumeSource{ + GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{ + PDName: "my-PD", + FSType: "ext4", + Partition: 1, + ReadOnly: false, + }, + }, + }, }, - "empty hosts": { - []api.Volume{{Name: "badhost", VolumeSource: emptyHosts}}, - field.ErrorTypeRequired, - "glusterfs.endpoints", "", + // AWSElasticBlockStore + { + name: "valid AWSElasticBlockStore", + vol: api.Volume{ + Name: "aws-ebs", + VolumeSource: api.VolumeSource{ + AWSElasticBlockStore: &api.AWSElasticBlockStoreVolumeSource{ + VolumeID: "my-PD", + FSType: "ext4", + Partition: 1, + ReadOnly: false, + }, + }, + }, }, - "empty path": { - []api.Volume{{Name: "badpath", VolumeSource: emptyPath}}, - field.ErrorTypeRequired, - "glusterfs.path", "", + // GitRepo + { + name: "valid GitRepo", + vol: api.Volume{ + Name: "git-repo", + VolumeSource: api.VolumeSource{ + GitRepo: &api.GitRepoVolumeSource{ + Repository: "my-repo", + Revision: "hashstring", + Directory: "target", + }, + }, + }, }, - "empty datasetName": { - []api.Volume{{Name: "badname", VolumeSource: emptyName}}, - field.ErrorTypeRequired, - "flocker.datasetName", "", + { + name: "valid GitRepo in .", + vol: api.Volume{ + Name: "git-repo-dot", + VolumeSource: api.VolumeSource{ + GitRepo: &api.GitRepoVolumeSource{ + Repository: "my-repo", + Directory: ".", + }, + }, + }, }, - "empty mon": { - []api.Volume{{Name: "badmon", VolumeSource: emptyMon}}, - field.ErrorTypeRequired, - "rbd.monitors", "", + { + name: "valid GitRepo with .. in name", + vol: api.Volume{ + Name: "git-repo-dot-dot-foo", + VolumeSource: api.VolumeSource{ + GitRepo: &api.GitRepoVolumeSource{ + Repository: "my-repo", + Directory: "..foo", + }, + }, + }, }, - "empty image": { - []api.Volume{{Name: "badimage", VolumeSource: emptyImage}}, - field.ErrorTypeRequired, - "rbd.image", "", + { + name: "GitRepo starts with ../", + vol: api.Volume{ + Name: "gitrepo", + VolumeSource: api.VolumeSource{ + GitRepo: &api.GitRepoVolumeSource{ + Repository: "foo", + Directory: "../dots/bar", + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "gitRepo.directory", + errdetail: `must not contain '..'`, }, - "empty cephfs mon": { - []api.Volume{{Name: "badmon", VolumeSource: emptyCephFSMon}}, - field.ErrorTypeRequired, - "cephfs.monitors", "", + { + name: "GitRepo contains ..", + vol: api.Volume{ + Name: "gitrepo", + VolumeSource: api.VolumeSource{ + GitRepo: &api.GitRepoVolumeSource{ + Repository: "foo", + Directory: "dots/../bar", + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "gitRepo.directory", + errdetail: `must not contain '..'`, }, - "empty metatada path": { - []api.Volume{{Name: "emptyname", VolumeSource: emptyPathName}}, - field.ErrorTypeRequired, - "downwardAPI.path", "", + { + name: "GitRepo absolute target", + vol: api.Volume{ + Name: "gitrepo", + VolumeSource: api.VolumeSource{ + GitRepo: &api.GitRepoVolumeSource{ + Repository: "foo", + Directory: "/abstarget", + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "gitRepo.directory", }, - "absolute path": { - []api.Volume{{Name: "absolutepath", VolumeSource: absolutePathName}}, - field.ErrorTypeInvalid, - "downwardAPI.path", "", + // ISCSI + { + name: "valid ISCSI", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "iqn.2015-02.example.com:test", + Lun: 1, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, }, - "dot dot path": { - []api.Volume{{Name: "dotdotpath", VolumeSource: dotDotInPath}}, - field.ErrorTypeInvalid, - "downwardAPI.path", `must not contain '..'`, + { + name: "empty portal", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "", + IQN: "iqn.2015-02.example.com:test", + Lun: 1, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "iscsi.targetPortal", }, - "dot dot file name": { - []api.Volume{{Name: "dotdotfilename", VolumeSource: dotDotPathName}}, - field.ErrorTypeInvalid, - "downwardAPI.path", `must not start with '..'`, + { + name: "empty iqn", + vol: api.Volume{ + Name: "iscsi", + VolumeSource: api.VolumeSource{ + ISCSI: &api.ISCSIVolumeSource{ + TargetPortal: "127.0.0.1", + IQN: "", + Lun: 1, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "iscsi.iqn", }, - "dot dot first level dirent": { - []api.Volume{{Name: "dotdotdirfilename", VolumeSource: dotDotFirstLevelDirent}}, - field.ErrorTypeInvalid, - "downwardAPI.path", `must not start with '..'`, + // Secret + { + name: "valid Secret", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "my-secret", + }, + }, + }, }, - "empty wwn": { - []api.Volume{{Name: "badimage", VolumeSource: zeroWWN}}, - field.ErrorTypeRequired, - "fc.targetWWNs", "", + { + name: "valid Secret with projection", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "my-secret", + Items: []api.KeyToPath{{ + Key: "key", + Path: "filename", + }}, + }, + }, + }, }, - "empty lun": { - []api.Volume{{Name: "badimage", VolumeSource: emptyLun}}, - field.ErrorTypeRequired, - "fc.lun", "", + { + name: "valid Secret with subdir projection", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "my-secret", + Items: []api.KeyToPath{{ + Key: "key", + Path: "dir/filename", + }}, + }, + }, + }, }, - "slash in datasetName": { - []api.Volume{{Name: "slashinname", VolumeSource: slashInName}}, - field.ErrorTypeInvalid, - "flocker.datasetName", "must not contain '/'", + { + name: "secret with missing path", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "s", + Items: []api.KeyToPath{{Key: "key", Path: ""}}, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "secret.items[0].path", }, - "starts with '..'": { - []api.Volume{{Name: "badprefix", VolumeSource: startsWithDots}}, - field.ErrorTypeInvalid, - "gitRepo.directory", `must not contain '..'`, + { + name: "secret with leading ..", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "s", + Items: []api.KeyToPath{{Key: "key", Path: "../foo"}}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "secret.items[0].path", }, - "contains '..'": { - []api.Volume{{Name: "containsdots", VolumeSource: containsDots}}, - field.ErrorTypeInvalid, - "gitRepo.directory", `must not contain '..'`, + { + name: "secret with .. inside", + vol: api.Volume{ + Name: "secret", + VolumeSource: api.VolumeSource{ + Secret: &api.SecretVolumeSource{ + SecretName: "s", + Items: []api.KeyToPath{{Key: "key", Path: "foo/../bar"}}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "secret.items[0].path", }, - "absolute target": { - []api.Volume{{Name: "absolutetarget", VolumeSource: absPath}}, - field.ErrorTypeInvalid, - "gitRepo.directory", "", + // ConfigMap + { + name: "valid ConfigMap", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{ + Name: "my-cfgmap", + }, + }, + }, + }, }, - "empty secret": { - []api.Volume{{Name: "emptyaccount", VolumeSource: emptyAzureSecret}}, - field.ErrorTypeRequired, - "azureFile.secretName", "", + { + name: "valid ConfigMap with projection", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{ + Name: "my-cfgmap"}, + Items: []api.KeyToPath{{ + Key: "key", + Path: "filename", + }}, + }, + }, + }, }, - "empty share": { - []api.Volume{{Name: "emptyaccount", VolumeSource: emptyAzureShare}}, - field.ErrorTypeRequired, - "azureFile.shareName", "", + { + name: "valid ConfigMap with subdir projection", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{ + Name: "my-cfgmap"}, + Items: []api.KeyToPath{{ + Key: "key", + Path: "dir/filename", + }}, + }, + }, + }, }, - "fieldRef and ResourceFieldRef together": { - []api.Volume{{Name: "testvolume", VolumeSource: fieldRefandResourceFieldRef}}, - field.ErrorTypeInvalid, - "downwardAPI", "fieldRef and resourceFieldRef can not be specified simultaneously", + { + name: "configmap with missing path", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{Name: "c"}, + Items: []api.KeyToPath{{Key: "key", Path: ""}}, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "configMap.items[0].path", + }, + { + name: "configmap with leading ..", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{Name: "c"}, + Items: []api.KeyToPath{{Key: "key", Path: "../foo"}}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "configMap.items[0].path", + }, + { + name: "configmap with .. inside", + vol: api.Volume{ + Name: "cfgmap", + VolumeSource: api.VolumeSource{ + ConfigMap: &api.ConfigMapVolumeSource{ + LocalObjectReference: api.LocalObjectReference{Name: "c"}, + Items: []api.KeyToPath{{Key: "key", Path: "foo/../bar"}}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "configMap.items[0].path", + }, + // Glusterfs + { + name: "valid Glusterfs", + vol: api.Volume{ + Name: "glusterfs", + VolumeSource: api.VolumeSource{ + Glusterfs: &api.GlusterfsVolumeSource{ + EndpointsName: "host1", + Path: "path", + ReadOnly: false, + }, + }, + }, + }, + { + name: "empty hosts", + vol: api.Volume{ + Name: "glusterfs", + VolumeSource: api.VolumeSource{ + Glusterfs: &api.GlusterfsVolumeSource{ + EndpointsName: "", + Path: "path", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "glusterfs.endpoints", + }, + { + name: "empty path", + vol: api.Volume{ + Name: "glusterfs", + VolumeSource: api.VolumeSource{ + Glusterfs: &api.GlusterfsVolumeSource{ + EndpointsName: "host", + Path: "", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "glusterfs.path", + }, + // Flocker + { + name: "valid Flocker", + vol: api.Volume{ + Name: "flocker", + VolumeSource: api.VolumeSource{ + Flocker: &api.FlockerVolumeSource{ + DatasetName: "datasetName", + }, + }, + }, + }, + { + name: "empty flocker datasetName", + vol: api.Volume{ + Name: "flocker", + VolumeSource: api.VolumeSource{ + Flocker: &api.FlockerVolumeSource{ + DatasetName: "", + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "flocker.datasetName", + }, + { + name: "slash in flocker datasetName", + vol: api.Volume{ + Name: "flocker", + VolumeSource: api.VolumeSource{ + Flocker: &api.FlockerVolumeSource{ + DatasetName: "foo/bar", + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "flocker.datasetName", + errdetail: "must not contain '/'", + }, + // RBD + { + name: "valid RBD", + vol: api.Volume{ + Name: "rbd", + VolumeSource: api.VolumeSource{ + RBD: &api.RBDVolumeSource{ + CephMonitors: []string{"foo"}, + RBDImage: "bar", + FSType: "ext4", + }, + }, + }, + }, + { + name: "empty rbd monitors", + vol: api.Volume{ + Name: "rbd", + VolumeSource: api.VolumeSource{ + RBD: &api.RBDVolumeSource{ + CephMonitors: []string{}, + RBDImage: "bar", + FSType: "ext4", + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "rbd.monitors", + }, + { + name: "empty image", + vol: api.Volume{ + Name: "rbd", + VolumeSource: api.VolumeSource{ + RBD: &api.RBDVolumeSource{ + CephMonitors: []string{"foo"}, + RBDImage: "", + FSType: "ext4", + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "rbd.image", + }, + // Cinder + { + name: "valid Cinder", + vol: api.Volume{ + Name: "cinder", + VolumeSource: api.VolumeSource{ + Cinder: &api.CinderVolumeSource{ + VolumeID: "29ea5088-4f60-4757-962e-dba678767887", + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + }, + // CephFS + { + name: "valid CephFS", + vol: api.Volume{ + Name: "cephfs", + VolumeSource: api.VolumeSource{ + CephFS: &api.CephFSVolumeSource{ + Monitors: []string{"foo"}, + }, + }, + }, + }, + { + name: "empty cephfs monitors", + vol: api.Volume{ + Name: "cephfs", + VolumeSource: api.VolumeSource{ + CephFS: &api.CephFSVolumeSource{ + Monitors: []string{}, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "cephfs.monitors", + }, + // DownwardAPI + { + name: "valid DownwardAPI", + vol: api.Volume{ + Name: "downwardapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{ + { + Path: "labels", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }, + { + Path: "annotations", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.annotations", + }, + }, + { + Path: "namespace", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.namespace", + }, + }, + { + Path: "name", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.name", + }, + }, + { + Path: "path/with/subdirs", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }, + { + Path: "path/./withdot", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }, + { + Path: "path/with/embedded..dotdot", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }, + { + Path: "path/with/leading/..dotdot", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }, + { + Path: "cpu_limit", + ResourceFieldRef: &api.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "limits.cpu", + }, + }, + { + Path: "cpu_request", + ResourceFieldRef: &api.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "requests.cpu", + }, + }, + { + Path: "memory_limit", + ResourceFieldRef: &api.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "limits.memory", + }, + }, + { + Path: "memory_request", + ResourceFieldRef: &api.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "requests.memory", + }, + }, + }, + }, + }, + }, + }, + { + name: "downapi empty metatada path", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Path: "", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }}, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "downwardAPI.path", + }, + { + name: "downapi absolute path", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Path: "/absolutepath", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI.path", + }, + { + name: "downapi dot dot path", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Path: "../../passwd", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI.path", + errdetail: `must not contain '..'`, + }, + { + name: "downapi dot dot file name", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Path: "..badFileName", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI.path", + errdetail: `must not start with '..'`, + }, + { + name: "downapi dot dot first level dirent", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Path: "..badDirName/goodFileName", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + }}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI.path", + errdetail: `must not start with '..'`, + }, + { + name: "downapi fieldRef and ResourceFieldRef together", + vol: api.Volume{ + Name: "downapi", + VolumeSource: api.VolumeSource{ + DownwardAPI: &api.DownwardAPIVolumeSource{ + Items: []api.DownwardAPIVolumeFile{{ + Path: "test", + FieldRef: &api.ObjectFieldSelector{ + APIVersion: "v1", + FieldPath: "metadata.labels", + }, + ResourceFieldRef: &api.ResourceFieldSelector{ + ContainerName: "test-container", + Resource: "requests.memory", + }, + }}, + }, + }, + }, + errtype: field.ErrorTypeInvalid, + errfield: "downwardAPI", + errdetail: "fieldRef and resourceFieldRef can not be specified simultaneously", + }, + // FC + { + name: "valid FC", + vol: api.Volume{ + Name: "fc", + VolumeSource: api.VolumeSource{ + FC: &api.FCVolumeSource{ + TargetWWNs: []string{"some_wwn"}, + Lun: newInt32(1), + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + }, + { + name: "fc empty wwn", + vol: api.Volume{ + Name: "fc", + VolumeSource: api.VolumeSource{ + FC: &api.FCVolumeSource{ + TargetWWNs: []string{}, + Lun: newInt32(1), + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "fc.targetWWNs", + }, + { + name: "fc empty lun", + vol: api.Volume{ + Name: "fc", + VolumeSource: api.VolumeSource{ + FC: &api.FCVolumeSource{ + TargetWWNs: []string{"wwn"}, + Lun: nil, + FSType: "ext4", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "fc.lun", + }, + // FlexVolume + { + name: "valid FlexVolume", + vol: api.Volume{ + Name: "flex-volume", + VolumeSource: api.VolumeSource{ + FlexVolume: &api.FlexVolumeSource{ + Driver: "kubernetes.io/blue", + FSType: "ext4", + }, + }, + }, + }, + // AzureFile + { + name: "valid AzureFile", + vol: api.Volume{ + Name: "azure-file", + VolumeSource: api.VolumeSource{ + AzureFile: &api.AzureFileVolumeSource{ + SecretName: "key", + ShareName: "share", + ReadOnly: false, + }, + }, + }, + }, + { + name: "AzureFile empty secret", + vol: api.Volume{ + Name: "azure-file", + VolumeSource: api.VolumeSource{ + AzureFile: &api.AzureFileVolumeSource{ + SecretName: "", + ShareName: "share", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "azureFile.secretName", + }, + { + name: "AzureFile empty share", + vol: api.Volume{ + Name: "azure-file", + VolumeSource: api.VolumeSource{ + AzureFile: &api.AzureFileVolumeSource{ + SecretName: "name", + ShareName: "", + ReadOnly: false, + }, + }, + }, + errtype: field.ErrorTypeRequired, + errfield: "azureFile.shareName", }, } - for k, v := range errorCases { - _, errs := validateVolumes(v.V, field.NewPath("field")) - if len(errs) == 0 { - t.Errorf("expected failure %s for %v", k, v.V) - continue - } - for i := range errs { - if errs[i].Type != v.T { - t.Errorf("%s: expected error to have type %q: %q", k, v.T, errs[i].Type) + + for i, tc := range testCases { + names, errs := validateVolumes([]api.Volume{tc.vol}, field.NewPath("field")) + if len(errs) > 0 && tc.errtype == "" { + t.Errorf("[%d: %q] unexpected error(s): %v", i, tc.name, errs) + } else if len(errs) > 1 { + t.Errorf("[%d: %q] expected 1 error, got %d: %v", i, tc.name, len(errs), errs) + } else if len(errs) == 0 && tc.errtype != "" { + t.Errorf("[%d: %q] expected error type %v", i, tc.name, tc.errtype) + } else if len(errs) == 1 { + if errs[0].Type != tc.errtype { + t.Errorf("[%d: %q] expected error type %v, got %v", i, tc.name, tc.errtype, errs[0].Type) + } else if !strings.HasSuffix(errs[0].Field, "."+tc.errfield) { + t.Errorf("[%d: %q] expected error on field %q, got %q", i, tc.name, tc.errfield, errs[0].Field) + } else if !strings.Contains(errs[0].Detail, tc.errdetail) { + t.Errorf("[%d: %q] expected error detail %q, got %q", i, tc.name, tc.errdetail, errs[0].Detail) } - if !strings.Contains(errs[i].Field, v.F) { - t.Errorf("%s: expected error field %q: %q", k, v.F, errs[i].Field) - } - if !strings.Contains(errs[i].Detail, v.D) { - t.Errorf("%s: expected error detail %q, got %q", k, v.D, errs[i].Detail) + } else { + if len(names) != 1 || !names.Has(tc.vol.Name) { + t.Errorf("[%d: %q] wrong names result: %v", i, tc.name, names) } } } + + dupsCase := []api.Volume{ + {Name: "abc", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + {Name: "abc", VolumeSource: api.VolumeSource{EmptyDir: &api.EmptyDirVolumeSource{}}}, + } + _, errs := validateVolumes(dupsCase, field.NewPath("field")) + if len(errs) == 0 { + t.Errorf("expected error") + } else if len(errs) != 1 { + t.Errorf("expected 1 error, got %d: %v", len(errs), errs) + } else if errs[0].Type != field.ErrorTypeDuplicate { + t.Errorf("expected error type %v, got %v", field.ErrorTypeDuplicate, errs[0].Type) + } } func TestValidatePorts(t *testing.T) {