pax_global_header00006660000000000000000000000064151607652060014522gustar00rootroot0000000000000052 comment=766d86225a98e701320f8393d4f75b4baf35ae12 golang-k8s-mount-utils-1.36.0~beta0/000077500000000000000000000000001516076520600171735ustar00rootroot00000000000000golang-k8s-mount-utils-1.36.0~beta0/.github/000077500000000000000000000000001516076520600205335ustar00rootroot00000000000000golang-k8s-mount-utils-1.36.0~beta0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000002251516076520600243330ustar00rootroot00000000000000Sorry, we do not accept changes directly against this repository. Please see CONTRIBUTING.md for information on where and how to contribute instead. golang-k8s-mount-utils-1.36.0~beta0/LICENSE000066400000000000000000000261351516076520600202070ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} 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. golang-k8s-mount-utils-1.36.0~beta0/OWNERS000066400000000000000000000003251516076520600201330ustar00rootroot00000000000000# See the OWNERS docs at https://go.k8s.io/owners reviewers: - jingxu97 - saad-ali - jsafrane - msau42 - andyzhangx - gnufied approvers: - jingxu97 - saad-ali - jsafrane labels: - sig/storage golang-k8s-mount-utils-1.36.0~beta0/README.md000066400000000000000000000030101516076520600204440ustar00rootroot00000000000000> ⚠️ **This is an automatically published [staged repository](https://git.k8s.io/kubernetes/staging#external-repository-staging-area) for Kubernetes**. > Contributions, including issues and pull requests, should be made to the main Kubernetes repository: [https://github.com/kubernetes/kubernetes](https://github.com/kubernetes/kubernetes). > This repository is read-only for importing, and not used for direct contributions. > See [CONTRIBUTING.md](./CONTRIBUTING.md) for more details. ## Purpose This repository defines an interface to mounting filesystems to be consumed by various Kubernetes and out-of-tree CSI components. Consumers of this repository can make use of functions like 'Mount' to mount source to target as fstype with given options, 'Unmount' to unmount a target. Other useful functions include 'List' all mounted file systems and find all mount references to a path using 'GetMountRefs' ## Community, discussion, contribution, and support Learn how to engage with the Kubernetes community on the [community page](http://kubernetes.io/community/). You can reach the maintainers of this repository at: - Slack: #sig-storage (on https://kubernetes.slack.com -- get an invite at slack.kubernetes.io) - Mailing List: https://groups.google.com/forum/#!forum/kubernetes-sig-storage ### Code of Conduct Participation in the Kubernetes community is governed by the [Kubernetes Code of Conduct](code-of-conduct.md). ### Contibution Guidelines See [CONTRIBUTING.md](CONTRIBUTING.md) for more information. golang-k8s-mount-utils-1.36.0~beta0/SECURITY_CONTACTS000066400000000000000000000010741516076520600216650ustar00rootroot00000000000000# Defined below are the security contacts for this repo. # # They are the contact point for the Product Security Committee to reach out # to for triaging and handling of incoming issues. # # The below names agree to abide by the # [Embargo Policy](https://git.k8s.io/security/private-distributors-list.md#embargo-policy) # and will be removed and replaced if they violate that agreement. # # DO NOT REPORT SECURITY VULNERABILITIES DIRECTLY TO THESE NAMES, FOLLOW THE # INSTRUCTIONS AT https://kubernetes.io/security/ saad-ali cjcullen joelsmith liggitt philips tallclair golang-k8s-mount-utils-1.36.0~beta0/code-of-conduct.md000066400000000000000000000002241516076520600224640ustar00rootroot00000000000000# Kubernetes Community Code of Conduct Please refer to our [Kubernetes Community Code of Conduct](https://git.k8s.io/community/code-of-conduct.md) golang-k8s-mount-utils-1.36.0~beta0/doc.go000066400000000000000000000012101516076520600202610ustar00rootroot00000000000000/* Copyright 2014 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 defines an interface to mounting filesystems. package mount golang-k8s-mount-utils-1.36.0~beta0/fake_mounter.go000066400000000000000000000165511516076520600222110ustar00rootroot00000000000000/* 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 mount import ( "os" "path/filepath" "sync" "k8s.io/klog/v2" ) // FakeMounter implements mount.Interface for tests. type FakeMounter struct { MountPoints []MountPoint log []FakeAction // Error to return for a path when calling IsLikelyNotMountPoint MountCheckErrors map[string]error // Some tests run things in parallel, make sure the mounter does not produce // any golang's DATA RACE warnings. mutex sync.Mutex UnmountFunc UnmountFunc skipMountPointCheck bool } // UnmountFunc is a function callback to be executed during the Unmount() call. type UnmountFunc func(path string) error var _ Interface = &FakeMounter{} const ( // FakeActionMount is the string for specifying mount as FakeAction.Action FakeActionMount = "mount" // FakeActionUnmount is the string for specifying unmount as FakeAction.Action FakeActionUnmount = "unmount" ) // FakeAction objects are logged every time a fake mount or unmount is called. type FakeAction struct { Action string // "mount" or "unmount" Target string // applies to both mount and unmount actions Source string // applies only to "mount" actions FSType string // applies only to "mount" actions } // NewFakeMounter returns a FakeMounter struct that implements Interface and is // suitable for testing purposes. func NewFakeMounter(mps []MountPoint) *FakeMounter { return &FakeMounter{ MountPoints: mps, } } func (f *FakeMounter) WithSkipMountPointCheck() *FakeMounter { f.skipMountPointCheck = true return f } // ResetLog clears all the log entries in FakeMounter func (f *FakeMounter) ResetLog() { f.mutex.Lock() defer f.mutex.Unlock() f.log = []FakeAction{} } // GetLog returns the slice of FakeActions taken by the mounter func (f *FakeMounter) GetLog() []FakeAction { f.mutex.Lock() defer f.mutex.Unlock() return f.log } // Mount records the mount event and updates the in-memory mount points for FakeMounter func (f *FakeMounter) Mount(source string, target string, fstype string, options []string) error { return f.MountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) } // Mount records the mount event and updates the in-memory mount points for FakeMounter // sensitiveOptions to be passed in a separate parameter from the normal // mount options and ensures the sensitiveOptions are never logged. This // method should be used by callers that pass sensitive material (like // passwords) as mount options. func (f *FakeMounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { f.mutex.Lock() defer f.mutex.Unlock() opts := []string{} for _, option := range options { // find 'bind' option if option == "bind" { // This is a bind-mount. In order to mimic linux behaviour, we must // use the original device of the bind-mount as the real source. // E.g. when mounted /dev/sda like this: // $ mount /dev/sda /mnt/test // $ mount -o bind /mnt/test /mnt/bound // then /proc/mount contains: // /dev/sda /mnt/test // /dev/sda /mnt/bound // (and not /mnt/test /mnt/bound) // I.e. we must use /dev/sda as source instead of /mnt/test in the // bind mount. for _, mnt := range f.MountPoints { if source == mnt.Path { source = mnt.Device break } } } // reuse MountPoint.Opts field to mark mount as readonly opts = append(opts, option) } // If target is a symlink, get its absolute path absTarget, err := filepath.EvalSymlinks(target) if err != nil { absTarget = target } f.MountPoints = append(f.MountPoints, MountPoint{Device: source, Path: absTarget, Type: fstype, Opts: append(opts, sensitiveOptions...)}) klog.V(5).Infof("Fake mounter: mounted %s to %s", source, absTarget) f.log = append(f.log, FakeAction{Action: FakeActionMount, Target: absTarget, Source: source, FSType: fstype}) return nil } func (f *FakeMounter) MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error { return f.MountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) } func (f *FakeMounter) MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error { return f.MountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) } // Unmount records the unmount event and updates the in-memory mount points for FakeMounter func (f *FakeMounter) Unmount(target string) error { f.mutex.Lock() defer f.mutex.Unlock() // If target is a symlink, get its absolute path absTarget, err := filepath.EvalSymlinks(target) if err != nil { absTarget = target } newMountpoints := []MountPoint{} for _, mp := range f.MountPoints { if mp.Path == absTarget { if f.UnmountFunc != nil { err := f.UnmountFunc(absTarget) if err != nil { return err } } klog.V(5).Infof("Fake mounter: unmounted %s from %s", mp.Device, absTarget) // Don't copy it to newMountpoints continue } newMountpoints = append(newMountpoints, MountPoint{Device: mp.Device, Path: mp.Path, Type: mp.Type}) } f.MountPoints = newMountpoints f.log = append(f.log, FakeAction{Action: FakeActionUnmount, Target: absTarget}) delete(f.MountCheckErrors, target) return nil } // List returns all the in-memory mountpoints for FakeMounter func (f *FakeMounter) List() ([]MountPoint, error) { f.mutex.Lock() defer f.mutex.Unlock() return f.MountPoints, nil } // IsLikelyNotMountPoint determines whether a path is a mountpoint by checking // if the absolute path to file is in the in-memory mountpoints func (f *FakeMounter) IsLikelyNotMountPoint(file string) (bool, error) { f.mutex.Lock() defer f.mutex.Unlock() err := f.MountCheckErrors[file] if err != nil { return false, err } _, err = os.Stat(file) if err != nil { return true, err } // If file is a symlink, get its absolute path absFile, err := filepath.EvalSymlinks(file) if err != nil { absFile = file } for _, mp := range f.MountPoints { if mp.Path == absFile { klog.V(5).Infof("isLikelyNotMountPoint for %s: mounted %s, false", file, mp.Path) return false, nil } } klog.V(5).Infof("isLikelyNotMountPoint for %s: true", file) return true, nil } func (f *FakeMounter) CanSafelySkipMountPointCheck() bool { return f.skipMountPointCheck } func (f *FakeMounter) IsMountPoint(file string) (bool, error) { notMnt, err := f.IsLikelyNotMountPoint(file) if err != nil { return false, err } return !notMnt, nil } // GetMountRefs finds all mount references to the path, returns a // list of paths. func (f *FakeMounter) GetMountRefs(pathname string) ([]string, error) { realpath, err := filepath.EvalSymlinks(pathname) if err != nil { // Ignore error in FakeMounter, because we actually didn't create files. realpath = pathname } return getMountRefsByDev(f, realpath) } golang-k8s-mount-utils-1.36.0~beta0/go.mod000066400000000000000000000013101516076520600202740ustar00rootroot00000000000000// This is a generated file. Do not edit directly. module k8s.io/mount-utils go 1.26.0 godebug default=go1.26 require ( github.com/moby/sys/mountinfo v0.7.2 github.com/stretchr/testify v1.11.1 golang.org/x/sys v0.40.0 k8s.io/klog/v2 v2.140.0 k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 ) require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) golang-k8s-mount-utils-1.36.0~beta0/go.sum000066400000000000000000000056511516076520600203350ustar00rootroot00000000000000github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= golang-k8s-mount-utils-1.36.0~beta0/mount.go000066400000000000000000000346751516076520600207030ustar00rootroot00000000000000/* Copyright 2014 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. */ // TODO(thockin): This whole pkg is pretty linux-centric. As soon as we have // an alternate platform, we will need to abstract further. package mount import ( "fmt" "path/filepath" "strings" "time" utilexec "k8s.io/utils/exec" ) const ( // Default mount command if mounter path is not specified. defaultMountCommand = "mount" // Log message where sensitive mount options were removed sensitiveOptionsRemoved = "" ) // Interface defines the set of methods to allow for mount operations on a system. type Interface interface { // Mount mounts source to target as fstype with given options. // options MUST not contain sensitive material (like passwords). Mount(source string, target string, fstype string, options []string) error // MountSensitive is the same as Mount() but this method allows // sensitiveOptions to be passed in a separate parameter from the normal // mount options and ensures the sensitiveOptions are never logged. This // method should be used by callers that pass sensitive material (like // passwords) as mount options. MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error // MountSensitiveWithoutSystemd is the same as MountSensitive() but this method disable using systemd mount. MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error // MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd() with additional mount flags MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error // Unmount unmounts given target. Unmount(target string) error // List returns a list of all mounted filesystems. This can be large. // On some platforms, reading mounts directly from the OS is not guaranteed // consistent (i.e. it could change between chunked reads). This is guaranteed // to be consistent. List() ([]MountPoint, error) // IsLikelyNotMountPoint uses heuristics to determine if a directory // is not a mountpoint. // It should return ErrNotExist when the directory does not exist. // IsLikelyNotMountPoint does NOT properly detect all mountpoint types // most notably linux bind mounts and symbolic link. For callers that do not // care about such situations, this is a faster alternative to calling List() // and scanning that output. IsLikelyNotMountPoint(file string) (bool, error) // CanSafelySkipMountPointCheck indicates whether this mounter returns errors on // operations for targets that are not mount points. If this returns true, no such // errors will be returned. CanSafelySkipMountPointCheck() bool // IsMountPoint determines if a directory is a mountpoint. // It should return ErrNotExist when the directory does not exist. // IsMountPoint is more expensive than IsLikelyNotMountPoint. // IsMountPoint detects bind mounts in linux. // IsMountPoint may enumerate all the mountpoints using List() and // the list of mountpoints may be large, then it uses // isMountPointMatch to evaluate whether the directory is a mountpoint. IsMountPoint(file string) (bool, error) // GetMountRefs finds all mount references to pathname, returning a slice of // paths. Pathname can be a mountpoint path or a normal directory // (for bind mount). On Linux, pathname is excluded from the slice. // For example, if /dev/sdc was mounted at /path/a and /path/b, // GetMountRefs("/path/a") would return ["/path/b"] // GetMountRefs("/path/b") would return ["/path/a"] // On Windows there is no way to query all mount points; as long as pathname is // a valid mount, it will be returned. GetMountRefs(pathname string) ([]string, error) } // Compile-time check to ensure all Mounter implementations satisfy // the mount interface. var _ Interface = &Mounter{} type MounterForceUnmounter interface { Interface // UnmountWithForce unmounts given target but will retry unmounting with force option // after given timeout. UnmountWithForce(target string, umountTimeout time.Duration) error } // MountPoint represents a single line in /proc/mounts or /etc/fstab. type MountPoint struct { Device string Path string Type string Opts []string // Opts may contain sensitive mount options (like passwords) and MUST be treated as such (e.g. not logged). Freq int Pass int } type MountErrorType string const ( FilesystemMismatch MountErrorType = "FilesystemMismatch" HasFilesystemErrors MountErrorType = "HasFilesystemErrors" UnformattedReadOnly MountErrorType = "UnformattedReadOnly" FormatFailed MountErrorType = "FormatFailed" GetDiskFormatFailed MountErrorType = "GetDiskFormatFailed" UnknownMountError MountErrorType = "UnknownMountError" ) type MountError struct { Type MountErrorType Message string } func (mountError MountError) String() string { return mountError.Message } func (mountError MountError) Error() string { return mountError.Message } func NewMountError(mountErrorValue MountErrorType, format string, args ...interface{}) error { mountError := MountError{ Type: mountErrorValue, Message: fmt.Sprintf(format, args...), } return mountError } // SafeFormatAndMount probes a device to see if it is formatted. // Namely it checks to see if a file system is present. If so it // mounts it otherwise the device is formatted first then mounted. type SafeFormatAndMount struct { Interface Exec utilexec.Interface formatSem chan any formatTimeout time.Duration } func NewSafeFormatAndMount(mounter Interface, exec utilexec.Interface, opts ...Option) *SafeFormatAndMount { res := &SafeFormatAndMount{ Interface: mounter, Exec: exec, } for _, opt := range opts { opt(res) } return res } type Option func(*SafeFormatAndMount) // WithMaxConcurrentFormat sets the maximum number of concurrent format // operations executed by the mounter. The timeout controls the maximum // duration of a format operation before its concurrency token is released. // Once a token is released, it can be acquired by another concurrent format // operation. The original operation is allowed to complete. // If n < 1, concurrency is set to unlimited. func WithMaxConcurrentFormat(n int, timeout time.Duration) Option { return func(mounter *SafeFormatAndMount) { if n > 0 { mounter.formatSem = make(chan any, n) mounter.formatTimeout = timeout } } } // FormatAndMount formats the given disk, if needed, and mounts it. // That is if the disk is not formatted and it is not being mounted as // read-only it will format it first then mount it. Otherwise, if the // disk is already formatted or it is being mounted as read-only, it // will be mounted without formatting. // options MUST not contain sensitive material (like passwords). func (mounter *SafeFormatAndMount) FormatAndMount(source string, target string, fstype string, options []string) error { return mounter.FormatAndMountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) } // FormatAndMountSensitive is the same as FormatAndMount but this method allows // sensitiveOptions to be passed in a separate parameter from the normal mount // options and ensures the sensitiveOptions are never logged. This method should // be used by callers that pass sensitive material (like passwords) as mount // options. func (mounter *SafeFormatAndMount) FormatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { return mounter.FormatAndMountSensitiveWithFormatOptions(source, target, fstype, options, sensitiveOptions, nil /* formatOptions */) } // FormatAndMountSensitiveWithFormatOptions behaves exactly the same as // FormatAndMountSensitive, but allows for options to be passed when the disk // is formatted. These options are NOT validated in any way and should never // come directly from untrusted user input as that would be an injection risk. func (mounter *SafeFormatAndMount) FormatAndMountSensitiveWithFormatOptions(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error { return mounter.formatAndMountSensitive(source, target, fstype, options, sensitiveOptions, formatOptions) } // getMountRefsByDev finds all references to the device provided // by mountPath; returns a list of paths. // Note that mountPath should be path after the evaluation of any symblolic links. func getMountRefsByDev(mounter Interface, mountPath string) ([]string, error) { mps, err := mounter.List() if err != nil { return nil, err } // Finding the device mounted to mountPath. diskDev := "" for i := range mps { if mountPath == mps[i].Path { diskDev = mps[i].Device break } } // Find all references to the device. var refs []string for i := range mps { if mps[i].Device == diskDev || mps[i].Device == mountPath { if mps[i].Path != mountPath { refs = append(refs, mps[i].Path) } } } return refs, nil } // IsNotMountPoint determines if a directory is a mountpoint. // It should return ErrNotExist when the directory does not exist. // IsNotMountPoint is more expensive than IsLikelyNotMountPoint // and depends on IsMountPoint. // // If an error occurs, it returns true (assuming it is not a mountpoint) // when ErrNotExist is returned for callers similar to IsLikelyNotMountPoint. // // Deprecated: This function is kept to keep changes backward compatible with // previous library version. Callers should prefer mounter.IsMountPoint. func IsNotMountPoint(mounter Interface, file string) (bool, error) { isMnt, err := mounter.IsMountPoint(file) if err != nil { return true, err } return !isMnt, nil } // GetDeviceNameFromMount given a mnt point, find the device from /proc/mounts // returns the device name, reference count, and error code. func GetDeviceNameFromMount(mounter Interface, mountPath string) (string, int, error) { mps, err := mounter.List() if err != nil { return "", 0, err } // Find the device name. // FIXME if multiple devices mounted on the same mount path, only the first one is returned. device := "" // If mountPath is symlink, need get its target path. slTarget, err := filepath.EvalSymlinks(mountPath) if err != nil { slTarget = mountPath } for i := range mps { if mps[i].Path == slTarget { device = mps[i].Device break } } // Find all references to the device. refCount := 0 for i := range mps { if mps[i].Device == device { refCount++ } } return device, refCount, nil } // MakeBindOpts detects whether a bind mount is being requested and makes the remount options to // use in case of bind mount, due to the fact that bind mount doesn't respect mount options. // The list equals: // // options - 'bind' + 'remount' (no duplicate) func MakeBindOpts(options []string) (bool, []string, []string) { bind, bindOpts, bindRemountOpts, _ := MakeBindOptsSensitive(options, nil /* sensitiveOptions */) return bind, bindOpts, bindRemountOpts } // MakeBindOptsSensitive is the same as MakeBindOpts but this method allows // sensitiveOptions to be passed in a separate parameter from the normal mount // options and ensures the sensitiveOptions are never logged. This method should // be used by callers that pass sensitive material (like passwords) as mount // options. func MakeBindOptsSensitive(options []string, sensitiveOptions []string) (bool, []string, []string, []string) { // Because we have an FD opened on the subpath bind mount, the "bind" option // needs to be included, otherwise the mount target will error as busy if you // remount as readonly. // // As a consequence, all read only bind mounts will no longer change the underlying // volume mount to be read only. bindRemountOpts := []string{"bind", "remount"} bindRemountSensitiveOpts := []string{} bind := false bindOpts := []string{"bind"} // _netdev is a userspace mount option and does not automatically get added when // bind mount is created and hence we must carry it over. if checkForNetDev(options, sensitiveOptions) { bindOpts = append(bindOpts, "_netdev") } for _, option := range options { switch option { case "bind": bind = true case "remount": default: bindRemountOpts = append(bindRemountOpts, option) } } for _, sensitiveOption := range sensitiveOptions { switch sensitiveOption { case "bind": bind = true case "remount": default: bindRemountSensitiveOpts = append(bindRemountSensitiveOpts, sensitiveOption) } } return bind, bindOpts, bindRemountOpts, bindRemountSensitiveOpts } func checkForNetDev(options []string, sensitiveOptions []string) bool { for _, option := range options { if option == "_netdev" { return true } } for _, sensitiveOption := range sensitiveOptions { if sensitiveOption == "_netdev" { return true } } return false } // PathWithinBase checks if give path is within given base directory. func PathWithinBase(fullPath, basePath string) bool { rel, err := filepath.Rel(basePath, fullPath) if err != nil { return false } if StartsWithBackstep(rel) { // Needed to escape the base path. return false } return true } // StartsWithBackstep checks if the given path starts with a backstep segment. func StartsWithBackstep(rel string) bool { // normalize to / and check for ../ return rel == ".." || strings.HasPrefix(filepath.ToSlash(rel), "../") } // sanitizedOptionsForLogging will return a comma separated string containing // options and sensitiveOptions. Each entry in sensitiveOptions will be // replaced with the string sensitiveOptionsRemoved // e.g. o1,o2,, func sanitizedOptionsForLogging(options []string, sensitiveOptions []string) string { separator := "" if len(options) > 0 && len(sensitiveOptions) > 0 { separator = "," } sensitiveOptionsStart := "" sensitiveOptionsEnd := "" if len(sensitiveOptions) > 0 { sensitiveOptionsStart = strings.Repeat(sensitiveOptionsRemoved+",", len(sensitiveOptions)-1) sensitiveOptionsEnd = sensitiveOptionsRemoved } return strings.Join(options, ",") + separator + sensitiveOptionsStart + sensitiveOptionsEnd } golang-k8s-mount-utils-1.36.0~beta0/mount_helper_common.go000066400000000000000000000130511516076520600235730ustar00rootroot00000000000000/* 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 mount import ( "fmt" "os" "time" "k8s.io/klog/v2" ) // CleanupMountPoint unmounts the given path and deletes the remaining directory // if successful. If extensiveMountPointCheck is true IsNotMountPoint will be // called instead of IsLikelyNotMountPoint. IsNotMountPoint is more expensive // but properly handles bind mounts within the same fs. func CleanupMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool) error { pathExists, pathErr := PathExists(mountPath) if !pathExists && pathErr == nil { klog.Warningf("Warning: mount cleanup skipped because path does not exist: %v", mountPath) return nil } corruptedMnt := IsCorruptedMnt(pathErr) if pathErr != nil && !corruptedMnt { return fmt.Errorf("Error checking path: %v", pathErr) } return doCleanupMountPoint(mountPath, mounter, extensiveMountPointCheck, corruptedMnt) } func CleanupMountWithForce(mountPath string, mounter MounterForceUnmounter, extensiveMountPointCheck bool, umountTimeout time.Duration) error { pathExists, pathErr := PathExists(mountPath) if !pathExists && pathErr == nil { klog.Warningf("Warning: mount cleanup skipped because path does not exist: %v", mountPath) return nil } corruptedMnt := IsCorruptedMnt(pathErr) if pathErr != nil && !corruptedMnt { return fmt.Errorf("Error checking path: %v", pathErr) } if corruptedMnt || mounter.CanSafelySkipMountPointCheck() { klog.V(4).Infof("unmounting %q (corruptedMount: %t, mounterCanSkipMountPointChecks: %t)", mountPath, corruptedMnt, mounter.CanSafelySkipMountPointCheck()) if err := mounter.UnmountWithForce(mountPath, umountTimeout); err != nil { return err } return removePath(mountPath) } notMnt, err := removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck) // if mountPath is not a mount point, it's just been removed or there was an error if err != nil || notMnt { return err } klog.V(4).Infof("%q is a mountpoint, unmounting", mountPath) if err := mounter.UnmountWithForce(mountPath, umountTimeout); err != nil { return err } notMnt, err = removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck) // if mountPath is not a mount point, it's either just been removed or there was an error if notMnt { return err } // mountPath is still a mount point return fmt.Errorf("failed to cleanup mount point %v", mountPath) } // doCleanupMountPoint unmounts the given path and // deletes the remaining directory if successful. // if extensiveMountPointCheck is true // IsNotMountPoint will be called instead of IsLikelyNotMountPoint. // IsNotMountPoint is more expensive but properly handles bind mounts within the same fs. // if corruptedMnt is true, it means that the mountPath is a corrupted mountpoint, and the mount point check // will be skipped. The mount point check will also be skipped if the mounter supports it. func doCleanupMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool, corruptedMnt bool) error { if corruptedMnt || mounter.CanSafelySkipMountPointCheck() { klog.V(4).Infof("unmounting %q (corruptedMount: %t, mounterCanSkipMountPointChecks: %t)", mountPath, corruptedMnt, mounter.CanSafelySkipMountPointCheck()) if err := mounter.Unmount(mountPath); err != nil { return err } return removePath(mountPath) } notMnt, err := removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck) // if mountPath is not a mount point, it's just been removed or there was an error if err != nil || notMnt { return err } klog.V(4).Infof("%q is a mountpoint, unmounting", mountPath) if err := mounter.Unmount(mountPath); err != nil { return err } notMnt, err = removePathIfNotMountPoint(mountPath, mounter, extensiveMountPointCheck) // if mountPath is not a mount point, it's either just been removed or there was an error if notMnt { return err } // mountPath is still a mount point return fmt.Errorf("failed to cleanup mount point %v", mountPath) } // removePathIfNotMountPoint verifies if given mountPath is a mount point if not it attempts // to remove the directory. Returns true and nil if directory was not a mount point and removed. func removePathIfNotMountPoint(mountPath string, mounter Interface, extensiveMountPointCheck bool) (bool, error) { var notMnt bool var err error if extensiveMountPointCheck { notMnt, err = IsNotMountPoint(mounter, mountPath) } else { notMnt, err = mounter.IsLikelyNotMountPoint(mountPath) } if err != nil { if os.IsNotExist(err) { klog.V(4).Infof("%q does not exist", mountPath) return true, nil } return notMnt, err } if notMnt { klog.V(4).Infof("%q is not a mountpoint, deleting", mountPath) return notMnt, os.Remove(mountPath) } return notMnt, nil } // removePath attempts to remove the directory. Returns nil if the directory was removed or does not exist. func removePath(mountPath string) error { klog.V(4).Infof("Deleting path %q", mountPath) err := os.Remove(mountPath) if os.IsNotExist(err) { klog.V(4).Infof("%q does not exist", mountPath) return nil } return err } golang-k8s-mount-utils-1.36.0~beta0/mount_helper_test.go000066400000000000000000000106461516076520600232710ustar00rootroot00000000000000/* 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 mount import ( "fmt" "os" "path/filepath" "runtime" "syscall" "testing" ) func TestDoCleanupMountPoint(t *testing.T) { if runtime.GOOS == "darwin" { t.Skipf("not supported on GOOS=%s", runtime.GOOS) } const testMount = "test-mount" const defaultPerm = 0o750 tests := map[string]struct { corruptedMnt bool // Function that prepares the directory structure for the test under // the given base directory. // Returns a fake MountPoint, a fake error for the mount point, // and error if the prepare function encountered a fatal error. prepareMnt func(base string) (MountPoint, error, error) // Function that prepares the FakeMounter for the test. prepareMntr func(mntr *FakeMounter) expectErr bool }{ "mount-ok": { prepareMnt: func(base string) (MountPoint, error, error) { path := filepath.Join(base, testMount) if err := os.MkdirAll(path, defaultPerm); err != nil { return MountPoint{}, nil, err } return MountPoint{Device: "/dev/sdb", Path: path}, nil, nil }, corruptedMnt: false, expectErr: false, }, "path-not-exist": { prepareMnt: func(base string) (MountPoint, error, error) { path := filepath.Join(base, testMount) return MountPoint{Device: "/dev/sdb", Path: path}, nil, nil }, corruptedMnt: false, expectErr: false, }, "mount-corrupted": { prepareMnt: func(base string) (MountPoint, error, error) { path := filepath.Join(base, testMount) if err := os.MkdirAll(path, defaultPerm); err != nil { return MountPoint{}, nil, err } return MountPoint{Device: "/dev/sdb", Path: path}, os.NewSyscallError("fake", syscall.ESTALE), nil }, corruptedMnt: true, expectErr: false, }, "mount-err-not-corrupted": { prepareMnt: func(base string) (MountPoint, error, error) { path := filepath.Join(base, testMount) if err := os.MkdirAll(path, defaultPerm); err != nil { return MountPoint{}, nil, err } return MountPoint{Device: "/dev/sdb", Path: path}, os.NewSyscallError("fake", syscall.ETIMEDOUT), nil }, corruptedMnt: false, expectErr: true, }, "skip-mount-point-check": { prepareMnt: func(base string) (MountPoint, error, error) { path := filepath.Join(base, testMount) if err := os.MkdirAll(path, defaultPerm); err != nil { return MountPoint{Device: "/dev/sdb", Path: path}, nil, err } return MountPoint{Device: "/dev/sdb", Path: path}, nil, nil }, prepareMntr: func(mntr *FakeMounter) { mntr.WithSkipMountPointCheck() }, expectErr: false, }, } for name, tt := range tests { t.Run(name, func(t *testing.T) { tmpDir := t.TempDir() if tt.prepareMnt == nil { t.Fatalf("prepareMnt function required") } mountPoint, mountError, err := tt.prepareMnt(tmpDir) if err != nil { t.Fatalf("failed to prepareMnt for test: %v", err) } fake := NewFakeMounter( []MountPoint{mountPoint}, ) fake.MountCheckErrors = map[string]error{mountPoint.Path: mountError} if tt.prepareMntr != nil { tt.prepareMntr(fake) } err = doCleanupMountPoint(mountPoint.Path, fake, true, tt.corruptedMnt) if tt.expectErr { if err == nil { t.Errorf("test %s failed, expected error, got none", name) } if err := validateDirExists(mountPoint.Path); err != nil { t.Errorf("test %s failed, mount path doesn't exist: %v", name, err) } } if !tt.expectErr { if err != nil { t.Errorf("test %s failed: %v", name, err) } if err := validateDirNotExists(mountPoint.Path); err != nil { t.Errorf("test %s failed, mount path still exists: %v", name, err) } } }) } } func validateDirExists(dir string) error { _, err := os.ReadDir(dir) return err } func validateDirNotExists(dir string) error { _, err := os.ReadDir(dir) if os.IsNotExist(err) { return nil } if err != nil { return err } return fmt.Errorf("dir %q still exists", dir) } golang-k8s-mount-utils-1.36.0~beta0/mount_helper_unix.go000066400000000000000000000166631516076520600233020ustar00rootroot00000000000000//go:build !windows /* Copyright 2019 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 ( "bytes" "errors" "fmt" "io/fs" "os" "strconv" "strings" "sync" "syscall" "golang.org/x/sys/unix" "k8s.io/klog/v2" utilio "k8s.io/utils/io" ) const ( // At least number of fields per line in /proc//mountinfo. expectedAtLeastNumFieldsPerMountInfo = 10 // How many times to retry for a consistent read of /proc/mounts. maxListTries = 10 ) // IsCorruptedMnt return true if err is about corrupted mount point func IsCorruptedMnt(err error) bool { if err == nil { return false } var underlyingError error switch pe := err.(type) { case nil: return false case *os.PathError: underlyingError = pe.Err case *os.LinkError: underlyingError = pe.Err case *os.SyscallError: underlyingError = pe.Err case syscall.Errno: underlyingError = err } return errors.Is(underlyingError, syscall.ENOTCONN) || errors.Is(underlyingError, syscall.ESTALE) || errors.Is(underlyingError, syscall.EIO) || errors.Is(underlyingError, syscall.EACCES) || errors.Is(underlyingError, syscall.EHOSTDOWN) || errors.Is(underlyingError, syscall.EWOULDBLOCK) || errors.Is(underlyingError, syscall.ENODEV) } // MountInfo represents a single line in /proc//mountinfo. type MountInfo struct { // Unique ID for the mount (maybe reused after umount). ID int // The ID of the parent mount (or of self for the root of this mount namespace's mount tree). ParentID int // Major indicates one half of the device ID which identifies the device class // (parsed from `st_dev` for files on this filesystem). Major int // Minor indicates one half of the device ID which identifies a specific // instance of device (parsed from `st_dev` for files on this filesystem). Minor int // The pathname of the directory in the filesystem which forms the root of this mount. Root string // Mount source, filesystem-specific information. e.g. device, tmpfs name. Source string // Mount point, the pathname of the mount point. MountPoint string // Optional fieds, zero or more fields of the form "tag[:value]". OptionalFields []string // The filesystem type in the form "type[.subtype]". FsType string // Per-mount options. MountOptions []string // Per-superblock options. SuperOptions []string } // ParseMountInfo parses /proc/xxx/mountinfo. func ParseMountInfo(filename string) ([]MountInfo, error) { content, err := readMountInfo(filename) if err != nil { return []MountInfo{}, err } contentStr := string(content) infos := []MountInfo{} for _, line := range strings.Split(contentStr, "\n") { if line == "" { // the last split() item is empty string following the last \n continue } // See `man proc` for authoritative description of format of the file. fields := strings.Fields(line) if len(fields) < expectedAtLeastNumFieldsPerMountInfo { return nil, fmt.Errorf("wrong number of fields in (expected at least %d, got %d): %s", expectedAtLeastNumFieldsPerMountInfo, len(fields), line) } id, err := strconv.Atoi(fields[0]) if err != nil { return nil, err } parentID, err := strconv.Atoi(fields[1]) if err != nil { return nil, err } mm := strings.Split(fields[2], ":") if len(mm) != 2 { return nil, fmt.Errorf("parsing '%s' failed: unexpected minor:major pair %s", line, mm) } major, err := strconv.Atoi(mm[0]) if err != nil { return nil, fmt.Errorf("parsing '%s' failed: unable to parse major device id, err:%v", mm[0], err) } minor, err := strconv.Atoi(mm[1]) if err != nil { return nil, fmt.Errorf("parsing '%s' failed: unable to parse minor device id, err:%v", mm[1], err) } info := MountInfo{ ID: id, ParentID: parentID, Major: major, Minor: minor, Root: fields[3], MountPoint: fields[4], MountOptions: splitMountOptions(fields[5]), } // All fields until "-" are "optional fields". i := 6 for ; i < len(fields) && fields[i] != "-"; i++ { info.OptionalFields = append(info.OptionalFields, fields[i]) } // Parse the rest 3 fields. i++ if len(fields)-i < 3 { return nil, fmt.Errorf("expect 3 fields in %s, got %d", line, len(fields)-i) } info.FsType = fields[i] info.Source = fields[i+1] info.SuperOptions = splitMountOptions(fields[i+2]) infos = append(infos, info) } return infos, nil } // splitMountOptions parses comma-separated list of mount options into an array. // It respects double quotes - commas in them are not considered as the option separator. func splitMountOptions(s string) []string { inQuotes := false list := strings.FieldsFunc(s, func(r rune) bool { if r == '"' { inQuotes = !inQuotes } // Report a new field only when outside of double quotes. return r == ',' && !inQuotes }) return list } // isMountPointMatch returns true if the path in mp is the same as dir. // Handles case where mountpoint dir has been renamed due to stale NFS mount. func isMountPointMatch(mp MountPoint, dir string) bool { return strings.TrimSuffix(mp.Path, "\\040(deleted)") == dir } // PathExists returns true if the specified path exists. // TODO: clean this up to use pkg/util/file/FileExists func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } else if errors.Is(err, fs.ErrNotExist) { err = syscall.Access(path, syscall.F_OK) if err == nil { // The access syscall says the file exists, the stat syscall says it // doesn't. This was observed on CIFS when the path was removed at // the server somehow. POSIX calls this a stale file handle, let's fake // that error and treat the path as existing but corrupted. klog.Warningf("Potential stale file handle detected: %s", path) return true, syscall.ESTALE } return false, nil } else if IsCorruptedMnt(err) { return true, err } return false, err } // These variables are used solely by kernelHasMountinfoBug. var ( hasMountinfoBug bool checkMountinfoBugOnce sync.Once ) // kernelHasMountinfoBug checks if the kernel bug that can lead to incomplete // mountinfo being read is fixed. It does so by checking the kernel version. // // The bug was fixed by the kernel commit 9f6c61f96f2d97 (since Linux 5.8). // Alas, there is no better way to check if the bug is fixed other than to // rely on the kernel version returned by uname. func kernelHasMountinfoBug() bool { checkMountinfoBugOnce.Do(func() { // Assume old kernel. hasMountinfoBug = true uname := unix.Utsname{} err := unix.Uname(&uname) if err != nil { return } end := bytes.IndexByte(uname.Release[:], 0) v := bytes.SplitN(uname.Release[:end], []byte{'.'}, 3) if len(v) != 3 { return } major, _ := strconv.Atoi(string(v[0])) minor, _ := strconv.Atoi(string(v[1])) if major > 5 || (major == 5 && minor >= 8) { hasMountinfoBug = false } }) return hasMountinfoBug } func readMountInfo(path string) ([]byte, error) { if kernelHasMountinfoBug() { return utilio.ConsistentRead(path, maxListTries) } return os.ReadFile(path) } golang-k8s-mount-utils-1.36.0~beta0/mount_helper_unix_test.go000066400000000000000000000364451516076520600243410ustar00rootroot00000000000000//go:build !windows /* Copyright 2019 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 ( "os" "path/filepath" "reflect" "testing" ) func writeFile(t *testing.T, content string) string { filename := filepath.Join(t.TempDir(), "mountinfo") err := os.WriteFile(filename, []byte(content), 0o600) if err != nil { t.Fatal(err) } return filename } func TestParseMountInfo(t *testing.T) { info := `62 0 253:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered 78 62 0:41 / /tmp rw,nosuid,nodev shared:30 - tmpfs tmpfs rw,seclabel 80 62 0:42 / /var/lib/nfs/rpc_pipefs rw,relatime shared:31 - rpc_pipefs sunrpc rw 82 62 0:43 / /var/lib/foo rw,relatime shared:32 - tmpfs tmpfs rw 83 63 0:44 / /var/lib/bar rw,relatime - tmpfs tmpfs rw 227 62 253:0 /var/lib/docker/devicemapper /var/lib/docker/devicemapper rw,relatime - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered 224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered 76 17 8:1 / /mnt/stateful_partition rw,nosuid,nodev,noexec,relatime - ext4 /dev/sda1 rw,commit=30,data=ordered 80 17 8:1 /var /var rw,nosuid,nodev,noexec,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered 189 80 8:1 /var/lib/kubelet /var/lib/kubelet rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered 818 77 8:40 / /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered 819 78 8:48 / /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered 900 100 8:48 /dir1 /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volume-subpaths/vol1/subpath1/0 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered 901 101 8:1 /dir1 /var/lib/kubelet/pods/c25464af-e52e-11e7-ab4d-42010a800002/volume-subpaths/vol1/subpath1/1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered 902 102 8:1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volumes/kubernetes.io~empty-dir/vol1/dir1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volume-subpaths/vol1/subpath1/0 rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered 903 103 8:1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volumes/kubernetes.io~empty-dir/vol2/dir1 /var/lib/kubelet/pods/d4076f24-e53a-11e7-ba15-42010a800002/volume-subpaths/vol1/subpath1/1 rw,relatime shared:30 - ext4 /dev/sda1 rw,commit=30,data=ordered 178 25 253:0 /etc/bar /var/lib/kubelet/pods/12345/volume-subpaths/vol1/subpath1/0 rw,relatime shared:1 - ext4 /dev/sdb2 rw,errors=remount-ro,data=ordered 698 186 0:41 /tmp1/dir1 /var/lib/kubelet/pods/41135147-e697-11e7-9342-42010a800002/volume-subpaths/vol1/subpath1/0 rw shared:26 - tmpfs tmpfs rw 918 77 8:50 / /var/lib/kubelet/pods/2345/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered 919 78 8:58 / /var/lib/kubelet/pods/2345/volumes/kubernetes.io~gce-pd/vol1 rw,relatime shared:290 - ext4 /dev/sdd rw,data=ordered 920 100 8:50 /dir1 /var/lib/kubelet/pods/2345/volume-subpaths/vol1/subpath1/0 rw,relatime shared:290 - ext4 /dev/sdc rw,data=ordered 150 23 1:58 / /media/nfs_vol rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 151 24 1:58 / /media/nfs_bindmount rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs/foo rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 134 23 0:58 / /var/lib/kubelet/pods/43219158-e5e1-11e7-a392-0e858b8eaf40/volumes/kubernetes.io~nfs/nfs1 rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 187 23 0:58 / /var/lib/kubelet/pods/1fc5ea21-eff4-11e7-ac80-0e858b8eaf40/volumes/kubernetes.io~nfs/nfs2 rw,relatime shared:96 - nfs4 172.18.4.223:/srv/nfs2 rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 188 24 0:58 / /var/lib/kubelet/pods/43219158-e5e1-11e7-a392-0e858b8eaf40/volume-subpaths/nfs1/subpath1/0 rw,relatime shared:89 - nfs4 172.18.4.223:/srv/nfs/foo rw,vers=4.0,rsize=524288,wsize=524288,namlen=255,hard,proto=tcp,port=0,timeo=600,retrans=2,sec=sys,clientaddr=172.18.4.223,local_lock=none,addr=172.18.4.223 347 60 0:71 / /var/lib/kubelet/pods/13195d46-f9fa-11e7-bbf1-5254007a695a/volumes/kubernetes.io~nfs/vol2 rw,relatime shared:170 - nfs 172.17.0.3:/exports/2 rw,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=172.17.0.3,mountvers=3,mountport=20048,mountproto=udp,local_lock=none,addr=172.17.0.3 222 24 253:0 /tmp/src /mnt/dst rw,relatime shared:1 - ext4 /dev/mapper/vagrant--vg-root rw,errors=remount-ro,data=ordered 28 18 0:24 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755 29 28 0:25 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd 31 28 0:27 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,cpuset 32 28 0:28 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,cpu,cpuacct 33 28 0:29 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,freezer 34 28 0:30 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,net_cls,net_prio 35 28 0:31 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,pids 36 28 0:32 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,devices 37 28 0:33 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb 38 28 0:34 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,blkio 39 28 0:35 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,memory 40 28 0:36 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,perf_event 761 60 8:0 / /var/lib/kubelet/plugins/kubernetes.io/iscsi/iface-default/127.0.0.1:3260-iqn.2003-01.org.linux-iscsi.f21.x8664:sn.4b0aae584f7c-lun-0 rw,relatime shared:421 - ext4 /dev/sda rw,context="system_u:object_r:container_file_t:s0:c314,c894",data=ordered ` filename := writeFile(t, info) tests := []struct { name string id int expectedInfo MountInfo }{ { "simple bind mount", 189, MountInfo{ ID: 189, ParentID: 80, Major: 8, Minor: 1, Root: "/var/lib/kubelet", Source: "/dev/sda1", MountPoint: "/var/lib/kubelet", OptionalFields: []string{"shared:30"}, FsType: "ext4", MountOptions: []string{"rw", "relatime"}, SuperOptions: []string{"rw", "commit=30", "data=ordered"}, }, }, { "bind mount a directory", 222, MountInfo{ ID: 222, ParentID: 24, Major: 253, Minor: 0, Root: "/tmp/src", Source: "/dev/mapper/vagrant--vg-root", MountPoint: "/mnt/dst", OptionalFields: []string{"shared:1"}, FsType: "ext4", MountOptions: []string{"rw", "relatime"}, SuperOptions: []string{"rw", "errors=remount-ro", "data=ordered"}, }, }, { "more than one optional fields", 224, MountInfo{ ID: 224, ParentID: 62, Major: 253, Minor: 0, Root: "/var/lib/docker/devicemapper/test/shared", Source: "/dev/mapper/ssd-root", MountPoint: "/var/lib/docker/devicemapper/test/shared", OptionalFields: []string{"master:1", "shared:44"}, FsType: "ext4", MountOptions: []string{"rw", "relatime"}, SuperOptions: []string{"rw", "seclabel", "data=ordered"}, }, }, { "cgroup-mountpoint", 28, MountInfo{ ID: 28, ParentID: 18, Major: 0, Minor: 24, Root: "/", Source: "tmpfs", MountPoint: "/sys/fs/cgroup", OptionalFields: []string{"shared:9"}, FsType: "tmpfs", MountOptions: []string{"ro", "nosuid", "nodev", "noexec"}, SuperOptions: []string{"ro", "mode=755"}, }, }, { "cgroup-subsystem-systemd-mountpoint", 29, MountInfo{ ID: 29, ParentID: 28, Major: 0, Minor: 25, Root: "/", Source: "cgroup", MountPoint: "/sys/fs/cgroup/systemd", OptionalFields: []string{"shared:10"}, FsType: "cgroup", MountOptions: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, SuperOptions: []string{"rw", "xattr", "release_agent=/lib/systemd/systemd-cgroups-agent", "name=systemd"}, }, }, { "cgroup-subsystem-cpuset-mountpoint", 31, MountInfo{ ID: 31, ParentID: 28, Major: 0, Minor: 27, Root: "/", Source: "cgroup", MountPoint: "/sys/fs/cgroup/cpuset", OptionalFields: []string{"shared:13"}, FsType: "cgroup", MountOptions: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}, SuperOptions: []string{"rw", "cpuset"}, }, }, { "mount option with commas inside quotes", 761, MountInfo{ ID: 761, ParentID: 60, Major: 8, Minor: 0, Root: "/", Source: "/dev/sda", MountPoint: "/var/lib/kubelet/plugins/kubernetes.io/iscsi/iface-default/127.0.0.1:3260-iqn.2003-01.org.linux-iscsi.f21.x8664:sn.4b0aae584f7c-lun-0", OptionalFields: []string{"shared:421"}, FsType: "ext4", MountOptions: []string{"rw", "relatime"}, SuperOptions: []string{"rw", "context=\"system_u:object_r:container_file_t:s0:c314,c894\"", "data=ordered"}, }, }, } infos, err := ParseMountInfo(filename) if err != nil { t.Fatalf("Cannot parse %s: %s", filename, err) } for _, test := range tests { found := false for _, info := range infos { if info.ID == test.id { found = true if !reflect.DeepEqual(info, test.expectedInfo) { t.Errorf("Test case %q:\n expected: %+v\n got: %+v", test.name, test.expectedInfo, info) } break } } if !found { t.Errorf("Test case %q: mountPoint %d not found", test.name, test.id) } } } func TestBadParseMountInfo(t *testing.T) { tests := []struct { info string name string id int expectedInfo *MountInfo error string }{ { `224 62 253:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered`, "good major:minor field", 224, &MountInfo{ ID: 224, ParentID: 62, Major: 253, Minor: 0, Root: "/var/lib/docker/devicemapper/test/shared", Source: "/dev/mapper/ssd-root", MountPoint: "/var/lib/docker/devicemapper/test/shared", OptionalFields: []string{"master:1", "shared:44"}, FsType: "ext4", MountOptions: []string{"rw", "relatime"}, SuperOptions: []string{"rw", "seclabel", "data=ordered"}, }, "", }, { `224 62 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered`, "missing major:minor field", 224, nil, `parsing '224 62 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered' failed: unexpected minor:major pair [/var/lib/docker/devicemapper/test/shared]`, }, { `224 62 :0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered`, "empty major field", 224, nil, `parsing '' failed: unable to parse major device id, err:strconv.Atoi: parsing "": invalid syntax`, }, { `224 62 253: /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered`, "empty minor field", 224, nil, `parsing '' failed: unable to parse minor device id, err:strconv.Atoi: parsing "": invalid syntax`, }, { `224 62 foo:0 /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered`, "alphabet in major field", 224, nil, `parsing 'foo' failed: unable to parse major device id, err:strconv.Atoi: parsing "foo": invalid syntax`, }, { `224 62 253:bar /var/lib/docker/devicemapper/test/shared /var/lib/docker/devicemapper/test/shared rw,relatime master:1 shared:44 - ext4 /dev/mapper/ssd-root rw,seclabel,data=ordered`, "alphabet in minor field", 224, nil, `parsing 'bar' failed: unable to parse minor device id, err:strconv.Atoi: parsing "bar": invalid syntax`, }, } for _, test := range tests { filename := writeFile(t, test.info) infos, err := ParseMountInfo(filename) if err != nil { if err.Error() != test.error { t.Errorf("Test case %q:\n expected error: %+v\n got: %+v", test.name, test.error, err.Error()) } continue } found := false for _, info := range infos { if info.ID == test.id { found = true if !reflect.DeepEqual(info, *test.expectedInfo) { t.Errorf("Test case %q:\n expected: %+v\n got: %+v", test.name, test.expectedInfo, info) } break } } if !found { t.Errorf("Test case %q: mountPoint %d not found", test.name, test.id) } } } func testIsMountPointMatch(t testing.TB) { mpCases := []struct { mp, dir string res bool }{ {"", "", true}, {"/", "/", true}, {"/some/path", "/some/path", true}, {"/a/different/kind/of/path\\040(deleted)", "/a/different/kind/of/path", true}, {"one", "two", false}, {"a somewhat long path that ends with A", "a somewhat long path that ends with B", false}, } for _, tc := range mpCases { mp := MountPoint{Path: tc.mp} res := isMountPointMatch(mp, tc.dir) if res != tc.res { t.Errorf("mp: %q, dir: %q, expected %v, got %v", tc.mp, tc.dir, tc.res, res) } } } func TestIsMountPointMatch(t *testing.T) { testIsMountPointMatch(t) } func BenchmarkIsMountPointMatch(b *testing.B) { for i := 0; i < b.N; i++ { testIsMountPointMatch(b) } } golang-k8s-mount-utils-1.36.0~beta0/mount_helper_windows.go000066400000000000000000000057771516076520600240150ustar00rootroot00000000000000//go:build windows /* Copyright 2019 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" "os" "strconv" "strings" "syscall" "k8s.io/klog/v2" ) // following failure codes are from https://docs.microsoft.com/en-us/windows/desktop/debug/system-error-codes--1300-1699- // ERROR_BAD_NETPATH = 53 // ERROR_NETWORK_BUSY = 54 // ERROR_UNEXP_NET_ERR = 59 // ERROR_NETNAME_DELETED = 64 // ERROR_NETWORK_ACCESS_DENIED = 65 // ERROR_BAD_DEV_TYPE = 66 // ERROR_BAD_NET_NAME = 67 // ERROR_SESSION_CREDENTIAL_CONFLICT = 1219 // ERROR_LOGON_FAILURE = 1326 // WSAEHOSTDOWN = 10064 var errorNoList = [...]int{53, 54, 59, 64, 65, 66, 67, 1219, 1326, 10064} // IsCorruptedMnt return true if err is about corrupted mount point func IsCorruptedMnt(err error) bool { if err == nil { return false } var underlyingError error switch pe := err.(type) { case nil: return false case *os.PathError: underlyingError = pe.Err case *os.LinkError: underlyingError = pe.Err case *os.SyscallError: underlyingError = pe.Err } if ee, ok := underlyingError.(syscall.Errno); ok { for _, errno := range errorNoList { if int(ee) == errno { klog.Warningf("IsCorruptedMnt failed with error: %v, error code: %v", err, errno) return true } } } return false } // NormalizeWindowsPath makes sure the given path is a valid path on Windows // systems by making sure all instances of `/` are replaced with `\\`, and the // path beings with `c:` func NormalizeWindowsPath(path string) string { normalizedPath := strings.Replace(path, "/", "\\", -1) if strings.HasPrefix(normalizedPath, "\\") { normalizedPath = "c:" + normalizedPath } return normalizedPath } // ValidateDiskNumber : disk number should be a number in [0, 99] func ValidateDiskNumber(disk string) error { if _, err := strconv.Atoi(disk); err != nil { return fmt.Errorf("wrong disk number format: %q, err: %v", disk, err) } return nil } // isMountPointMatch determines if the mountpoint matches the dir func isMountPointMatch(mp MountPoint, dir string) bool { return mp.Path == dir } // PathExists returns true if the specified path exists. // TODO: clean this up to use pkg/util/file/FileExists func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } else if os.IsNotExist(err) { return false, nil } else if IsCorruptedMnt(err) { return true, err } return false, err } golang-k8s-mount-utils-1.36.0~beta0/mount_helper_windows_test.go000066400000000000000000000041641516076520600250410ustar00rootroot00000000000000//go:build windows /* 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 ( "testing" ) func TestNormalizeWindowsPath(t *testing.T) { path := `/var/lib/kubelet/pods/146f8428-83e7-11e7-8dd4-000d3a31dac4/volumes/kubernetes.io~azure-disk` normalizedPath := NormalizeWindowsPath(path) if normalizedPath != `c:\var\lib\kubelet\pods\146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` { t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) } path = `/var/lib/kubelet/pods/146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` normalizedPath = NormalizeWindowsPath(path) if normalizedPath != `c:\var\lib\kubelet\pods\146f8428-83e7-11e7-8dd4-000d3a31dac4\volumes\kubernetes.io~azure-disk` { t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) } path = `/` normalizedPath = NormalizeWindowsPath(path) if normalizedPath != `c:\` { t.Errorf("normizeWindowsPath test failed, normalizedPath : %q", normalizedPath) } } func TestValidateDiskNumber(t *testing.T) { tests := []struct { diskNum string expectError bool }{ { diskNum: "0", expectError: false, }, { diskNum: "invalid", expectError: true, }, { diskNum: "99", expectError: false, }, { diskNum: "100", expectError: false, }, { diskNum: "200", expectError: false, }, } for _, test := range tests { err := ValidateDiskNumber(test.diskNum) if (err != nil) != test.expectError { t.Errorf("TestValidateDiskNumber test failed, disk number: %s, error: %v", test.diskNum, err) } } } golang-k8s-mount-utils-1.36.0~beta0/mount_linux.go000066400000000000000000001046361516076520600221150ustar00rootroot00000000000000//go:build linux /* Copyright 2014 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 ( "context" "errors" "fmt" "io/fs" "os" "os/exec" "path/filepath" "strconv" "strings" "syscall" "time" "github.com/moby/sys/mountinfo" "golang.org/x/sys/unix" "k8s.io/klog/v2" utilexec "k8s.io/utils/exec" ) const ( // Number of fields per line in /proc/mounts as per the fstab man page. expectedNumFieldsPerLine = 6 // Location of the mount file to use procMountsPath = "/proc/mounts" // Location of the mountinfo file procMountInfoPath = "/proc/self/mountinfo" // 'fsck' found errors and corrected them fsckErrorsCorrected = 1 // 'fsck' found errors but exited without correcting them fsckErrorsUncorrected = 4 // Error thrown by exec cmd.Run() when process spawned by cmd.Start() completes before cmd.Wait() is called (see - k/k issue #103753) errNoChildProcesses = "wait: no child processes" // Error returned by some `umount` implementations when the specified path is not a mount point errNotMounted = "not mounted" ) var ( // Error statx support since Linux 4.11, https://man7.org/linux/man-pages/man2/statx.2.html errStatxNotSupport = errors.New("the statx syscall is not supported. At least Linux kernel 4.11 is needed") ) // Mounter provides the default implementation of mount.Interface // for the linux platform. This implementation assumes that the // kubelet is running in the host's root mount namespace. type Mounter struct { mounterPath string withSystemd *bool trySystemd bool withSafeNotMountedBehavior bool } var _ MounterForceUnmounter = &Mounter{} // New returns a mount.Interface for the current system. // It provides options to override the default mounter behavior. // mounterPath allows using an alternative to `/bin/mount` for mounting. func New(mounterPath string) Interface { return &Mounter{ mounterPath: mounterPath, trySystemd: true, withSafeNotMountedBehavior: detectSafeNotMountedBehavior(), } } // NewWithoutSystemd returns a Linux specific mount.Interface for the current // system. It provides options to override the default mounter behavior. // mounterPath allows using an alternative to `/bin/mount` for mounting. Any // detection for systemd functionality is disabled with this Mounter. func NewWithoutSystemd(mounterPath string) Interface { return &Mounter{ mounterPath: mounterPath, trySystemd: false, withSafeNotMountedBehavior: detectSafeNotMountedBehavior(), } } // hasSystemd validates that the withSystemd bool is set, if it is not, // detectSystemd will be called once for this Mounter instance. func (mounter *Mounter) hasSystemd() bool { if !mounter.trySystemd { mounter.withSystemd = &mounter.trySystemd } if mounter.withSystemd == nil { withSystemd := detectSystemd() mounter.withSystemd = &withSystemd } return *mounter.withSystemd } // Map unix.Statfs mount flags ro, nodev, noexec, nosuid, noatime, relatime, // nodiratime to mount option flag strings. func getBindMountOptions(path string, statfs func(path string, buf *unix.Statfs_t) (err error)) ([]string, error) { var s unix.Statfs_t var mountOpts []string if err := statfs(path, &s); err != nil { return nil, &os.PathError{Op: "statfs", Path: path, Err: err} } flagMapping := map[int]string{ unix.MS_RDONLY: "ro", unix.MS_NODEV: "nodev", unix.MS_NOEXEC: "noexec", unix.MS_NOSUID: "nosuid", unix.MS_NOATIME: "noatime", unix.MS_RELATIME: "relatime", unix.MS_NODIRATIME: "nodiratime", } for k, v := range flagMapping { if int(s.Flags)&k == k { mountOpts = append(mountOpts, v) } } return mountOpts, nil } // Performs a bind mount with the specified options, and then remounts // the mount point with the same `nodev`, `nosuid`, `noexec`, `nosuid`, `noatime`, // `relatime`, `nodiratime` options as the original mount point. func (mounter *Mounter) bindMountSensitive(mounterPath string, mountCmd string, source string, target string, fstype string, bindOpts []string, bindRemountOpts []string, bindRemountOptsSensitive []string, mountFlags []string, systemdMountRequired bool) error { err := mounter.doMount(mounterPath, mountCmd, source, target, fstype, bindOpts, bindRemountOptsSensitive, mountFlags, systemdMountRequired) if err != nil { return err } // Check if the source has ro, nodev, noexec, nosuid, noatime, relatime, // nodiratime flag... fixMountOpts, err := getBindMountOptions(source, unix.Statfs) if err != nil { return &os.PathError{Op: "statfs", Path: source, Err: err} } // ... and retry the mount with flags found above. bindRemountOpts = append(bindRemountOpts, fixMountOpts...) return mounter.doMount(mounterPath, mountCmd, source, target, fstype, bindRemountOpts, bindRemountOptsSensitive, mountFlags, systemdMountRequired) } // Mount mounts source to target as fstype with given options. 'source' and 'fstype' must // be an empty string in case it's not required, e.g. for remount, or for auto filesystem // type, where kernel handles fstype for you. The mount 'options' is a list of options, // currently come from mount(8), e.g. "ro", "remount", "bind", etc. If no more option is // required, call Mount with an empty string list or nil. func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { return mounter.MountSensitive(source, target, fstype, options, nil) } // MountSensitive is the same as Mount() but this method allows // sensitiveOptions to be passed in a separate parameter from the normal // mount options and ensures the sensitiveOptions are never logged. This // method should be used by callers that pass sensitive material (like // passwords) as mount options. func (mounter *Mounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { // Path to mounter binary if containerized mounter is needed. Otherwise, it is set to empty. // All Linux distros are expected to be shipped with a mount utility that a support bind mounts. mounterPath := "" bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions) if bind { return mounter.bindMountSensitive(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOpts, bindRemountOptsSensitive, nil /* mountFlags */, mounter.trySystemd) } // The list of filesystems that require containerized mounter on GCI image cluster fsTypesNeedMounter := map[string]struct{}{ "nfs": {}, "glusterfs": {}, "ceph": {}, "cifs": {}, } if _, ok := fsTypesNeedMounter[fstype]; ok { mounterPath = mounter.mounterPath } return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, nil /* mountFlags */, mounter.trySystemd) } // MountSensitiveWithoutSystemd is the same as MountSensitive() but disable using systemd mount. func (mounter *Mounter) MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error { return mounter.MountSensitiveWithoutSystemdWithMountFlags(source, target, fstype, options, sensitiveOptions, nil /* mountFlags */) } // MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd with additional mount flags. func (mounter *Mounter) MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error { mounterPath := "" bind, bindOpts, bindRemountOpts, bindRemountOptsSensitive := MakeBindOptsSensitive(options, sensitiveOptions) if bind { return mounter.bindMountSensitive(mounterPath, defaultMountCommand, source, target, fstype, bindOpts, bindRemountOpts, bindRemountOptsSensitive, mountFlags, false) } // The list of filesystems that require containerized mounter on GCI image cluster fsTypesNeedMounter := map[string]struct{}{ "nfs": {}, "glusterfs": {}, "ceph": {}, "cifs": {}, } if _, ok := fsTypesNeedMounter[fstype]; ok { mounterPath = mounter.mounterPath } return mounter.doMount(mounterPath, defaultMountCommand, source, target, fstype, options, sensitiveOptions, mountFlags, false) } // doMount runs the mount command. mounterPath is the path to mounter binary if containerized mounter is used. // sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material) // systemdMountRequired is an extension of option to decide whether uses systemd mount. func (mounter *Mounter) doMount(mounterPath string, mountCmd string, source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string, systemdMountRequired bool) error { mountArgs, mountArgsLogStr := MakeMountArgsSensitiveWithMountFlags(source, target, fstype, options, sensitiveOptions, mountFlags) if len(mounterPath) > 0 { mountArgs = append([]string{mountCmd}, mountArgs...) mountArgsLogStr = mountCmd + " " + mountArgsLogStr mountCmd = mounterPath } if systemdMountRequired && mounter.hasSystemd() { // Try to run mount via systemd-run --scope. This will escape the // service where kubelet runs and any fuse daemons will be started in a // specific scope. kubelet service than can be restarted without killing // these fuse daemons. // // Complete command line (when mounterPath is not used): // systemd-run --description=... --scope -- mount -t // // Expected flow: // * systemd-run creates a transient scope (=~ cgroup) and executes its // argument (/bin/mount) there. // * mount does its job, forks a fuse daemon if necessary and finishes. // (systemd-run --scope finishes at this point, returning mount's exit // code and stdout/stderr - thats one of --scope benefits). // * systemd keeps the fuse daemon running in the scope (i.e. in its own // cgroup) until the fuse daemon dies (another --scope benefit). // Kubelet service can be restarted and the fuse daemon survives. // * When the fuse daemon dies (e.g. during unmount) systemd removes the // scope automatically. // // systemd-mount is not used because it's too new for older distros // (CentOS 7, Debian Jessie). mountCmd, mountArgs, mountArgsLogStr = AddSystemdScopeSensitive("systemd-run", target, mountCmd, mountArgs, mountArgsLogStr) // } else { // No systemd-run on the host (or we failed to check it), assume kubelet // does not run as a systemd service. // No code here, mountCmd and mountArgs are already populated. } // Logging with sensitive mount options removed. klog.V(4).Infof("Mounting cmd (%s) with arguments (%s)", mountCmd, mountArgsLogStr) command := exec.Command(mountCmd, mountArgs...) output, err := command.CombinedOutput() if err != nil { if err.Error() == errNoChildProcesses { if command.ProcessState.Success() { // We don't consider errNoChildProcesses an error if the process itself succeeded (see - k/k issue #103753). return nil } // Rewrite err with the actual exit error of the process. err = &exec.ExitError{ProcessState: command.ProcessState} } klog.Errorf("Mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s\n", err, mountCmd, mountArgsLogStr, string(output)) return fmt.Errorf("mount failed: %v\nMounting command: %s\nMounting arguments: %s\nOutput: %s", err, mountCmd, mountArgsLogStr, string(output)) } return err } // detectSystemd returns true if OS runs with systemd as init. When not sure // (permission errors, ...), it returns false. // There may be different ways how to detect systemd, this one makes sure that // systemd-runs (needed by Mount()) works. func detectSystemd() bool { if _, err := exec.LookPath("systemd-run"); err != nil { klog.V(2).Infof("Detected OS without systemd") return false } // Try to run systemd-run --scope /bin/true, that should be enough // to make sure that systemd is really running and not just installed, // which happens when running in a container with a systemd-based image // but with different pid 1. cmd := exec.Command("systemd-run", "--description=Kubernetes systemd probe", "--scope", "true") output, err := cmd.CombinedOutput() if err != nil { klog.V(2).Infof("Cannot run systemd-run, assuming non-systemd OS") klog.V(4).Infof("systemd-run output: %s, failed with: %v", string(output), err) return false } klog.V(2).Infof("Detected OS with systemd") return true } // detectSafeNotMountedBehavior returns true if the umount implementation replies "not mounted" // when the specified path is not mounted. When not sure (permission errors, ...), it returns false. // When possible, we will trust umount's message and avoid doing our own mount point checks. // More info: https://github.com/util-linux/util-linux/blob/v2.2/mount/umount.c#L179 func detectSafeNotMountedBehavior() bool { return detectSafeNotMountedBehaviorWithExec(utilexec.New()) } // detectSafeNotMountedBehaviorWithExec is for testing with FakeExec. func detectSafeNotMountedBehaviorWithExec(exec utilexec.Interface) bool { // create a temp dir and try to umount it path, err := os.MkdirTemp("", "kubelet-detect-safe-umount") if err != nil { klog.V(4).Infof("Cannot create temp dir to detect safe 'not mounted' behavior: %v", err) return false } defer os.RemoveAll(path) cmd := exec.Command("umount", path) output, err := cmd.CombinedOutput() if err != nil { if strings.Contains(string(output), errNotMounted) { klog.V(4).Infof("Detected umount with safe 'not mounted' behavior") return true } klog.V(4).Infof("'umount %s' failed with: %v, output: %s", path, err, string(output)) } klog.V(4).Infof("Detected umount with unsafe 'not mounted' behavior") return false } // MakeMountArgs makes the arguments to the mount(8) command. // options MUST not contain sensitive material (like passwords). func MakeMountArgs(source, target, fstype string, options []string) (mountArgs []string) { mountArgs, _ = MakeMountArgsSensitive(source, target, fstype, options, nil /* sensitiveOptions */) return mountArgs } // MakeMountArgsSensitive makes the arguments to the mount(8) command. // sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material) func MakeMountArgsSensitive(source, target, fstype string, options []string, sensitiveOptions []string) (mountArgs []string, mountArgsLogStr string) { return MakeMountArgsSensitiveWithMountFlags(source, target, fstype, options, sensitiveOptions, nil /* mountFlags */) } // MakeMountArgsSensitiveWithMountFlags makes the arguments to the mount(8) command. // sensitiveOptions is an extension of options except they will not be logged (because they may contain sensitive material) // mountFlags are additional mount flags that are not related with the fstype // and mount options func MakeMountArgsSensitiveWithMountFlags(source, target, fstype string, options []string, sensitiveOptions []string, mountFlags []string) (mountArgs []string, mountArgsLogStr string) { // Build mount command as follows: // mount [$mountFlags] [-t $fstype] [-o $options] [$source] $target mountArgs = []string{} mountArgsLogStr = "" mountArgs = append(mountArgs, mountFlags...) mountArgsLogStr += strings.Join(mountFlags, " ") if len(fstype) > 0 { mountArgs = append(mountArgs, "-t", fstype) mountArgsLogStr += strings.Join(mountArgs, " ") } if len(options) > 0 || len(sensitiveOptions) > 0 { combinedOptions := []string{} combinedOptions = append(combinedOptions, options...) combinedOptions = append(combinedOptions, sensitiveOptions...) mountArgs = append(mountArgs, "-o", strings.Join(combinedOptions, ",")) // exclude sensitiveOptions from log string mountArgsLogStr += " -o " + sanitizedOptionsForLogging(options, sensitiveOptions) } if len(source) > 0 { mountArgs = append(mountArgs, source) mountArgsLogStr += " " + source } mountArgs = append(mountArgs, target) mountArgsLogStr += " " + target return mountArgs, mountArgsLogStr } // AddSystemdScope adds "system-run --scope" to given command line // If args contains sensitive material, use AddSystemdScopeSensitive to construct // a safe to log string. func AddSystemdScope(systemdRunPath, mountName, command string, args []string) (string, []string) { descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName) systemdRunArgs := []string{descriptionArg, "--scope", "--", command} return systemdRunPath, append(systemdRunArgs, args...) } // AddSystemdScopeSensitive adds "system-run --scope" to given command line // It also accepts takes a sanitized string containing mount arguments, mountArgsLogStr, // and returns the string appended to the systemd command for logging. func AddSystemdScopeSensitive(systemdRunPath, mountName, command string, args []string, mountArgsLogStr string) (string, []string, string) { descriptionArg := fmt.Sprintf("--description=Kubernetes transient mount for %s", mountName) systemdRunArgs := []string{descriptionArg, "--scope", "--", command} return systemdRunPath, append(systemdRunArgs, args...), strings.Join(systemdRunArgs, " ") + " " + mountArgsLogStr } // Unmount unmounts the target. // If the mounter has safe "not mounted" behavior, no error will be returned when the target is not a mount point. func (mounter *Mounter) Unmount(target string) error { klog.V(4).Infof("Unmounting %s", target) command := exec.Command("umount", target) output, err := command.CombinedOutput() if err != nil { return checkUmountError(target, command, output, err, mounter.withSafeNotMountedBehavior) } return nil } // UnmountWithForce unmounts given target but will retry unmounting with force option // after given timeout. func (mounter *Mounter) UnmountWithForce(target string, umountTimeout time.Duration) error { err := tryUnmount(target, mounter.withSafeNotMountedBehavior, umountTimeout) if err != nil { if err == context.DeadlineExceeded { klog.V(2).Infof("Timed out waiting for unmount of %s, trying with -f", target) err = forceUmount(target, mounter.withSafeNotMountedBehavior) } return err } return nil } // List returns a list of all mounted filesystems. func (*Mounter) List() ([]MountPoint, error) { return ListProcMounts(procMountsPath) } func statx(file string) (unix.Statx_t, error) { var stat unix.Statx_t if err := unix.Statx(unix.AT_FDCWD, file, unix.AT_STATX_DONT_SYNC, 0, &stat); err != nil { if err == unix.ENOSYS { return stat, errStatxNotSupport } return stat, err } return stat, nil } func (mounter *Mounter) isLikelyNotMountPointStat(file string) (bool, error) { stat, err := os.Stat(file) if err != nil { return true, err } rootStat, err := os.Stat(filepath.Dir(strings.TrimSuffix(file, "/"))) if err != nil { return true, err } // If the directory has a different device as parent, then it is a mountpoint. if stat.Sys().(*syscall.Stat_t).Dev != rootStat.Sys().(*syscall.Stat_t).Dev { return false, nil } return true, nil } func (mounter *Mounter) isLikelyNotMountPointStatx(file string) (bool, error) { var stat, rootStat unix.Statx_t var err error if stat, err = statx(file); err != nil { return true, err } if stat.Attributes_mask != 0 { if stat.Attributes_mask&unix.STATX_ATTR_MOUNT_ROOT != 0 { if stat.Attributes&unix.STATX_ATTR_MOUNT_ROOT != 0 { // file is a mountpoint return false, nil } else { // no need to check rootStat if unix.STATX_ATTR_MOUNT_ROOT supported return true, nil } } } root := filepath.Dir(strings.TrimSuffix(file, "/")) if rootStat, err = statx(root); err != nil { return true, err } return (stat.Dev_major == rootStat.Dev_major && stat.Dev_minor == rootStat.Dev_minor), nil } // IsLikelyNotMountPoint determines if a directory is not a mountpoint. // It is fast but not necessarily ALWAYS correct. If the path is in fact // a bind mount from one part of a mount to another it will not be detected. // It also can not distinguish between mountpoints and symbolic links. // mkdir /tmp/a /tmp/b; mount --bind /tmp/a /tmp/b; IsLikelyNotMountPoint("/tmp/b") // will return true. When in fact /tmp/b is a mount point. If this situation // is of interest to you, don't use this function... func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { notMountPoint, err := mounter.isLikelyNotMountPointStatx(file) if errors.Is(err, errStatxNotSupport) { // fall back to isLikelyNotMountPointStat return mounter.isLikelyNotMountPointStat(file) } return notMountPoint, err } // CanSafelySkipMountPointCheck relies on the detected behavior of umount when given a target that is not a mount point. func (mounter *Mounter) CanSafelySkipMountPointCheck() bool { return mounter.withSafeNotMountedBehavior } // GetMountRefs finds all mount references to pathname, returns a // list of paths. Path could be a mountpoint or a normal // directory (for bind mount). func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { pathExists, pathErr := PathExists(pathname) if !pathExists { return []string{}, nil } else if IsCorruptedMnt(pathErr) { klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", pathname) return []string{}, nil } else if pathErr != nil { return nil, fmt.Errorf("error checking path %s: %v", pathname, pathErr) } realpath, err := filepath.EvalSymlinks(pathname) if err != nil { return nil, err } return SearchMountPoints(realpath, procMountInfoPath) } // checkAndRepairFileSystem checks and repairs filesystems using command fsck. func (mounter *SafeFormatAndMount) checkAndRepairFilesystem(source string) error { klog.V(4).Infof("Checking for issues with fsck on disk: %s", source) args := []string{"-a", source} out, err := mounter.Exec.Command("fsck", args...).CombinedOutput() if err != nil { ee, isExitError := err.(utilexec.ExitError) switch { case err == utilexec.ErrExecutableNotFound: klog.Warningf("'fsck' not found on system; continuing mount without running 'fsck'.") case isExitError && ee.ExitStatus() == fsckErrorsCorrected: klog.Infof("Device %s has errors which were corrected by fsck.", source) case isExitError && ee.ExitStatus() == fsckErrorsUncorrected: return NewMountError(HasFilesystemErrors, "'fsck' found errors on device %s but could not correct them: %s", source, string(out)) case isExitError && ee.ExitStatus() > fsckErrorsUncorrected: klog.Infof("`fsck` error %s", string(out)) default: klog.Warningf("fsck on device %s failed with error %v, output: %v", source, err, string(out)) } } return nil } // formatAndMount uses unix utils to format and mount the given disk func (mounter *SafeFormatAndMount) formatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error { readOnly := false for _, option := range options { if option == "ro" { readOnly = true break } } if !readOnly { // Check sensitiveOptions for ro for _, option := range sensitiveOptions { if option == "ro" { readOnly = true break } } } options = append(options, "defaults") mountErrorValue := UnknownMountError // Check if the disk is already formatted existingFormat, err := mounter.GetDiskFormat(source) if err != nil { return NewMountError(GetDiskFormatFailed, "failed to get disk format of disk %s: %v", source, err) } // Use 'ext4' as the default if len(fstype) == 0 { fstype = "ext4" } if existingFormat == "" { // Do not attempt to format the disk if mounting as readonly, return an error to reflect this. if readOnly { return NewMountError(UnformattedReadOnly, "cannot mount unformatted disk %s as we are manipulating it in read-only mode", source) } // Disk is unformatted so format it. args := []string{source} if fstype == "ext4" || fstype == "ext3" { args = []string{ "-F", // Force flag "-m0", // Zero blocks reserved for super-user source, } } else if fstype == "xfs" { args = []string{ "-f", // force flag source, } } args = append(formatOptions, args...) klog.Infof("Disk %q appears to be unformatted, attempting to format as type: %q with options: %v", source, fstype, args) output, err := mounter.format(fstype, args) if err != nil { // Do not log sensitiveOptions only options sensitiveOptionsLog := sanitizedOptionsForLogging(options, sensitiveOptions) detailedErr := fmt.Sprintf("format of disk %q failed: type:(%q) target:(%q) options:(%q) errcode:(%v) output:(%v) ", source, fstype, target, sensitiveOptionsLog, err, string(output)) klog.Error(detailedErr) return NewMountError(FormatFailed, "%s", detailedErr) } klog.Infof("Disk successfully formatted (mkfs): %s - %s %s", fstype, source, target) } else { if fstype != existingFormat { // Verify that the disk is formatted with filesystem type we are expecting mountErrorValue = FilesystemMismatch klog.Warningf("Configured to mount disk %s as %s but current format is %s, things might break", source, fstype, existingFormat) } if !readOnly { // Run check tools on the disk to fix repairable issues, only do this for formatted volumes requested as rw. err := mounter.checkAndRepairFilesystem(source) if err != nil { return err } } } // Mount the disk klog.V(4).Infof("Attempting to mount disk %s in %s format at %s", source, fstype, target) if err := mounter.MountSensitive(source, target, fstype, options, sensitiveOptions); err != nil { return NewMountError(mountErrorValue, "%s", err.Error()) } return nil } func (mounter *SafeFormatAndMount) format(fstype string, args []string) ([]byte, error) { if mounter.formatSem != nil { done := make(chan struct{}) defer close(done) mounter.formatSem <- struct{}{} go func() { defer func() { <-mounter.formatSem }() timeout := time.NewTimer(mounter.formatTimeout) defer timeout.Stop() select { case <-done: case <-timeout.C: } }() } return mounter.Exec.Command("mkfs."+fstype, args...).CombinedOutput() } func getDiskFormat(exec utilexec.Interface, disk string) (string, error) { args := []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", disk} klog.V(4).Infof("Attempting to determine if disk %q is formatted using blkid with args: (%v)", disk, args) dataOut, err := exec.Command("blkid", args...).CombinedOutput() output := string(dataOut) klog.V(4).Infof("Output: %q", output) if err != nil { if exit, ok := err.(utilexec.ExitError); ok { if exit.ExitStatus() == 2 { // Disk device is unformatted. // For `blkid`, if the specified token (TYPE/PTTYPE, etc) was // not found, or no (specified) devices could be identified, an // exit code of 2 is returned. return "", nil } } klog.Errorf("Could not determine if disk %q is formatted (%v)", disk, err) return "", err } var fstype, pttype string lines := strings.Split(output, "\n") for _, l := range lines { if len(l) <= 0 { // Ignore empty line. continue } cs := strings.Split(l, "=") if len(cs) != 2 { return "", fmt.Errorf("blkid returns invalid output: %s", output) } // TYPE is filesystem type, and PTTYPE is partition table type, according // to https://www.kernel.org/pub/linux/utils/util-linux/v2.21/libblkid-docs/. if cs[0] == "TYPE" { fstype = cs[1] } else if cs[0] == "PTTYPE" { pttype = cs[1] } } if len(pttype) > 0 { klog.V(4).Infof("Disk %s detected partition table type: %s", disk, pttype) // Returns a special non-empty string as filesystem type, then kubelet // will not format it. return "unknown data, probably partitions", nil } return fstype, nil } // GetDiskFormat uses 'blkid' to see if the given disk is unformatted func (mounter *SafeFormatAndMount) GetDiskFormat(disk string) (string, error) { return getDiskFormat(mounter.Exec, disk) } // ListProcMounts returns a list of all mounted filesystems. func ListProcMounts(mountFilePath string) ([]MountPoint, error) { content, err := readMountInfo(mountFilePath) if err != nil { return nil, err } return parseProcMounts(content) } func parseProcMounts(content []byte) ([]MountPoint, error) { out := []MountPoint{} lines := strings.Split(string(content), "\n") for _, line := range lines { if line == "" { // the last split() item is empty string following the last \n continue } fields := strings.Fields(line) if len(fields) != expectedNumFieldsPerLine { // Do not log line in case it contains sensitive Mount options return nil, fmt.Errorf("wrong number of fields (expected %d, got %d)", expectedNumFieldsPerLine, len(fields)) } mp := MountPoint{ Device: fields[0], Path: fields[1], Type: fields[2], Opts: strings.Split(fields[3], ","), } freq, err := strconv.Atoi(fields[4]) if err != nil { return nil, err } mp.Freq = freq pass, err := strconv.Atoi(fields[5]) if err != nil { return nil, err } mp.Pass = pass out = append(out, mp) } return out, nil } // SearchMountPoints finds all mount references to the source, returns a list of // mountpoints. // The source can be a mount point or a normal directory (bind mount). We // didn't support device because there is no use case by now. // Some filesystems may share a source name, e.g. tmpfs. And for bind mounting, // it's possible to mount a non-root path of a filesystem, so we need to use // root path and major:minor to represent mount source uniquely. func SearchMountPoints(hostSource, mountInfoPath string) ([]string, error) { mis, err := ParseMountInfo(mountInfoPath) if err != nil { return nil, err } mountID := 0 rootPath := "" major := -1 minor := -1 // Finding the underlying root path and major:minor if possible. // We need search in backward order because it's possible for later mounts // to overlap earlier mounts. for i := len(mis) - 1; i >= 0; i-- { if hostSource == mis[i].MountPoint || PathWithinBase(hostSource, mis[i].MountPoint) { // If it's a mount point or path under a mount point. mountID = mis[i].ID rootPath = filepath.Join(mis[i].Root, strings.TrimPrefix(hostSource, mis[i].MountPoint)) major = mis[i].Major minor = mis[i].Minor break } } if rootPath == "" || major == -1 || minor == -1 { return nil, fmt.Errorf("failed to get root path and major:minor for %s", hostSource) } var refs []string for i := range mis { if mis[i].ID == mountID { // Ignore mount entry for mount source itself. continue } if mis[i].Root == rootPath && mis[i].Major == major && mis[i].Minor == minor { refs = append(refs, mis[i].MountPoint) } } return refs, nil } // IsMountPoint determines if a file is a mountpoint. // It first detects bind & any other mountpoints using // MountedFast function. If the MountedFast function returns // sure as true and err as nil, then a mountpoint is detected // successfully. When an error is returned by MountedFast, the // following is true: // 1. All errors are returned with IsMountPoint as false // except os.IsPermission. // 2. When os.IsPermission is returned by MountedFast, List() // is called to confirm if the given file is a mountpoint are not. // // os.ErrNotExist should always be returned if a file does not exist // as callers have in past relied on this error and not fallback. // // When MountedFast returns sure as false and err as nil (eg: in // case of bindmounts on kernel version 5.10- ); mounter.List() // endpoint is called to enumerate all the mountpoints and check if // it is mountpoint match or not. func (mounter *Mounter) IsMountPoint(file string) (bool, error) { isMnt, sure, isMntErr := mountinfo.MountedFast(file) if sure && isMntErr == nil { return isMnt, nil } if isMntErr != nil { if errors.Is(isMntErr, fs.ErrNotExist) { return false, fs.ErrNotExist } // We were not allowed to do the simple stat() check, e.g. on NFS with // root_squash. Fall back to /proc/mounts check below when // fs.ErrPermission is returned. if !errors.Is(isMntErr, fs.ErrPermission) { return false, isMntErr } } // Resolve any symlinks in file, kernel would do the same and use the resolved path in /proc/mounts. resolvedFile, err := filepath.EvalSymlinks(file) if err != nil { if errors.Is(err, fs.ErrNotExist) { return false, fs.ErrNotExist } return false, err } // check all mountpoints since MountedFast is not sure. // is not reliable for some mountpoint types. mountPoints, mountPointsErr := mounter.List() if mountPointsErr != nil { return false, mountPointsErr } for _, mp := range mountPoints { if isMountPointMatch(mp, resolvedFile) { return true, nil } } return false, nil } // tryUnmount calls plain "umount" and waits for unmountTimeout for it to finish. func tryUnmount(target string, withSafeNotMountedBehavior bool, unmountTimeout time.Duration) error { klog.V(4).Infof("Unmounting %s", target) ctx, cancel := context.WithTimeout(context.Background(), unmountTimeout) defer cancel() command := exec.CommandContext(ctx, "umount", target) output, err := command.CombinedOutput() // CombinedOutput() does not return DeadlineExceeded, make sure it's // propagated on timeout. if ctx.Err() != nil { return ctx.Err() } if err != nil { return checkUmountError(target, command, output, err, withSafeNotMountedBehavior) } return nil } func forceUmount(target string, withSafeNotMountedBehavior bool) error { command := exec.Command("umount", "-f", target) output, err := command.CombinedOutput() if err != nil { return checkUmountError(target, command, output, err, withSafeNotMountedBehavior) } return nil } // checkUmountError checks a result of umount command and determine a return value. func checkUmountError(target string, command *exec.Cmd, output []byte, err error, withSafeNotMountedBehavior bool) error { if err.Error() == errNoChildProcesses { if command.ProcessState.Success() { // We don't consider errNoChildProcesses an error if the process itself succeeded (see - k/k issue #103753). return nil } // Rewrite err with the actual exit error of the process. err = &exec.ExitError{ProcessState: command.ProcessState} } if withSafeNotMountedBehavior && strings.Contains(string(output), errNotMounted) { klog.V(4).Infof("ignoring 'not mounted' error for %s", target) return nil } return fmt.Errorf("unmount failed: %v\nUnmounting arguments: %s\nOutput: %s", err, target, string(output)) } golang-k8s-mount-utils-1.36.0~beta0/mount_linux_test.go000066400000000000000000000737141516076520600231560ustar00rootroot00000000000000//go:build linux /* Copyright 2014 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 ( "errors" "fmt" "os" "os/exec" "path/filepath" "reflect" "sort" "strings" "sync" "testing" "time" "github.com/stretchr/testify/assert" "golang.org/x/sys/unix" utilexec "k8s.io/utils/exec" testexec "k8s.io/utils/exec/testing" "k8s.io/utils/ptr" ) func TestReadProcMountsFrom(t *testing.T) { successCase := `/dev/0 /path/to/0 type0 flags 0 0 /dev/1 /path/to/1 type1 flags 1 1 /dev/2 /path/to/2 type2 flags,1,2=3 2 2 ` // NOTE: readProcMountsFrom has been updated to using fnv.New32a() mounts, err := parseProcMounts([]byte(successCase)) if err != nil { t.Errorf("expected success, got %v", err) } if len(mounts) != 3 { t.Fatalf("expected 3 mounts, got %d", len(mounts)) } mp := MountPoint{"/dev/0", "/path/to/0", "type0", []string{"flags"}, 0, 0} if !mountPointsEqual(&mounts[0], &mp) { t.Errorf("got unexpected MountPoint[0]: %#v", mounts[0]) } mp = MountPoint{"/dev/1", "/path/to/1", "type1", []string{"flags"}, 1, 1} if !mountPointsEqual(&mounts[1], &mp) { t.Errorf("got unexpected MountPoint[1]: %#v", mounts[1]) } mp = MountPoint{"/dev/2", "/path/to/2", "type2", []string{"flags", "1", "2=3"}, 2, 2} if !mountPointsEqual(&mounts[2], &mp) { t.Errorf("got unexpected MountPoint[2]: %#v", mounts[2]) } errorCases := []string{ "/dev/0 /path/to/mount\n", "/dev/1 /path/to/mount type flags a 0\n", "/dev/2 /path/to/mount type flags 0 b\n", } for _, ec := range errorCases { _, err := parseProcMounts([]byte(ec)) if err == nil { t.Errorf("expected error") } } } func mountPointsEqual(a, b *MountPoint) bool { if a.Device != b.Device || a.Path != b.Path || a.Type != b.Type || !reflect.DeepEqual(a.Opts, b.Opts) || a.Pass != b.Pass || a.Freq != b.Freq { return false } return true } func TestGetMountRefs(t *testing.T) { fm := NewFakeMounter( []MountPoint{ {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, }) tests := []struct { mountPath string expectedRefs []string }{ { "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", []string{ "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", }, }, { "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", []string{ "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", }, }, { "/var/fake/directory/that/doesnt/exist", []string{}, }, } for i, test := range tests { if refs, err := fm.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { t.Errorf("%d. getMountRefs(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) } } } func setEquivalent(set1, set2 []string) bool { map1 := make(map[string]bool) map2 := make(map[string]bool) for _, s := range set1 { map1[s] = true } for _, s := range set2 { map2[s] = true } for s := range map1 { if !map2[s] { return false } } for s := range map2 { if !map1[s] { return false } } return true } func TestGetDeviceNameFromMount(t *testing.T) { fm := NewFakeMounter( []MountPoint{ { Device: "/dev/disk/by-path/prefix-lun-1", Path: "/mnt/111", }, { Device: "/dev/disk/by-path/prefix-lun-1", Path: "/mnt/222", }, }) tests := []struct { mountPath string expectedDevice string expectedRefs int }{ { "/mnt/222", "/dev/disk/by-path/prefix-lun-1", 2, }, } for i, test := range tests { if device, refs, err := GetDeviceNameFromMount(fm, test.mountPath); err != nil || test.expectedRefs != refs || test.expectedDevice != device { t.Errorf("%d. GetDeviceNameFromMount(%s) = (%s, %d), %v; expected (%s,%d), nil", i, test.mountPath, device, refs, err, test.expectedDevice, test.expectedRefs) } } } func TestGetMountRefsByDev(t *testing.T) { fm := NewFakeMounter( []MountPoint{ {Device: "/dev/sdb", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd"}, {Device: "/dev/sdb", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod"}, {Device: "/dev/sdc", Path: "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2"}, {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1"}, {Device: "/dev/sdc", Path: "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2"}, }) tests := []struct { mountPath string expectedRefs []string }{ { "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd", []string{ "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd-in-pod", }, }, { "/var/lib/kubelet/plugins/kubernetes.io/gce-pd/mounts/gce-pd2", []string{ "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod1", "/var/lib/kubelet/pods/some-pod/volumes/kubernetes.io~gce-pd/gce-pd2-in-pod2", }, }, } for i, test := range tests { if refs, err := getMountRefsByDev(fm, test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { t.Errorf("%d. getMountRefsByDev(%q) = %v, %v; expected %v, nil", i, test.mountPath, refs, err, test.expectedRefs) } } } func TestPathWithinBase(t *testing.T) { tests := []struct { name string fullPath string basePath string expected bool }{ { name: "good subpath", fullPath: "/a/b/c", basePath: "/a", expected: true, }, { name: "good subpath 2", fullPath: "/a/b/c", basePath: "/a/b", expected: true, }, { name: "good subpath end slash", fullPath: "/a/b/c/", basePath: "/a/b", expected: true, }, { name: "good subpath backticks", fullPath: "/a/b/../c", basePath: "/a", expected: true, }, { name: "good subpath equal", fullPath: "/a/b/c", basePath: "/a/b/c", expected: true, }, { name: "good subpath equal 2", fullPath: "/a/b/c/", basePath: "/a/b/c", expected: true, }, { name: "good subpath root", fullPath: "/a", basePath: "/", expected: true, }, { name: "bad subpath parent", fullPath: "/a/b/c", basePath: "/a/b/c/d", expected: false, }, { name: "bad subpath outside", fullPath: "/b/c", basePath: "/a/b/c", expected: false, }, { name: "bad subpath prefix", fullPath: "/a/b/cd", basePath: "/a/b/c", expected: false, }, { name: "bad subpath backticks", fullPath: "/a/../b", basePath: "/a", expected: false, }, { name: "configmap subpath", fullPath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config/..timestamp/file.txt", basePath: "/var/lib/kubelet/pods/uuid/volumes/kubernetes.io~configmap/config", expected: true, }, } for _, test := range tests { if PathWithinBase(test.fullPath, test.basePath) != test.expected { t.Errorf("test %q failed: expected %v", test.name, test.expected) } } } func TestSearchMountPoints(t *testing.T) { base := ` 19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw 20 25 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw 21 25 0:6 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=4058156k,nr_inodes=1014539,mode=755 22 21 0:14 / /dev/pts rw,nosuid,noexec,relatime shared:3 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 23 25 0:19 / /run rw,nosuid,noexec,relatime shared:5 - tmpfs tmpfs rw,size=815692k,mode=755 25 0 252:0 / / rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered 26 19 0:12 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:8 - securityfs securityfs rw 27 21 0:21 / /dev/shm rw,nosuid,nodev shared:4 - tmpfs tmpfs rw 28 23 0:22 / /run/lock rw,nosuid,nodev,noexec,relatime shared:6 - tmpfs tmpfs rw,size=5120k 29 19 0:23 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:9 - tmpfs tmpfs ro,mode=755 30 29 0:24 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:10 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd 31 19 0:25 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:11 - pstore pstore rw 32 29 0:26 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:13 - cgroup cgroup rw,devices 33 29 0:27 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:14 - cgroup cgroup rw,freezer 34 29 0:28 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,pids 35 29 0:29 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:16 - cgroup cgroup rw,blkio 36 29 0:30 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:17 - cgroup cgroup rw,memory 37 29 0:31 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:18 - cgroup cgroup rw,perf_event 38 29 0:32 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:19 - cgroup cgroup rw,hugetlb 39 29 0:33 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:20 - cgroup cgroup rw,cpu,cpuacct 40 29 0:34 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:21 - cgroup cgroup rw,cpuset 41 29 0:35 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:22 - cgroup cgroup rw,net_cls,net_prio 58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordere ` testcases := []struct { name string source string mountInfos string expectedRefs []string expectedErr error }{ { "dir", "/mnt/disks/vol1", base, nil, nil, }, { "dir-used", "/mnt/disks/vol1", base + ` 56 25 252:0 /mnt/disks/vol1 /var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered 57 25 0:45 / /mnt/disks/vol rw,relatime shared:36 - tmpfs tmpfs rw `, []string{"/var/lib/kubelet/pods/1890aef5-5a60-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, nil, }, { "tmpfs-vol", "/mnt/disks/vol1", base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k `, nil, nil, }, { "tmpfs-vol-used-by-two-pods", "/mnt/disks/vol1", base + `120 25 0:76 / /mnt/disks/vol1 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k 196 25 0:76 / /var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k 228 25 0:76 / /var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585 rw,relatime shared:41 - tmpfs vol1 rw,size=10000k `, []string{ "/var/lib/kubelet/pods/ade3ac21-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", "/var/lib/kubelet/pods/ac60532d-5a5b-11e8-8559-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-8f263585", }, nil, }, { "tmpfs-subdir-used-indirectly-via-bindmount-dir-by-one-pod", "/mnt/vol1/foo", base + `177 25 0:46 / /mnt/data rw,relatime shared:37 - tmpfs data rw 190 25 0:46 /vol1 /mnt/vol1 rw,relatime shared:37 - tmpfs data rw 191 25 0:46 /vol2 /mnt/vol2 rw,relatime shared:37 - tmpfs data rw 62 25 0:46 /vol1/foo /var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:37 - tmpfs data rw `, []string{"/var/lib/kubelet/pods/e25f2f01-5b06-11e8-8694-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, nil, }, { "dir-bindmounted", "/mnt/disks/vol2", base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered `, nil, nil, }, { "dir-bindmounted-used-by-one-pod", "/mnt/disks/vol2", base + `342 25 252:0 /mnt/disks/vol2 /mnt/disks/vol2 rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered 77 25 252:0 /mnt/disks/vol2 /var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c rw,relatime shared:1 - ext4 /dev/mapper/ubuntu--vg-root rw,errors=remount-ro,data=ordered `, []string{"/var/lib/kubelet/pods/f30dc360-5a5d-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-1fb30a1c"}, nil, }, { "blockfs", "/mnt/disks/blkvol1", base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered `, nil, nil, }, { "blockfs-used-by-one-pod", "/mnt/disks/blkvol1", base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered 62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered `, []string{"/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test"}, nil, }, { "blockfs-used-by-two-pods", "/mnt/disks/blkvol1", base + `58 25 7:1 / /mnt/disks/blkvol1 rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered 62 25 7:1 / /var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered 95 25 7:1 / /var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test rw,relatime shared:38 - ext4 /dev/loop1 rw,data=ordered `, []string{ "/var/lib/kubelet/pods/f19fe4e2-5a63-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test", "/var/lib/kubelet/pods/4854a48b-5a64-11e8-962f-000c29bb0377/volumes/kubernetes.io~local-volume/local-pv-test", }, nil, }, } tmpFile, err := os.CreateTemp("", "test-get-filetype") if err != nil { t.Fatal(err) } defer os.Remove(tmpFile.Name()) defer tmpFile.Close() for _, v := range testcases { assert.NoError(t, tmpFile.Truncate(0)) _, err := tmpFile.Seek(0, 0) assert.NoError(t, err) _, err = tmpFile.WriteString(v.mountInfos) assert.NoError(t, err) assert.NoError(t, tmpFile.Sync()) refs, err := SearchMountPoints(v.source, tmpFile.Name()) if !reflect.DeepEqual(refs, v.expectedRefs) { t.Errorf("test %q: expected Refs: %#v, got %#v", v.name, v.expectedRefs, refs) } if err != v.expectedErr { t.Errorf("test %q: expected err: %v, got %v", v.name, v.expectedErr, err) } } } func TestSensitiveMountOptions(t *testing.T) { // Arrange testcases := []struct { source string target string fstype string options []string sensitiveOptions []string mountFlags []string }{ { source: "mySrc", target: "myTarget", fstype: "myFS", options: []string{"o1", "o2"}, sensitiveOptions: []string{"s1", "s2"}, mountFlags: []string{}, }, { source: "mySrc", target: "myTarget", fstype: "myFS", options: []string{}, sensitiveOptions: []string{"s1", "s2"}, mountFlags: []string{}, }, { source: "mySrc", target: "myTarget", fstype: "myFS", options: []string{"o1", "o2"}, sensitiveOptions: []string{}, mountFlags: []string{}, }, { source: "mySrc", target: "myTarget", fstype: "myFS", options: []string{"o1", "o2"}, sensitiveOptions: []string{"s1", "s2"}, mountFlags: []string{"--no-canonicalize"}, }, } for _, v := range testcases { // Act mountArgs, mountArgsLogStr := MakeMountArgsSensitiveWithMountFlags(v.source, v.target, v.fstype, v.options, v.sensitiveOptions, v.mountFlags) // Assert t.Logf("\r\nmountArgs =%q\r\nmountArgsLogStr=%q", mountArgs, mountArgsLogStr) for _, mountFlag := range v.mountFlags { if found := mountArgsContainString(t, mountArgs, mountFlag); !found { t.Errorf("Expected mountFlag (%q) to exist in returned mountArgs (%q), but it does not", mountFlag, mountArgs) } if !strings.Contains(mountArgsLogStr, mountFlag) { t.Errorf("Expected mountFlag (%q) to exist in returned mountArgsLogStr (%q), but it does", mountFlag, mountArgsLogStr) } } for _, option := range v.options { if found := mountArgsContainOption(t, mountArgs, option); !found { t.Errorf("Expected option (%q) to exist in returned mountArgs (%q), but it does not", option, mountArgs) } if !strings.Contains(mountArgsLogStr, option) { t.Errorf("Expected option (%q) to exist in returned mountArgsLogStr (%q), but it does", option, mountArgsLogStr) } } for _, sensitiveOption := range v.sensitiveOptions { if found := mountArgsContainOption(t, mountArgs, sensitiveOption); !found { t.Errorf("Expected sensitiveOption (%q) to exist in returned mountArgs (%q), but it does not", sensitiveOption, mountArgs) } if strings.Contains(mountArgsLogStr, sensitiveOption) { t.Errorf("Expected sensitiveOption (%q) to not exist in returned mountArgsLogStr (%q), but it does", sensitiveOption, mountArgsLogStr) } } } } func TestHasSystemd(t *testing.T) { mounter := &Mounter{} _ = mounter.hasSystemd() if mounter.withSystemd == nil { t.Error("Failed to run detectSystemd()") } } func mountArgsContainString(t *testing.T, mountArgs []string, wanted string) bool { for _, mountArg := range mountArgs { if mountArg == wanted { return true } } return false } func mountArgsContainOption(t *testing.T, mountArgs []string, option string) bool { optionsIndex := -1 for i, s := range mountArgs { if s == "-o" { optionsIndex = i + 1 break } } if optionsIndex < 0 || optionsIndex >= len(mountArgs) { return false } return strings.Contains(mountArgs[optionsIndex], option) } func TestDetectSafeNotMountedBehavior(t *testing.T) { // Example output for umount from util-linux 2.30.2 notMountedOutput := "umount: /foo: not mounted." testcases := []struct { fakeCommandAction testexec.FakeCommandAction expectedSafe bool }{ { fakeCommandAction: makeFakeCommandAction(notMountedOutput, errors.New("any error"), nil), expectedSafe: true, }, { fakeCommandAction: makeFakeCommandAction(notMountedOutput, nil, nil), expectedSafe: false, }, { fakeCommandAction: makeFakeCommandAction("any output", nil, nil), expectedSafe: false, }, { fakeCommandAction: makeFakeCommandAction("any output", errors.New("any error"), nil), expectedSafe: false, }, } for _, v := range testcases { fakeexec := &testexec.FakeExec{ LookPathFunc: func(s string) (string, error) { return "fake-umount", nil }, CommandScript: []testexec.FakeCommandAction{v.fakeCommandAction}, } if detectSafeNotMountedBehaviorWithExec(fakeexec) != v.expectedSafe { var adj string if v.expectedSafe { adj = "safe" } else { adj = "unsafe" } t.Errorf("Expected to detect %s umount behavior, but did not", adj) } } } func TestCheckUmountError(t *testing.T) { target := "/test/path" withSafeNotMountedBehavior := true command := exec.Command("uname", "-r") // dummy command return status 0 if err := command.Run(); err != nil { t.Errorf("Faild to exec dummy command. err: %s", err) } testcases := []struct { output []byte err error expected bool }{ { err: errors.New("wait: no child processes"), expected: true, }, { output: []byte("umount: /test/path: not mounted."), err: errors.New("exit status 1"), expected: true, }, { output: []byte("umount: /test/path: No such file or directory"), err: errors.New("exit status 1"), expected: false, }, } for _, v := range testcases { if err := checkUmountError(target, command, v.output, v.err, withSafeNotMountedBehavior); (err == nil) != v.expected { if v.expected { t.Errorf("Expected to return nil, but did not. err: %s", err) } else { t.Errorf("Expected to return error, but did not.") } } } } // TODO https://github.com/kubernetes/kubernetes/pull/117539#discussion_r1181873355 func TestFormatConcurrency(t *testing.T) { const ( formatCount = 5 fstype = "ext4" output = "complete" defaultTimeout = 1 * time.Minute ) tests := []struct { desc string max int timeout time.Duration }{ { max: 2, }, { max: 3, }, { max: 4, }, } for _, tc := range tests { t.Run(fmt.Sprintf("max=%d,timeout=%s", tc.max, tc.timeout.String()), func(t *testing.T) { if tc.timeout == 0 { tc.timeout = defaultTimeout } var concurrent int var mu sync.Mutex witness := make(chan struct{}) exec := &testexec.FakeExec{} for i := 0; i < formatCount; i++ { exec.CommandScript = append(exec.CommandScript, makeFakeCommandAction(output, nil, func() { mu.Lock() concurrent++ mu.Unlock() <-witness mu.Lock() concurrent-- mu.Unlock() })) } mounter := NewSafeFormatAndMount(nil, exec, WithMaxConcurrentFormat(tc.max, tc.timeout)) // we run max+1 goroutines and block the command execution // only max goroutine should be running and the additional one should wait // for one to be released for i := 0; i < tc.max+1; i++ { go func() { _, err := mounter.format(fstype, nil) if err != nil { t.Errorf("format(%q): %v", fstype, err) } }() } // wait for all goorutines to be scheduled time.Sleep(100 * time.Millisecond) mu.Lock() if concurrent != tc.max { t.Errorf("SafeFormatAndMount.format() got concurrency: %d, want: %d", concurrent, tc.max) } mu.Unlock() // signal the commands to finish the goroutines, this will allow the command // that is pending to be executed for i := 0; i < tc.max; i++ { witness <- struct{}{} } // wait for all goroutines to acquire the lock and decrement the counter time.Sleep(100 * time.Millisecond) mu.Lock() if concurrent != 1 { t.Errorf("SafeFormatAndMount.format() got concurrency: %d, want: 1", concurrent) } mu.Unlock() // signal the pending command to finish, no more command should be running close(witness) // wait a few for the last goroutine to acquire the lock and decrements the counter down to zero time.Sleep(10 * time.Millisecond) mu.Lock() if concurrent != 0 { t.Errorf("SafeFormatAndMount.format() got concurrency: %d, want: 0", concurrent) } mu.Unlock() }) } } // TODO https://github.com/kubernetes/kubernetes/pull/117539#discussion_r1181873355 func TestFormatTimeout(t *testing.T) { const ( formatCount = 5 fstype = "ext4" output = "complete" maxConcurrency = 4 timeout = 200 * time.Millisecond ) var concurrent int var mu sync.Mutex witness := make(chan struct{}) exec := &testexec.FakeExec{} for i := 0; i < formatCount; i++ { exec.CommandScript = append(exec.CommandScript, makeFakeCommandAction(output, nil, func() { mu.Lock() concurrent++ mu.Unlock() <-witness mu.Lock() concurrent-- mu.Unlock() })) } mounter := NewSafeFormatAndMount(nil, exec, WithMaxConcurrentFormat(maxConcurrency, timeout)) for i := 0; i < maxConcurrency+1; i++ { go func() { _, err := mounter.format(fstype, nil) if err != nil { t.Errorf("format(%q): %v", fstype, err) } }() } // wait a bit more than the configured timeout time.Sleep(timeout + 100*time.Millisecond) mu.Lock() if concurrent != maxConcurrency+1 { t.Errorf("SafeFormatAndMount.format() got concurrency: %d, want: %d", concurrent, maxConcurrency+1) } mu.Unlock() // signal the pending commands to finish close(witness) // wait for all goroutines to acquire the lock and decrement the counter time.Sleep(100 * time.Millisecond) mu.Lock() if concurrent != 0 { t.Errorf("SafeFormatAndMount.format() got concurrency: %d, want: 0", concurrent) } mu.Unlock() } // Replicate some types found in "golang.org/x/exp/constraints" // to avoid a dependency on that package. type Signed interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } type Unsigned interface { ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr } type Integer interface { Signed | Unsigned } // Some platforms define unix.Statfs_t.Flags differently. Our need here is // pretty constrained, so some aggressive type-conversion is OK. func mkStatfsFlags[T1 Integer, T2 Integer](orig T1, add T2) T1 { return orig | T1(add) } func TestGetBindMountOptions(t *testing.T) { var testCases = map[string]struct { flags int32 // smallest size used by any platform we care about mountoptions string }{ "ro": {flags: unix.MS_RDONLY, mountoptions: "ro"}, "nodev": {flags: unix.MS_NODEV, mountoptions: "nodev"}, "noexec": {flags: unix.MS_NOEXEC, mountoptions: "noexec"}, "nosuid": {flags: unix.MS_NOSUID, mountoptions: "nosuid"}, "noatime": {flags: unix.MS_NOATIME, mountoptions: "noatime"}, "relatime": {flags: unix.MS_RELATIME, mountoptions: "relatime"}, "nodiratime": {flags: unix.MS_NODIRATIME, mountoptions: "nodiratime"}, "ronodev": {flags: unix.MS_RDONLY | unix.MS_NODEV, mountoptions: "nodev,ro"}, "ronodevnoexec": {flags: unix.MS_RDONLY | unix.MS_NODEV | unix.MS_NOEXEC, mountoptions: "nodev,noexec,ro"}, } statfsMock := func(path string, buf *unix.Statfs_t) (err error) { *buf = unix.Statfs_t{} buf.Flags = mkStatfsFlags(buf.Flags, testCases[path].flags) return nil } testGetBindMountOptionsSingleCase := func(t *testing.T) { path := strings.Split(t.Name(), "/")[1] options, _ := getBindMountOptions(path, statfsMock) sort.Strings(options) optionString := strings.Join(options, ",") mountOptions := testCases[path].mountoptions if optionString != mountOptions { t.Fatalf(`Mountoptions differ. Wanted: %s, returned: %s`, mountOptions, optionString) } } for k := range testCases { t.Run(k, testGetBindMountOptionsSingleCase) } } func makeFakeCommandAction(stdout string, err error, cmdFn func()) testexec.FakeCommandAction { c := testexec.FakeCmd{ CombinedOutputScript: []testexec.FakeAction{ func() ([]byte, []byte, error) { if cmdFn != nil { cmdFn() } return []byte(stdout), nil, err }, }, } return func(cmd string, args ...string) utilexec.Cmd { return testexec.InitFakeCmd(&c, cmd, args...) } } func TestIsLikelyNotMountPoint(t *testing.T) { mounter := Mounter{"fake/path", ptr.To(true), true, true} tests := []struct { fileName string targetLinkName string setUp func(base, fileName, targetLinkName string) error cleanUp func(base, fileName, targetLinkName string) error expectedResult bool expectError bool }{ { "Dir", "", func(base, fileName, targetLinkName string) error { return os.Mkdir(filepath.Join(base, fileName), 0o750) }, func(base, fileName, targetLinkName string) error { return os.Remove(filepath.Join(base, fileName)) }, true, false, }, { "InvalidDir", "", func(base, fileName, targetLinkName string) error { return nil }, func(base, fileName, targetLinkName string) error { return nil }, true, true, }, { "ValidSymLink", "targetSymLink", func(base, fileName, targetLinkName string) error { targeLinkPath := filepath.Join(base, targetLinkName) if err := os.Mkdir(targeLinkPath, 0o750); err != nil { return err } filePath := filepath.Join(base, fileName) if err := os.Symlink(targeLinkPath, filePath); err != nil { return err } return nil }, func(base, fileName, targetLinkName string) error { if err := os.Remove(filepath.Join(base, fileName)); err != nil { return err } return os.Remove(filepath.Join(base, targetLinkName)) }, true, false, }, { "InvalidSymLink", "targetSymLink2", func(base, fileName, targetLinkName string) error { targeLinkPath := filepath.Join(base, targetLinkName) if err := os.Mkdir(targeLinkPath, 0o750); err != nil { return err } filePath := filepath.Join(base, fileName) if err := os.Symlink(targeLinkPath, filePath); err != nil { return err } return os.Remove(targeLinkPath) }, func(base, fileName, targetLinkName string) error { return os.Remove(filepath.Join(base, fileName)) }, true, true, }, } for _, test := range tests { // test with absolute and relative path baseList := []string{t.TempDir(), "./"} for _, base := range baseList { if err := test.setUp(base, test.fileName, test.targetLinkName); err != nil { t.Fatalf("unexpected error in setUp(%s, %s): %v", test.fileName, test.targetLinkName, err) } filePath := filepath.Join(base, test.fileName) result, err := mounter.IsLikelyNotMountPoint(filePath) if result != test.expectedResult { t.Errorf("Expect result not equal with IsLikelyNotMountPoint(%s) return: %t, expected: %t", filePath, result, test.expectedResult) } if base == "./" { if err := test.cleanUp(base, test.fileName, test.targetLinkName); err != nil { t.Fatalf("unexpected error in cleanUp(%s, %s): %v", test.fileName, test.targetLinkName, err) } } if (err != nil) != test.expectError { if test.expectError { t.Errorf("Expect error during IsLikelyNotMountPoint(%s)", filePath) } else { t.Errorf("Expect error is nil during IsLikelyNotMountPoint(%s): %v", filePath, err) } } } } } golang-k8s-mount-utils-1.36.0~beta0/mount_test.go000066400000000000000000000156741516076520600217400ustar00rootroot00000000000000/* 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 mount import ( "fmt" "reflect" "strings" "testing" "time" ) func TestMakeBindOpts(t *testing.T) { tests := []struct { mountOption []string isBind bool expectedBindOpts []string expectedRemountOpts []string }{ { []string{"vers=2", "ro", "_netdev"}, false, []string{}, []string{}, }, { []string{"bind", "vers=2", "ro", "_netdev"}, true, []string{"bind", "_netdev"}, []string{"bind", "remount", "vers=2", "ro", "_netdev"}, }, } for _, test := range tests { bind, bindOpts, bindRemountOpts := MakeBindOpts(test.mountOption) if bind != test.isBind { t.Errorf("Expected bind to be %v but got %v", test.isBind, bind) } if test.isBind { if !reflect.DeepEqual(test.expectedBindOpts, bindOpts) { t.Errorf("Expected bind mount options to be %+v got %+v", test.expectedBindOpts, bindOpts) } if !reflect.DeepEqual(test.expectedRemountOpts, bindRemountOpts) { t.Errorf("Expected remount options to be %+v got %+v", test.expectedRemountOpts, bindRemountOpts) } } } } func TestMakeBindOptsSensitive(t *testing.T) { tests := []struct { mountOptions []string sensitiveMountOptions []string isBind bool expectedBindOpts []string expectedRemountOpts []string expectedSensitiveRemountOpts []string }{ { mountOptions: []string{"vers=2", "ro", "_netdev"}, sensitiveMountOptions: []string{"user=foo", "pass=bar"}, isBind: false, expectedBindOpts: []string{}, expectedRemountOpts: []string{}, expectedSensitiveRemountOpts: []string{"user=foo", "pass=bar"}, }, { mountOptions: []string{"vers=2", "ro", "_netdev"}, sensitiveMountOptions: []string{"user=foo", "pass=bar", "bind"}, isBind: true, expectedBindOpts: []string{"bind", "_netdev"}, expectedRemountOpts: []string{"bind", "remount", "vers=2", "ro", "_netdev"}, expectedSensitiveRemountOpts: []string{"user=foo", "pass=bar"}, }, { mountOptions: []string{"vers=2", "remount", "ro", "_netdev"}, sensitiveMountOptions: []string{"user=foo", "pass=bar"}, isBind: false, expectedBindOpts: []string{}, expectedRemountOpts: []string{}, expectedSensitiveRemountOpts: []string{"user=foo", "pass=bar"}, }, { mountOptions: []string{"vers=2", "ro", "_netdev"}, sensitiveMountOptions: []string{"user=foo", "pass=bar", "remount"}, isBind: false, expectedBindOpts: []string{}, expectedRemountOpts: []string{}, expectedSensitiveRemountOpts: []string{"user=foo", "pass=bar"}, }, { mountOptions: []string{"vers=2", "bind", "ro", "_netdev"}, sensitiveMountOptions: []string{"user=foo", "remount", "pass=bar"}, isBind: true, expectedBindOpts: []string{"bind", "_netdev"}, expectedRemountOpts: []string{"bind", "remount", "vers=2", "ro", "_netdev"}, expectedSensitiveRemountOpts: []string{"user=foo", "pass=bar"}, }, { mountOptions: []string{"vers=2", "bind", "ro", "_netdev"}, sensitiveMountOptions: []string{"user=foo", "remount", "pass=bar"}, isBind: true, expectedBindOpts: []string{"bind", "_netdev"}, expectedRemountOpts: []string{"bind", "remount", "vers=2", "ro", "_netdev"}, expectedSensitiveRemountOpts: []string{"user=foo", "pass=bar"}, }, } for _, test := range tests { bind, bindOpts, bindRemountOpts, bindRemountSensitiveOpts := MakeBindOptsSensitive(test.mountOptions, test.sensitiveMountOptions) if bind != test.isBind { t.Errorf("Expected bind to be %v but got %v", test.isBind, bind) } if test.isBind { if !reflect.DeepEqual(test.expectedBindOpts, bindOpts) { t.Errorf("Expected bind mount options to be %+v got %+v", test.expectedBindOpts, bindOpts) } if !reflect.DeepEqual(test.expectedRemountOpts, bindRemountOpts) { t.Errorf("Expected remount options to be %+v got %+v", test.expectedRemountOpts, bindRemountOpts) } if !reflect.DeepEqual(test.expectedSensitiveRemountOpts, bindRemountSensitiveOpts) { t.Errorf("Expected sensitive remount options to be %+v got %+v", test.expectedSensitiveRemountOpts, bindRemountSensitiveOpts) } } } } func TestOptionsForLogging(t *testing.T) { testcases := []struct { options []string sensitiveOptions []string }{ { options: []string{"o1", "o2"}, sensitiveOptions: []string{"s1"}, }, { options: []string{"o1", "o2"}, sensitiveOptions: []string{"s1", "s2"}, }, { sensitiveOptions: []string{"s1", "s2"}, }, { options: []string{"o1", "o2"}, }, {}, } for _, v := range testcases { maskedStr := sanitizedOptionsForLogging(v.options, v.sensitiveOptions) for _, sensitiveOption := range v.sensitiveOptions { if strings.Contains(maskedStr, sensitiveOption) { t.Errorf("Found sensitive log option %q in %q", sensitiveOption, maskedStr) } } actualCount := strings.Count(maskedStr, sensitiveOptionsRemoved) expectedCount := len(v.sensitiveOptions) if actualCount != expectedCount { t.Errorf("Found %v instances of %q in %q. Expected %v", actualCount, sensitiveOptionsRemoved, maskedStr, expectedCount) } } } func TestWithMaxConcurrentFormat(t *testing.T) { const timeout = 1 * time.Minute tests := []struct { max int wantSem bool }{ { max: 0, }, { max: -1, }, { max: 1, wantSem: true, }, { max: 3, wantSem: true, }, } for _, tc := range tests { t.Run(fmt.Sprintf("max=%d,timeout=%s", tc.max, timeout.String()), func(t *testing.T) { mounter := NewSafeFormatAndMount(nil, nil, WithMaxConcurrentFormat(tc.max, timeout)) if gotSem := mounter.formatSem != nil; gotSem != tc.wantSem { t.Errorf("NewSafeFormatAndMount() got formatSem: %t, want: %t", gotSem, tc.wantSem) } if tc.wantSem { if gotCap := cap(mounter.formatSem); gotCap != tc.max { t.Errorf("NewSafeFormatAndMount() got cap(formatSem): %d, want: %d", gotCap, tc.max) } if mounter.formatTimeout != timeout { t.Errorf("NewSafeFormatAndMount() got formatTimeout: %s, want: %s", mounter.formatTimeout.String(), timeout.String()) } } }) } } golang-k8s-mount-utils-1.36.0~beta0/mount_unsupported.go000066400000000000000000000071701516076520600233410ustar00rootroot00000000000000//go:build !linux && !windows /* Copyright 2014 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 ( "errors" ) // Mounter implements mount.Interface for unsupported platforms type Mounter struct { mounterPath string } var errUnsupported = errors.New("util/mount on this platform is not supported") // New returns a mount.Interface for the current system. // It provides options to override the default mounter behavior. // mounterPath allows using an alternative to `/bin/mount` for mounting. func New(mounterPath string) Interface { return &Mounter{ mounterPath: mounterPath, } } // Mount always returns an error on unsupported platforms func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { return errUnsupported } // MountSensitive always returns an error on unsupported platforms func (mounter *Mounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { return errUnsupported } // MountSensitiveWithoutSystemd always returns an error on unsupported platforms func (mounter *Mounter) MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error { return errUnsupported } // MountSensitiveWithoutSystemdWithMountFlags always returns an error on unsupported platforms func (mounter *Mounter) MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error { return errUnsupported } // Unmount always returns an error on unsupported platforms func (mounter *Mounter) Unmount(target string) error { return errUnsupported } // List always returns an error on unsupported platforms func (mounter *Mounter) List() ([]MountPoint, error) { return []MountPoint{}, errUnsupported } // IsLikelyNotMountPoint always returns an error on unsupported platforms func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { return true, errUnsupported } // CanSafelySkipMountPointCheck always returns false on unsupported platforms func (mounter *Mounter) CanSafelySkipMountPointCheck() bool { return false } // IsMountPoint determines if a directory is a mountpoint. // It always returns an error on unsupported platforms. func (mounter *Mounter) IsMountPoint(file string) (bool, error) { return false, errUnsupported } // GetMountRefs always returns an error on unsupported platforms func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { return nil, errUnsupported } func (mounter *SafeFormatAndMount) formatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error { return mounter.Interface.Mount(source, target, fstype, options) } func (mounter *SafeFormatAndMount) diskLooksUnformatted(disk string) (bool, error) { return true, errUnsupported } // IsMountPoint determines if a directory is a mountpoint. // It always returns an error on unsupported platforms. func (mounter *SafeFormatAndMount) IsMountPoint(file string) (bool, error) { return false, errUnsupported } golang-k8s-mount-utils-1.36.0~beta0/mount_windows.go000066400000000000000000000316321516076520600224430ustar00rootroot00000000000000//go:build windows /* 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" "os" "os/exec" "path/filepath" "strings" "k8s.io/klog/v2" "k8s.io/utils/keymutex" ) const ( accessDenied string = "access is denied" ) // Mounter provides the default implementation of mount.Interface // for the windows platform. This implementation assumes that the // kubelet is running in the host's root mount namespace. type Mounter struct { mounterPath string } // New returns a mount.Interface for the current system. // It provides options to override the default mounter behavior. // mounterPath allows using an alternative to `/bin/mount` for mounting. func New(mounterPath string) Interface { return &Mounter{ mounterPath: mounterPath, } } // acquire lock for smb mount var getSMBMountMutex = keymutex.NewHashed(0) // Mount : mounts source to target with given options. // currently only supports cifs(smb), bind mount(for disk) func (mounter *Mounter) Mount(source string, target string, fstype string, options []string) error { return mounter.MountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) } // MountSensitiveWithoutSystemd is the same as MountSensitive() but disable using ssytemd mount. // Windows not supported systemd mount, this function degrades to MountSensitive(). func (mounter *Mounter) MountSensitiveWithoutSystemd(source string, target string, fstype string, options []string, sensitiveOptions []string) error { return mounter.MountSensitive(source, target, fstype, options, sensitiveOptions /* sensitiveOptions */) } // MountSensitiveWithoutSystemdWithMountFlags is the same as MountSensitiveWithoutSystemd with additional mount flags // Windows not supported systemd mount, this function degrades to MountSensitive(). func (mounter *Mounter) MountSensitiveWithoutSystemdWithMountFlags(source string, target string, fstype string, options []string, sensitiveOptions []string, mountFlags []string) error { return mounter.MountSensitive(source, target, fstype, options, sensitiveOptions /* sensitiveOptions */) } // MountSensitive is the same as Mount() but this method allows // sensitiveOptions to be passed in a separate parameter from the normal // mount options and ensures the sensitiveOptions are never logged. This // method should be used by callers that pass sensitive material (like // passwords) as mount options. func (mounter *Mounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { target = NormalizeWindowsPath(target) sanitizedOptionsForLogging := sanitizedOptionsForLogging(options, sensitiveOptions) if source == "tmpfs" { klog.V(3).Infof("mounting source (%q), target (%q), with options (%q)", source, target, sanitizedOptionsForLogging) return os.MkdirAll(target, 0o755) } parentDir := filepath.Dir(target) if err := os.MkdirAll(parentDir, 0o755); err != nil { return err } klog.V(4).Infof("mount options(%q) source:%q, target:%q, fstype:%q, begin to mount", sanitizedOptionsForLogging, source, target, fstype) bindSource := source if bind, _, _, _ := MakeBindOptsSensitive(options, sensitiveOptions); bind { bindSource = NormalizeWindowsPath(source) } else { allOptions := []string{} allOptions = append(allOptions, options...) allOptions = append(allOptions, sensitiveOptions...) if len(allOptions) < 2 { return fmt.Errorf("mount options(%q) should have at least 2 options, current number:%d, source:%q, target:%q", sanitizedOptionsForLogging, len(allOptions), source, target) } // currently only cifs mount is supported if strings.ToLower(fstype) != "cifs" { return fmt.Errorf("only cifs mount is supported now, fstype: %q, mounting source (%q), target (%q), with options (%q)", fstype, source, target, sanitizedOptionsForLogging) } // lock smb mount for the same source getSMBMountMutex.LockKey(source) defer getSMBMountMutex.UnlockKey(source) username := allOptions[0] password := allOptions[1] if output, err := newSMBMapping(username, password, source); err != nil { klog.Warningf("SMB Mapping(%s) returned with error(%v), output(%s)", source, err, string(output)) if isSMBMappingExist(source) { valid, err := isValidPath(source) if !valid { if err == nil || isAccessDeniedError(err) { klog.V(2).Infof("SMB Mapping(%s) already exists while it's not valid, return error: %v, now begin to remove and remount", source, err) if output, err = removeSMBMapping(source); err != nil { return fmt.Errorf("Remove-SmbGlobalMapping failed: %v, output: %q", err, output) } if output, err := newSMBMapping(username, password, source); err != nil { return fmt.Errorf("New-SmbGlobalMapping(%s) failed: %v, output: %q", source, err, output) } } } else { klog.V(2).Infof("SMB Mapping(%s) already exists and is still valid, skip error(%v)", source, err) } } else { return fmt.Errorf("New-SmbGlobalMapping(%s) failed: %v, output: %q", source, err, output) } } } // There is an issue in golang where EvalSymlinks fails on Windows when passed a // UNC share root path without a trailing backslash. // Ex: \\SERVER\share will fail to resolve but \\SERVER\share\ will resolve // containerD on Windows calls EvalSymlinks so we'll add the backslash when making the symlink if it is missing. // https://github.com/golang/go/pull/42096 fixes this issue in golang but a fix will not be available until // golang v1.16 mklinkSource := bindSource if !strings.HasSuffix(mklinkSource, "\\") { mklinkSource = mklinkSource + "\\" } err := os.Symlink(mklinkSource, target) if err != nil { klog.Errorf("symlink failed: %v, source(%q) target(%q)", err, mklinkSource, target) return err } klog.V(2).Infof("symlink source(%q) on target(%q) successfully", mklinkSource, target) return nil } // do the SMB mount with username, password, remotepath // return (output, error) func newSMBMapping(username, password, remotepath string) (string, error) { if username == "" || password == "" || remotepath == "" { return "", fmt.Errorf("invalid parameter(username: %s, password: %s, remotepath: %s)", username, sensitiveOptionsRemoved, remotepath) } // use PowerShell Environment Variables to store user input string to prevent command line injection // https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_environment_variables?view=powershell-5.1 cmdLine := `$PWord = ConvertTo-SecureString -String $Env:smbpassword -AsPlainText -Force` + `;$Credential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $Env:smbuser, $PWord` + `;New-SmbGlobalMapping -RemotePath $Env:smbremotepath -Credential $Credential -RequirePrivacy $true` cmd := exec.Command("powershell", "/c", cmdLine) cmd.Env = append(os.Environ(), fmt.Sprintf("smbuser=%s", username), fmt.Sprintf("smbpassword=%s", password), fmt.Sprintf("smbremotepath=%s", remotepath)) output, err := cmd.CombinedOutput() return string(output), err } // check whether remotepath is already mounted func isSMBMappingExist(remotepath string) bool { cmd := exec.Command("powershell", "/c", `Get-SmbGlobalMapping -RemotePath $Env:smbremotepath`) cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotepath)) _, err := cmd.CombinedOutput() return err == nil } // check whether remotepath is valid // return (true, nil) if remotepath is valid func isValidPath(remotepath string) (bool, error) { cmd := exec.Command("powershell", "/c", `Test-Path $Env:remotepath`) cmd.Env = append(os.Environ(), fmt.Sprintf("remotepath=%s", remotepath)) output, err := cmd.CombinedOutput() if err != nil { return false, fmt.Errorf("returned output: %s, error: %v", string(output), err) } return strings.HasPrefix(strings.ToLower(string(output)), "true"), nil } func isAccessDeniedError(err error) bool { return err != nil && strings.Contains(strings.ToLower(err.Error()), accessDenied) } // remove SMB mapping func removeSMBMapping(remotepath string) (string, error) { cmd := exec.Command("powershell", "/c", `Remove-SmbGlobalMapping -RemotePath $Env:smbremotepath -Force`) cmd.Env = append(os.Environ(), fmt.Sprintf("smbremotepath=%s", remotepath)) output, err := cmd.CombinedOutput() return string(output), err } // Unmount unmounts the target. func (mounter *Mounter) Unmount(target string) error { klog.V(4).Infof("Unmount target (%q)", target) target = NormalizeWindowsPath(target) if err := os.Remove(target); err != nil { klog.Errorf("removing directory %s failed: %v", target, err) return err } return nil } // List returns a list of all mounted filesystems. todo func (mounter *Mounter) List() ([]MountPoint, error) { return []MountPoint{}, nil } // IsLikelyNotMountPoint determines if a directory is not a mountpoint. func (mounter *Mounter) IsLikelyNotMountPoint(file string) (bool, error) { stat, err := os.Lstat(file) if err != nil { return true, err } if stat.Mode()&os.ModeSymlink != 0 { return false, err } // go1.23 behavior change: https://github.com/golang/go/issues/63703#issuecomment-2535941458 if stat.Mode()&os.ModeIrregular != 0 { return false, err } return true, nil } // CanSafelySkipMountPointCheck always returns false on Windows func (mounter *Mounter) CanSafelySkipMountPointCheck() bool { return false } // IsMountPoint: determines if a directory is a mountpoint. func (mounter *Mounter) IsMountPoint(file string) (bool, error) { isNotMnt, err := mounter.IsLikelyNotMountPoint(file) if err != nil { return false, err } return !isNotMnt, nil } // GetMountRefs : empty implementation here since there is no place to query all mount points on Windows func (mounter *Mounter) GetMountRefs(pathname string) ([]string, error) { windowsPath := NormalizeWindowsPath(pathname) pathExists, pathErr := PathExists(windowsPath) if !pathExists { return []string{}, nil } else if IsCorruptedMnt(pathErr) { klog.Warningf("GetMountRefs found corrupted mount at %s, treating as unmounted path", windowsPath) return []string{}, nil } else if pathErr != nil { return nil, fmt.Errorf("error checking path %s: %v", windowsPath, pathErr) } return []string{pathname}, nil } func (mounter *SafeFormatAndMount) formatAndMountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string, formatOptions []string) error { // Try to mount the disk klog.V(4).Infof("Attempting to formatAndMount disk: %s %s %s", fstype, source, target) if err := ValidateDiskNumber(source); err != nil { klog.Errorf("diskMount: formatAndMount failed, err: %v", err) return err } if len(fstype) == 0 { // Use 'NTFS' as the default fstype = "NTFS" } if len(formatOptions) > 0 { return fmt.Errorf("diskMount: formatOptions are not supported on Windows") } cmdString := "Get-Disk -Number $env:source | Where partitionstyle -eq 'raw' | Initialize-Disk -PartitionStyle GPT -PassThru" + " | New-Partition -UseMaximumSize | Format-Volume -FileSystem $env:fstype -Confirm:$false" cmd := mounter.Exec.Command("powershell", "/c", cmdString) env := append(os.Environ(), fmt.Sprintf("source=%s", source), fmt.Sprintf("fstype=%s", fstype), ) cmd.SetEnv(env) klog.V(8).Infof("Executing command: %q", cmdString) if output, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("diskMount: format disk failed, error: %v, output: %q", err, string(output)) } klog.V(4).Infof("diskMount: Disk successfully formatted, disk: %q, fstype: %q", source, fstype) volumeIds, err := ListVolumesOnDisk(source) if err != nil { return err } driverPath := volumeIds[0] return mounter.MountSensitive(driverPath, target, fstype, options, sensitiveOptions) } // ListVolumesOnDisk - returns back list of volumes(volumeIDs) in the disk (requested in diskID). func ListVolumesOnDisk(diskID string) (volumeIDs []string, err error) { // If a Disk has multiple volumes, Get-Volume may not return items in the same order. cmd := exec.Command("powershell", "/c", "(Get-Disk -DeviceId $env:diskID | Get-Partition | Get-Volume | Sort-Object -Property UniqueId).UniqueId") cmd.Env = append(os.Environ(), fmt.Sprintf("diskID=%s", diskID)) klog.V(8).Infof("Executing command: %q", cmd.String()) output, err := cmd.CombinedOutput() klog.V(4).Infof("ListVolumesOnDisk id from %s: %s", diskID, string(output)) if err != nil { return []string{}, fmt.Errorf("error list volumes on disk. cmd: %s, output: %s, error: %v", cmd, string(output), err) } volumeIds := strings.Split(strings.TrimSpace(string(output)), "\r\n") return volumeIds, nil } golang-k8s-mount-utils-1.36.0~beta0/mount_windows_test.go000066400000000000000000000234611516076520600235030ustar00rootroot00000000000000//go:build windows /* 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" "os" "os/exec" "path/filepath" "testing" "github.com/stretchr/testify/assert" testingexec "k8s.io/utils/exec/testing" ) func makeLink(link, target string) error { if output, err := exec.Command("cmd", "/c", "mklink", "/D", link, target).CombinedOutput(); err != nil { return fmt.Errorf("mklink failed: %v, link(%q) target(%q) output: %q", err, link, target, string(output)) } return nil } func removeLink(link string) error { if output, err := exec.Command("cmd", "/c", "rmdir", link).CombinedOutput(); err != nil { return fmt.Errorf("rmdir failed: %v, output: %q", err, string(output)) } return nil } func setEquivalent(set1, set2 []string) bool { map1 := make(map[string]bool) map2 := make(map[string]bool) for _, s := range set1 { map1[s] = true } for _, s := range set2 { map2[s] = true } for s := range map1 { if !map2[s] { return false } } for s := range map2 { if !map1[s] { return false } } return true } // this func must run in admin mode, otherwise it will fail func TestGetMountRefs(t *testing.T) { tests := []struct { mountPath string expectedRefs []string }{ { mountPath: `c:\windows`, expectedRefs: []string{`c:\windows`}, }, { mountPath: `c:\doesnotexist`, expectedRefs: []string{}, }, } mounter := Mounter{"fake/path"} for _, test := range tests { if refs, err := mounter.GetMountRefs(test.mountPath); err != nil || !setEquivalent(test.expectedRefs, refs) { t.Errorf("getMountRefs(%q) = %v, error: %v; expected %v", test.mountPath, refs, err, test.expectedRefs) } } } func TestPathWithinBase(t *testing.T) { tests := []struct { fullPath string basePath string expectedResult bool }{ { fullPath: `c:\tmp\a\b\c`, basePath: `c:\tmp`, expectedResult: true, }, { fullPath: `c:\tmp1`, basePath: `c:\tmp2`, expectedResult: false, }, { fullPath: `c:\tmp`, basePath: `c:\tmp`, expectedResult: true, }, { fullPath: `c:\tmp`, basePath: `c:\tmp\a\b\c`, expectedResult: false, }, { fullPath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config\..timestamp\file.txt`, basePath: `c:\kubelet\pods\uuid\volumes\kubernetes.io~configmap\config`, expectedResult: true, }, } for _, test := range tests { result := PathWithinBase(test.fullPath, test.basePath) assert.Equal(t, result, test.expectedResult, "Expect result not equal with PathWithinBase(%s, %s) return: %q, expected: %q", test.fullPath, test.basePath, result, test.expectedResult) } } func TestIsLikelyNotMountPoint(t *testing.T) { mounter := Mounter{"fake/path"} tests := []struct { fileName string targetLinkName string setUp func(base, fileName, targetLinkName string) error expectedResult bool expectError bool }{ { "Dir", "", func(base, fileName, targetLinkName string) error { return os.Mkdir(filepath.Join(base, fileName), 0o750) }, true, false, }, { "InvalidDir", "", func(base, fileName, targetLinkName string) error { return nil }, true, true, }, { "ValidSymLink", "targetSymLink", func(base, fileName, targetLinkName string) error { targeLinkPath := filepath.Join(base, targetLinkName) if err := os.Mkdir(targeLinkPath, 0o750); err != nil { return err } filePath := filepath.Join(base, fileName) if err := makeLink(filePath, targeLinkPath); err != nil { return err } return nil }, false, false, }, { "InvalidSymLink", "targetSymLink2", func(base, fileName, targetLinkName string) error { targeLinkPath := filepath.Join(base, targetLinkName) if err := os.Mkdir(targeLinkPath, 0o750); err != nil { return err } filePath := filepath.Join(base, fileName) if err := makeLink(filePath, targeLinkPath); err != nil { return err } return removeLink(targeLinkPath) }, false, false, }, { "junction", "targetDir", func(base, fileName, targetLinkName string) error { target := filepath.Join(base, targetLinkName) if err := os.Mkdir(target, 0o750); err != nil { return err } // create a Junction file type on Windows junction := filepath.Join(base, fileName) if output, err := exec.Command("cmd", "/c", "mklink", "/J", junction, target).CombinedOutput(); err != nil { return fmt.Errorf("mklink failed: %v, link(%q) target(%q) output: %q", err, junction, target, string(output)) } return nil }, false, false, }, } for _, test := range tests { base := t.TempDir() if err := test.setUp(base, test.fileName, test.targetLinkName); err != nil { t.Fatalf("unexpected error in setUp(%s, %s): %v", test.fileName, test.targetLinkName, err) } filePath := filepath.Join(base, test.fileName) result, err := mounter.IsLikelyNotMountPoint(filePath) assert.Equal(t, test.expectedResult, result, "Expect result not equal with IsLikelyNotMountPoint(%s) return: %q, expected: %q", filePath, result, test.expectedResult) if test.expectError { assert.NotNil(t, err, "Expect error during IsLikelyNotMountPoint(%s)", filePath) } else { assert.Nil(t, err, "Expect error is nil during IsLikelyNotMountPoint(%s)", filePath) } } } func TestFormatAndMount(t *testing.T) { tests := []struct { device string target string fstype string execScripts []ExecArgs mountOptions []string expectError bool }{ { device: "0", target: "disk", fstype: "NTFS", execScripts: []ExecArgs{ {"powershell", []string{"/c", "Get-Disk", "-Number"}, "0", nil}, {"powershell", []string{"/c", "Get-Partition", "-DiskNumber"}, "0", nil}, {"cmd", []string{"/c", "mklink", "/D"}, "", nil}, }, mountOptions: []string{}, expectError: false, }, { device: "0", target: "disk", fstype: "", execScripts: []ExecArgs{ {"powershell", []string{"/c", "Get-Disk", "-Number"}, "0", nil}, {"powershell", []string{"/c", "Get-Partition", "-DiskNumber"}, "0", nil}, {"cmd", []string{"/c", "mklink", "/D"}, "", nil}, }, mountOptions: []string{}, expectError: false, }, { device: "invalidDevice", target: "disk", fstype: "NTFS", mountOptions: []string{}, expectError: true, }, } for _, test := range tests { fakeMounter := ErrorMounter{NewFakeMounter(nil), 0, nil} fakeExec := &testingexec.FakeExec{} for _, script := range test.execScripts { fakeCmd := &testingexec.FakeCmd{} cmdAction := makeFakeCmd(fakeCmd, script.command, script.args...) outputAction := makeFakeOutput(script.output, script.err) fakeCmd.CombinedOutputScript = append(fakeCmd.CombinedOutputScript, outputAction) fakeExec.CommandScript = append(fakeExec.CommandScript, cmdAction) } mounter := SafeFormatAndMount{ Interface: &fakeMounter, Exec: fakeExec, } target := filepath.Join(t.TempDir(), test.target) err := mounter.FormatAndMount(test.device, target, test.fstype, test.mountOptions) if test.expectError { assert.NotNil(t, err, "Expect error during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) } else { assert.Nil(t, err, "Expect error is nil during FormatAndMount(%s, %s, %s, %v)", test.device, test.target, test.fstype, test.mountOptions) } } } func TestNewSMBMapping(t *testing.T) { tests := []struct { username string password string remotepath string expectError bool }{ { "", "password", `\\remotepath`, true, }, { "username", "", `\\remotepath`, true, }, { "username", "password", "", true, }, } for _, test := range tests { _, err := newSMBMapping(test.username, test.password, test.remotepath) if test.expectError { assert.NotNil(t, err, "Expect error during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) } else { assert.Nil(t, err, "Expect error is nil during newSMBMapping(%s, %s, %s, %v)", test.username, test.password, test.remotepath) } } } func TestIsValidPath(t *testing.T) { tests := []struct { remotepath string expectedResult bool expectError bool }{ { "c:", true, false, }, { "invalid-path", false, false, }, } for _, test := range tests { result, err := isValidPath(test.remotepath) assert.Equal(t, result, test.expectedResult, "Expect result not equal with isValidPath(%s) return: %q, expected: %q, error: %v", test.remotepath, result, test.expectedResult, err) if test.expectError { assert.NotNil(t, err, "Expect error during isValidPath(%s)", test.remotepath) } else { assert.Nil(t, err, "Expect error is nil during isValidPath(%s)", test.remotepath) } } } func TestIsAccessDeniedError(t *testing.T) { tests := []struct { err error expectedResult bool }{ { nil, false, }, { fmt.Errorf("other error message"), false, }, { fmt.Errorf(`PathValid(\\xxx\share) failed with returned output: Test-Path : Access is denied`), true, }, } for _, test := range tests { result := isAccessDeniedError(test.err) assert.Equal(t, result, test.expectedResult, "Expect result not equal with isAccessDeniedError(%v) return: %q, expected: %q", test.err, result, test.expectedResult) } } golang-k8s-mount-utils-1.36.0~beta0/resizefs_linux.go000066400000000000000000000162441516076520600226020ustar00rootroot00000000000000//go:build linux /* Copyright 2021 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" "strconv" "strings" "k8s.io/klog/v2" utilexec "k8s.io/utils/exec" ) const ( blockDev = "blockdev" ) // ResizeFs Provides support for resizing file systems type ResizeFs struct { exec utilexec.Interface } // NewResizeFs returns new instance of resizer func NewResizeFs(exec utilexec.Interface) *ResizeFs { return &ResizeFs{exec: exec} } // Resize perform resize of file system func (resizefs *ResizeFs) Resize(devicePath string, deviceMountPath string) (bool, error) { format, err := getDiskFormat(resizefs.exec, devicePath) if err != nil { formatErr := fmt.Errorf("ResizeFS.Resize - error checking format for device %s: %v", devicePath, err) return false, formatErr } // If disk has no format, there is no need to resize the disk because mkfs.* // by default will use whole disk anyways. if format == "" { return false, nil } klog.V(3).Infof("ResizeFS.Resize - Expanding mounted volume %s", devicePath) switch format { case "ext3", "ext4": return resizefs.extResize(devicePath) case "xfs": return resizefs.xfsResize(deviceMountPath) case "btrfs": return resizefs.btrfsResize(deviceMountPath) } return false, fmt.Errorf("ResizeFS.Resize - resize of format %s is not supported for device %s mounted at %s", format, devicePath, deviceMountPath) } func (resizefs *ResizeFs) extResize(devicePath string) (bool, error) { output, err := resizefs.exec.Command("resize2fs", devicePath).CombinedOutput() if err == nil { klog.V(2).Infof("Device %s resized successfully", devicePath) return true, nil } resizeError := fmt.Errorf("resize of device %s failed: %v. resize2fs output: %s", devicePath, err, string(output)) return false, resizeError } func (resizefs *ResizeFs) xfsResize(deviceMountPath string) (bool, error) { args := []string{"-d", deviceMountPath} output, err := resizefs.exec.Command("xfs_growfs", args...).CombinedOutput() if err == nil { klog.V(2).Infof("Device %s resized successfully", deviceMountPath) return true, nil } resizeError := fmt.Errorf("resize of device %s failed: %v. xfs_growfs output: %s", deviceMountPath, err, string(output)) return false, resizeError } func (resizefs *ResizeFs) btrfsResize(deviceMountPath string) (bool, error) { args := []string{"filesystem", "resize", "max", deviceMountPath} output, err := resizefs.exec.Command("btrfs", args...).CombinedOutput() if err == nil { klog.V(2).Infof("Device %s resized successfully", deviceMountPath) return true, nil } resizeError := fmt.Errorf("resize of device %s failed: %v. btrfs output: %s", deviceMountPath, err, string(output)) return false, resizeError } func (resizefs *ResizeFs) NeedResize(devicePath string, deviceMountPath string) (bool, error) { // Do nothing if device is mounted as readonly readonly, err := resizefs.getDeviceRO(devicePath) if err != nil { return false, err } if readonly { klog.V(3).Infof("ResizeFs.needResize - no resize possible since filesystem %s is readonly", devicePath) return false, nil } format, err := getDiskFormat(resizefs.exec, devicePath) if err != nil { formatErr := fmt.Errorf("ResizeFS.Resize - error checking format for device %s: %v", devicePath, err) return false, formatErr } // If disk has no format, there is no need to resize the disk because mkfs.* // by default will use whole disk anyways. if format == "" { return false, nil } switch format { case "ext3", "ext4", "xfs": // For ext3/ext4/xfs, recommendation received from linux filesystem folks is to let // resize2fs/xfs_growfs do the check for us. So we will not do any check here. return true, nil case "btrfs": deviceSize, err := resizefs.getDeviceSize(devicePath) if err != nil { return false, err } blockSize, fsSize, err := resizefs.getBtrfsSize(devicePath) klog.V(5).Infof("Btrfs size: filesystem size=%d, block size=%d, err=%v", fsSize, blockSize, err) if err != nil { return false, err } if deviceSize <= fsSize+blockSize { return false, nil } return true, nil default: return false, fmt.Errorf("could not parse fs info of given filesystem format: %s. Supported fs types are: xfs, ext3, ext4", format) } } func (resizefs *ResizeFs) getDeviceSize(devicePath string) (uint64, error) { output, err := resizefs.exec.Command(blockDev, "--getsize64", devicePath).CombinedOutput() outStr := strings.TrimSpace(string(output)) if err != nil { return 0, fmt.Errorf("failed to read size of device %s: %s: %s", devicePath, err, outStr) } size, err := strconv.ParseUint(outStr, 10, 64) if err != nil { return 0, fmt.Errorf("failed to parse size of device %s %s: %s", devicePath, outStr, err) } return size, nil } func (resizefs *ResizeFs) getBtrfsSize(devicePath string) (uint64, uint64, error) { output, err := resizefs.exec.Command("btrfs", "inspect-internal", "dump-super", "-f", devicePath).CombinedOutput() if err != nil { return 0, 0, fmt.Errorf("failed to read size of filesystem on %s: %s: %s", devicePath, err, string(output)) } blockSize, totalBytes, _ := resizefs.parseBtrfsInfoOutput(string(output), "sectorsize", "total_bytes") if blockSize == 0 { return 0, 0, fmt.Errorf("could not find block size of device %s", devicePath) } if totalBytes == 0 { return 0, 0, fmt.Errorf("could not find total size of device %s", devicePath) } return blockSize, totalBytes, nil } func (resizefs *ResizeFs) parseBtrfsInfoOutput(cmdOutput string, blockSizeKey string, totalBytesKey string) (uint64, uint64, error) { lines := strings.Split(cmdOutput, "\n") var blockSize, blockCount uint64 var err error for _, line := range lines { tokens := strings.Fields(line) if len(tokens) != 2 { continue } key, value := strings.ToLower(strings.TrimSpace(tokens[0])), strings.ToLower(strings.TrimSpace(tokens[1])) if key == blockSizeKey { blockSize, err = strconv.ParseUint(value, 10, 64) if err != nil { return 0, 0, fmt.Errorf("failed to parse block size %s: %s", value, err) } } if key == totalBytesKey { blockCount, err = strconv.ParseUint(value, 10, 64) if err != nil { return 0, 0, fmt.Errorf("failed to parse total size %s: %s", value, err) } } } return blockSize, blockCount, err } func (resizefs *ResizeFs) getDeviceRO(devicePath string) (bool, error) { output, err := resizefs.exec.Command(blockDev, "--getro", devicePath).CombinedOutput() outStr := strings.TrimSpace(string(output)) if err != nil { return false, fmt.Errorf("failed to get readonly bit from device %s: %w: %s", devicePath, err, outStr) } switch outStr { case "0": return false, nil case "1": return true, nil default: return false, fmt.Errorf("failed readonly device check. Expected 1 or 0, got '%s'", outStr) } } golang-k8s-mount-utils-1.36.0~beta0/resizefs_linux_test.go000066400000000000000000000332411516076520600236350ustar00rootroot00000000000000//go:build linux /* Copyright 2021 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" "testing" "k8s.io/utils/exec" fakeexec "k8s.io/utils/exec/testing" ) func TestGetFileSystemSize(t *testing.T) { cmdOutputSuccessBtrfs := `superblock: bytenr=65536, device=/dev/loop0 --------------------------------------------------------- csum_type 0 (crc32c) csum_size 4 csum 0x31693b11 [match] bytenr 65536 flags 0x1 ( WRITTEN ) magic _BHRfS_M [match] fsid 3f53c8f7-3c57-4185-bf1d-b305b42cce97 metadata_uuid 3f53c8f7-3c57-4185-bf1d-b305b42cce97 label generation 7 root 30441472 sys_array_size 129 chunk_root_generation 6 root_level 0 chunk_root 22036480 chunk_root_level 0 log_root 0 log_root_transid 0 log_root_level 0 total_bytes 1048576000 bytes_used 147456 sectorsize 4096 nodesize 16384 leafsize (deprecated) 16384 stripesize 4096 root_dir 6 num_devices 1 compat_flags 0x0 compat_ro_flags 0x3 ( FREE_SPACE_TREE | FREE_SPACE_TREE_VALID ) incompat_flags 0x341 ( MIXED_BACKREF | EXTENDED_IREF | SKINNY_METADATA | NO_HOLES ) cache_generation 0 uuid_tree_generation 7 dev_item.uuid 987c8423-fba3-4168-9892-560a116feb81 dev_item.fsid 3f53c8f7-3c57-4185-bf1d-b305b42cce97 [match] dev_item.type 0 dev_item.total_bytes 1048576000 dev_item.bytes_used 130023424 dev_item.io_align 4096 dev_item.io_width 4096 dev_item.sector_size 4096 dev_item.devid 1 dev_item.dev_group 0 dev_item.seek_speed 0 dev_item.bandwidth 0 dev_item.generation 0 sys_chunk_array[2048]: item 0 key (FIRST_CHUNK_TREE CHUNK_ITEM 22020096) length 8388608 owner 2 stripe_len 65536 type SYSTEM|DUP io_align 65536 io_width 65536 sector_size 4096 num_stripes 2 sub_stripes 1 stripe 0 devid 1 offset 22020096 dev_uuid 987c8423-fba3-4168-9892-560a116feb81 stripe 1 devid 1 offset 30408704 dev_uuid 987c8423-fba3-4168-9892-560a116feb81 backup_roots[4]: backup 0: backup_tree_root: 30441472 gen: 5 level: 0 backup_chunk_root: 22020096 gen: 5 level: 0 backup_extent_root: 30474240 gen: 5 level: 0 backup_fs_root: 30425088 gen: 5 level: 0 backup_dev_root: 30457856 gen: 5 level: 0 backup_csum_root: 30490624 gen: 5 level: 0 backup_total_bytes: 1048576000 backup_bytes_used: 147456 backup_num_devices: 1 backup 1: backup_tree_root: 30588928 gen: 6 level: 0 backup_chunk_root: 22036480 gen: 6 level: 0 backup_extent_root: 30408704 gen: 6 level: 0 backup_fs_root: 30425088 gen: 5 level: 0 backup_dev_root: 30556160 gen: 6 level: 0 backup_csum_root: 30490624 gen: 5 level: 0 backup_total_bytes: 1048576000 backup_bytes_used: 147456 backup_num_devices: 1 backup 2: backup_tree_root: 30441472 gen: 7 level: 0 backup_chunk_root: 22036480 gen: 6 level: 0 backup_extent_root: 30474240 gen: 7 level: 0 backup_fs_root: 30425088 gen: 5 level: 0 backup_dev_root: 30457856 gen: 7 level: 0 backup_csum_root: 30490624 gen: 5 level: 0 backup_total_bytes: 1048576000 backup_bytes_used: 147456 backup_num_devices: 1 backup 3: backup_tree_root: 30408704 gen: 4 level: 0 backup_chunk_root: 1064960 gen: 4 level: 0 backup_extent_root: 5341184 gen: 4 level: 0 backup_fs_root: 5324800 gen: 3 level: 0 backup_dev_root: 5242880 gen: 4 level: 0 backup_csum_root: 1130496 gen: 1 level: 0 backup_total_bytes: 1048576000 backup_bytes_used: 114688 backup_num_devices: 1 ` cmdOutputNoDataBtrfs := `superblock: bytenr=65536, device=/dev/loop0 --------------------------------------------------------- csum_type 0 (crc32c) csum_size 4 csum 0x31693b11 [match] bytenr 65536 flags 0x1 ( WRITTEN ) magic _BHRfS_M [match] fsid 3f53c8f7-3c57-4185-bf1d-b305b42cce97 metadata_uuid 3f53c8f7-3c57-4185-bf1d-b305b42cce97 label generation 7 root 30441472 sys_array_size 129 chunk_root_generation 6 root_level 0 chunk_root 22036480 chunk_root_level 0 log_root 0 log_root_transid 0 log_root_level 0 bytes_used 147456 nodesize 16384 leafsize (deprecated) 16384 stripesize 4096 root_dir 6 num_devices 1 compat_flags 0x0 compat_ro_flags 0x3 ( FREE_SPACE_TREE | FREE_SPACE_TREE_VALID ) incompat_flags 0x341 ( MIXED_BACKREF | EXTENDED_IREF | SKINNY_METADATA | NO_HOLES ) cache_generation 0 uuid_tree_generation 7 dev_item.uuid 987c8423-fba3-4168-9892-560a116feb81 dev_item.fsid 3f53c8f7-3c57-4185-bf1d-b305b42cce97 [match] dev_item.type 0 dev_item.total_bytes 1048576000 dev_item.bytes_used 130023424 dev_item.io_align 4096 dev_item.io_width 4096 dev_item.sector_size 4096 dev_item.devid 1 dev_item.dev_group 0 dev_item.seek_speed 0 dev_item.bandwidth 0 dev_item.generation 0 sys_chunk_array[2048]: item 0 key (FIRST_CHUNK_TREE CHUNK_ITEM 22020096) length 8388608 owner 2 stripe_len 65536 type SYSTEM|DUP io_align 65536 io_width 65536 sector_size 4096 num_stripes 2 sub_stripes 1 stripe 0 devid 1 offset 22020096 dev_uuid 987c8423-fba3-4168-9892-560a116feb81 stripe 1 devid 1 offset 30408704 dev_uuid 987c8423-fba3-4168-9892-560a116feb81 backup_roots[4]: backup 0: backup_tree_root: 30441472 gen: 5 level: 0 backup_chunk_root: 22020096 gen: 5 level: 0 backup_extent_root: 30474240 gen: 5 level: 0 backup_fs_root: 30425088 gen: 5 level: 0 backup_dev_root: 30457856 gen: 5 level: 0 backup_csum_root: 30490624 gen: 5 level: 0 backup_total_bytes: 1048576000 backup_bytes_used: 147456 backup_num_devices: 1 backup 1: backup_tree_root: 30588928 gen: 6 level: 0 backup_chunk_root: 22036480 gen: 6 level: 0 backup_extent_root: 30408704 gen: 6 level: 0 backup_fs_root: 30425088 gen: 5 level: 0 backup_dev_root: 30556160 gen: 6 level: 0 backup_csum_root: 30490624 gen: 5 level: 0 backup_total_bytes: 1048576000 backup_bytes_used: 147456 backup_num_devices: 1 backup 2: backup_tree_root: 30441472 gen: 7 level: 0 backup_chunk_root: 22036480 gen: 6 level: 0 backup_extent_root: 30474240 gen: 7 level: 0 backup_fs_root: 30425088 gen: 5 level: 0 backup_dev_root: 30457856 gen: 7 level: 0 backup_csum_root: 30490624 gen: 5 level: 0 backup_total_bytes: 1048576000 backup_bytes_used: 147456 backup_num_devices: 1 backup 3: backup_tree_root: 30408704 gen: 4 level: 0 backup_chunk_root: 1064960 gen: 4 level: 0 backup_extent_root: 5341184 gen: 4 level: 0 backup_fs_root: 5324800 gen: 3 level: 0 backup_dev_root: 5242880 gen: 4 level: 0 backup_csum_root: 1130496 gen: 1 level: 0 backup_total_bytes: 1048576000 backup_bytes_used: 114688 backup_num_devices: 1 ` testcases := []struct { name string devicePath string blocksize uint64 blockCount uint64 cmdOutput string expectError bool fsType string }{ { name: "success parse btrfs info", devicePath: "/dev/test1", blocksize: 4096, blockCount: 256000, cmdOutput: cmdOutputSuccessBtrfs, expectError: false, fsType: "btrfs", }, { name: "block size not present - btrfs", devicePath: "/dev/test1", blocksize: 0, blockCount: 0, cmdOutput: cmdOutputNoDataBtrfs, expectError: true, fsType: "btrfs", }, } for _, test := range testcases { t.Run(test.name, func(t *testing.T) { fcmd := fakeexec.FakeCmd{ CombinedOutputScript: []fakeexec.FakeAction{ func() ([]byte, []byte, error) { return []byte(test.cmdOutput), nil, nil }, }, } fexec := &fakeexec.FakeExec{ CommandScript: []fakeexec.FakeCommandAction{ func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, }, } resizefs := ResizeFs{exec: fexec} blockSize, fsSize, err := resizefs.getBtrfsSize(test.devicePath) if blockSize != test.blocksize { t.Fatalf("Parse wrong block size value, expect %d, but got %d", test.blocksize, blockSize) } if fsSize != test.blocksize*test.blockCount { t.Fatalf("Parse wrong fs size value, expect %d, but got %d", test.blocksize*test.blockCount, fsSize) } if !test.expectError && err != nil { t.Fatalf("Expect no error but got %v", err) } }) } } func TestNeedResize(t *testing.T) { testcases := []struct { name string devicePath string deviceMountPath string readonly string deviceSize string extSize string cmdOutputFsType string expectError bool expectResult bool }{ { name: "True", devicePath: "/dev/test1", deviceMountPath: "/mnt/test1", readonly: "0", deviceSize: "2048", cmdOutputFsType: "TYPE=ext3", extSize: "20", expectError: false, expectResult: true, }, { name: "False - needed by size but fs is readonly", devicePath: "/dev/test1", deviceMountPath: "/mnt/test1", readonly: "1", deviceSize: "2048", cmdOutputFsType: "TYPE=ext3", extSize: "20", expectError: false, expectResult: false, }, { name: "False - Not needed by size for btrfs", devicePath: "/dev/test1", deviceMountPath: "/mnt/test1", readonly: "0", deviceSize: "20", cmdOutputFsType: "TYPE=btrfs", extSize: "2048", expectError: false, expectResult: false, }, { name: "True - needed by size for btrfs", devicePath: "/dev/test1", deviceMountPath: "/mnt/test1", readonly: "0", deviceSize: "2048", cmdOutputFsType: "TYPE=btrfs", extSize: "20", expectError: false, expectResult: true, }, { name: "False - Unsupported fs type", devicePath: "/dev/test1", deviceMountPath: "/mnt/test1", readonly: "0", deviceSize: "2048", extSize: "1", cmdOutputFsType: "TYPE=ntfs", expectError: true, expectResult: false, }, } for _, test := range testcases { t.Run(test.name, func(t *testing.T) { fcmd := fakeexec.FakeCmd{ CombinedOutputScript: []fakeexec.FakeAction{ func() ([]byte, []byte, error) { return []byte(test.readonly), nil, nil }, func() ([]byte, []byte, error) { return []byte(test.cmdOutputFsType), nil, nil }, }, } fexec := &fakeexec.FakeExec{ CommandScript: []fakeexec.FakeCommandAction{ func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }, }, } if test.cmdOutputFsType == "TYPE=btrfs" { t.Logf("Adding btrfs size command") fcmd.CombinedOutputScript = append(fcmd.CombinedOutputScript, func() ([]byte, []byte, error) { return []byte(test.deviceSize), nil, nil }) fcmd.CombinedOutputScript = append(fcmd.CombinedOutputScript, func() ([]byte, []byte, error) { return []byte(fmt.Sprintf("sectorsize %s\ntotal_bytes 1\n", test.extSize)), nil, nil }) fexec.CommandScript = append(fexec.CommandScript, func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }) fexec.CommandScript = append(fexec.CommandScript, func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) }) } resizefs := ResizeFs{exec: fexec} needResize, err := resizefs.NeedResize(test.devicePath, test.deviceMountPath) if !test.expectError && err != nil { t.Fatalf("Expect no error but got %v", err) } if needResize != test.expectResult { t.Fatalf("Expect result is %v but got %v", test.expectResult, needResize) } }) } } golang-k8s-mount-utils-1.36.0~beta0/resizefs_unsupported.go000066400000000000000000000024541516076520600240310ustar00rootroot00000000000000//go:build !linux /* Copyright 2021 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" utilexec "k8s.io/utils/exec" ) // ResizeFs Provides support for resizing file systems type ResizeFs struct { exec utilexec.Interface } // NewResizeFs returns new instance of resizer func NewResizeFs(exec utilexec.Interface) *ResizeFs { return &ResizeFs{exec: exec} } // Resize perform resize of file system func (resizefs *ResizeFs) Resize(devicePath string, deviceMountPath string) (bool, error) { return false, fmt.Errorf("Resize is not supported for this build") } // NeedResize check whether mounted volume needs resize func (resizefs *ResizeFs) NeedResize(devicePath string, deviceMountPath string) (bool, error) { return false, fmt.Errorf("NeedResize is not supported for this build") } golang-k8s-mount-utils-1.36.0~beta0/safe_format_and_mount_test.go000066400000000000000000000267551516076520600251320ustar00rootroot00000000000000/* Copyright 2014 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" "runtime" "strings" "testing" "k8s.io/utils/exec" testingexec "k8s.io/utils/exec/testing" ) type ErrorMounter struct { *FakeMounter errIndex int err []error } func (mounter *ErrorMounter) Mount(source string, target string, fstype string, options []string) error { return mounter.MountSensitive(source, target, fstype, options, nil /* sensitiveOptions */) } func (mounter *ErrorMounter) MountSensitive(source string, target string, fstype string, options []string, sensitiveOptions []string) error { i := mounter.errIndex mounter.errIndex++ if mounter.err != nil && mounter.err[i] != nil { return mounter.err[i] } return mounter.FakeMounter.Mount(source, target, fstype, options) } type ExecArgs struct { command string args []string output string err error } func TestSafeFormatAndMount(t *testing.T) { if runtime.GOOS == "darwin" || runtime.GOOS == "windows" { t.Skipf("not supported on GOOS=%s", runtime.GOOS) } tests := []struct { description string fstype string mountOptions []string sensitiveMountOptions []string formatOptions []string execScripts []ExecArgs mountErrs []error expErrorType MountErrorType }{ { description: "Test a read only mount of an already formatted device", fstype: "ext4", mountOptions: []string{"ro"}, execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil}, }, }, { description: "Test a normal mount of an already formatted device", fstype: "ext4", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil}, {"fsck", []string{"-a", "/dev/foo"}, "", nil}, }, }, { description: "Test a read only mount of unformatted device", fstype: "ext4", mountOptions: []string{"ro"}, execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, }, expErrorType: UnformattedReadOnly, }, { description: "Test a normal mount of unformatted device", fstype: "ext4", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, {"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", nil}, }, }, { description: "Test 'fsck' fails with exit status 4", fstype: "ext4", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil}, {"fsck", []string{"-a", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 4}}, }, expErrorType: HasFilesystemErrors, }, { description: "Test 'fsck' fails with exit status 1 (errors found and corrected)", fstype: "ext4", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil}, {"fsck", []string{"-a", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 1}}, }, }, { description: "Test 'fsck' fails with exit status other than 1 and 4 (likely unformatted device)", fstype: "ext4", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil}, {"fsck", []string{"-a", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 8}}, }, }, { description: "Test that 'blkid' is called and fails", fstype: "ext4", mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'")}, execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nPTTYPE=dos\n", nil}, {"fsck", []string{"-a", "/dev/foo"}, "", nil}, }, expErrorType: FilesystemMismatch, }, { description: "Test that 'blkid' is called and confirms unformatted disk, format fails", fstype: "ext4", mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'")}, execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, {"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", fmt.Errorf("formatting failed")}, }, expErrorType: FormatFailed, }, { description: "Test that 'blkid' is called and confirms unformatted disk, format passes, second mount fails", fstype: "ext4", mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'")}, execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, {"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", nil}, }, expErrorType: UnknownMountError, }, { description: "Test that 'blkid' is called and confirms unformatted disk, format passes, mount passes", fstype: "ext4", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, {"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", nil}, }, }, { description: "Test that 'blkid' is called and confirms unformatted disk, format passes, mount passes with ext3", fstype: "ext3", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, {"mkfs.ext3", []string{"-F", "-m0", "/dev/foo"}, "", nil}, }, }, { description: "test that none ext4 fs does not get called with ext4 options.", fstype: "xfs", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, {"mkfs.xfs", []string{"-f", "/dev/foo"}, "", nil}, }, }, { description: "Test that 'blkid' is called and reports ext4 partition", fstype: "ext4", execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "DEVNAME=/dev/foo\nTYPE=ext4\n", nil}, {"fsck", []string{"-a", "/dev/foo"}, "", nil}, }, }, { description: "Test that 'blkid' is called but has some usage or other errors (an exit code of 4 is returned)", fstype: "xfs", mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'"), nil}, execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 4}}, {"mkfs.xfs", []string{"-f", "/dev/foo"}, "", nil}, }, expErrorType: GetDiskFormatFailed, }, { description: "Test that 'blkid' is called and confirms unformatted disk, format fails with sensitive options", fstype: "ext4", sensitiveMountOptions: []string{"mySecret"}, mountErrs: []error{fmt.Errorf("unknown filesystem type '(null)'")}, execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, {"mkfs.ext4", []string{"-F", "-m0", "/dev/foo"}, "", fmt.Errorf("formatting failed")}, }, expErrorType: FormatFailed, }, { description: "Test that 'blkid' is called and confirms unformatted disk, format passes, mount passes (with format options)", fstype: "ext4", formatOptions: []string{"-b", "1024"}, execScripts: []ExecArgs{ {"blkid", []string{"-p", "-s", "TYPE", "-s", "PTTYPE", "-o", "export", "/dev/foo"}, "", &testingexec.FakeExitError{Status: 2}}, {"mkfs.ext4", []string{"-b", "1024", "-F", "-m0", "/dev/foo"}, "", nil}, }, }, } for _, test := range tests { fakeMounter := ErrorMounter{NewFakeMounter(nil), 0, test.mountErrs} fakeExec := &testingexec.FakeExec{ExactOrder: true} for _, script := range test.execScripts { fakeCmd := &testingexec.FakeCmd{} cmdAction := makeFakeCmd(fakeCmd, script.command, script.args...) outputAction := makeFakeOutput(script.output, script.err) fakeCmd.CombinedOutputScript = append(fakeCmd.CombinedOutputScript, outputAction) fakeExec.CommandScript = append(fakeExec.CommandScript, cmdAction) } mounter := SafeFormatAndMount{ Interface: &fakeMounter, Exec: fakeExec, } device := "/dev/foo" dest := t.TempDir() var err error if len(test.formatOptions) > 0 { err = mounter.FormatAndMountSensitiveWithFormatOptions(device, dest, test.fstype, test.mountOptions, test.sensitiveMountOptions, test.formatOptions) } else if len(test.sensitiveMountOptions) == 0 { err = mounter.FormatAndMount(device, dest, test.fstype, test.mountOptions) } else { err = mounter.FormatAndMountSensitive(device, dest, test.fstype, test.mountOptions, test.sensitiveMountOptions) } if len(test.expErrorType) == 0 { if err != nil { t.Errorf("test \"%s\" unexpected non-error: %v", test.description, err) } // Check that something was mounted on the directory isNotMountPoint, err := fakeMounter.IsLikelyNotMountPoint(dest) if err != nil || isNotMountPoint { t.Errorf("test \"%s\" the directory was not mounted", test.description) } // check that the correct device was mounted mountedDevice, _, err := GetDeviceNameFromMount(fakeMounter.FakeMounter, dest) if err != nil || mountedDevice != device { t.Errorf("test \"%s\" the correct device was not mounted", test.description) } } else { mntErr, ok := err.(MountError) if !ok { t.Errorf("mount error not of mount error type: %v", err) } if mntErr.Type != test.expErrorType { t.Errorf("test \"%s\" unexpected error: \n [%v]. \nExpecting err type[%v]", test.description, err, test.expErrorType) } if len(test.sensitiveMountOptions) == 0 { if strings.Contains(mntErr.Error(), sensitiveOptionsRemoved) { t.Errorf("test \"%s\" returned an error unexpectedly containing the string %q: %v", test.description, sensitiveOptionsRemoved, err) } } else { if !strings.Contains(err.Error(), sensitiveOptionsRemoved) { t.Errorf("test \"%s\" returned an error without the string %q: %v", test.description, sensitiveOptionsRemoved, err) } for _, sensitiveOption := range test.sensitiveMountOptions { if strings.Contains(err.Error(), sensitiveOption) { t.Errorf("test \"%s\" returned an error with a sensitive string (%q): %v", test.description, sensitiveOption, err) } } } } } } func makeFakeCmd(fakeCmd *testingexec.FakeCmd, cmd string, args ...string) testingexec.FakeCommandAction { c := cmd a := args return func(cmd string, args ...string) exec.Cmd { command := testingexec.InitFakeCmd(fakeCmd, c, a...) return command } } func makeFakeOutput(output string, err error) testingexec.FakeAction { o := output return func() ([]byte, []byte, error) { return []byte(o), nil, err } }