Skip to content

Commit

Permalink
feat: add support for FUSE
Browse files Browse the repository at this point in the history
  • Loading branch information
enocom committed Sep 15, 2022
1 parent 5c1b3bd commit 3b90964
Show file tree
Hide file tree
Showing 14 changed files with 874 additions and 41 deletions.
23 changes: 21 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"net/url"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
Expand Down Expand Up @@ -174,6 +175,11 @@ down when the number of open connections reaches 0 or when
the maximum time has passed. Defaults to 0s.`)
cmd.PersistentFlags().StringVar(&c.conf.APIEndpointURL, "alloydbadmin-api-endpoint", "https://alloydb.googleapis.com/v1beta",
"When set, the proxy uses this host as the base API path.")
cmd.PersistentFlags().StringVar(&c.conf.FUSEDir, "fuse", "",
"Mount a directory at the path using FUSE to access Cloud SQL instances.")
cmd.PersistentFlags().StringVar(&c.conf.FUSETempDir, "fuse-tmp-dir",
filepath.Join(os.TempDir(), "csql-tmp"),
"Temp dir for Unix sockets created with FUSE")

cmd.PersistentFlags().StringVar(&c.telemetryProject, "telemetry-project", "",
"Enable Cloud Monitoring and Cloud Trace integration with the provided project ID.")
Expand Down Expand Up @@ -208,11 +214,24 @@ only. Uses the port specified by the http-port flag.`)
}

