From 1a45e37d17de3a6de690dba98675c16ccb93a892 Mon Sep 17 00:00:00 2001 From: Chris Alfonso Date: Tue, 10 Feb 2015 14:00:11 -0500 Subject: [PATCH] NFSMount storage plugin for kubelet. * If you want to test this out when an actual NFS export a good place to start is by running the NFS server in a container: docker run -d --name nfs --privileged cpuguy83/nfs-server /tmp More detail can be found here: https://github.com/cpuguy83/docker-nfs-server --- cmd/kubelet/app/plugins.go | 2 + examples/nfs/test.yaml | 21 ++ .../storage/persistentvolume-nfs-example.yaml | 9 + pkg/api/testing/fuzzer.go | 2 +- pkg/api/types.go | 15 ++ pkg/api/v1beta1/conversion.go | 6 + pkg/api/v1beta1/types.go | 15 ++ pkg/api/v1beta2/conversion.go | 6 + pkg/api/v1beta2/types.go | 15 ++ pkg/api/v1beta3/types.go | 15 ++ pkg/api/validation/validation.go | 19 ++ pkg/kubelet/volume/nfs/nfs.go | 182 ++++++++++++++++++ pkg/kubelet/volume/nfs/nfs_mount.go | 71 +++++++ pkg/kubelet/volume/nfs/nfs_test.go | 147 ++++++++++++++ 14 files changed, 524 insertions(+), 1 deletion(-) create mode 100644 examples/nfs/test.yaml create mode 100644 examples/storage/persistentvolume-nfs-example.yaml create mode 100644 pkg/kubelet/volume/nfs/nfs.go create mode 100644 pkg/kubelet/volume/nfs/nfs_mount.go create mode 100644 pkg/kubelet/volume/nfs/nfs_test.go diff --git a/cmd/kubelet/app/plugins.go b/cmd/kubelet/app/plugins.go index 130e72f153d..39055d6b0a6 100644 --- a/cmd/kubelet/app/plugins.go +++ b/cmd/kubelet/app/plugins.go @@ -26,6 +26,7 @@ import ( "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/gce_pd" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/git_repo" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/host_path" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/nfs" "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume/secret" ) @@ -41,6 +42,7 @@ func ProbeVolumePlugins() []volume.Plugin { allPlugins = append(allPlugins, git_repo.ProbeVolumePlugins()...) allPlugins = append(allPlugins, host_path.ProbeVolumePlugins()...) allPlugins = append(allPlugins, secret.ProbeVolumePlugins()...) + allPlugins = append(allPlugins, nfs.ProbeVolumePlugins()...) return allPlugins } diff --git a/examples/nfs/test.yaml b/examples/nfs/test.yaml new file mode 100644 index 00000000000..810700baf48 --- /dev/null +++ b/examples/nfs/test.yaml @@ -0,0 +1,21 @@ +apiVersion: v1beta1 +desiredState: + manifest: + containers: + - name: testpd + image: dockerfile/nginx + volumeMounts: + # name must match the volume name below + - name: nfs + mountPath: "/var/www/html/mount-test" + id: nfspd + version: v1beta1 + volumes: + - name: nfs + source: + nfs: + server: "172.17.0.2" + path: "/tmp" + readOnly: false +id: nfspd +kind: Pod diff --git a/examples/storage/persistentvolume-nfs-example.yaml b/examples/storage/persistentvolume-nfs-example.yaml new file mode 100644 index 00000000000..50a99762b1a --- /dev/null +++ b/examples/storage/persistentvolume-nfs-example.yaml @@ -0,0 +1,9 @@ +id: pv0003 +kind: PersistentVolume +apiVersion: v1beta1 +spec: + source: + nfsMount: + server: "172.17.0.2" + path: "/tmp" + readOnly: false diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 7dd393b9a40..ea2993655f8 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -167,7 +167,7 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { func(vs *api.VolumeSource, c fuzz.Continue) { // Exactly one of the fields should be set. //FIXME: the fuzz can still end up nil. What if fuzz allowed me to say that? - fuzzOneOf(c, &vs.HostPath, &vs.EmptyDir, &vs.GCEPersistentDisk, &vs.GitRepo, &vs.Secret) + fuzzOneOf(c, &vs.HostPath, &vs.EmptyDir, &vs.GCEPersistentDisk, &vs.GitRepo, &vs.Secret, &vs.NFS) }, func(d *api.DNSPolicy, c fuzz.Continue) { policies := []api.DNSPolicy{api.DNSClusterFirst, api.DNSDefault} diff --git a/pkg/api/types.go b/pkg/api/types.go index 3a463c492e6..1c89290011c 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -180,6 +180,8 @@ type VolumeSource struct { GitRepo *GitRepoVolumeSource `json:"gitRepo"` // Secret represents a secret that should populate this volume. Secret *SecretVolumeSource `json:"secret"` + // NFS represents an NFS mount on the host that shares a pod's lifetime + NFS *NFSVolumeSource `json:"nfs"` } // HostPathVolumeSource represents a host directory mapped into a pod. @@ -256,6 +258,19 @@ type SecretVolumeSource struct { Target ObjectReference `json:"target"` } +// NFSVolumeSource represents an NFS Mount that lasts the lifetime of a pod +type NFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `json:"server"` + + // Path is the exported NFS share + Path string `json:"path"` + + // Optional: Defaults to false (read/write). ReadOnly here will force + // the NFS export to be mounted with read-only permissions + ReadOnly bool `json:"readOnly,omitempty"` +} + // ContainerPort represents a network port in a single container type ContainerPort struct { // Optional: If specified, this must be a DNS_LABEL. Each named port diff --git a/pkg/api/v1beta1/conversion.go b/pkg/api/v1beta1/conversion.go index 48c6a2d1786..5a0437960d2 100644 --- a/pkg/api/v1beta1/conversion.go +++ b/pkg/api/v1beta1/conversion.go @@ -1054,6 +1054,9 @@ func init() { if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil { return err } + if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { + return err + } return nil }, func(in *VolumeSource, out *newer.VolumeSource, s conversion.Scope) error { @@ -1072,6 +1075,9 @@ func init() { if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil { return err } + if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { + return err + } return nil }, diff --git a/pkg/api/v1beta1/types.go b/pkg/api/v1beta1/types.go index 44161aa0b0f..2f361f08c2b 100644 --- a/pkg/api/v1beta1/types.go +++ b/pkg/api/v1beta1/types.go @@ -105,6 +105,8 @@ type VolumeSource struct { GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` // Secret represents a secret to populate the volume with Secret *SecretVolumeSource `json:"secret" description:"secret to populate volume with"` + // NFS represents an NFS mount on the host that shares a pod's lifetime + NFS *NFSVolumeSource `json:"nfs" description:"NFS volume that will be mounted in the host machine "` } // HostPathVolumeSource represents bare host directory volume. @@ -1154,6 +1156,19 @@ type ResourceQuotaList struct { Items []ResourceQuota `json:"items" description:"items is a list of ResourceQuota objects"` } +// NFSVolumeSource represents an NFS Mount that lasts the lifetime of a pod +type NFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `json:"server" description:"the hostname or IP address of the NFS server"` + + // Path is the exported NFS share + Path string `json:"path" description:"the path that is exported by the NFS server"` + + // Optional: Defaults to false (read/write). ReadOnly here will force + // the NFS export to be mounted as read-only permissions + ReadOnly bool `json:"readOnly,omitempty" description:"forces the NFS export to be mounted with read-only permissions"` +} + // Secret holds secret data of a certain type. The total bytes of the values in // the Data field must be less than MaxSecretSize bytes. type Secret struct { diff --git a/pkg/api/v1beta2/conversion.go b/pkg/api/v1beta2/conversion.go index a262c166a4f..a315cefacd6 100644 --- a/pkg/api/v1beta2/conversion.go +++ b/pkg/api/v1beta2/conversion.go @@ -982,6 +982,9 @@ func init() { if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil { return err } + if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { + return err + } return nil }, func(in *VolumeSource, out *newer.VolumeSource, s conversion.Scope) error { @@ -1000,6 +1003,9 @@ func init() { if err := s.Convert(&in.Secret, &out.Secret, 0); err != nil { return err } + if err := s.Convert(&in.NFS, &out.NFS, 0); err != nil { + return err + } return nil }, diff --git a/pkg/api/v1beta2/types.go b/pkg/api/v1beta2/types.go index 397e036618a..e543d26fed8 100644 --- a/pkg/api/v1beta2/types.go +++ b/pkg/api/v1beta2/types.go @@ -78,6 +78,8 @@ type VolumeSource struct { GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` // Secret is a secret to populate the volume with Secret *SecretVolumeSource `json:"secret" description:"secret to populate volume"` + // NFS represents an NFS mount on the host that shares a pod's lifetime + NFS *NFSVolumeSource `json:"nfs" description:"NFS volume that will be mounted in the host machine"` } // HostPathVolumeSource represents bare host directory volume. @@ -1215,6 +1217,19 @@ type ResourceQuotaList struct { Items []ResourceQuota `json:"items" description:"items is a list of ResourceQuota objects"` } +// NFSVolumeSource represents an NFS mount that lasts the lifetime of a pod +type NFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `json:"server" description:"the hostname or IP address of the NFS server"` + + // Path is the exported NFS share + Path string `json:"path" description:"the path that is exported by the NFS server"` + + // Optional: Defaults to false (read/write). ReadOnly here will force + // the NFS export to be mounted with read-only permissions + ReadOnly bool `json:"readOnly,omitempty" description:"forces the NFS export to be mounted with read-only permissions"` +} + // Secret holds secret data of a certain type. The total bytes of the values in // the Data field must be less than MaxSecretSize bytes. // diff --git a/pkg/api/v1beta3/types.go b/pkg/api/v1beta3/types.go index 87f5288a2e5..198e6b7d669 100644 --- a/pkg/api/v1beta3/types.go +++ b/pkg/api/v1beta3/types.go @@ -199,6 +199,8 @@ type VolumeSource struct { GitRepo *GitRepoVolumeSource `json:"gitRepo" description:"git repository at a particular revision"` // Secret represents a secret that should populate this volume. Secret *SecretVolumeSource `json:"secret" description:"secret to populate volume"` + // NFS represents an NFS mount on the host that shares a pod's lifetime + NFS *NFSVolumeSource `json:"nfs" description:"NFS volume that will be mounted in the host machine"` } // HostPathVolumeSource represents bare host directory volume. @@ -266,6 +268,19 @@ type SecretVolumeSource struct { Target ObjectReference `json:"target" description:"target is a reference to a secret"` } +// NFSVolumeSource represents an NFS mount that lasts the lifetime of a pod +type NFSVolumeSource struct { + // Server is the hostname or IP address of the NFS server + Server string `json:"server" description:"the hostname or IP address of the NFS server"` + + // Path is the exported NFS share + Path string `json:"path" description:"the path that is exported by the NFS server"` + + // Optional: Defaults to false (read/write). ReadOnly here will force + // the NFS export to be mounted with read-only permissions + ReadOnly bool `json:"readOnly,omitempty" description:"forces the NFS export to be mounted with read-only permissions"` +} + // ContainerPort represents a network port in a single container. type ContainerPort struct { // Optional: If specified, this must be a DNS_LABEL. Each named port diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 63d9ef5fffc..4502005917a 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -18,6 +18,7 @@ package validation import ( "fmt" + "path" "strings" "github.com/GoogleCloudPlatform/kubernetes/pkg/api" @@ -290,6 +291,10 @@ func validateSource(source *api.VolumeSource) errs.ValidationErrorList { numVolumes++ allErrs = append(allErrs, validateSecretVolumeSource(source.Secret).Prefix("secret")...) } + if source.NFS != nil { + numVolumes++ + allErrs = append(allErrs, validateNFS(source.NFS).Prefix("nfs")...) + } if numVolumes != 1 { allErrs = append(allErrs, errs.NewFieldInvalid("", source, "exactly 1 volume type is required")) } @@ -340,6 +345,20 @@ func validateSecretVolumeSource(secretSource *api.SecretVolumeSource) errs.Valid return allErrs } +func validateNFS(nfs *api.NFSVolumeSource) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if nfs.Server == "" { + allErrs = append(allErrs, errs.NewFieldRequired("server")) + } + if nfs.Path == "" { + allErrs = append(allErrs, errs.NewFieldRequired("path")) + } + if !path.IsAbs(nfs.Path) { + allErrs = append(allErrs, errs.NewFieldInvalid("path", nfs.Path, "must be an absolute path")) + } + return allErrs +} + var supportedPortProtocols = util.NewStringSet(string(api.ProtocolTCP), string(api.ProtocolUDP)) func validatePorts(ports []api.ContainerPort) errs.ValidationErrorList { diff --git a/pkg/kubelet/volume/nfs/nfs.go b/pkg/kubelet/volume/nfs/nfs.go new file mode 100644 index 00000000000..b77ef7c2ab9 --- /dev/null +++ b/pkg/kubelet/volume/nfs/nfs.go @@ -0,0 +1,182 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 nfs + +import ( + "os" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" + "github.com/golang/glog" +) + +// This is the primary entrypoint for volume plugins. +func ProbeVolumePlugins() []volume.Plugin { + return []volume.Plugin{&nfsPlugin{nil, newNFSMounter()}} +} + +type nfsPlugin struct { + host volume.Host + mounter nfsMountInterface +} + +var _ volume.Plugin = &nfsPlugin{} + +const ( + nfsPluginName = "kubernetes.io/nfs" +) + +func (plugin *nfsPlugin) Init(host volume.Host) { + plugin.host = host +} + +func (plugin *nfsPlugin) Name() string { + return nfsPluginName +} + +func (plugin *nfsPlugin) CanSupport(spec *api.Volume) bool { + if spec.VolumeSource.NFS != nil { + return true + } + return false +} + +func (plugin *nfsPlugin) NewBuilder(spec *api.Volume, podRef *api.ObjectReference) (volume.Builder, error) { + return plugin.newBuilderInternal(spec, podRef, plugin.mounter) +} + +func (plugin *nfsPlugin) newBuilderInternal(spec *api.Volume, podRef *api.ObjectReference, mounter nfsMountInterface) (volume.Builder, error) { + return &nfs{ + volName: spec.Name, + server: spec.VolumeSource.NFS.Server, + exportPath: spec.VolumeSource.NFS.Path, + readOnly: spec.VolumeSource.NFS.ReadOnly, + mounter: mounter, + podRef: podRef, + plugin: plugin, + }, nil +} + +func (plugin *nfsPlugin) NewCleaner(volName string, podUID types.UID) (volume.Cleaner, error) { + return plugin.newCleanerInternal(volName, podUID, plugin.mounter) +} + +func (plugin *nfsPlugin) newCleanerInternal(volName string, podUID types.UID, mounter nfsMountInterface) (volume.Cleaner, error) { + return &nfs{ + volName: volName, + server: "", + exportPath: "", + readOnly: false, + mounter: mounter, + podRef: &api.ObjectReference{UID: podUID}, + plugin: plugin, + }, nil +} + +// NFS volumes represent a bare host file or directory mount of an NFS export. +type nfs struct { + volName string + podRef *api.ObjectReference + server string + exportPath string + readOnly bool + mounter nfsMountInterface + plugin *nfsPlugin +} + +// SetUp attaches the disk and bind mounts to the volume path. +func (nfsVolume *nfs) SetUp() error { + return nfsVolume.SetUpAt(nfsVolume.GetPath()) +} + +func (nfsVolume *nfs) SetUpAt(dir string) error { + mountpoint, err := nfsVolume.mounter.IsMountPoint(dir) + glog.V(4).Infof("NFS mount set up: %s %v %v", dir, mountpoint, err) + if err != nil && !os.IsNotExist(err) { + return err + } + if mountpoint { + return nil + } + exportDir := nfsVolume.exportPath + os.MkdirAll(dir, 0750) + err = nfsVolume.mounter.Mount(nfsVolume.server, exportDir, dir, nfsVolume.readOnly) + if err != nil { + mountpoint, mntErr := nfsVolume.mounter.IsMountPoint(dir) + if mntErr != nil { + glog.Errorf("IsMountpoint check failed: %v", mntErr) + return err + } + if mountpoint { + if mntErr = nfsVolume.mounter.Unmount(dir); mntErr != nil { + glog.Errorf("Failed to unmount: %v", mntErr) + return err + } + mountpoint, mntErr := nfsVolume.mounter.IsMountPoint(dir) + if mntErr != nil { + glog.Errorf("IsMountpoint check failed: %v", mntErr) + return err + } + if mountpoint { + // This is very odd, we don't expect it. We'll try again next sync loop. + glog.Errorf("%s is still mounted, despite call to unmount(). Will try again next sync loop.", dir) + return err + } + } + os.Remove(dir) + return err + } + return nil +} + +func (nfsVolume *nfs) GetPath() string { + name := nfsPluginName + return nfsVolume.plugin.host.GetPodVolumeDir(nfsVolume.podRef.UID, volume.EscapePluginName(name), nfsVolume.volName) +} + +func (nfsVolume *nfs) TearDown() error { + return nfsVolume.TearDownAt(nfsVolume.GetPath()) +} + +func (nfsVolume *nfs) TearDownAt(dir string) error { + mountpoint, err := nfsVolume.mounter.IsMountPoint(dir) + if err != nil { + glog.Errorf("Error checking IsMountPoint: %v", err) + return err + } + if !mountpoint { + return os.Remove(dir) + } + + if err := nfsVolume.mounter.Unmount(dir); err != nil { + glog.Errorf("Unmounting failed: %v", err) + return err + } + mountpoint, mntErr := nfsVolume.mounter.IsMountPoint(dir) + if mntErr != nil { + glog.Errorf("IsMountpoint check failed: %v", mntErr) + return mntErr + } + if !mountpoint { + if err := os.Remove(dir); err != nil { + return err + } + } + + return nil +} diff --git a/pkg/kubelet/volume/nfs/nfs_mount.go b/pkg/kubelet/volume/nfs/nfs_mount.go new file mode 100644 index 00000000000..cb8a8e35e61 --- /dev/null +++ b/pkg/kubelet/volume/nfs/nfs_mount.go @@ -0,0 +1,71 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 nfs + +import ( + "os/exec" + "syscall" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" + "github.com/golang/glog" +) + +type nfsMountInterface interface { + // Mount takes an NFS host ip or hostname, a source directory (the exported directory), a target directory where the source directory will be mounted, and a boolean readOnly + Mount(server string, source string, target string, readOnly bool) error + + // Umount wraps syscall.Mount(). + Unmount(target string) error + + List() ([]mount.MountPoint, error) + + IsMountPoint(dir string) (bool, error) +} + +// newNFSMounter returns an nfsMountInterface for the current system. +func newNFSMounter() nfsMountInterface { + return &nfsMounter{} +} + +type nfsMounter struct{} + +func (mounter *nfsMounter) Mount(server string, exportDir string, mountDir string, readOnly bool) error { + mountOptions := "rw" + if readOnly { + mountOptions = "ro" + } + mountArgs := []string{"-t", "nfs", server + ":" + exportDir, mountDir, "-o", mountOptions} + command := exec.Command("mount", mountArgs...) + output, errs := command.CombinedOutput() + if errs != nil { + glog.Errorf("NFS mounting failed: %v\n\tMount args are: %v\n\texportDir is: %v\n\tmountDir is: %v\n\tserver is: %v\n\tmount output is: %v", errs, mountArgs, exportDir, mountDir, server, string(output)) + return errs + } + return nil +} + +func (mounter *nfsMounter) Unmount(target string) error { + return syscall.Unmount(target, 0) +} + +func (mounter *nfsMounter) List() ([]mount.MountPoint, error) { + return nil, nil +} + +func (mounter *nfsMounter) IsMountPoint(dir string) (bool, error) { + return mount.IsMountPoint(dir) +} diff --git a/pkg/kubelet/volume/nfs/nfs_test.go b/pkg/kubelet/volume/nfs/nfs_test.go new file mode 100644 index 00000000000..ed5765a4c86 --- /dev/null +++ b/pkg/kubelet/volume/nfs/nfs_test.go @@ -0,0 +1,147 @@ +/* +Copyright 2014 Google Inc. All rights reserved. + +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 nfs + +import ( + "fmt" + "os" + "testing" + + "github.com/GoogleCloudPlatform/kubernetes/pkg/api" + "github.com/GoogleCloudPlatform/kubernetes/pkg/kubelet/volume" + "github.com/GoogleCloudPlatform/kubernetes/pkg/types" + "github.com/GoogleCloudPlatform/kubernetes/pkg/util/mount" +) + +func TestCanSupport(t *testing.T) { + plugMgr := volume.PluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeHost("fake", nil, nil)) + plug, err := plugMgr.FindPluginByName("kubernetes.io/nfs") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + if plug.Name() != "kubernetes.io/nfs" { + t.Errorf("Wrong name: %s", plug.Name()) + } + if !plug.CanSupport(&api.Volume{VolumeSource: api.VolumeSource{NFS: &api.NFSVolumeSource{}}}) { + t.Errorf("Expected true") + } + if plug.CanSupport(&api.Volume{VolumeSource: api.VolumeSource{}}) { + t.Errorf("Expected false") + } +} + +type fakeNFSMounter struct { + FakeMounter mount.FakeMounter +} + +func (fake *fakeNFSMounter) Mount(server string, source string, target string, readOnly bool) error { + flags := 0 + if readOnly { + flags |= mount.FlagReadOnly + } + fake.FakeMounter.MountPoints = append(fake.FakeMounter.MountPoints, mount.MountPoint{Device: server, Path: target, Type: "nfs", Opts: nil, Freq: 0, Pass: 0}) + return fake.FakeMounter.Mount(fmt.Sprintf("%s:%s", server, source), target, "nfs", 0, "") +} + +func (fake *fakeNFSMounter) Unmount(target string) error { + fake.FakeMounter.MountPoints = []mount.MountPoint{} + return fake.FakeMounter.Unmount(target, 0) +} + +func (fake *fakeNFSMounter) List() ([]mount.MountPoint, error) { + list, _ := fake.FakeMounter.List() + return list, nil +} + +func (fake *fakeNFSMounter) IsMountPoint(dir string) (bool, error) { + list, _ := fake.FakeMounter.List() + isMount := len(list) > 0 + return isMount, nil +} + +func TestPlugin(t *testing.T) { + plugMgr := volume.PluginMgr{} + plugMgr.InitPlugins(ProbeVolumePlugins(), volume.NewFakeHost("/tmp/fake", nil, nil)) + plug, err := plugMgr.FindPluginByName("kubernetes.io/nfs") + if err != nil { + t.Errorf("Can't find the plugin by name") + } + spec := &api.Volume{ + Name: "vol1", + VolumeSource: api.VolumeSource{NFS: &api.NFSVolumeSource{"localhost", "/tmp", false}}, + } + fake := &fakeNFSMounter{} + builder, err := plug.(*nfsPlugin).newBuilderInternal(spec, &api.ObjectReference{UID: types.UID("poduid")}, fake) + volumePath := builder.GetPath() + if err != nil { + t.Errorf("Failed to make a new Builder: %v", err) + } + if builder == nil { + t.Errorf("Got a nil Builder: %v") + } + path := builder.GetPath() + if path != "/tmp/fake/pods/poduid/volumes/kubernetes.io~nfs/vol1" { + t.Errorf("Got unexpected path: %s", path) + } + if err := builder.SetUp(); err != nil { + t.Errorf("Expected success, got: %v", err) + } + if _, err := os.Stat(volumePath); err != nil { + if os.IsNotExist(err) { + t.Errorf("SetUp() failed, volume path not created: %s", volumePath) + } else { + t.Errorf("SetUp() failed: %v", err) + } + } + if builder.(*nfs).readOnly { + t.Errorf("The volume source should not be read-only and it is.") + } + if len(fake.FakeMounter.Log) != 1 { + t.Errorf("Mount was not called exactly one time. It was called %d times.", len(fake.FakeMounter.Log)) + } else { + if fake.FakeMounter.Log[0].Action != mount.FakeActionMount { + t.Errorf("Unexpected mounter action: %#v", fake.FakeMounter.Log[0]) + } + } + fake.FakeMounter.ResetLog() + + cleaner, err := plug.(*nfsPlugin).newCleanerInternal("vol1", types.UID("poduid"), fake) + if err != nil { + t.Errorf("Failed to make a new Cleaner: %v", err) + } + if cleaner == nil { + t.Errorf("Got a nil Cleaner: %v") + } + if err := cleaner.TearDown(); err != nil { + t.Errorf("Expected success, got: %v", err) + } + if _, err := os.Stat(volumePath); err == nil { + t.Errorf("TearDown() failed, volume path still exists: %s", volumePath) + } else if !os.IsNotExist(err) { + t.Errorf("SetUp() failed: %v", err) + } + if len(fake.FakeMounter.Log) != 1 { + t.Errorf("Unmount was not called exactly one time. It was called %d times.", len(fake.FakeMounter.Log)) + } else { + if fake.FakeMounter.Log[0].Action != mount.FakeActionUnmount { + t.Errorf("Unexpected mounter action: %#v", fake.FakeMounter.Log[0]) + } + } + + fake.FakeMounter.ResetLog() +}