Add flocker volume plugin

Flocker [1] is an open-source container data volume manager for
Dockerized applications.

This PR adds a volume plugin for Flocker.
The plugin interfaces the Flocker Control Service REST API [2] to
attachment attach the volume to the pod.

Each kubelet host should run Flocker agents (Container Agent and Dataset
Agent).

The kubelet will also require environment variables that contain the
host and port of the Flocker Control Service. (see Flocker architecture
[3] for more).

- `FLOCKER_CONTROL_SERVICE_HOST`
- `FLOCKER_CONTROL_SERVICE_PORT`

The contribution introduces a new 'flocker' volume type to the API with
fields:

- `datasetName`: which indicates the name of the dataset in Flocker
  added to metadata;
- `size`: a human-readable number that indicates the maximum size of the
  requested dataset.

Full documentation can be found docs/user-guide/volumes.md and examples
can be found at the examples/ folder

[1] https://clusterhq.com/flocker/introduction/
[2] https://docs.clusterhq.com/en/1.3.1/reference/api.html
[3] https://docs.clusterhq.com/en/1.3.1/concepts/architecture.html
This commit is contained in:
Álex González
2015-09-25 20:22:23 +01:00
parent bd8ee1c4c4
commit fa39c2b032
27 changed files with 1655 additions and 18 deletions

View File

