Merge pull request #53440 from jsafrane/mount-container4-10-03

Automatic merge from submit-queue (batch tested with PRs 54005, 55127, 53850, 55486, 53440). If you want to cherry-pick this change to another branch, please follow the instructions <a href="https://github.com/kubernetes/community/blob/master/contributors/devel/cherry-picks.md">here</a>.

Containerized mount utilities

This is implementation of https://github.com/kubernetes/community/pull/589

@tallclair @vishh @dchen1107 PTAL
@kubernetes/sig-node-pr-reviews 

**Release note**:
```release-note
Kubelet supports running mount utilities and final mount in a container instead running them on the host.
```
pull/6/head
Kubernetes Submit Queue 2017-11-13 16:45:33 -08:00 committed by GitHub
commit 2d64ce5e8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 709 additions and 6 deletions

View File

@ -169,6 +169,12 @@ const (
//
// Enable nodes to exclude themselves from service load balancers
ServiceNodeExclusion utilfeature.Feature = "ServiceNodeExclusion"
// owner: @jsafrane
// alpha: v1.9
//
// Enable running mount utilities in containers.
MountContainers utilfeature.Feature = "MountContainers"
)
func init() {
@ -201,6 +207,7 @@ var defaultKubernetesFeatureGates = map[utilfeature.Feature]utilfeature.FeatureS
ExpandPersistentVolumes: {Default: false, PreRelease: utilfeature.Alpha},
CPUManager: {Default: false, PreRelease: utilfeature.Alpha},
ServiceNodeExclusion: {Default: false, PreRelease: utilfeature.Alpha},
MountContainers: {Default: false, PreRelease: utilfeature.Alpha},
// inherited features from generic apiserver, relisted here to get a conflict if it is changed
// unintentionally on either side:

View File

@ -64,6 +64,7 @@ go_library(
"//pkg/kubelet/kuberuntime:go_default_library",
"//pkg/kubelet/lifecycle:go_default_library",
"//pkg/kubelet/metrics:go_default_library",
"//pkg/kubelet/mountpod:go_default_library",
"//pkg/kubelet/network:go_default_library",
"//pkg/kubelet/pleg:go_default_library",
"//pkg/kubelet/pod:go_default_library",
@ -269,6 +270,7 @@ filegroup(
"//pkg/kubelet/leaky:all-srcs",
"//pkg/kubelet/lifecycle:all-srcs",
"//pkg/kubelet/metrics:all-srcs",
"//pkg/kubelet/mountpod:all-srcs",
"//pkg/kubelet/network:all-srcs",
"//pkg/kubelet/pleg:all-srcs",
"//pkg/kubelet/pod:all-srcs",

View File

@ -17,8 +17,9 @@ limitations under the License.
package config
const (
DefaultKubeletPodsDirName = "pods"
DefaultKubeletVolumesDirName = "volumes"
DefaultKubeletPluginsDirName = "plugins"
DefaultKubeletContainersDirName = "containers"
DefaultKubeletPodsDirName = "pods"
DefaultKubeletVolumesDirName = "volumes"
DefaultKubeletPluginsDirName = "plugins"
DefaultKubeletContainersDirName = "containers"
DefaultKubeletPluginContainersDirName = "plugin-containers"
)

View File

@ -0,0 +1,45 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = ["mount_pod.go"],
importpath = "k8s.io/kubernetes/pkg/kubelet/mountpod",
visibility = ["//visibility:public"],
deps = [
"//pkg/kubelet/config:go_default_library",
"//pkg/kubelet/pod:go_default_library",
"//pkg/util/strings:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
],
)
go_test(
name = "go_default_test",
srcs = ["mount_pod_test.go"],
importpath = "k8s.io/kubernetes/pkg/kubelet/mountpod",
library = ":go_default_library",
deps = [
"//pkg/kubelet/configmap:go_default_library",
"//pkg/kubelet/pod:go_default_library",
"//pkg/kubelet/pod/testing:go_default_library",
"//pkg/kubelet/secret:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/api/core/v1:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library",
"//vendor/k8s.io/client-go/util/testing:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)

View File

@ -0,0 +1,120 @@
/*
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 mountpod
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path"
"k8s.io/api/core/v1"
"k8s.io/kubernetes/pkg/kubelet/config"
kubepod "k8s.io/kubernetes/pkg/kubelet/pod"
"k8s.io/kubernetes/pkg/util/strings"
)
// Manager is an interface that tracks pods with mount utilities for individual
// volume plugins.
type Manager interface {
GetMountPod(pluginName string) (pod *v1.Pod, container string, err error)
}
// basicManager is simple implementation of Manager. Pods with mount utilities
// are registered by placing a JSON file into
// /var/lib/kubelet/plugin-containers/<plugin name>.json and this manager just
// finds them there.
type basicManager struct {
registrationDirectory string
podManager kubepod.Manager
}
// volumePluginRegistration specified format of the json files placed in
// /var/lib/kubelet/plugin-containers/
type volumePluginRegistration struct {
PodName string `json:"podName"`
PodNamespace string `json:"podNamespace"`
PodUID string `json:"podUID"`
ContainerName string `json:"containerName"`
}
// NewManager returns a new mount pod manager.
func NewManager(rootDirectory string, podManager kubepod.Manager) (Manager, error) {
regPath := path.Join(rootDirectory, config.DefaultKubeletPluginContainersDirName)
// Create the directory on startup
os.MkdirAll(regPath, 0700)
return &basicManager{
registrationDirectory: regPath,
podManager: podManager,
}, nil
}
func (m *basicManager) getVolumePluginRegistrationPath(pluginName string) string {
// sanitize plugin name so it does not escape directory
safePluginName := strings.EscapePluginName(pluginName) + ".json"
return path.Join(m.registrationDirectory, safePluginName)
}
func (m *basicManager) GetMountPod(pluginName string) (pod *v1.Pod, containerName string, err error) {
// Read /var/lib/kubelet/plugin-containers/<plugin name>.json
regPath := m.getVolumePluginRegistrationPath(pluginName)
regBytes, err := ioutil.ReadFile(regPath)
if err != nil {
if os.IsNotExist(err) {
// No pod is registered for this plugin
return nil, "", nil
}
return nil, "", fmt.Errorf("cannot read %s: %v", regPath, err)
}
// Parse json
var reg volumePluginRegistration
if err := json.Unmarshal(regBytes, &reg); err != nil {
return nil, "", fmt.Errorf("unable to parse %s: %s", regPath, err)
}
if len(reg.ContainerName) == 0 {
return nil, "", fmt.Errorf("unable to parse %s: \"containerName\" is not set", regPath)
}
if len(reg.PodUID) == 0 {
return nil, "", fmt.Errorf("unable to parse %s: \"podUID\" is not set", regPath)
}
if len(reg.PodNamespace) == 0 {
return nil, "", fmt.Errorf("unable to parse %s: \"podNamespace\" is not set", regPath)
}
if len(reg.PodName) == 0 {
return nil, "", fmt.Errorf("unable to parse %s: \"podName\" is not set", regPath)
}
pod, ok := m.podManager.GetPodByName(reg.PodNamespace, reg.PodName)
if !ok {
return nil, "", fmt.Errorf("unable to process %s: pod %s/%s not found", regPath, reg.PodNamespace, reg.PodName)
}
if string(pod.UID) != reg.PodUID {
return nil, "", fmt.Errorf("unable to process %s: pod %s/%s has unexpected UID", regPath, reg.PodNamespace, reg.PodName)
}
// make sure that reg.ContainerName exists in the pod
for i := range pod.Spec.Containers {
if pod.Spec.Containers[i].Name == reg.ContainerName {
return pod, reg.ContainerName, nil
}
}
return nil, "", fmt.Errorf("unable to process %s: pod %s/%s has no container named %q", regPath, reg.PodNamespace, reg.PodName, reg.ContainerName)
}

View File

@ -0,0 +1,160 @@
/*
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 mountpod
import (
"io/ioutil"
"os"
"path"
"testing"
"github.com/golang/glog"
"k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
utiltesting "k8s.io/client-go/util/testing"
"k8s.io/kubernetes/pkg/kubelet/configmap"
kubepod "k8s.io/kubernetes/pkg/kubelet/pod"
podtest "k8s.io/kubernetes/pkg/kubelet/pod/testing"
"k8s.io/kubernetes/pkg/kubelet/secret"
)
func TestGetVolumeExec(t *testing.T) {
// prepare PodManager
pods := []*v1.Pod{
{
ObjectMeta: metav1.ObjectMeta{
UID: "12345678",
Name: "foo",
Namespace: "bar",
},
Spec: v1.PodSpec{
Containers: []v1.Container{
{Name: "baz"},
},
},
},
}
fakeSecretManager := secret.NewFakeManager()
fakeConfigMapManager := configmap.NewFakeManager()
podManager := kubepod.NewBasicPodManager(
podtest.NewFakeMirrorClient(), fakeSecretManager, fakeConfigMapManager)
podManager.SetPods(pods)
// Prepare fake /var/lib/kubelet
basePath, err := utiltesting.MkTmpdir("kubelet")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(basePath)
regPath := path.Join(basePath, "plugin-containers")
mgr, err := NewManager(basePath, podManager)
if err != nil {
t.Fatal(err)
}
tests := []struct {
name string
json string
expectError bool
}{
{
"invalid json",
"{{{}",
true,
},
{
"missing json",
"", // this means no json file should be created
false,
},
{
"missing podNamespace",
`{"podName": "foo", "podUID": "87654321", "containerName": "baz"}`,
true,
},
{
"missing podName",
`{"podNamespace": "bar", "podUID": "87654321", "containerName": "baz"}`,
true,
},
{
"missing containerName",
`{"podNamespace": "bar", "podName": "foo", "podUID": "87654321"}`,
true,
},
{
"missing podUID",
`{"podNamespace": "bar", "podName": "foo", "containerName": "baz"}`,
true,
},
{
"missing pod",
`{"podNamespace": "bar", "podName": "non-existing-pod", "podUID": "12345678", "containerName": "baz"}`,
true,
},
{
"invalid uid",
`{"podNamespace": "bar", "podName": "foo", "podUID": "87654321", "containerName": "baz"}`,
true,
},
{
"invalid container",
`{"podNamespace": "bar", "podName": "foo", "podUID": "12345678", "containerName": "invalid"}`,
true,
},
{
"valid pod",
`{"podNamespace": "bar", "podName": "foo", "podUID": "12345678", "containerName": "baz"}`,
false,
},
}
for _, test := range tests {
p := path.Join(regPath, "kubernetes.io~glusterfs.json")
if len(test.json) > 0 {
if err := ioutil.WriteFile(p, []byte(test.json), 0600); err != nil {
t.Errorf("test %q: error writing %s: %v", test.name, p, err)
continue
}
} else {
// "" means no JSON file
os.Remove(p)
}
pod, container, err := mgr.GetMountPod("kubernetes.io/glusterfs")
if err != nil {
glog.V(5).Infof("test %q returned error %s", test.name, err)
}
if err == nil && test.expectError {
t.Errorf("test %q: expected error, got none", test.name)
}
if err != nil && !test.expectError {
t.Errorf("test %q: unexpected error: %v", test.name, err)
}
if err == nil {
// Pod must be returned when the json file was not empty
if pod == nil && len(test.json) != 0 {
t.Errorf("test %q: expected exec, got nil", test.name)
}
// Both pod and container must be returned
if pod != nil && len(container) == 0 {
t.Errorf("test %q: expected container name, got %q", test.name, container)
}
}
}
}

View File

@ -20,11 +20,17 @@ import (
"fmt"
"net"
"github.com/golang/glog"
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
utilfeature "k8s.io/apiserver/pkg/util/feature"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/kubernetes/pkg/cloudprovider"
"k8s.io/kubernetes/pkg/features"
"k8s.io/kubernetes/pkg/kubelet/configmap"
"k8s.io/kubernetes/pkg/kubelet/container"
"k8s.io/kubernetes/pkg/kubelet/mountpod"
"k8s.io/kubernetes/pkg/kubelet/secret"
"k8s.io/kubernetes/pkg/util/io"
"k8s.io/kubernetes/pkg/util/mount"
@ -43,11 +49,17 @@ func NewInitializedVolumePluginMgr(
configMapManager configmap.Manager,
plugins []volume.VolumePlugin,
prober volume.DynamicPluginProber) (*volume.VolumePluginMgr, error) {
mountPodManager, err := mountpod.NewManager(kubelet.getRootDir(), kubelet.podManager)
if err != nil {
return nil, err
}
kvh := &kubeletVolumeHost{
kubelet: kubelet,
volumePluginMgr: volume.VolumePluginMgr{},
secretManager: secretManager,
configMapManager: configMapManager,
mountPodManager: mountPodManager,
}
if err := kvh.volumePluginMgr.InitPlugins(plugins, prober, kvh); err != nil {
@ -71,6 +83,7 @@ type kubeletVolumeHost struct {
volumePluginMgr volume.VolumePluginMgr
secretManager secret.Manager
configMapManager configmap.Manager
mountPodManager mountpod.Manager
}
func (kvh *kubeletVolumeHost) GetPodVolumeDir(podUID types.UID, pluginName string, volumeName string) string {
@ -119,7 +132,16 @@ func (kvh *kubeletVolumeHost) GetCloudProvider() cloudprovider.Interface {
}
func (kvh *kubeletVolumeHost) GetMounter(pluginName string) mount.Interface {
return kvh.kubelet.mounter
exec, err := kvh.getMountExec(pluginName)
if err != nil {
glog.V(2).Info("Error finding mount pod for plugin %s: %s", pluginName, err.Error())
// Use the default mounter
exec = nil
}
if exec == nil {
return kvh.kubelet.mounter
}
return mount.NewExecMounter(exec, kvh.kubelet.mounter)
}
func (kvh *kubeletVolumeHost) GetWriter() io.Writer {
@ -159,5 +181,56 @@ func (kvh *kubeletVolumeHost) GetNodeLabels() (map[string]string, error) {
}
func (kvh *kubeletVolumeHost) GetExec(pluginName string) mount.Exec {
return mount.NewOsExec()
exec, err := kvh.getMountExec(pluginName)
if err != nil {
glog.V(2).Info("Error finding mount pod for plugin %s: %s", pluginName, err.Error())
// Use the default exec
exec = nil
}
if exec == nil {
return mount.NewOsExec()
}
return exec
}
// getMountExec returns mount.Exec implementation that leads to pod with mount
// utilities. It returns nil,nil when there is no such pod and default mounter /
// os.Exec should be used.
func (kvh *kubeletVolumeHost) getMountExec(pluginName string) (mount.Exec, error) {
if !utilfeature.DefaultFeatureGate.Enabled(features.MountContainers) {
glog.V(5).Infof("using default mounter/exec for %s", pluginName)
return nil, nil
}
pod, container, err := kvh.mountPodManager.GetMountPod(pluginName)
if err != nil {
return nil, err
}
if pod == nil {
// Use default mounter/exec for this plugin
glog.V(5).Infof("using default mounter/exec for %s", pluginName)
return nil, nil
}
glog.V(5).Infof("using container %s/%s/%s to execute mount utilites for %s", pod.Namespace, pod.Name, container, pluginName)
return &containerExec{
pod: pod,
containerName: container,
kl: kvh.kubelet,
}, nil
}
// containerExec is implementation of mount.Exec that executes commands in given
// container in given pod.
type containerExec struct {
pod *v1.Pod
containerName string
kl *Kubelet
}
var _ mount.Exec = &containerExec{}
func (e *containerExec) Run(cmd string, args ...string) ([]byte, error) {
cmdline := append([]string{cmd}, args...)
glog.V(5).Infof("Exec mounter running in pod %s/%s/%s: %v", e.pod.Namespace, e.pod.Name, e.containerName, cmdline)
return e.kl.RunInContainer(container.GetPodFullName(e.pod), e.pod.UID, e.containerName, cmdline)
}

View File

@ -17,6 +17,7 @@ go_library(
"nsenter_mount_unsupported.go",
] + select({
"@io_bazel_rules_go//go/platform:linux_amd64": [
"exec_mount.go",
"mount_linux.go",
"nsenter_mount.go",
],
@ -46,6 +47,7 @@ go_test(
"safe_format_and_mount_test.go",
] + select({
"@io_bazel_rules_go//go/platform:linux_amd64": [
"exec_mount_test.go",
"mount_linux_test.go",
"nsenter_mount_test.go",
],

View File

@ -0,0 +1,140 @@
// +build linux
/*
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 mount
import (
"fmt"
"github.com/golang/glog"
)
// ExecMounter is a mounter that uses provided Exec interface to mount and
// unmount a filesystem. For all other calls it uses a wrapped mounter.
type execMounter struct {
wrappedMounter Interface
exec Exec
}
func NewExecMounter(exec Exec, wrapped Interface) Interface {
return &execMounter{
wrappedMounter: wrapped,
exec: exec,
}
}
// execMounter implements mount.Interface
var _ Interface = &execMounter{}
// Mount runs mount(8) using given exec interface.
func (m *execMounter) Mount(source string, target string, fstype string, options []string) error {
bind, bindRemountOpts := isBind(options)
if bind {
err := m.doExecMount(source, target, fstype, []string{"bind"})
if err != nil {
return err
}
return m.doExecMount(source, target, fstype, bindRemountOpts)
}
return m.doExecMount(source, target, fstype, options)
}
// doExecMount calls exec(mount <waht> <where>) using given exec interface.
func (m *execMounter) doExecMount(source, target, fstype string, options []string) error {
glog.V(5).Infof("Exec Mounting %s %s %s %v", source, target, fstype, options)
mountArgs := makeMountArgs(source, target, fstype, options)
output, err := m.exec.Run("mount", mountArgs...)
glog.V(5).Infof("Exec mounted %v: %v: %s", mountArgs, err, string(output))
if err != nil {
return fmt.Errorf("mount failed: %v\nMounting command: %s\nMounting arguments: %s %s %s %v\nOutput: %s\n",
err, "mount", source, target, fstype, options, string(output))
}
return err
}
// Unmount runs umount(8) using given exec interface.
func (m *execMounter) Unmount(target string) error {
outputBytes, err := m.exec.Run("umount", target)
if err == nil {
glog.V(5).Infof("Exec unmounted %s: %s", target, string(outputBytes))
} else {
glog.V(5).Infof("Failed to exec unmount %s: err: %q, umount output: %s", target, err, string(outputBytes))
}
return err
}
// List returns a list of all mounted filesystems.
func (m *execMounter) List() ([]MountPoint, error) {
return m.wrappedMounter.List()
}
// IsLikelyNotMountPoint determines whether a path is a mountpoint.
func (m *execMounter) IsLikelyNotMountPoint(file string) (bool, error) {
return m.wrappedMounter.IsLikelyNotMountPoint(file)
}
// DeviceOpened checks if block device in use by calling Open with O_EXCL flag.
// Returns true if open returns errno EBUSY, and false if errno is nil.
// Returns an error if errno is any error other than EBUSY.
// Returns with error if pathname is not a device.
func (m *execMounter) DeviceOpened(pathname string) (bool, error) {
return m.wrappedMounter.DeviceOpened(pathname)
}
// PathIsDevice uses FileInfo returned from os.Stat to check if path refers
// to a device.
func (m *execMounter) PathIsDevice(pathname string) (bool, error) {
return m.wrappedMounter.PathIsDevice(pathname)
}
//GetDeviceNameFromMount given a mount point, find the volume id from checking /proc/mounts
func (m *execMounter) GetDeviceNameFromMount(mountPath, pluginDir string) (string, error) {
return m.wrappedMounter.GetDeviceNameFromMount(mountPath, pluginDir)
}
func (m *execMounter) IsMountPointMatch(mp MountPoint, dir string) bool {
return m.wrappedMounter.IsMountPointMatch(mp, dir)
}
func (m *execMounter) IsNotMountPoint(dir string) (bool, error) {
return m.wrappedMounter.IsNotMountPoint(dir)
}
func (m *execMounter) MakeRShared(path string) error {
return m.wrappedMounter.MakeRShared(path)
}
func (m *execMounter) GetFileType(pathname string) (FileType, error) {
return m.wrappedMounter.GetFileType(pathname)
}
func (m *execMounter) MakeFile(pathname string) error {
return m.wrappedMounter.MakeFile(pathname)
}
func (m *execMounter) MakeDir(pathname string) error {
return m.wrappedMounter.MakeDir(pathname)
}
func (m *execMounter) ExistsPath(pathname string) bool {
return m.wrappedMounter.ExistsPath(pathname)
}

View File

@ -0,0 +1,153 @@
// +build linux
/*
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 mount
import (
"fmt"
"reflect"
"strings"
"testing"
)
var (
sourcePath = "/mnt/srv"
destinationPath = "/mnt/dst"
fsType = "xfs"
mountOptions = []string{"vers=1", "foo=bar"}
)
func TestMount(t *testing.T) {
exec := NewFakeExec(func(cmd string, args ...string) ([]byte, error) {
if cmd != "mount" {
t.Errorf("expected mount command, got %q", cmd)
}
// mount -t fstype -o options source target
expectedArgs := []string{"-t", fsType, "-o", strings.Join(mountOptions, ","), sourcePath, destinationPath}
if !reflect.DeepEqual(expectedArgs, args) {
t.Errorf("expected arguments %q, got %q", strings.Join(expectedArgs, " "), strings.Join(args, " "))
}
return nil, nil
})
wrappedMounter := &fakeMounter{t}
mounter := NewExecMounter(exec, wrappedMounter)
mounter.Mount(sourcePath, destinationPath, fsType, mountOptions)
}
func TestBindMount(t *testing.T) {
cmdCount := 0
exec := NewFakeExec(func(cmd string, args ...string) ([]byte, error) {
cmdCount++
if cmd != "mount" {
t.Errorf("expected mount command, got %q", cmd)
}
var expectedArgs []string
switch cmdCount {
case 1:
// mount -t fstype -o "bind" source target
expectedArgs = []string{"-t", fsType, "-o", "bind", sourcePath, destinationPath}
case 2:
// mount -t fstype -o "remount,opts" source target
expectedArgs = []string{"-t", fsType, "-o", "remount," + strings.Join(mountOptions, ","), sourcePath, destinationPath}
}
if !reflect.DeepEqual(expectedArgs, args) {
t.Errorf("expected arguments %q, got %q", strings.Join(expectedArgs, " "), strings.Join(args, " "))
}
return nil, nil
})
wrappedMounter := &fakeMounter{t}
mounter := NewExecMounter(exec, wrappedMounter)
bindOptions := append(mountOptions, "bind")
mounter.Mount(sourcePath, destinationPath, fsType, bindOptions)
}
func TestUnmount(t *testing.T) {
exec := NewFakeExec(func(cmd string, args ...string) ([]byte, error) {
if cmd != "umount" {
t.Errorf("expected unmount command, got %q", cmd)
}
// unmount $target
expectedArgs := []string{destinationPath}
if !reflect.DeepEqual(expectedArgs, args) {
t.Errorf("expected arguments %q, got %q", strings.Join(expectedArgs, " "), strings.Join(args, " "))
}
return nil, nil
})
wrappedMounter := &fakeMounter{t}
mounter := NewExecMounter(exec, wrappedMounter)
mounter.Unmount(destinationPath)
}
/* Fake wrapped mounter */
type fakeMounter struct {
t *testing.T
}
func (fm *fakeMounter) Mount(source string, target string, fstype string, options []string) error {
// Mount() of wrapped mounter should never be called. We call exec instead.
fm.t.Errorf("Unexpected wrapped mount call")
return fmt.Errorf("Unexpected wrapped mount call")
}
func (fm *fakeMounter) Unmount(target string) error {
// umount() of wrapped mounter should never be called. We call exec instead.
fm.t.Errorf("Unexpected wrapped mount call")
return fmt.Errorf("Unexpected wrapped mount call")
}
func (fm *fakeMounter) List() ([]MountPoint, error) {
return nil, nil
}
func (fm *fakeMounter) IsMountPointMatch(mp MountPoint, dir string) bool {
return false
}
func (fm *fakeMounter) IsNotMountPoint(file string) (bool, error) {
return false, nil
}
func (fm *fakeMounter) IsLikelyNotMountPoint(file string) (bool, error) {
return false, nil
}
func (fm *fakeMounter) DeviceOpened(pathname string) (bool, error) {
return false, nil
}
func (fm *fakeMounter) PathIsDevice(pathname string) (bool, error) {
return false, nil
}
func (fm *fakeMounter) GetDeviceNameFromMount(mountPath, pluginDir string) (string, error) {
return "", nil
}
func (fm *fakeMounter) MakeRShared(path string) error {
return nil
}
func (fm *fakeMounter) MakeFile(pathname string) error {
return nil
}
func (fm *fakeMounter) MakeDir(pathname string) error {
return nil
}
func (fm *fakeMounter) ExistsPath(pathname string) bool {
return false
}
func (fm *fakeMounter) GetFileType(pathname string) (FileType, error) {
return FileTypeFile, nil
}