tools/mount_gcsfuse/main.go (169 lines of code) (raw):

// Copyright 2015 Google LLC // // 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. // A helper that allows using gcsfuse with mount(8). // // Can be invoked using a command-line of the form expected for mount helpers. // Calls the gcsfuse binary, which it finds from one of a list of expected // locations, and waits for it to complete. The device and mount point are // passed on as positional arguments, and other known options are converted to // appropriate flags. // // This binary returns with exit code zero only after gcsfuse has reported that // it has successfully mounted the file system. Further output from gcsfuse is // suppressed. package main // Example invocation on OS X: // // mount -t porp -o foo=bar\ baz -o ro,blah bucket ~/tmp/mp // // becomes the following arguments: // // Arg 0: "/sbin/mount_gcsfuse " // Arg 1: "-o" // Arg 2: "foo=bar baz" // Arg 3: "-o" // Arg 4: "ro" // Arg 5: "-o" // Arg 6: "blah" // Arg 7: "bucket" // Arg 8: "/path/to/mp" // // On Linux, the fstab entry // // bucket /path/to/mp porp user,foo=bar\040baz // // becomes // // Arg 0: "/sbin/mount.gcsfuse" // Arg 1: "bucket" // Arg 2: "/path/to/mp" // Arg 3: "-o" // Arg 4: "rw,noexec,nosuid,nodev,user,foo=bar baz" // import ( "fmt" "os" "os/exec" "path" "path/filepath" "slices" "strings" "github.com/googlecloudplatform/gcsfuse/v2/cfg" "github.com/googlecloudplatform/gcsfuse/v2/internal/mount" "github.com/spf13/pflag" ) func flagTypes(flagSet *pflag.FlagSet) (boolFlags, nonBoolFlags []string) { flagSet.VisitAll(func(flag *pflag.Flag) { if flag.Value.Type() == "bool" { boolFlags = append(boolFlags, flag.Name) } else { nonBoolFlags = append(nonBoolFlags, flag.Name) } }) return boolFlags, nonBoolFlags } func isEquiv(s1, s2 string) bool { return strings.ReplaceAll(s1, "_", "-") == strings.ReplaceAll(s2, "_", "-") } func findEquivFlag(s string, lst []string) string { idx := slices.IndexFunc(lst, func(e string) bool { return isEquiv(s, e) }) if idx == -1 { return "" } return lst[idx] } // Turn mount-style options into gcsfuse arguments. Skip known detritus that // the mount command gives us. // // The result of this function should be appended to exec.Command.Args. func makeGcsfuseArgs( device string, mountPoint string, opts map[string]string) (args []string, err error) { var flagSet pflag.FlagSet if err := cfg.BuildFlagSet(&flagSet); err != nil { return nil, err } boolFlags, nonBoolFlags := flagTypes(&flagSet) // Handle "o" by not treating it as a flag for persistent mounting. nonBoolFlags = slices.DeleteFunc(nonBoolFlags, func(s string) bool { return s == "o" }) nonBoolFlags = append(nonBoolFlags, cfg.ConfigFileFlagName) // 'rw' mount option is implicitly passed as CLI parameter by /etc/fstab // entries and overrides mount options in config file. Ignoring is safe, as // 'rw' is default, so that config file mount options take effect. noopOptions := []string{"rw", "user", "nouser", "auto", "noauto", "_netdev", "no_netdev"} // Deal with options. for name, value := range opts { if flg := findEquivFlag(name, noopOptions); flg != "" { // Don't pass through options that are relevant to mount(8) but not to // gcsfuse, and that fusermount chokes on with "Invalid argument" on Linux. continue } if flg := findEquivFlag(name, boolFlags); flg != "" { if value == "" { value = "true" } args = append(args, fmt.Sprintf("--%s=%s", flg, value)) } else if flg := findEquivFlag(name, nonBoolFlags); flg != "" { args = append(args, fmt.Sprintf("--%s", flg), value) } else { // Pass through everything else formatted := name if value != "" { formatted = fmt.Sprintf("%s=%s", name, value) } args = append(args, "-o", formatted) } } // Set the bucket and mount point. return append(args, device, mountPoint), nil } // Parse the supplied command-line arguments from a mount(8) invocation on OS X // or Linux. func parseArgs( args []string) ( device string, mountPoint string, opts map[string]string, err error) { opts = make(map[string]string) // Process each argument in turn. positionalCount := 0 for i, s := range args { switch { // Skip the program name. case i == 0: continue // "-o" is illegal only when at the end. We handle its argument in the case // below. case s == "-o": if i == len(args)-1 { err = fmt.Errorf("unexpected -o at end of args") return } // systemd passes -n (alias --no-mtab) to the mount helper. This seems to // be a result of the new setup on many Linux systems with /etc/mtab as a // symlink pointing to /proc/self/mounts. /proc/self/mounts is read-only, // so any helper that would normally write to /etc/mtab should be // configured not to do so. Because systemd does not provide a way to // disable this behavior for mount helpers that do not write to /etc/mtab, // we ignore the flag. case s == "-n": continue // Is this an options string following a "-o"? case i > 0 && args[i-1] == "-o": mount.ParseOptions(opts, s) // Is this the device? case positionalCount == 0: device = s // The kernel might be converting the bucket name to a path if a directory with the // same name as the bucket exists in the folder where the mount command is executed. // // As of October 10th, 2024, bucket names don't support "/", so it is safe to // assume any received bucket name containing "/" is actually a path and // extract the base file name. // Ref: https://cloud.google.com/storage/docs/buckets#naming if strings.Contains(device, "/") { // Get the last part of the path (bucket name) device = filepath.Base(device) } positionalCount++ // Is this the mount point? case positionalCount == 1: mountPoint = s positionalCount++ default: err = fmt.Errorf("unexpected arg %d: %q", i, s) return } } if positionalCount != 2 { err = fmt.Errorf("expected two positional arguments; got %d", positionalCount) return } return } func run(args []string) (err error) { // If invoked with a single "--help" argument, print a usage message and exit // successfully. if len(args) == 2 && args[1] == "--help" { fmt.Fprintf( os.Stderr, "Usage: %s [-o options] bucket_name mount_point\n", args[0]) return } // Find the path to gcsfuse. gcsfusePath, err := findGcsfuse() if err != nil { err = fmt.Errorf("findGcsfuse: %w", err) return } // Find the path to fusermount. fusermountPath, err := findFusermount() if err != nil { err = fmt.Errorf("findFusermount: %w", err) return } // Attempt to parse arguments. device, mountPoint, opts, err := parseArgs(args) if err != nil { err = fmt.Errorf("parseArgs: %w", err) return } // Choose gcsfuse args. gcsfuseArgs, err := makeGcsfuseArgs(device, mountPoint, opts) if err != nil { err = fmt.Errorf("makeGcsfuseArgs: %w", err) return } fmt.Fprintf( os.Stderr, "Calling gcsfuse with arguments: %s\n", strings.Join(gcsfuseArgs, " ")) // Run gcsfuse. cmd := exec.Command(gcsfusePath, gcsfuseArgs...) cmd.Env = append(cmd.Env, fmt.Sprintf("PATH=%s", path.Dir(fusermountPath))) // Pass through the https_proxy/http_proxy environment variable, // in case the host requires a proxy server to reach the GCS endpoint. // http_proxy has precedence over http_proxy, in case both are set if p, ok := os.LookupEnv("https_proxy"); ok { cmd.Env = append(cmd.Env, fmt.Sprintf("https_proxy=%s", p)) } else if p, ok := os.LookupEnv("http_proxy"); ok { cmd.Env = append(cmd.Env, fmt.Sprintf("http_proxy=%s", p)) } // Pass through the no_proxy enviroment variable. Whenever // using the http(s)_proxy environment variables. This should // also be included to know for which hosts the use of proxies // should be ignored. if p, ok := os.LookupEnv("no_proxy"); ok { cmd.Env = append(cmd.Env, fmt.Sprintf("no_proxy=%s", p)) } cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Run() if err != nil { err = fmt.Errorf("running gcsfuse: %w", err) return } return } func main() { err := run(os.Args) if err != nil { fmt.Fprintf(os.Stderr, "%v\n", err) os.Exit(1) } }