@@ -571,6 +571,11 @@ func deepCopy_api_FCVolumeSource(in FCVolumeSource, out *FCVolumeSource, c *conv
return nil
}
func deepCopy_api_FlockerVolumeSource(in FlockerVolumeSource, out *FlockerVolumeSource, c *conversion.Cloner) error {
out.DatasetName = in.DatasetName
return nil
}
func deepCopy_api_GCEPersistentDiskVolumeSource(in GCEPersistentDiskVolumeSource, out *GCEPersistentDiskVolumeSource, c *conversion.Cloner) error {
out.PDName = in.PDName
out.FSType = in.FSType
@@ -1295,6 +1300,14 @@ func deepCopy_api_PersistentVolumeSource(in PersistentVolumeSource, out *Persist
} else {
out.FC = nil
}
if in.Flocker != nil {
out.Flocker = new(FlockerVolumeSource)
if err := deepCopy_api_FlockerVolumeSource(*in.Flocker, out.Flocker, c); err != nil {
return err
}
} else {
out.Flocker = nil
}
return nil
}
@@ -2211,6 +2224,14 @@ func deepCopy_api_VolumeSource(in VolumeSource, out *VolumeSource, c *conversion
} else {
out.CephFS = nil
}
if in.Flocker != nil {
out.Flocker = new(FlockerVolumeSource)
if err := deepCopy_api_FlockerVolumeSource(*in.Flocker, out.Flocker, c); err != nil {
return err
}
} else {
out.Flocker = nil
}
if in.DownwardAPI != nil {
out.DownwardAPI = new(DownwardAPIVolumeSource)
if err := deepCopy_api_DownwardAPIVolumeSource(*in.DownwardAPI, out.DownwardAPI, c); err != nil {
@@ -2308,6 +2329,7 @@ func init() {
deepCopy_api_EventSource,
deepCopy_api_ExecAction,
deepCopy_api_FCVolumeSource,
deepCopy_api_FlockerVolumeSource,
deepCopy_api_GCEPersistentDiskVolumeSource,
deepCopy_api_GitRepoVolumeSource,
deepCopy_api_GlusterfsVolumeSource,

View File

@@ -204,6 +204,9 @@ type VolumeSource struct {
// CephFS represents a Cephfs mount on the host that shares a pod's lifetime
CephFS *CephFSVolumeSource `json:"cephfs,omitempty"`
// Flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running
Flocker *FlockerVolumeSource `json:"flocker,omitempty"`
// DownwardAPI represents metadata about the pod that should populate this volume
DownwardAPI *DownwardAPIVolumeSource `json:"downwardAPI,omitempty"`
// FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.
@@ -239,6 +242,8 @@ type PersistentVolumeSource struct {
CephFS *CephFSVolumeSource `json:"cephfs,omitempty"`
// FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.
FC *FCVolumeSource `json:"fc,omitempty"`
// Flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running
Flocker *FlockerVolumeSource `json:"flocker,omitempty"`
}
type PersistentVolumeClaimVolumeSource struct {
@@ -592,6 +597,12 @@ type CephFSVolumeSource struct {
ReadOnly bool `json:"readOnly,omitempty"`
}
// FlockerVolumeSource represents a Flocker volume mounted by the Flocker agent.
type FlockerVolumeSource struct {
// Required: the volume name. This is going to be store on metadata -> name on the payload for Flocker
DatasetName string `json:"datasetName"`
}
// DownwardAPIVolumeSource represents a volume containing downward API info
type DownwardAPIVolumeSource struct {
// Items is a list of DownwardAPIVolume file

View File

@@ -790,6 +790,18 @@ func convert_api_FCVolumeSource_To_v1_FCVolumeSource(in *api.FCVolumeSource, out
return autoconvert_api_FCVolumeSource_To_v1_FCVolumeSource(in, out, s)
}
func autoconvert_api_FlockerVolumeSource_To_v1_FlockerVolumeSource(in *api.FlockerVolumeSource, out *FlockerVolumeSource, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*api.FlockerVolumeSource))(in)
}
out.DatasetName = in.DatasetName
return nil
}
func convert_api_FlockerVolumeSource_To_v1_FlockerVolumeSource(in *api.FlockerVolumeSource, out *FlockerVolumeSource, s conversion.Scope) error {
return autoconvert_api_FlockerVolumeSource_To_v1_FlockerVolumeSource(in, out, s)
}
func autoconvert_api_GCEPersistentDiskVolumeSource_To_v1_GCEPersistentDiskVolumeSource(in *api.GCEPersistentDiskVolumeSource, out *GCEPersistentDiskVolumeSource, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*api.GCEPersistentDiskVolumeSource))(in)
@@ -1777,6 +1789,14 @@ func autoconvert_api_PersistentVolumeSource_To_v1_PersistentVolumeSource(in *api
} else {
out.FC = nil
}
if in.Flocker != nil {
out.Flocker = new(FlockerVolumeSource)
if err := convert_api_FlockerVolumeSource_To_v1_FlockerVolumeSource(in.Flocker, out.Flocker, s); err != nil {
return err
}
} else {
out.Flocker = nil
}
return nil
}
@@ -2984,6 +3004,14 @@ func autoconvert_api_VolumeSource_To_v1_VolumeSource(in *api.VolumeSource, out *
} else {
out.CephFS = nil
}
if in.Flocker != nil {
out.Flocker = new(FlockerVolumeSource)
if err := convert_api_FlockerVolumeSource_To_v1_FlockerVolumeSource(in.Flocker, out.Flocker, s); err != nil {
return err
}
} else {
out.Flocker = nil
}
if in.DownwardAPI != nil {
out.DownwardAPI = new(DownwardAPIVolumeSource)
if err := convert_api_DownwardAPIVolumeSource_To_v1_DownwardAPIVolumeSource(in.DownwardAPI, out.DownwardAPI, s); err != nil {
@@ -3771,6 +3799,18 @@ func convert_v1_FCVolumeSource_To_api_FCVolumeSource(in *FCVolumeSource, out *ap
return autoconvert_v1_FCVolumeSource_To_api_FCVolumeSource(in, out, s)
}
func autoconvert_v1_FlockerVolumeSource_To_api_FlockerVolumeSource(in *FlockerVolumeSource, out *api.FlockerVolumeSource, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*FlockerVolumeSource))(in)
}
out.DatasetName = in.DatasetName
return nil
}
func convert_v1_FlockerVolumeSource_To_api_FlockerVolumeSource(in *FlockerVolumeSource, out *api.FlockerVolumeSource, s conversion.Scope) error {
return autoconvert_v1_FlockerVolumeSource_To_api_FlockerVolumeSource(in, out, s)
}
func autoconvert_v1_GCEPersistentDiskVolumeSource_To_api_GCEPersistentDiskVolumeSource(in *GCEPersistentDiskVolumeSource, out *api.GCEPersistentDiskVolumeSource, s conversion.Scope) error {
if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found {
defaulting.(func(*GCEPersistentDiskVolumeSource))(in)
@@ -4758,6 +4798,14 @@ func autoconvert_v1_PersistentVolumeSource_To_api_PersistentVolumeSource(in *Per
} else {
out.FC = nil
}
if in.Flocker != nil {
out.Flocker = new(api.FlockerVolumeSource)
if err := convert_v1_FlockerVolumeSource_To_api_FlockerVolumeSource(in.Flocker, out.Flocker, s); err != nil {
return err
}
} else {
out.Flocker = nil
}
return nil
}
@@ -5965,6 +6013,14 @@ func autoconvert_v1_VolumeSource_To_api_VolumeSource(in *VolumeSource, out *api.
} else {
out.CephFS = nil
}
if in.Flocker != nil {
out.Flocker = new(api.FlockerVolumeSource)
if err := convert_v1_FlockerVolumeSource_To_api_FlockerVolumeSource(in.Flocker, out.Flocker, s); err != nil {
return err
}
} else {
out.Flocker = nil
}
if in.DownwardAPI != nil {
out.DownwardAPI = new(api.DownwardAPIVolumeSource)
if err := convert_v1_DownwardAPIVolumeSource_To_api_DownwardAPIVolumeSource(in.DownwardAPI, out.DownwardAPI, s); err != nil {
@@ -6022,6 +6078,7 @@ func init() {
autoconvert_api_Event_To_v1_Event,
autoconvert_api_ExecAction_To_v1_ExecAction,
autoconvert_api_FCVolumeSource_To_v1_FCVolumeSource,
autoconvert_api_FlockerVolumeSource_To_v1_FlockerVolumeSource,
autoconvert_api_GCEPersistentDiskVolumeSource_To_v1_GCEPersistentDiskVolumeSource,
autoconvert_api_GitRepoVolumeSource_To_v1_GitRepoVolumeSource,
autoconvert_api_GlusterfsVolumeSource_To_v1_GlusterfsVolumeSource,
@@ -6139,6 +6196,7 @@ func init() {
autoconvert_v1_Event_To_api_Event,
autoconvert_v1_ExecAction_To_api_ExecAction,
autoconvert_v1_FCVolumeSource_To_api_FCVolumeSource,
autoconvert_v1_FlockerVolumeSource_To_api_FlockerVolumeSource,
autoconvert_v1_GCEPersistentDiskVolumeSource_To_api_GCEPersistentDiskVolumeSource,
autoconvert_v1_GitRepoVolumeSource_To_api_GitRepoVolumeSource,
autoconvert_v1_GlusterfsVolumeSource_To_api_GlusterfsVolumeSource,

View File

@@ -607,6 +607,11 @@ func deepCopy_v1_FCVolumeSource(in FCVolumeSource, out *FCVolumeSource, c *conve
return nil
}
func deepCopy_v1_FlockerVolumeSource(in FlockerVolumeSource, out *FlockerVolumeSource, c *conversion.Cloner) error {
out.DatasetName = in.DatasetName
return nil
}
func deepCopy_v1_GCEPersistentDiskVolumeSource(in GCEPersistentDiskVolumeSource, out *GCEPersistentDiskVolumeSource, c *conversion.Cloner) error {
out.PDName = in.PDName
out.FSType = in.FSType
@@ -1315,6 +1320,14 @@ func deepCopy_v1_PersistentVolumeSource(in PersistentVolumeSource, out *Persiste
} else {
out.FC = nil
}
if in.Flocker != nil {
out.Flocker = new(FlockerVolumeSource)
if err := deepCopy_v1_FlockerVolumeSource(*in.Flocker, out.Flocker, c); err != nil {
return err
}
} else {
out.Flocker = nil
}
return nil
}
@@ -2245,6 +2258,14 @@ func deepCopy_v1_VolumeSource(in VolumeSource, out *VolumeSource, c *conversion.
} else {
out.CephFS = nil
}
if in.Flocker != nil {
out.Flocker = new(FlockerVolumeSource)
if err := deepCopy_v1_FlockerVolumeSource(*in.Flocker, out.Flocker, c); err != nil {
return err
}
} else {
out.Flocker = nil
}
if in.DownwardAPI != nil {
out.DownwardAPI = new(DownwardAPIVolumeSource)
if err := deepCopy_v1_DownwardAPIVolumeSource(*in.DownwardAPI, out.DownwardAPI, c); err != nil {
@@ -2321,6 +2342,7 @@ func init() {
deepCopy_v1_EventSource,
deepCopy_v1_ExecAction,
deepCopy_v1_FCVolumeSource,
deepCopy_v1_FlockerVolumeSource,
deepCopy_v1_GCEPersistentDiskVolumeSource,
deepCopy_v1_GitRepoVolumeSource,
deepCopy_v1_GlusterfsVolumeSource,

View File

@@ -250,6 +250,9 @@ type VolumeSource struct {
// CephFS represents a Ceph FS mount on the host that shares a pod's lifetime
CephFS *CephFSVolumeSource `json:"cephfs,omitempty"`
// Flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running
Flocker *FlockerVolumeSource `json:"flocker,omitempty"`
// DownwardAPI represents downward API about the pod that should populate this volume
DownwardAPI *DownwardAPIVolumeSource `json:"downwardAPI,omitempty"`
// FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.
@@ -306,6 +309,8 @@ type PersistentVolumeSource struct {
CephFS *CephFSVolumeSource `json:"cephfs,omitempty"`
// FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.
FC *FCVolumeSource `json:"fc,omitempty"`
// Flocker represents a Flocker volume attached to a kubelet's host machine and exposed to the pod for its usage. This depends on the Flocker control service being running
Flocker *FlockerVolumeSource `json:"flocker,omitempty"`
}
// PersistentVolume (PV) is a storage resource provisioned by an administrator.
@@ -589,6 +594,12 @@ type CephFSVolumeSource struct {
ReadOnly bool `json:"readOnly,omitempty"`
}
// FlockerVolumeSource represents a Flocker volume mounted by the Flocker agent.
type FlockerVolumeSource struct {
// Required: the volume name. This is going to be store on metadata -> name on the payload for Flocker
DatasetName string `json:"datasetName"`
}
const (
StorageMediumDefault StorageMedium = "" // use whatever the default is for the node
StorageMediumMemory StorageMedium = "Memory" // use memory (tmpfs)

View File

@@ -389,6 +389,15 @@ func (FCVolumeSource) SwaggerDoc() map[string]string {
return map_FCVolumeSource
}
var map_FlockerVolumeSource = map[string]string{
"": "FlockerVolumeSource represents a Flocker volume mounted by the Flocker agent.",
"datasetName": "Required: the volume name. This is going to be store on metadata -> name on the payload for Flocker",
}
func (FlockerVolumeSource) SwaggerDoc() map[string]string {
return map_FlockerVolumeSource
}
var map_GCEPersistentDiskVolumeSource = map[string]string{
"": "GCEPersistentDiskVolumeSource represents a Persistent Disk resource in Google Compute Engine.\n\nA GCE PD must exist and be formatted before mounting to a container. The disk must also be in the same GCE project and zone as the kubelet. A GCE PD can only be mounted as read/write once.",
"pdName": "Unique name of the PD resource in GCE. Used to identify the disk in GCE. More info: http://releases.k8s.io/HEAD/docs/user-guide/volumes.md#gcepersistentdisk",
@@ -847,6 +856,7 @@ var map_PersistentVolumeSource = map[string]string{
"cinder": "Cinder represents a cinder volume attached and mounted on kubelets host machine More info: http://releases.k8s.io/HEAD/examples/mysql-cinder-pd/README.md",
"cephfs": "CephFS represents a Ceph FS mount on the host that shares a pod's lifetime",
"fc": "FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.",
"flocker": "Flocker represents a Flocker volume attached to a kubelet's host machine and exposed to the pod for its usage. This depends on the Flocker control service being running",
}
func (PersistentVolumeSource) SwaggerDoc() map[string]string {
@@ -1358,6 +1368,7 @@ var map_VolumeSource = map[string]string{
"rbd": "RBD represents a Rados Block Device mount on the host that shares a pod's lifetime. More info: http://releases.k8s.io/HEAD/examples/rbd/README.md",
"cinder": "Cinder represents a cinder volume attached and mounted on kubelets host machine More info: http://releases.k8s.io/HEAD/examples/mysql-cinder-pd/README.md",
"cephfs": "CephFS represents a Ceph FS mount on the host that shares a pod's lifetime",
"flocker": "Flocker represents a Flocker volume attached to a kubelet's host machine. This depends on the Flocker control service being running",
"downwardAPI": "DownwardAPI represents downward API about the pod that should populate this volume",
"fc": "FC represents a Fibre Channel resource that is attached to a kubelet's host machine and then exposed to the pod.",
}

View File

@@ -379,6 +379,10 @@ func validateSource(source *api.VolumeSource) errs.ValidationErrorList {
numVolumes++
allErrs = append(allErrs, validateGlusterfs(source.Glusterfs).Prefix("glusterfs")...)
}
if source.Flocker != nil {
numVolumes++
allErrs = append(allErrs, validateFlocker(source.Flocker).Prefix("flocker")...)
}
if source.PersistentVolumeClaim != nil {
numVolumes++
allErrs = append(allErrs, validatePersistentClaimVolumeSource(source.PersistentVolumeClaim).Prefix("persistentVolumeClaim")...)
@@ -531,6 +535,17 @@ func validateGlusterfs(glusterfs *api.GlusterfsVolumeSource) errs.ValidationErro
return allErrs
}
func validateFlocker(flocker *api.FlockerVolumeSource) errs.ValidationErrorList {
allErrs := errs.ValidationErrorList{}
if flocker.DatasetName == "" {
allErrs = append(allErrs, errs.NewFieldRequired("datasetName"))
}
if strings.Contains(flocker.DatasetName, "/") {
allErrs = append(allErrs, errs.NewFieldInvalid("datasetName", flocker.DatasetName, "must not contain '/'"))
}
return allErrs
}
var validDownwardAPIFieldPathExpressions = sets.NewString("metadata.name", "metadata.namespace", "metadata.labels", "metadata.annotations")
func validateDownwardAPIVolumeSource(downwardAPIVolume *api.DownwardAPIVolumeSource) errs.ValidationErrorList {
@@ -636,6 +651,10 @@ func ValidatePersistentVolume(pv *api.PersistentVolume) errs.ValidationErrorList
numVolumes++
allErrs = append(allErrs, validateGlusterfs(pv.Spec.Glusterfs).Prefix("glusterfs")...)
}
if pv.Spec.Flocker != nil {
numVolumes++
allErrs = append(allErrs, validateFlocker(pv.Spec.Flocker).Prefix("flocker")...)
}
if pv.Spec.NFS != nil {
numVolumes++
allErrs = append(allErrs, validateNFS(pv.Spec.NFS).Prefix("nfs")...)

View File

@@ -458,6 +458,7 @@ func TestValidateVolumes(t *testing.T) {
{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: "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{"29ea5088-4f60-4757-962e-dba678767887", "ext4", false}}},
{Name: "cephfs", VolumeSource: api.VolumeSource{CephFS: &api.CephFSVolumeSource{Monitors: []string{"foo"}}}},
@@ -501,6 +502,7 @@ func TestValidateVolumes(t *testing.T) {
emptyIQN := api.VolumeSource{ISCSI: &api.ISCSIVolumeSource{TargetPortal: "127.0.0.1", IQN: "", Lun: 1, FSType: "ext4", ReadOnly: false}}
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{}}}
@@ -531,30 +533,33 @@ func TestValidateVolumes(t *testing.T) {
}}
zeroWWN := api.VolumeSource{FC: &api.FCVolumeSource{[]string{}, &lun, "ext4", false}}
emptyLun := api.VolumeSource{FC: &api.FCVolumeSource{[]string{"wwn"}, nil, "ext4", false}}
slashInName := api.VolumeSource{Flocker: &api.FlockerVolumeSource{DatasetName: "foo/bar"}}
errorCases := map[string]struct {
V []api.Volume
T errors.ValidationErrorType
F string
D string
}{
"zero-length name": {[]api.Volume{{Name: "", VolumeSource: emptyVS}}, errors.ValidationErrorTypeRequired, "[0].name", ""},
"name > 63 characters": {[]api.Volume{{Name: strings.Repeat("a", 64), VolumeSource: emptyVS}}, errors.ValidationErrorTypeInvalid, "[0].name", "must be a DNS label (at most 63 characters, matching regex [a-z0-9]([-a-z0-9]*[a-z0-9])?): e.g. \"my-name\""},
"name not a DNS label": {[]api.Volume{{Name: "a.b.c", VolumeSource: emptyVS}}, errors.ValidationErrorTypeInvalid, "[0].name", "must be a DNS label (at most 63 characters, matching regex [a-z0-9]([-a-z0-9]*[a-z0-9])?): e.g. \"my-name\""},
"name not unique": {[]api.Volume{{Name: "abc", VolumeSource: emptyVS}, {Name: "abc", VolumeSource: emptyVS}}, errors.ValidationErrorTypeDuplicate, "[1].name", ""},
"empty portal": {[]api.Volume{{Name: "badportal", VolumeSource: emptyPortal}}, errors.ValidationErrorTypeRequired, "[0].source.iscsi.targetPortal", ""},
"empty iqn": {[]api.Volume{{Name: "badiqn", VolumeSource: emptyIQN}}, errors.ValidationErrorTypeRequired, "[0].source.iscsi.iqn", ""},
"empty hosts": {[]api.Volume{{Name: "badhost", VolumeSource: emptyHosts}}, errors.ValidationErrorTypeRequired, "[0].source.glusterfs.endpoints", ""},
"empty path": {[]api.Volume{{Name: "badpath", VolumeSource: emptyPath}}, errors.ValidationErrorTypeRequired, "[0].source.glusterfs.path", ""},
"empty mon": {[]api.Volume{{Name: "badmon", VolumeSource: emptyMon}}, errors.ValidationErrorTypeRequired, "[0].source.rbd.monitors", ""},
"empty image": {[]api.Volume{{Name: "badimage", VolumeSource: emptyImage}}, errors.ValidationErrorTypeRequired, "[0].source.rbd.image", ""},
"empty cephfs mon": {[]api.Volume{{Name: "badmon", VolumeSource: emptyCephFSMon}}, errors.ValidationErrorTypeRequired, "[0].source.cephfs.monitors", ""},
"empty metatada path": {[]api.Volume{{Name: "emptyname", VolumeSource: emptyPathName}}, errors.ValidationErrorTypeRequired, "[0].source.downwardApi.path", ""},
"absolute path": {[]api.Volume{{Name: "absolutepath", VolumeSource: absolutePathName}}, errors.ValidationErrorTypeForbidden, "[0].source.downwardApi.path", ""},
"dot dot path": {[]api.Volume{{Name: "dotdotpath", VolumeSource: dotDotInPath}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not contain \"..\"."},
"dot dot file name": {[]api.Volume{{Name: "dotdotfilename", VolumeSource: dotDotPathName}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not start with \"..\"."},
"dot dot first level dirent ": {[]api.Volume{{Name: "dotdotdirfilename", VolumeSource: dotDotFirstLevelDirent}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not start with \"..\"."},
"empty wwn": {[]api.Volume{{Name: "badimage", VolumeSource: zeroWWN}}, errors.ValidationErrorTypeRequired, "[0].source.fc.targetWWNs", ""},
"empty lun": {[]api.Volume{{Name: "badimage", VolumeSource: emptyLun}}, errors.ValidationErrorTypeRequired, "[0].source.fc.lun", ""},
"zero-length name": {[]api.Volume{{Name: "", VolumeSource: emptyVS}}, errors.ValidationErrorTypeRequired, "[0].name", ""},
"name > 63 characters": {[]api.Volume{{Name: strings.Repeat("a", 64), VolumeSource: emptyVS}}, errors.ValidationErrorTypeInvalid, "[0].name", "must be a DNS label (at most 63 characters, matching regex [a-z0-9]([-a-z0-9]*[a-z0-9])?): e.g. \"my-name\""},
"name not a DNS label": {[]api.Volume{{Name: "a.b.c", VolumeSource: emptyVS}}, errors.ValidationErrorTypeInvalid, "[0].name", "must be a DNS label (at most 63 characters, matching regex [a-z0-9]([-a-z0-9]*[a-z0-9])?): e.g. \"my-name\""},
"name not unique": {[]api.Volume{{Name: "abc", VolumeSource: emptyVS}, {Name: "abc", VolumeSource: emptyVS}}, errors.ValidationErrorTypeDuplicate, "[1].name", ""},
"empty portal": {[]api.Volume{{Name: "badportal", VolumeSource: emptyPortal}}, errors.ValidationErrorTypeRequired, "[0].source.iscsi.targetPortal", ""},
"empty iqn": {[]api.Volume{{Name: "badiqn", VolumeSource: emptyIQN}}, errors.ValidationErrorTypeRequired, "[0].source.iscsi.iqn", ""},
"empty hosts": {[]api.Volume{{Name: "badhost", VolumeSource: emptyHosts}}, errors.ValidationErrorTypeRequired, "[0].source.glusterfs.endpoints", ""},
"empty path": {[]api.Volume{{Name: "badpath", VolumeSource: emptyPath}}, errors.ValidationErrorTypeRequired, "[0].source.glusterfs.path", ""},
"empty datasetName": {[]api.Volume{{Name: "badname", VolumeSource: emptyName}}, errors.ValidationErrorTypeRequired, "[0].source.flocker.datasetName", ""},
"empty mon": {[]api.Volume{{Name: "badmon", VolumeSource: emptyMon}}, errors.ValidationErrorTypeRequired, "[0].source.rbd.monitors", ""},
"empty image": {[]api.Volume{{Name: "badimage", VolumeSource: emptyImage}}, errors.ValidationErrorTypeRequired, "[0].source.rbd.image", ""},
"empty cephfs mon": {[]api.Volume{{Name: "badmon", VolumeSource: emptyCephFSMon}}, errors.ValidationErrorTypeRequired, "[0].source.cephfs.monitors", ""},
"empty metatada path": {[]api.Volume{{Name: "emptyname", VolumeSource: emptyPathName}}, errors.ValidationErrorTypeRequired, "[0].source.downwardApi.path", ""},
"absolute path": {[]api.Volume{{Name: "absolutepath", VolumeSource: absolutePathName}}, errors.ValidationErrorTypeForbidden, "[0].source.downwardApi.path", ""},
"dot dot path": {[]api.Volume{{Name: "dotdotpath", VolumeSource: dotDotInPath}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not contain \"..\"."},
"dot dot file name": {[]api.Volume{{Name: "dotdotfilename", VolumeSource: dotDotPathName}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not start with \"..\"."},
"dot dot first level dirent": {[]api.Volume{{Name: "dotdotdirfilename", VolumeSource: dotDotFirstLevelDirent}}, errors.ValidationErrorTypeInvalid, "[0].source.downwardApi.path", "must not start with \"..\"."},
"empty wwn": {[]api.Volume{{Name: "badimage", VolumeSource: zeroWWN}}, errors.ValidationErrorTypeRequired, "[0].source.fc.targetWWNs", ""},
"empty lun": {[]api.Volume{{Name: "badimage", VolumeSource: emptyLun}}, errors.ValidationErrorTypeRequired, "[0].source.fc.lun", ""},
"slash in datasetName": {[]api.Volume{{Name: "slashinname", VolumeSource: slashInName}}, errors.ValidationErrorTypeInvalid, "[0].source.flocker.datasetName", "must not contain '/'"},
}
for k, v := range errorCases {
_, errs := validateVolumes(v.V)