func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
// If no instance connection names were provided, error.
if len(args) == 0 {
// If no instance connection names were provided AND FUSE isn't enabled,
// error.
if len(args) == 0 && conf.FUSEDir == "" {
return newBadCommandError("missing instance uri (e.g., projects/$PROJECTS/locations/$LOCTION/clusters/$CLUSTER/instances/$INSTANCES)")
}

if conf.FUSEDir != "" {
if err := proxy.SupportsFUSE(); err != nil {
return newBadCommandError(
fmt.Sprintf("--fuse is not supported: %v", err),
)
}
}

if len(args) == 0 && conf.FUSEDir == "" && conf.FUSETempDir != "" {
return newBadCommandError("cannot specify --fuse-tmp-dir without --fuse")
}

userHasSet := func(f string) bool {
return cmd.PersistentFlags().Lookup(f).Changed
}
Expand Down
73 changes: 73 additions & 0 deletions cmd/root_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
// Copyright 2022 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.

package cmd

import (
"os"
"path/filepath"
"testing"

"github.com/spf13/cobra"
)

func TestNewCommandArgumentsOnLinux(t *testing.T) {
defaultTmp := filepath.Join(os.TempDir(), "csql-tmp")
tcs := []struct {
desc string
args []string
wantDir string
wantTempDir string
}{
{
desc: "using the fuse flag",
args: []string{"--fuse", "/cloudsql"},
wantDir: "/cloudsql",
wantTempDir: defaultTmp,
},
{
desc: "using the fuse temporary directory flag",
args: []string{"--fuse", "/cloudsql", "--fuse-tmp-dir", "/mycooldir"},
wantDir: "/cloudsql",
wantTempDir: "/mycooldir",
},
}

for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
c := NewCommand()
// Keep the test output quiet
c.SilenceUsage = true
c.SilenceErrors = true
// Disable execute behavior
c.RunE = func(*cobra.Command, []string) error {
return nil
}
c.SetArgs(tc.args)

err := c.Execute()
if err != nil {
t.Fatalf("want error = nil, got = %v", err)
}

if got, want := c.conf.FUSEDir, tc.wantDir; got != want {
t.Fatalf("FUSEDir: want = %v, got = %v", want, got)
}

if got, want := c.conf.FUSETempDir, tc.wantTempDir; got != want {
t.Fatalf("FUSEDir: want = %v, got = %v", want, got)
}
})
}
}
19 changes: 15 additions & 4 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"fmt"
"net"
"net/http"
"os"
"path/filepath"
"sync"
"testing"
"time"
Expand All @@ -41,11 +43,16 @@ func TestNewCommandArguments(t *testing.T) {
if c.Port == 0 {
c.Port = 5432
}
if c.Instances == nil {
c.Instances = []proxy.InstanceConnConfig{{}}
if c.FUSEDir == "" {
if c.Instances == nil {
c.Instances = []proxy.InstanceConnConfig{{}}
}
if i := &c.Instances[0]; i.Name == "" {
i.Name = "projects/proj/locations/region/clusters/clust/instances/inst"
}
}
if i := &c.Instances[0]; i.Name == "" {
i.Name = "projects/proj/locations/region/clusters/clust/instances/inst"
if c.FUSETempDir == "" {
c.FUSETempDir = filepath.Join(os.TempDir(), "csql-tmp")
}
if c.APIEndpointURL == "" {
c.APIEndpointURL = "https://alloydb.googleapis.com/v1beta"
Expand Down Expand Up @@ -354,6 +361,10 @@ func TestNewCommandWithErrors(t *testing.T) {
desc: "using an invalid url for host flag",
args: []string{"--host", "https://invalid:url[/]", "proj:region:inst"},
},
{
desc: "using fuse-tmp-dir without fuse",
args: []string{"--fuse-tmp-dir", "/mydir"},
},
}

for _, tc := range tcs {
Expand Down
36 changes: 36 additions & 0 deletions cmd/root_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2022 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.

package cmd

import (
"testing"

"github.com/spf13/cobra"
)

func TestWindowsDoesNotSupportFUSE(t *testing.T) {
c := NewCommand()
// Keep the test output quiet
c.SilenceUsage = true
c.SilenceErrors = true
// Disable execute behavior
c.RunE = func(*cobra.Command, []string) error { return nil }
c.SetArgs([]string{"--fuse"})

err := c.Execute()
if err == nil {
t.Fatal("want error != nil, got = nil")
}
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ require (
contrib.go.opencensus.io/exporter/prometheus v0.4.2
contrib.go.opencensus.io/exporter/stackdriver v0.13.13
github.com/google/go-cmp v0.5.8
github.com/hanwen/go-fuse v1.0.0
github.com/hanwen/go-fuse/v2 v2.1.0
github.com/spf13/cobra v1.5.0
go.opencensus.io v0.23.0
go.uber.org/zap v1.23.0
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,10 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
github.com/hanwen/go-fuse v1.0.0 h1:GxS9Zrn6c35/BnfiVsZVWmsG803xwE7eVRDvcf/BEVc=
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek=
github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
Expand Down Expand Up @@ -784,6 +788,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
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/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
Expand Down
85 changes: 85 additions & 0 deletions internal/proxy/fuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright 2022 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
//
// https://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 proxy

import (
"context"
"syscall"

"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
)

// symlink implements a symbolic link, returning the underlying path when
// Readlink is called.
type symlink struct {
fs.Inode
path string
}

// Readlink implements fs.NodeReadlinker and returns the symlink's path.
func (s *symlink) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
return []byte(s.path), fs.OK
}

// readme represents a static read-only text file.
type readme struct {
fs.Inode
}

const readmeText = `
When applications attempt to open files in this directory, a remote connection
to the AlloyDB instance of the same name will be established.
For example, when you run one of the following commands, the proxy will initiate
a connection to the corresponding Cloud SQL instance, given you have the correct
IAM permissions.
psql "host=/somedir/project.region.cluster.instance dbname=mydb user=myuser"
The proxy will create a directory with the instance short name, and create a
socket inside that directory with the special Postgres name: .s.PGSQL.5432.
Listing the contents of this directory will show all instances with active
connections.
`

// Getattr implements fs.NodeGetattrer and indicates that this file is a regular
// file.
func (*readme) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
*out = fuse.AttrOut{Attr: fuse.Attr{
Mode: 0444 | syscall.S_IFREG,
Size: uint64(len(readmeText)),
}}
return fs.OK
}

// Read implements fs.NodeReader and supports incremental reads.
func (*readme) Read(ctx context.Context, f fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
end := int(off) + len(dest)
if end > len(readmeText) {
end = len(readmeText)
}
return fuse.ReadResultData([]byte(readmeText[off:end])), fs.OK
}

// Open implements fs.NodeOpener and supports opening the README as a read-only
// file.
func (*readme) Open(ctx context.Context, mode uint32) (fs.FileHandle, uint32, syscall.Errno) {
df := nodefs.NewDataFile([]byte(readmeText))
rf := nodefs.NewReadOnlyFile(df)
return rf, 0, fs.OK
}
41 changes: 41 additions & 0 deletions internal/proxy/fuse_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright 2022 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.

package proxy

import (
"errors"
"os"
)

const (
macfusePath = "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse"
osxfusePath = "/Library/Filesystems/osxfuse.fs/Contents/Resources/mount_osxfuse"
)

// SupportsFUSE checks if macfuse or osxfuse are installed on the host by
// looking for both in their known installation location.
func SupportsFUSE() error {
// This code follows the same strategy as hanwen/go-fuse.
// See https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_darwin.go#L121-L124.

// check for macfuse first (newer version of osxfuse)
if _, err := os.Stat(macfusePath); err != nil {
// if that fails, check for osxfuse next
if _, err := os.Stat(osxfusePath); err != nil {
return errors.New("failed to find osxfuse or macfuse: verify FUSE installation and try again (see https://osxfuse.github.io).")
}
}
return nil
}
33 changes: 33 additions & 0 deletions internal/proxy/fuse_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2022 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.

package proxy

import (
"errors"
"os/exec"
)

// SupportsFUSE checks if the fusermount binary is present in the PATH or a well
// known location.
func SupportsFUSE() error {
// This code follows the same strategy found in hanwen/go-fuse.
// See https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_linux.go#L184-L198.
if _, err := exec.LookPath("fusermount"); err != nil {
if _, err := exec.LookPath("/bin/fusermount"); err != nil {
return errors.New("fusermount binary not found in PATH or /bin")
}
}
return nil
}
Loading

0 comments on commit 3b90964

Please sign in to comment.