diff --git a/pkg/api/fuzzer/fuzzer.go b/pkg/api/fuzzer/fuzzer.go index 9d0f94a267..d6da270821 100644 --- a/pkg/api/fuzzer/fuzzer.go +++ b/pkg/api/fuzzer/fuzzer.go @@ -355,6 +355,15 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} { r.Keyring = "/etc/ceph/keyring" } }, + func(obj *api.HostPathVolumeSource, c fuzz.Continue) { + c.FuzzNoCustom(obj) + types := []api.HostPathType{api.HostPathDirectoryOrCreate, api.HostPathDirectory, api.HostPathFileOrCreate, + api.HostPathFile, api.HostPathSocket, api.HostPathCharDev, api.HostPathBlockDev} + typeVol := types[c.Rand.Intn(len(types))] + if obj.Path != "" && obj.Type == nil { + obj.Type = &typeVol + } + }, func(pv *api.PersistentVolume, c fuzz.Continue) { c.FuzzNoCustom(pv) // fuzz self without calling this function again types := []api.PersistentVolumePhase{api.VolumeAvailable, api.VolumePending, api.VolumeBound, api.VolumeReleased, api.VolumeFailed} diff --git a/pkg/api/types.go b/pkg/api/types.go index 9757327572..cd0a48fa86 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -597,10 +597,34 @@ const ( ClaimLost PersistentVolumeClaimPhase = "Lost" ) +type HostPathType string + +const ( + // If nothing exists at the given path, an empty directory will be created there + // as needed with file mode 0755, having the same group and ownership with Kubelet. + HostPathDirectoryOrCreate HostPathType = "DirectoryOrCreate" + // A directory must exist at the given path + HostPathDirectory HostPathType = "Directory" + // If nothing exists at the given path, an empty file will be created there + // as needed with file mode 0644, having the same group and ownership with Kubelet. + HostPathFileOrCreate HostPathType = "FileOrCreate" + // A file must exist at the given path + HostPathFile HostPathType = "File" + // A UNIX socket must exist at the given path + HostPathSocket HostPathType = "Socket" + // A character device must exist at the given path + HostPathCharDev HostPathType = "CharDevice" + // A block device must exist at the given path + HostPathBlockDev HostPathType = "BlockDevice" +) + // Represents a host path mapped into a pod. // Host path volumes do not support ownership management or SELinux relabeling. type HostPathVolumeSource struct { + // If the path is a symlink, it will follow the link to the real path. Path string + // Defaults to DirectoryOrCreate + Type *HostPathType } // Represents an empty directory for a pod. diff --git a/pkg/api/v1/defaults.go b/pkg/api/v1/defaults.go index 70c1d709ef..83bcf15093 100644 --- a/pkg/api/v1/defaults.go +++ b/pkg/api/v1/defaults.go @@ -369,3 +369,10 @@ func SetDefaults_ScaleIOVolumeSource(obj *v1.ScaleIOVolumeSource) { obj.FSType = "xfs" } } + +func SetDefaults_HostPathVolumeSource(obj *v1.HostPathVolumeSource) { + typeVol := v1.HostPathDirectoryOrCreate + if obj.Type == nil { + obj.Type = &typeVol + } +} diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index f3765cd548..fcd2af9126 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -623,6 +623,7 @@ func validateHostPathVolumeSource(hostPath *api.HostPathVolumeSource, fldPath *f } allErrs = append(allErrs, validatePathNoBacksteps(hostPath.Path, fldPath.Child("path"))...) + allErrs = append(allErrs, validateHostPathType(hostPath.Type, fldPath.Child("type"))...) return allErrs } @@ -974,6 +975,25 @@ func validateProjectedVolumeSource(projection *api.ProjectedVolumeSource, fldPat return allErrs } +var supportedHostPathTypes = sets.NewString( + string(api.HostPathDirectoryOrCreate), + string(api.HostPathDirectory), + string(api.HostPathFileOrCreate), + string(api.HostPathFile), + string(api.HostPathSocket), + string(api.HostPathCharDev), + string(api.HostPathBlockDev)) + +func validateHostPathType(hostPathType *api.HostPathType, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if !supportedHostPathTypes.Has(string(*hostPathType)) { + allErrs = append(allErrs, field.NotSupported(fldPath, hostPathType, supportedHostPathTypes.List())) + } + + return allErrs +} + // This validate will make sure targetPath: // 1. is not abs path // 2. does not have any element which is ".." diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 3ae3d8d2df..e79fa2aad3 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -43,6 +43,12 @@ const ( envVarNameErrMsg = "a valid environment variable name must consist of" ) +func newHostPathType(pathType string) *api.HostPathType { + hostPathType := new(api.HostPathType) + *hostPathType = api.HostPathType(pathType) + return hostPathType +} + func testVolume(name string, namespace string, spec api.PersistentVolumeSpec) *api.PersistentVolume { objMeta := metav1.ObjectMeta{Name: name} if namespace != "" { @@ -86,7 +92,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, StorageClassName: "valid", }), @@ -99,7 +108,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimRetain, }), @@ -112,7 +124,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{"fakemode"}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, }), }, @@ -124,7 +139,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, PersistentVolumeReclaimPolicy: "fakeReclaimPolicy", }), @@ -137,7 +155,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, }), }, @@ -149,7 +170,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, }), }, @@ -173,7 +197,10 @@ func TestValidatePersistentVolumes(t *testing.T) { api.ResourceName(api.ResourceStorage): resource.MustParse("10G"), }, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, }), }, @@ -184,7 +211,10 @@ func TestValidatePersistentVolumes(t *testing.T) { api.ResourceName(api.ResourceStorage): resource.MustParse("5G"), }, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "foo", FSType: "ext4"}, }, }), @@ -197,7 +227,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimRecycle, }), @@ -210,7 +243,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/a/.."}, + HostPath: &api.HostPathVolumeSource{ + Path: "/a/..", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimRecycle, }), @@ -223,7 +259,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, StorageClassName: "-invalid-", }), @@ -272,7 +311,10 @@ func TestValidatePersistentVolumes(t *testing.T) { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo/.."}, + HostPath: &api.HostPathVolumeSource{ + Path: "/foo/..", + Type: newHostPathType(string(api.HostPathDirectory)), + }, }, StorageClassName: "backstep-hostpath", }), @@ -1109,6 +1151,7 @@ func TestValidateVolumes(t *testing.T) { EmptyDir: &api.EmptyDirVolumeSource{}, HostPath: &api.HostPathVolumeSource{ Path: "/mnt/path", + Type: newHostPathType(string(api.HostPathDirectory)), }, }, }, @@ -1116,7 +1159,20 @@ func TestValidateVolumes(t *testing.T) { errfield: "hostPath", errdetail: "may not specify more than 1 volume", }, - // HostPath + // HostPath Default + { + name: "default HostPath", + vol: api.Volume{ + Name: "hostpath", + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/mnt/path", + Type: newHostPathType(string(api.HostPathDirectory)), + }, + }, + }, + }, + // HostPath Supported { name: "valid HostPath", vol: api.Volume{ @@ -1124,10 +1180,26 @@ func TestValidateVolumes(t *testing.T) { VolumeSource: api.VolumeSource{ HostPath: &api.HostPathVolumeSource{ Path: "/mnt/path", + Type: newHostPathType(string(api.HostPathSocket)), }, }, }, }, + // HostPath Invalid + { + name: "invalid HostPath", + vol: api.Volume{ + Name: "hostpath", + VolumeSource: api.VolumeSource{ + HostPath: &api.HostPathVolumeSource{ + Path: "/mnt/path", + Type: newHostPathType("invalid"), + }, + }, + }, + errtype: field.ErrorTypeNotSupported, + errfield: "type", + }, { name: "invalid HostPath backsteps", vol: api.Volume{ @@ -1135,6 +1207,7 @@ func TestValidateVolumes(t *testing.T) { VolumeSource: api.VolumeSource{ HostPath: &api.HostPathVolumeSource{ Path: "/mnt/path/..", + Type: newHostPathType(string(api.HostPathDirectory)), }, }, }, diff --git a/pkg/kubelet/kubelet_volumes.go b/pkg/kubelet/kubelet_volumes.go index 2baaf66747..2bb3c73c37 100644 --- a/pkg/kubelet/kubelet_volumes.go +++ b/pkg/kubelet/kubelet_volumes.go @@ -69,6 +69,7 @@ func (kl *Kubelet) newVolumeMounterFromPlugins(spec *volume.Spec, pod *v1.Pod, o if err != nil { return nil, fmt.Errorf("can't use volume plugins for %s: %v", spec.Name(), err) } + opts.Containerized = kl.kubeletConfiguration.Containerized physicalMounter, err := plugin.NewMounter(spec, pod, opts) if err != nil { return nil, fmt.Errorf("failed to instantiate mounter for volume: %s using plugin: %s with a root cause: %v", spec.Name(), plugin.GetPluginName(), err) diff --git a/pkg/registry/core/persistentvolume/storage/storage_test.go b/pkg/registry/core/persistentvolume/storage/storage_test.go index 264d159f03..3b419ad342 100644 --- a/pkg/registry/core/persistentvolume/storage/storage_test.go +++ b/pkg/registry/core/persistentvolume/storage/storage_test.go @@ -46,6 +46,12 @@ func newStorage(t *testing.T) (*REST, *StatusREST, *etcdtesting.EtcdTestServer) return persistentVolumeStorage, statusStorage, server } +func newHostPathType(pathType string) *api.HostPathType { + hostPathType := new(api.HostPathType) + *hostPathType = api.HostPathType(pathType) + return hostPathType +} + func validNewPersistentVolume(name string) *api.PersistentVolume { pv := &api.PersistentVolume{ ObjectMeta: metav1.ObjectMeta{ @@ -57,7 +63,7 @@ func validNewPersistentVolume(name string) *api.PersistentVolume { }, AccessModes: []api.PersistentVolumeAccessMode{api.ReadWriteOnce}, PersistentVolumeSource: api.PersistentVolumeSource{ - HostPath: &api.HostPathVolumeSource{Path: "/foo"}, + HostPath: &api.HostPathVolumeSource{Path: "/foo", Type: newHostPathType(string(api.HostPathDirectoryOrCreate))}, }, PersistentVolumeReclaimPolicy: api.PersistentVolumeReclaimRetain, }, diff --git a/pkg/volume/host_path/BUILD b/pkg/volume/host_path/BUILD index d55dabee5c..a3746f8af8 100644 --- a/pkg/volume/host_path/BUILD +++ b/pkg/volume/host_path/BUILD @@ -11,6 +11,7 @@ go_library( srcs = [ "doc.go", "host_path.go", + "host_path_unix.go", ], deps = [ "//pkg/volume:go_default_library", diff --git a/pkg/volume/host_path/host_path.go b/pkg/volume/host_path/host_path.go index 13767047bd..5785391860 100644 --- a/pkg/volume/host_path/host_path.go +++ b/pkg/volume/host_path/host_path.go @@ -99,14 +99,15 @@ func (plugin *hostPathPlugin) GetAccessModes() []v1.PersistentVolumeAccessMode { } } -func (plugin *hostPathPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, _ volume.VolumeOptions) (volume.Mounter, error) { +func (plugin *hostPathPlugin) NewMounter(spec *volume.Spec, pod *v1.Pod, opts volume.VolumeOptions) (volume.Mounter, error) { hostPathVolumeSource, readOnly, err := getVolumeSource(spec) if err != nil { return nil, err } + path := hostPathVolumeSource.Path return &hostPathMounter{ - hostPath: &hostPath{path: hostPathVolumeSource.Path}, + hostPath: &hostPath{path: path, pathType: hostPathVolumeSource.Type}, readOnly: readOnly, }, nil } @@ -175,7 +176,8 @@ func newProvisioner(options volume.VolumeOptions, host volume.VolumeHost, plugin // HostPath volumes represent a bare host file or directory mount. // The direct at the specified path will be directly exposed to the container. type hostPath struct { - path string + path string + pathType *v1.HostPathType volume.MetricsNil } @@ -211,7 +213,8 @@ func (b *hostPathMounter) SetUp(fsGroup *int64) error { if err != nil { return fmt.Errorf("invalid HostPath `%s`: %v", b.GetPath(), err) } - return nil + + return checkType(b.GetPath(), b.pathType) } // SetUpAt does not make sense for host paths - probably programmer error. @@ -314,3 +317,170 @@ func getVolumeSource(spec *volume.Spec) (*v1.HostPathVolumeSource, bool, error) return nil, false, fmt.Errorf("Spec does not reference an HostPath volume type") } + +type hostPathTypeChecker interface { + Exists() bool + IsFile() bool + MakeFile() error + IsDir() bool + MakeDir() error + IsBlock() bool + IsChar() bool + IsSocket() bool + GetPath() string +} + +type fileTypeChecker interface { + getFileType(fileInfo os.FileInfo) (v1.HostPathType, error) +} + +// this is implemented in per-OS files +type defaultFileTypeChecker struct{} + +type osFileTypeChecker struct { + path string + exists bool + info os.FileInfo + checker fileTypeChecker +} + +func (ftc *osFileTypeChecker) Exists() bool { + return ftc.exists +} + +func (ftc *osFileTypeChecker) IsFile() bool { + if !ftc.Exists() { + return false + } + return !ftc.info.IsDir() +} + +func (ftc *osFileTypeChecker) MakeFile() error { + f, err := os.OpenFile(ftc.path, os.O_CREATE, os.FileMode(0644)) + defer f.Close() + if err != nil { + if !os.IsExist(err) { + return err + } + } + return nil +} + +func (ftc *osFileTypeChecker) IsDir() bool { + if !ftc.Exists() { + return false + } + return ftc.info.IsDir() +} + +func (ftc *osFileTypeChecker) MakeDir() error { + err := os.MkdirAll(ftc.path, os.FileMode(0755)) + if err != nil { + if !os.IsExist(err) { + return err + } + } + return nil +} + +func (ftc *osFileTypeChecker) IsBlock() bool { + if !ftc.Exists() { + return false + } + + blkDevType, err := ftc.checker.getFileType(ftc.info) + if err != nil { + return false + } + return blkDevType == v1.HostPathBlockDev +} + +func (ftc *osFileTypeChecker) IsChar() bool { + if !ftc.Exists() { + return false + } + + charDevType, err := ftc.checker.getFileType(ftc.info) + if err != nil { + return false + } + return charDevType == v1.HostPathCharDev +} + +func (ftc *osFileTypeChecker) IsSocket() bool { + if !ftc.Exists() { + return false + } + + socketType, err := ftc.checker.getFileType(ftc.info) + if err != nil { + return false + } + return socketType == v1.HostPathSocket +} + +func (ftc *osFileTypeChecker) GetPath() string { + return ftc.path +} + +func newOSFileTypeChecker(path string, checker fileTypeChecker) (hostPathTypeChecker, error) { + ftc := osFileTypeChecker{path: path, checker: checker} + info, err := os.Stat(path) + if err != nil { + ftc.exists = false + if !os.IsNotExist(err) { + return nil, err + } + } else { + ftc.info = info + ftc.exists = true + } + return &ftc, nil +} + +func checkType(path string, pathType *v1.HostPathType) error { + ftc, err := newOSFileTypeChecker(path, &defaultFileTypeChecker{}) + if err != nil { + return err + } + return checkTypeInternal(ftc, pathType) +} + +func checkTypeInternal(ftc hostPathTypeChecker, pathType *v1.HostPathType) error { + switch *pathType { + case v1.HostPathDirectoryOrCreate: + if !ftc.Exists() { + return ftc.MakeDir() + } + fallthrough + case v1.HostPathDirectory: + if !ftc.IsDir() { + return fmt.Errorf("hostPath type check failed: %s is not a directory", ftc.GetPath()) + } + case v1.HostPathFileOrCreate: + if !ftc.Exists() { + return ftc.MakeFile() + } + fallthrough + case v1.HostPathFile: + if !ftc.IsFile() { + return fmt.Errorf("hostPath type check failed: %s is not a file", ftc.GetPath()) + } + case v1.HostPathSocket: + if !ftc.IsSocket() { + return fmt.Errorf("hostPath type check failed: %s is not a socket file", ftc.GetPath()) + } + case v1.HostPathCharDev: + if !ftc.IsChar() { + return fmt.Errorf("hostPath type check failed: %s is not a character device", ftc.GetPath()) + } + case v1.HostPathBlockDev: + if !ftc.IsBlock() { + return fmt.Errorf("hostPath type check failed: %s is not a block device", ftc.GetPath()) + } + default: + return fmt.Errorf("%s is an invalid volume type", *pathType) + } + + return nil +} diff --git a/pkg/volume/host_path/host_path_test.go b/pkg/volume/host_path/host_path_test.go index a16b2ee89a..c588099773 100644 --- a/pkg/volume/host_path/host_path_test.go +++ b/pkg/volume/host_path/host_path_test.go @@ -34,6 +34,21 @@ import ( volumetest "k8s.io/kubernetes/pkg/volume/testing" ) +func newHostPathType(pathType string) *v1.HostPathType { + hostPathType := new(v1.HostPathType) + *hostPathType = v1.HostPathType(pathType) + return hostPathType +} + +func newHostPathTypeList(pathType ...string) []*v1.HostPathType { + typeList := []*v1.HostPathType{} + for _, ele := range pathType { + typeList = append(typeList, newHostPathType(ele)) + } + + return typeList +} + func TestCanSupport(t *testing.T) { plugMgr := volume.VolumePluginMgr{} plugMgr.InitPlugins(ProbeVolumePlugins(volume.VolumeConfig{}), volumetest.NewFakeVolumeHost("fake", nil, nil)) @@ -108,7 +123,7 @@ func TestDeleter(t *testing.T) { if err := deleter.Delete(); err != nil { t.Errorf("Mock Recycler expected to return nil but got %s", err) } - if exists, _ := utilfile.FileExists("foo"); exists { + if exists, _ := utilfile.FileExists(tempPath); exists { t.Errorf("Temp path expected to be deleted, but was found at %s", tempPath) } } @@ -215,11 +230,14 @@ func TestPlugin(t *testing.T) { if err != nil { t.Errorf("Can't find the plugin by name") } + + volPath := "/tmp/vol1" spec := &v1.Volume{ Name: "vol1", - VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: "/vol1"}}, + VolumeSource: v1.VolumeSource{HostPath: &v1.HostPathVolumeSource{Path: volPath, Type: newHostPathType(string(v1.HostPathDirectoryOrCreate))}}, } pod := &v1.Pod{ObjectMeta: metav1.ObjectMeta{UID: types.UID("poduid")}} + defer os.RemoveAll(volPath) mounter, err := plug.NewMounter(volume.NewSpecFromVolume(spec), pod, volume.VolumeOptions{}) if err != nil { t.Errorf("Failed to make a new Mounter: %v", err) @@ -229,7 +247,7 @@ func TestPlugin(t *testing.T) { } path := mounter.GetPath() - if path != "/vol1" { + if path != volPath { t.Errorf("Got unexpected path: %s", path) } @@ -257,13 +275,14 @@ func TestPersistentClaimReadOnlyFlag(t *testing.T) { }, Spec: v1.PersistentVolumeSpec{ PersistentVolumeSource: v1.PersistentVolumeSource{ - HostPath: &v1.HostPathVolumeSource{Path: "foo"}, + HostPath: &v1.HostPathVolumeSource{Path: "foo", Type: newHostPathType(string(v1.HostPathDirectoryOrCreate))}, }, ClaimRef: &v1.ObjectReference{ Name: "claimA", }, }, } + defer os.RemoveAll("foo") claim := &v1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ @@ -293,3 +312,297 @@ func TestPersistentClaimReadOnlyFlag(t *testing.T) { t.Errorf("Expected true for mounter.IsReadOnly") } } + +type fakeFileTypeChecker struct { + desiredType string +} + +func (fftc *fakeFileTypeChecker) getFileType(_ os.FileInfo) (v1.HostPathType, error) { + return *newHostPathType(fftc.desiredType), nil +} + +func setUp() error { + err := os.MkdirAll("/tmp/ExistingFolder", os.FileMode(0755)) + if err != nil { + return err + } + + f, err := os.OpenFile("/tmp/ExistingFolder/foo", os.O_CREATE, os.FileMode(0644)) + defer f.Close() + if err != nil { + return err + } + + return nil +} + +func tearDown() { + os.RemoveAll("/tmp/ExistingFolder") +} + +func TestOSFileTypeChecker(t *testing.T) { + err := setUp() + if err != nil { + t.Error(err) + } + defer tearDown() + testCases := []struct { + name string + path string + desiredType string + isDir bool + isFile bool + isSocket bool + isBlock bool + isChar bool + }{ + { + name: "Existing Folder", + path: "/tmp/ExistingFolder", + isDir: true, + }, + { + name: "Existing File", + path: "/tmp/ExistingFolder/foo", + isFile: true, + }, + { + name: "Existing Socket File", + path: "/tmp/ExistingFolder/foo", + desiredType: string(v1.HostPathSocket), + isSocket: true, + }, + { + name: "Existing Character Device", + path: "/tmp/ExistingFolder/foo", + desiredType: string(v1.HostPathCharDev), + isChar: true, + }, + { + name: "Existing Block Device", + path: "/tmp/ExistingFolder/foo", + desiredType: string(v1.HostPathBlockDev), + isBlock: true, + }, + } + + for i, tc := range testCases { + oftc, err := newOSFileTypeChecker(tc.path, + &fakeFileTypeChecker{desiredType: tc.desiredType}) + if err != nil { + t.Errorf("[%d: %q] expect nil, but got %v", i, tc.name, err) + } + + path := oftc.GetPath() + if path != tc.path { + t.Errorf("[%d: %q] got unexpected path: %s", i, tc.name, path) + } + + exist := oftc.Exists() + if !exist { + t.Errorf("[%d: %q] path: %s does not exist", i, tc.name, path) + } + + if tc.isDir { + if !oftc.IsDir() { + t.Errorf("[%d: %q] expected folder, got unexpected: %s", i, tc.name, path) + } + if oftc.IsFile() { + t.Errorf("[%d: %q] expected folder, got unexpected file: %s", i, tc.name, path) + } + if oftc.IsSocket() { + t.Errorf("[%d: %q] expected folder, got unexpected socket file: %s", i, tc.name, path) + } + if oftc.IsBlock() { + t.Errorf("[%d: %q] expected folder, got unexpected block device: %s", i, tc.name, path) + } + if oftc.IsChar() { + t.Errorf("[%d: %q] expected folder, got unexpected character device: %s", i, tc.name, path) + } + } + + if tc.isFile { + if !oftc.IsFile() { + t.Errorf("[%d: %q] expected file, got unexpected: %s", i, tc.name, path) + } + if oftc.IsDir() { + t.Errorf("[%d: %q] expected file, got unexpected folder: %s", i, tc.name, path) + } + if oftc.IsSocket() { + t.Errorf("[%d: %q] expected file, got unexpected socket file: %s", i, tc.name, path) + } + if oftc.IsBlock() { + t.Errorf("[%d: %q] expected file, got unexpected block device: %s", i, tc.name, path) + } + if oftc.IsChar() { + t.Errorf("[%d: %q] expected file, got unexpected character device: %s", i, tc.name, path) + } + } + + if tc.isSocket { + if !oftc.IsSocket() { + t.Errorf("[%d: %q] expected socket file, got unexpected: %s", i, tc.name, path) + } + if oftc.IsDir() { + t.Errorf("[%d: %q] expected socket file, got unexpected folder: %s", i, tc.name, path) + } + if !oftc.IsFile() { + t.Errorf("[%d: %q] expected socket file, got unexpected file: %s", i, tc.name, path) + } + if oftc.IsBlock() { + t.Errorf("[%d: %q] expected socket file, got unexpected block device: %s", i, tc.name, path) + } + if oftc.IsChar() { + t.Errorf("[%d: %q] expected socket file, got unexpected character device: %s", i, tc.name, path) + } + } + + if tc.isChar { + if !oftc.IsChar() { + t.Errorf("[%d: %q] expected character device, got unexpected: %s", i, tc.name, path) + } + if oftc.IsDir() { + t.Errorf("[%d: %q] expected character device, got unexpected folder: %s", i, tc.name, path) + } + if !oftc.IsFile() { + t.Errorf("[%d: %q] expected character device, got unexpected file: %s", i, tc.name, path) + } + if oftc.IsSocket() { + t.Errorf("[%d: %q] expected character device, got unexpected socket file: %s", i, tc.name, path) + } + if oftc.IsBlock() { + t.Errorf("[%d: %q] expected character device, got unexpected block device: %s", i, tc.name, path) + } + } + + if tc.isBlock { + if !oftc.IsBlock() { + t.Errorf("[%d: %q] expected block device, got unexpected: %s", i, tc.name, path) + } + if oftc.IsDir() { + t.Errorf("[%d: %q] expected block device, got unexpected folder: %s", i, tc.name, path) + } + if !oftc.IsFile() { + t.Errorf("[%d: %q] expected block device, got unexpected file: %s", i, tc.name, path) + } + if oftc.IsSocket() { + t.Errorf("[%d: %q] expected block device, got unexpected socket file: %s", i, tc.name, path) + } + if oftc.IsChar() { + t.Errorf("[%d: %q] expected block device, got unexpected character device: %s", i, tc.name, path) + } + } + } + +} + +type fakeHostPathTypeChecker struct { + name string + path string + exists bool + isDir bool + isFile bool + isSocket bool + isBlock bool + isChar bool + validpathType []*v1.HostPathType + invalidpathType []*v1.HostPathType +} + +func (ftc *fakeHostPathTypeChecker) MakeFile() error { return nil } +func (ftc *fakeHostPathTypeChecker) MakeDir() error { return nil } +func (ftc *fakeHostPathTypeChecker) Exists() bool { return ftc.exists } +func (ftc *fakeHostPathTypeChecker) IsFile() bool { return ftc.isFile } +func (ftc *fakeHostPathTypeChecker) IsDir() bool { return ftc.isDir } +func (ftc *fakeHostPathTypeChecker) IsBlock() bool { return ftc.isBlock } +func (ftc *fakeHostPathTypeChecker) IsChar() bool { return ftc.isChar } +func (ftc *fakeHostPathTypeChecker) IsSocket() bool { return ftc.isSocket } +func (ftc *fakeHostPathTypeChecker) GetPath() string { return ftc.path } + +func TestHostPathTypeCheckerInternal(t *testing.T) { + testCases := []fakeHostPathTypeChecker{ + { + name: "Existing Folder", + path: "/existingFolder", + isDir: true, + exists: true, + validpathType: newHostPathTypeList(string(v1.HostPathDirectoryOrCreate), string(v1.HostPathDirectory)), + invalidpathType: newHostPathTypeList(string(v1.HostPathFileOrCreate), string(v1.HostPathFile), + string(v1.HostPathSocket), string(v1.HostPathCharDev), string(v1.HostPathBlockDev)), + }, + { + name: "New Folder", + path: "/newFolder", + isDir: false, + exists: false, + validpathType: newHostPathTypeList(string(v1.HostPathDirectoryOrCreate)), + invalidpathType: newHostPathTypeList(string(v1.HostPathDirectory), string(v1.HostPathFile), + string(v1.HostPathSocket), string(v1.HostPathCharDev), string(v1.HostPathBlockDev)), + }, + { + name: "Existing File", + path: "/existingFile", + isFile: true, + exists: true, + validpathType: newHostPathTypeList(string(v1.HostPathFileOrCreate), string(v1.HostPathFile)), + invalidpathType: newHostPathTypeList(string(v1.HostPathDirectoryOrCreate), string(v1.HostPathDirectory), + string(v1.HostPathSocket), string(v1.HostPathCharDev), string(v1.HostPathBlockDev)), + }, + { + name: "New File", + path: "/newFile", + isFile: false, + exists: false, + validpathType: newHostPathTypeList(string(v1.HostPathFileOrCreate)), + invalidpathType: newHostPathTypeList(string(v1.HostPathFile), string(v1.HostPathDirectory), + string(v1.HostPathSocket), string(v1.HostPathCharDev), string(v1.HostPathBlockDev)), + }, + { + name: "Existing Socket", + path: "/existing.socket", + isSocket: true, + isFile: true, + exists: true, + validpathType: newHostPathTypeList(string(v1.HostPathSocket), string(v1.HostPathFileOrCreate), string(v1.HostPathFile)), + invalidpathType: newHostPathTypeList(string(v1.HostPathDirectoryOrCreate), string(v1.HostPathDirectory), + string(v1.HostPathCharDev), string(v1.HostPathBlockDev)), + }, + { + name: "Existing Character Device", + path: "/existing.char", + isChar: true, + isFile: true, + exists: true, + validpathType: newHostPathTypeList(string(v1.HostPathCharDev), string(v1.HostPathFileOrCreate), string(v1.HostPathFile)), + invalidpathType: newHostPathTypeList(string(v1.HostPathDirectoryOrCreate), string(v1.HostPathDirectory), + string(v1.HostPathSocket), string(v1.HostPathBlockDev)), + }, + { + name: "Existing Block Device", + path: "/existing.block", + isBlock: true, + isFile: true, + exists: true, + validpathType: newHostPathTypeList(string(v1.HostPathBlockDev), string(v1.HostPathFileOrCreate), string(v1.HostPathFile)), + invalidpathType: newHostPathTypeList(string(v1.HostPathDirectoryOrCreate), string(v1.HostPathDirectory), + string(v1.HostPathSocket), string(v1.HostPathCharDev)), + }, + } + + for i, tc := range testCases { + for _, pathType := range tc.validpathType { + err := checkTypeInternal(&tc, pathType) + if err != nil { + t.Errorf("[%d: %q] [%q] expected nil, got %v", i, tc.name, string(*pathType), err) + } + } + + for _, pathType := range tc.invalidpathType { + checkResult := checkTypeInternal(&tc, pathType) + if checkResult == nil { + t.Errorf("[%d: %q] [%q] expected error, got nil", i, tc.name, string(*pathType)) + } + } + } + +} diff --git a/pkg/volume/host_path/host_path_unix.go b/pkg/volume/host_path/host_path_unix.go new file mode 100644 index 0000000000..05deaaed5b --- /dev/null +++ b/pkg/volume/host_path/host_path_unix.go @@ -0,0 +1,40 @@ +// +build linux darwin + +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package host_path + +import ( + "fmt" + "os" + "syscall" + + "k8s.io/api/core/v1" +) + +func (dftc *defaultFileTypeChecker) getFileType(info os.FileInfo) (v1.HostPathType, error) { + mode := info.Sys().(*syscall.Stat_t).Mode + switch mode & syscall.S_IFMT { + case syscall.S_IFSOCK: + return v1.HostPathSocket, nil + case syscall.S_IFBLK: + return v1.HostPathBlockDev, nil + case syscall.S_IFCHR: + return v1.HostPathCharDev, nil + } + return "", fmt.Errorf("only recognise socket, block device and character device") +} diff --git a/pkg/volume/host_path/host_path_windows.go b/pkg/volume/host_path/host_path_windows.go new file mode 100644 index 0000000000..ff27e49371 --- /dev/null +++ b/pkg/volume/host_path/host_path_windows.go @@ -0,0 +1,38 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package host_path + +import ( + "fmt" + "os" + "syscall" + + "k8s.io/api/core/v1" +) + +func (dftc *defaultFileTypeChecker) getFileType(info os.FileInfo) (v1.HostPathType, error) { + mode := info.Sys().(*syscall.Win32FileAttributeData).FileAttributes + switch mode & syscall.S_IFMT { + case syscall.S_IFSOCK: + return v1.HostPathSocket, nil + case syscall.S_IFBLK: + return v1.HostPathBlockDev, nil + case syscall.S_IFCHR: + return v1.HostPathCharDev, nil + } + return "", fmt.Errorf("only recognise socket, block device and character device") +} diff --git a/pkg/volume/plugins.go b/pkg/volume/plugins.go index 60d6358d41..7ba0d3ec67 100644 --- a/pkg/volume/plugins.go +++ b/pkg/volume/plugins.go @@ -66,6 +66,8 @@ type VolumeOptions struct { CloudTags *map[string]string // Volume provisioning parameters from StorageClass Parameters map[string]string + // This flag helps identify whether kubelet is running in a container + Containerized bool } // VolumePlugin is an interface to volume plugins that can be used on a diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 26ce08ad7b..210b06c4e6 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -681,12 +681,39 @@ const ( ClaimLost PersistentVolumeClaimPhase = "Lost" ) +type HostPathType string + +const ( + // If nothing exists at the given path, an empty directory will be created there + // as needed with file mode 0755, having the same group and ownership with Kubelet. + HostPathDirectoryOrCreate HostPathType = "DirectoryOrCreate" + // A directory must exist at the given path + HostPathDirectory HostPathType = "Directory" + // If nothing exists at the given path, an empty file will be created there + // as needed with file mode 0644, having the same group and ownership with Kubelet. + HostPathFileOrCreate HostPathType = "FileOrCreate" + // A file must exist at the given path + HostPathFile HostPathType = "File" + // A UNIX socket must exist at the given path + HostPathSocket HostPathType = "Socket" + // A character device must exist at the given path + HostPathCharDev HostPathType = "CharDevice" + // A block device must exist at the given path + HostPathBlockDev HostPathType = "BlockDevice" +) + // Represents a host path mapped into a pod. // Host path volumes do not support ownership management or SELinux relabeling. type HostPathVolumeSource struct { // Path of the directory on the host. + // If the path is a symlink, it will follow the link to the real path. // More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath Path string `json:"path" protobuf:"bytes,1,opt,name=path"` + // Type for HostPath Volume + // Defaults to DirectoryOrCreate + // More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + // +optional + Type *HostPathType `json:"type,omitempty" protobuf:"bytes,2,opt,name=type"` } // Represents an empty directory for a pod.