From ea76c5bec1c370ce60acb830a242719d1c6cdb34 Mon Sep 17 00:00:00 2001 From: Sunny Date: Fri, 25 Jan 2019 13:33:47 +0530 Subject: [PATCH] hostpath: Add block volume support This change adds block volume support to hostpath driver. When a block volume request is received, a block file is created at provisionRoot with the requested capacity as size and a loop device is created associated with the block file. At node publish, a bind mount of the loop device is created at the publish target path. At node unpublish, the target path is unmounted and deleted. At volume delete, loop device is disassociated and the block file is deleted. Add plugins-dir to hostpath plugin daemonset The volume publish target path for block devices are usually under /var/lib/kubelet/plugins directory. Hence, adding plugins directory to the pod volumes with bidirectional mount propagation. Run the plugin as privileged to use loop devices In order to share loop devices with the host, the plugin container must be run as a privileged container. --- Dockerfile | 2 + Gopkg.lock | 11 +- deploy/hostpath/csi-hostpath-plugin.yaml | 9 + pkg/hostpath/controllerserver.go | 106 +++++++- pkg/hostpath/hostpath.go | 9 +- pkg/hostpath/nodeserver.go | 145 ++++++++--- vendor/k8s.io/apimachinery/pkg/types/doc.go | 18 ++ .../apimachinery/pkg/types/namespacedname.go | 43 ++++ .../k8s.io/apimachinery/pkg/types/nodename.go | 43 ++++ vendor/k8s.io/apimachinery/pkg/types/patch.go | 28 +++ vendor/k8s.io/apimachinery/pkg/types/uid.go | 22 ++ .../pkg/volume/util/volumepathhandler/BUILD | 30 +++ .../volumepathhandler/volume_path_handler.go | 233 ++++++++++++++++++ .../volume_path_handler_linux.go | 108 ++++++++ .../volume_path_handler_unsupported.go | 39 +++ 15 files changed, 797 insertions(+), 49 deletions(-) create mode 100644 vendor/k8s.io/apimachinery/pkg/types/doc.go create mode 100644 vendor/k8s.io/apimachinery/pkg/types/namespacedname.go create mode 100644 vendor/k8s.io/apimachinery/pkg/types/nodename.go create mode 100644 vendor/k8s.io/apimachinery/pkg/types/patch.go create mode 100644 vendor/k8s.io/apimachinery/pkg/types/uid.go create mode 100644 vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/BUILD create mode 100644 vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler.go create mode 100644 vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler_linux.go create mode 100644 vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler_unsupported.go diff --git a/Dockerfile b/Dockerfile index 8b6b550bf..b58e1f1a5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,5 +2,7 @@ FROM alpine LABEL maintainers="Kubernetes Authors" LABEL description="HostPath Driver" +# Add util-linux to get a new version of losetup. +RUN apk add util-linux COPY ./bin/hostpathplugin /hostpathplugin ENTRYPOINT ["/hostpathplugin"] diff --git a/Gopkg.lock b/Gopkg.lock index 7507907f5..1e78468c9 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -150,20 +150,24 @@ [[projects]] branch = "master" - digest = "1:f5fb6c4c9e14909a66efe7d3e49403234cd52523eb3997c5753a880635e8be2d" + digest = "1:69c5526be047163a9f3f1dcb879fdb6247b7afae0d4a89d531f3af06075c66db" name = "k8s.io/apimachinery" - packages = ["pkg/util/sets"] + packages = [ + "pkg/types", + "pkg/util/sets", + ] pruneopts = "UT" revision = "2a7c9300402896b3c073f2f47df85527c94f83a0" [[projects]] - digest = "1:54f05d2b09982c6ce153e2157eef013d98133cfea437a992a1a40d702a8770e4" + digest = "1:e59d7d4e595ef65c1059a53e4110fc179ce05a56eb149bcc270505325bb5cc68" name = "k8s.io/kubernetes" packages = [ "pkg/util/file", "pkg/util/io", "pkg/util/mount", "pkg/util/nsenter", + "pkg/volume/util/volumepathhandler", ] pruneopts = "UT" revision = "17c77c7898218073f14c8d573582e8d2313dc740" @@ -192,6 +196,7 @@ "google.golang.org/grpc/codes", "google.golang.org/grpc/status", "k8s.io/kubernetes/pkg/util/mount", + "k8s.io/kubernetes/pkg/volume/util/volumepathhandler", "k8s.io/utils/exec", ] solver-name = "gps-cdcl" diff --git a/deploy/hostpath/csi-hostpath-plugin.yaml b/deploy/hostpath/csi-hostpath-plugin.yaml index d28862cc7..4a0c178cf 100644 --- a/deploy/hostpath/csi-hostpath-plugin.yaml +++ b/deploy/hostpath/csi-hostpath-plugin.yaml @@ -24,6 +24,8 @@ spec: - --v=5 - --csi-address=/csi/csi.sock - --kubelet-registration-path=/var/lib/kubelet/plugins/csi-hostpath/csi.sock + securityContext: + privileged: true env: - name: KUBE_NODE_NAME valueFrom: @@ -59,6 +61,9 @@ spec: - mountPath: /var/lib/kubelet/pods mountPropagation: Bidirectional name: mountpoint-dir + - mountPath: /var/lib/kubelet/plugins + mountPropagation: Bidirectional + name: plugins-dir volumes: - hostPath: path: /var/lib/kubelet/plugins/csi-hostpath @@ -72,3 +77,7 @@ spec: path: /var/lib/kubelet/plugins_registry type: Directory name: registration-dir + - hostPath: + path: /var/lib/kubelet/plugins + type: Directory + name: plugins-dir diff --git a/pkg/hostpath/controllerserver.go b/pkg/hostpath/controllerserver.go index f4c53546f..5e194946d 100644 --- a/pkg/hostpath/controllerserver.go +++ b/pkg/hostpath/controllerserver.go @@ -32,6 +32,7 @@ import ( "google.golang.org/grpc/status" "github.com/container-storage-interface/spec/lib/go/csi" + "k8s.io/kubernetes/pkg/volume/util/volumepathhandler" utilexec "k8s.io/utils/exec" ) @@ -42,6 +43,13 @@ const ( maxStorageCapacity = tib ) +type accessType int + +const ( + mountAccess accessType = iota + blockAccess +) + type controllerServer struct { caps []*csi.ControllerServiceCapability } @@ -67,9 +75,41 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol if len(req.GetName()) == 0 { return nil, status.Error(codes.InvalidArgument, "Name missing in request") } - if req.GetVolumeCapabilities() == nil { + caps := req.GetVolumeCapabilities() + if caps == nil { return nil, status.Error(codes.InvalidArgument, "Volume Capabilities missing in request") } + + // Keep a record of the requested access types. + var accessTypeMount, accessTypeBlock bool + + for _, cap := range caps { + if cap.GetBlock() != nil { + accessTypeBlock = true + } + if cap.GetMount() != nil { + accessTypeMount = true + } + } + // A real driver would also need to check that the other + // fields in VolumeCapabilities are sane. The check above is + // just enough to pass the "[Testpattern: Dynamic PV (block + // volmode)] volumeMode should fail in binding dynamic + // provisioned PV to PVC" storage E2E test. + + if accessTypeBlock && accessTypeMount { + return nil, status.Error(codes.InvalidArgument, "cannot have both block and mount access type") + } + + var requestedAccessType accessType + + if accessTypeBlock { + requestedAccessType = blockAccess + } else { + // Default to mount. + requestedAccessType = mountAccess + } + // Need to check for already existing volume name, and if found // check for the requested capacity and already allocated capacity if exVol, err := getVolumeByName(req.GetName()); err == nil { @@ -94,13 +134,35 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol if capacity >= maxStorageCapacity { return nil, status.Errorf(codes.OutOfRange, "Requested capacity %d exceeds maximum allowed %d", capacity, maxStorageCapacity) } + volumeID := uuid.NewUUID().String() path := provisionRoot + volumeID - err := os.MkdirAll(path, 0777) - if err != nil { - glog.V(3).Infof("failed to create volume: %v", err) - return nil, err + + switch requestedAccessType { + case blockAccess: + executor := utilexec.New() + size := fmt.Sprintf("%dM", capacity/mib) + // Create a block file. + out, err := executor.Command("fallocate", "-l", size, path).CombinedOutput() + if err != nil { + glog.V(3).Infof("failed to create block device: %v", string(out)) + return nil, err + } + + // Associate block file with the loop device. + volPathHandler := volumepathhandler.VolumePathHandler{} + _, err = volPathHandler.AttachFileDevice(path) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to attach device: %v", err)) + } + case mountAccess: + err := os.MkdirAll(path, 0777) + if err != nil { + glog.V(3).Infof("failed to create volume: %v", err) + return nil, err + } } + if req.GetVolumeContentSource() != nil { contentSource := req.GetVolumeContentSource() if contentSource.GetSnapshot() != nil { @@ -127,6 +189,7 @@ func (cs *controllerServer) CreateVolume(ctx context.Context, req *csi.CreateVol hostPathVol.VolID = volumeID hostPathVol.VolSize = capacity hostPathVol.VolPath = path + hostPathVol.VolAccessType = requestedAccessType hostPathVolumes[volumeID] = hostPathVol return &csi.CreateVolumeResponse{ Volume: &csi.Volume{ @@ -148,11 +211,34 @@ func (cs *controllerServer) DeleteVolume(ctx context.Context, req *csi.DeleteVol glog.V(3).Infof("invalid delete volume req: %v", req) return nil, err } - volumeID := req.VolumeId - glog.V(4).Infof("deleting volume %s", volumeID) - path := provisionRoot + volumeID - os.RemoveAll(path) - delete(hostPathVolumes, volumeID) + + vol, err := getVolumeByID(req.GetVolumeId()) + if err != nil { + // Return OK if the volume is not found. + return &csi.DeleteVolumeResponse{}, nil + } + glog.V(4).Infof("deleting volume %s", vol.VolID) + + if vol.VolAccessType == blockAccess { + + volPathHandler := volumepathhandler.VolumePathHandler{} + // Get the associated loop device. + device, err := volPathHandler.GetLoopDevice(provisionRoot + vol.VolID) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to get the loop device: %v", err)) + } + + if device != "" { + // Remove any associated loop device. + glog.V(4).Infof("deleting loop device %s", device) + if err := volPathHandler.RemoveLoopDevice(device); err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to remove loop device: %v", err)) + } + } + } + + os.RemoveAll(vol.VolPath) + delete(hostPathVolumes, vol.VolID) return &csi.DeleteVolumeResponse{}, nil } diff --git a/pkg/hostpath/hostpath.go b/pkg/hostpath/hostpath.go index 299ac4b97..4d7d9538b 100644 --- a/pkg/hostpath/hostpath.go +++ b/pkg/hostpath/hostpath.go @@ -45,10 +45,11 @@ type hostPath struct { } type hostPathVolume struct { - VolName string `json:"volName"` - VolID string `json:"volID"` - VolSize int64 `json:"volSize"` - VolPath string `json:"volPath"` + VolName string `json:"volName"` + VolID string `json:"volID"` + VolSize int64 `json:"volSize"` + VolPath string `json:"volPath"` + VolAccessType accessType `json:"volAccessType"` } type hostPathSnapshot struct { diff --git a/pkg/hostpath/nodeserver.go b/pkg/hostpath/nodeserver.go index fbc8eb978..238eccc0a 100644 --- a/pkg/hostpath/nodeserver.go +++ b/pkg/hostpath/nodeserver.go @@ -17,6 +17,7 @@ limitations under the License. package hostpath import ( + "fmt" "os" "github.com/golang/glog" @@ -26,6 +27,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/kubernetes/pkg/util/mount" + "k8s.io/kubernetes/pkg/volume/util/volumepathhandler" ) type nodeServer struct { @@ -52,45 +54,106 @@ func (ns *nodeServer) NodePublishVolume(ctx context.Context, req *csi.NodePublis } targetPath := req.GetTargetPath() - notMnt, err := mount.New("").IsLikelyNotMountPoint(targetPath) + + if req.GetVolumeCapability().GetBlock() != nil && + req.GetVolumeCapability().GetMount() != nil { + return nil, status.Error(codes.InvalidArgument, "cannot have both block and mount access type") + } + + vol, err := getVolumeByID(req.GetVolumeId()) if err != nil { + return nil, status.Error(codes.NotFound, err.Error()) + } + + if req.GetVolumeCapability().GetBlock() != nil { + if vol.VolAccessType != blockAccess { + return nil, status.Error(codes.InvalidArgument, "cannot publish a non-block volume as block volume") + } + + volPathHandler := volumepathhandler.VolumePathHandler{} + + // Get loop device from the volume path. + loopDevice, err := volPathHandler.GetLoopDevice(vol.VolPath) + if err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to get the loop device: %v", err)) + } + + mounter := mount.New("") + + // Check if the target path exists. Create if not present. + _, err = os.Lstat(targetPath) if os.IsNotExist(err) { - if err = os.MkdirAll(targetPath, 0750); err != nil { + if err = mounter.MakeFile(targetPath); err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to create target path: %s: %v", targetPath, err)) + } + } + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to check if the target block file exists: %v", err) + } + + // Check if the target path is already mounted. Prevent remounting. + notMount, err := mounter.IsNotMountPoint(targetPath) + if err != nil { + if !os.IsNotExist(err) { + return nil, status.Errorf(codes.Internal, "error checking path %s for mount: %s", targetPath, err) + } + notMount = true + } + if !notMount { + // It's already mounted. + glog.V(5).Infof("Skipping bind-mounting subpath %s: already mounted", targetPath) + return &csi.NodePublishVolumeResponse{}, nil + } + + options := []string{"bind"} + if err := mount.New("").Mount(loopDevice, targetPath, "", options); err != nil { + return nil, status.Error(codes.Internal, fmt.Sprintf("failed to mount block device: %s at %s: %v", loopDevice, targetPath, err)) + } + } else if req.GetVolumeCapability().GetMount() != nil { + if vol.VolAccessType != mountAccess { + return nil, status.Error(codes.InvalidArgument, "cannot publish a non-mount volume as mount volume") + } + + notMnt, err := mount.New("").IsLikelyNotMountPoint(targetPath) + if err != nil { + if os.IsNotExist(err) { + if err = os.MkdirAll(targetPath, 0750); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + notMnt = true + } else { return nil, status.Error(codes.Internal, err.Error()) } - notMnt = true - } else { - return nil, status.Error(codes.Internal, err.Error()) } - } - if !notMnt { - return &csi.NodePublishVolumeResponse{}, nil - } + if !notMnt { + return &csi.NodePublishVolumeResponse{}, nil + } - fsType := req.GetVolumeCapability().GetMount().GetFsType() + fsType := req.GetVolumeCapability().GetMount().GetFsType() - deviceId := "" - if req.GetPublishContext() != nil { - deviceId = req.GetPublishContext()[deviceID] - } + deviceId := "" + if req.GetPublishContext() != nil { + deviceId = req.GetPublishContext()[deviceID] + } - readOnly := req.GetReadonly() - volumeId := req.GetVolumeId() - attrib := req.GetVolumeContext() - mountFlags := req.GetVolumeCapability().GetMount().GetMountFlags() + readOnly := req.GetReadonly() + volumeId := req.GetVolumeId() + attrib := req.GetVolumeContext() + mountFlags := req.GetVolumeCapability().GetMount().GetMountFlags() - glog.V(4).Infof("target %v\nfstype %v\ndevice %v\nreadonly %v\nvolumeId %v\nattributes %v\nmountflags %v\n", - targetPath, fsType, deviceId, readOnly, volumeId, attrib, mountFlags) + glog.V(4).Infof("target %v\nfstype %v\ndevice %v\nreadonly %v\nvolumeId %v\nattributes %v\nmountflags %v\n", + targetPath, fsType, deviceId, readOnly, volumeId, attrib, mountFlags) - options := []string{"bind"} - if readOnly { - options = append(options, "ro") - } - mounter := mount.New("") - path := provisionRoot + volumeId - if err := mounter.Mount(path, targetPath, "", options); err != nil { - return nil, err + options := []string{"bind"} + if readOnly { + options = append(options, "ro") + } + mounter := mount.New("") + path := provisionRoot + volumeId + if err := mounter.Mount(path, targetPath, "", options); err != nil { + return nil, err + } } return &csi.NodePublishVolumeResponse{}, nil @@ -108,12 +171,30 @@ func (ns *nodeServer) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpu targetPath := req.GetTargetPath() volumeID := req.GetVolumeId() - // Unmounting the image - err := mount.New("").Unmount(req.GetTargetPath()) + vol, err := getVolumeByID(volumeID) if err != nil { - return nil, status.Error(codes.Internal, err.Error()) + return nil, status.Error(codes.NotFound, err.Error()) + } + + switch vol.VolAccessType { + case blockAccess: + // Unmount and delete the block file. + err = mount.New("").Unmount(targetPath) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + if err = os.RemoveAll(targetPath); err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + glog.V(4).Infof("hostpath: volume %s has been unpublished.", targetPath) + case mountAccess: + // Unmounting the image + err = mount.New("").Unmount(req.GetTargetPath()) + if err != nil { + return nil, status.Error(codes.Internal, err.Error()) + } + glog.V(4).Infof("hostpath: volume %s/%s has been unmounted.", targetPath, volumeID) } - glog.V(4).Infof("hostpath: volume %s/%s has been unmounted.", targetPath, volumeID) return &csi.NodeUnpublishVolumeResponse{}, nil } diff --git a/vendor/k8s.io/apimachinery/pkg/types/doc.go b/vendor/k8s.io/apimachinery/pkg/types/doc.go new file mode 100644 index 000000000..5667fa992 --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/types/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2015 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 types implements various generic types used throughout kubernetes. +package types // import "k8s.io/apimachinery/pkg/types" diff --git a/vendor/k8s.io/apimachinery/pkg/types/namespacedname.go b/vendor/k8s.io/apimachinery/pkg/types/namespacedname.go new file mode 100644 index 000000000..88f0de36d --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/types/namespacedname.go @@ -0,0 +1,43 @@ +/* +Copyright 2015 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 types + +import ( + "fmt" +) + +// NamespacedName comprises a resource name, with a mandatory namespace, +// rendered as "/". Being a type captures intent and +// helps make sure that UIDs, namespaced names and non-namespaced names +// do not get conflated in code. For most use cases, namespace and name +// will already have been format validated at the API entry point, so we +// don't do that here. Where that's not the case (e.g. in testing), +// consider using NamespacedNameOrDie() in testing.go in this package. + +type NamespacedName struct { + Namespace string + Name string +} + +const ( + Separator = '/' +) + +// String returns the general purpose string representation +func (n NamespacedName) String() string { + return fmt.Sprintf("%s%c%s", n.Namespace, Separator, n.Name) +} diff --git a/vendor/k8s.io/apimachinery/pkg/types/nodename.go b/vendor/k8s.io/apimachinery/pkg/types/nodename.go new file mode 100644 index 000000000..fee348d7e --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/types/nodename.go @@ -0,0 +1,43 @@ +/* +Copyright 2015 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 types + +// NodeName is a type that holds a api.Node's Name identifier. +// Being a type captures intent and helps make sure that the node name +// is not confused with similar concepts (the hostname, the cloud provider id, +// the cloud provider name etc) +// +// To clarify the various types: +// +// * Node.Name is the Name field of the Node in the API. This should be stored in a NodeName. +// Unfortunately, because Name is part of ObjectMeta, we can't store it as a NodeName at the API level. +// +// * Hostname is the hostname of the local machine (from uname -n). +// However, some components allow the user to pass in a --hostname-override flag, +// which will override this in most places. In the absence of anything more meaningful, +// kubelet will use Hostname as the Node.Name when it creates the Node. +// +// * The cloudproviders have the own names: GCE has InstanceName, AWS has InstanceId. +// +// For GCE, InstanceName is the Name of an Instance object in the GCE API. On GCE, Instance.Name becomes the +// Hostname, and thus it makes sense also to use it as the Node.Name. But that is GCE specific, and it is up +// to the cloudprovider how to do this mapping. +// +// For AWS, the InstanceID is not yet suitable for use as a Node.Name, so we actually use the +// PrivateDnsName for the Node.Name. And this is _not_ always the same as the hostname: if +// we are using a custom DHCP domain it won't be. +type NodeName string diff --git a/vendor/k8s.io/apimachinery/pkg/types/patch.go b/vendor/k8s.io/apimachinery/pkg/types/patch.go new file mode 100644 index 000000000..d522d1dbd --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/types/patch.go @@ -0,0 +1,28 @@ +/* +Copyright 2015 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 types + +// Similarly to above, these are constants to support HTTP PATCH utilized by +// both the client and server that didn't make sense for a whole package to be +// dedicated to. +type PatchType string + +const ( + JSONPatchType PatchType = "application/json-patch+json" + MergePatchType PatchType = "application/merge-patch+json" + StrategicMergePatchType PatchType = "application/strategic-merge-patch+json" +) diff --git a/vendor/k8s.io/apimachinery/pkg/types/uid.go b/vendor/k8s.io/apimachinery/pkg/types/uid.go new file mode 100644 index 000000000..869339222 --- /dev/null +++ b/vendor/k8s.io/apimachinery/pkg/types/uid.go @@ -0,0 +1,22 @@ +/* +Copyright 2015 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 types + +// UID is a type that holds unique ID values, including UUIDs. Because we +// don't ONLY use UUIDs, this is an alias to string. Being a type captures +// intent and helps make sure that UIDs and names do not get conflated. +type UID string diff --git a/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/BUILD b/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/BUILD new file mode 100644 index 000000000..e61e3973e --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/BUILD @@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "go_default_library", + srcs = [ + "volume_path_handler.go", + "volume_path_handler_linux.go", + "volume_path_handler_unsupported.go", + ], + importpath = "k8s.io/kubernetes/pkg/volume/util/volumepathhandler", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/types:go_default_library", + "//vendor/github.com/golang/glog: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"], +) diff --git a/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler.go b/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler.go new file mode 100644 index 000000000..61680c115 --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler.go @@ -0,0 +1,233 @@ +/* +Copyright 2018 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 volumepathhandler + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "path/filepath" + + "github.com/golang/glog" + + "k8s.io/apimachinery/pkg/types" +) + +const ( + losetupPath = "losetup" + ErrDeviceNotFound = "device not found" + ErrDeviceNotSupported = "device not supported" +) + +// BlockVolumePathHandler defines a set of operations for handling block volume-related operations +type BlockVolumePathHandler interface { + // MapDevice creates a symbolic link to block device under specified map path + MapDevice(devicePath string, mapPath string, linkName string) error + // UnmapDevice removes a symbolic link to block device under specified map path + UnmapDevice(mapPath string, linkName string) error + // RemovePath removes a file or directory on specified map path + RemoveMapPath(mapPath string) error + // IsSymlinkExist retruns true if specified symbolic link exists + IsSymlinkExist(mapPath string) (bool, error) + // GetDeviceSymlinkRefs searches symbolic links under global map path + GetDeviceSymlinkRefs(devPath string, mapPath string) ([]string, error) + // FindGlobalMapPathUUIDFromPod finds {pod uuid} symbolic link under globalMapPath + // corresponding to map path symlink, and then return global map path with pod uuid. + FindGlobalMapPathUUIDFromPod(pluginDir, mapPath string, podUID types.UID) (string, error) + // AttachFileDevice takes a path to a regular file and makes it available as an + // attached block device. + AttachFileDevice(path string) (string, error) + // GetLoopDevice returns the full path to the loop device associated with the given path. + GetLoopDevice(path string) (string, error) + // RemoveLoopDevice removes specified loopback device + RemoveLoopDevice(device string) error +} + +// NewBlockVolumePathHandler returns a new instance of BlockVolumeHandler. +func NewBlockVolumePathHandler() BlockVolumePathHandler { + var volumePathHandler VolumePathHandler + return volumePathHandler +} + +// VolumePathHandler is path related operation handlers for block volume +type VolumePathHandler struct { +} + +// MapDevice creates a symbolic link to block device under specified map path +func (v VolumePathHandler) MapDevice(devicePath string, mapPath string, linkName string) error { + // Example of global map path: + // globalMapPath/linkName: plugins/kubernetes.io/{PluginName}/{DefaultKubeletVolumeDevicesDirName}/{volumePluginDependentPath}/{podUid} + // linkName: {podUid} + // + // Example of pod device map path: + // podDeviceMapPath/linkName: pods/{podUid}/{DefaultKubeletVolumeDevicesDirName}/{escapeQualifiedPluginName}/{volumeName} + // linkName: {volumeName} + if len(devicePath) == 0 { + return fmt.Errorf("Failed to map device to map path. devicePath is empty") + } + if len(mapPath) == 0 { + return fmt.Errorf("Failed to map device to map path. mapPath is empty") + } + if !filepath.IsAbs(mapPath) { + return fmt.Errorf("The map path should be absolute: map path: %s", mapPath) + } + glog.V(5).Infof("MapDevice: devicePath %s", devicePath) + glog.V(5).Infof("MapDevice: mapPath %s", mapPath) + glog.V(5).Infof("MapDevice: linkName %s", linkName) + + // Check and create mapPath + _, err := os.Stat(mapPath) + if err != nil && !os.IsNotExist(err) { + glog.Errorf("cannot validate map path: %s", mapPath) + return err + } + if err = os.MkdirAll(mapPath, 0750); err != nil { + return fmt.Errorf("Failed to mkdir %s, error %v", mapPath, err) + } + // Remove old symbolic link(or file) then create new one. + // This should be done because current symbolic link is + // stale across node reboot. + linkPath := path.Join(mapPath, string(linkName)) + if err = os.Remove(linkPath); err != nil && !os.IsNotExist(err) { + return err + } + err = os.Symlink(devicePath, linkPath) + return err +} + +// UnmapDevice removes a symbolic link associated to block device under specified map path +func (v VolumePathHandler) UnmapDevice(mapPath string, linkName string) error { + if len(mapPath) == 0 { + return fmt.Errorf("Failed to unmap device from map path. mapPath is empty") + } + glog.V(5).Infof("UnmapDevice: mapPath %s", mapPath) + glog.V(5).Infof("UnmapDevice: linkName %s", linkName) + + // Check symbolic link exists + linkPath := path.Join(mapPath, string(linkName)) + if islinkExist, checkErr := v.IsSymlinkExist(linkPath); checkErr != nil { + return checkErr + } else if !islinkExist { + glog.Warningf("Warning: Unmap skipped because symlink does not exist on the path: %v", linkPath) + return nil + } + err := os.Remove(linkPath) + return err +} + +// RemoveMapPath removes a file or directory on specified map path +func (v VolumePathHandler) RemoveMapPath(mapPath string) error { + if len(mapPath) == 0 { + return fmt.Errorf("Failed to remove map path. mapPath is empty") + } + glog.V(5).Infof("RemoveMapPath: mapPath %s", mapPath) + err := os.RemoveAll(mapPath) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +// IsSymlinkExist returns true if specified file exists and the type is symbolik link. +// If file doesn't exist, or file exists but not symbolick link, return false with no error. +// On other cases, return false with error from Lstat(). +func (v VolumePathHandler) IsSymlinkExist(mapPath string) (bool, error) { + fi, err := os.Lstat(mapPath) + if err == nil { + // If file exits and it's symbolick link, return true and no error + if fi.Mode()&os.ModeSymlink == os.ModeSymlink { + return true, nil + } + // If file exits but it's not symbolick link, return fale and no error + return false, nil + } + // If file doesn't exist, return false and no error + if os.IsNotExist(err) { + return false, nil + } + // Return error from Lstat() + return false, err +} + +// GetDeviceSymlinkRefs searches symbolic links under global map path +func (v VolumePathHandler) GetDeviceSymlinkRefs(devPath string, mapPath string) ([]string, error) { + var refs []string + files, err := ioutil.ReadDir(mapPath) + if err != nil { + return nil, fmt.Errorf("Directory cannot read %v", err) + } + for _, file := range files { + if file.Mode()&os.ModeSymlink != os.ModeSymlink { + continue + } + filename := file.Name() + filepath, err := os.Readlink(path.Join(mapPath, filename)) + if err != nil { + return nil, fmt.Errorf("Symbolic link cannot be retrieved %v", err) + } + glog.V(5).Infof("GetDeviceSymlinkRefs: filepath: %v, devPath: %v", filepath, devPath) + if filepath == devPath { + refs = append(refs, path.Join(mapPath, filename)) + } + } + glog.V(5).Infof("GetDeviceSymlinkRefs: refs %v", refs) + return refs, nil +} + +// FindGlobalMapPathUUIDFromPod finds {pod uuid} symbolic link under globalMapPath +// corresponding to map path symlink, and then return global map path with pod uuid. +// ex. mapPath symlink: pods/{podUid}}/{DefaultKubeletVolumeDevicesDirName}/{escapeQualifiedPluginName}/{volumeName} -> /dev/sdX +// globalMapPath/{pod uuid}: plugins/kubernetes.io/{PluginName}/{DefaultKubeletVolumeDevicesDirName}/{volumePluginDependentPath}/{pod uuid} -> /dev/sdX +func (v VolumePathHandler) FindGlobalMapPathUUIDFromPod(pluginDir, mapPath string, podUID types.UID) (string, error) { + var globalMapPathUUID string + // Find symbolic link named pod uuid under plugin dir + err := filepath.Walk(pluginDir, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + if (fi.Mode()&os.ModeSymlink == os.ModeSymlink) && (fi.Name() == string(podUID)) { + glog.V(5).Infof("FindGlobalMapPathFromPod: path %s, mapPath %s", path, mapPath) + if res, err := compareSymlinks(path, mapPath); err == nil && res { + globalMapPathUUID = path + } + } + return nil + }) + if err != nil { + return "", err + } + glog.V(5).Infof("FindGlobalMapPathFromPod: globalMapPathUUID %s", globalMapPathUUID) + // Return path contains global map path + {pod uuid} + return globalMapPathUUID, nil +} + +func compareSymlinks(global, pod string) (bool, error) { + devGlobal, err := os.Readlink(global) + if err != nil { + return false, err + } + devPod, err := os.Readlink(pod) + if err != nil { + return false, err + } + glog.V(5).Infof("CompareSymlinks: devGloBal %s, devPod %s", devGlobal, devPod) + if devGlobal == devPod { + return true, nil + } + return false, nil +} diff --git a/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler_linux.go b/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler_linux.go new file mode 100644 index 000000000..f9a886d7d --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler_linux.go @@ -0,0 +1,108 @@ +// +build linux + +/* +Copyright 2018 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 volumepathhandler + +import ( + "errors" + "fmt" + "os" + "os/exec" + "strings" + + "github.com/golang/glog" +) + +// AttachFileDevice takes a path to a regular file and makes it available as an +// attached block device. +func (v VolumePathHandler) AttachFileDevice(path string) (string, error) { + blockDevicePath, err := v.GetLoopDevice(path) + if err != nil && err.Error() != ErrDeviceNotFound { + return "", err + } + + // If no existing loop device for the path, create one + if blockDevicePath == "" { + glog.V(4).Infof("Creating device for path: %s", path) + blockDevicePath, err = makeLoopDevice(path) + if err != nil { + return "", err + } + } + return blockDevicePath, nil +} + +// GetLoopDevice returns the full path to the loop device associated with the given path. +func (v VolumePathHandler) GetLoopDevice(path string) (string, error) { + _, err := os.Stat(path) + if os.IsNotExist(err) { + return "", errors.New(ErrDeviceNotFound) + } + if err != nil { + return "", fmt.Errorf("not attachable: %v", err) + } + + args := []string{"-j", path} + cmd := exec.Command(losetupPath, args...) + out, err := cmd.CombinedOutput() + if err != nil { + glog.V(2).Infof("Failed device discover command for path %s: %v %s", path, err, out) + return "", err + } + return parseLosetupOutputForDevice(out) +} + +func makeLoopDevice(path string) (string, error) { + args := []string{"-f", "--show", path} + cmd := exec.Command(losetupPath, args...) + out, err := cmd.CombinedOutput() + if err != nil { + glog.V(2).Infof("Failed device create command for path: %s %v %s ", path, err, out) + return "", err + } + return parseLosetupOutputForDevice(out) +} + +// RemoveLoopDevice removes specified loopback device +func (v VolumePathHandler) RemoveLoopDevice(device string) error { + args := []string{"-d", device} + cmd := exec.Command(losetupPath, args...) + out, err := cmd.CombinedOutput() + if err != nil { + if _, err := os.Stat(device); os.IsNotExist(err) { + return nil + } + glog.V(2).Infof("Failed to remove loopback device: %s: %v %s", device, err, out) + return err + } + return nil +} + +func parseLosetupOutputForDevice(output []byte) (string, error) { + if len(output) == 0 { + return "", errors.New(ErrDeviceNotFound) + } + + // losetup returns device in the format: + // /dev/loop1: [0073]:148662 (/dev/sda) + device := strings.TrimSpace(strings.SplitN(string(output), ":", 2)[0]) + if len(device) == 0 { + return "", errors.New(ErrDeviceNotFound) + } + return device, nil +} diff --git a/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler_unsupported.go b/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler_unsupported.go new file mode 100644 index 000000000..266398b1d --- /dev/null +++ b/vendor/k8s.io/kubernetes/pkg/volume/util/volumepathhandler/volume_path_handler_unsupported.go @@ -0,0 +1,39 @@ +// +build !linux + +/* +Copyright 2018 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 volumepathhandler + +import ( + "fmt" +) + +// AttachFileDevice takes a path to a regular file and makes it available as an +// attached block device. +func (v VolumePathHandler) AttachFileDevice(path string) (string, error) { + return "", fmt.Errorf("AttachFileDevice not supported for this build.") +} + +// GetLoopDevice returns the full path to the loop device associated with the given path. +func (v VolumePathHandler) GetLoopDevice(path string) (string, error) { + return "", fmt.Errorf("GetLoopDevice not supported for this build.") +} + +// RemoveLoopDevice removes specified loopback device +func (v VolumePathHandler) RemoveLoopDevice(device string) error { + return fmt.Errorf("RemoveLoopDevice not supported for this build.") +}