Skip to content

Commit

Permalink
feat: add support for FUSE (#1381)
Browse files Browse the repository at this point in the history
* feat: add support for README in FUSE mode (#1312)

* feat: add support for FUSE connections (#1373)

This commit also ensures that closing the proxy.Client blocks until all
listeners are closed.
  • Loading branch information
enocom committed Sep 7, 2022
1 parent 4fd5b86 commit 6cf4d25
Show file tree
Hide file tree
Showing 14 changed files with 957 additions and 41 deletions.
28 changes: 25 additions & 3 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 @@ -241,7 +242,15 @@ func NewCommand(opts ...Option) *Command {
cmd.PersistentFlags().StringVar(&c.conf.APIEndpointURL, "sqladmin-api-endpoint", "",
"API endpoint for all Cloud SQL Admin API requests. (default: https://sqladmin.googleapis.com)")
cmd.PersistentFlags().StringVar(&c.conf.QuotaProject, "quota-project", "",
`Specifies the project for Cloud SQL Admin API quota tracking. Must have "serviceusage.service.use" IAM permission.`)
`Specifies the project to use for Cloud SQL Admin API quota tracking.
The IAM principal must have the "serviceusage.services.use" permission
for the given project. See https://cloud.google.com/service-usage/docs/overview and
https://cloud.google.com/storage/docs/requester-pays`)
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")

// Global and per instance flags
cmd.PersistentFlags().StringVarP(&c.conf.Addr, "address", "a", "127.0.0.1",
Expand All @@ -259,11 +268,24 @@ func NewCommand(opts ...Option) *Command {
}

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_connection_name (e.g., project:region:instance)")
}

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 @@ -19,6 +19,8 @@ import (
"errors"
"net"
"net/http"
"os"
"path/filepath"
"sync"
"testing"
"time"
Expand All @@ -41,11 +43,16 @@ func TestNewCommandArguments(t *testing.T) {
if c.Addr == "" {
c.Addr = "127.0.0.1"
}
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 = "proj:region:inst"
}
}
if i := &c.Instances[0]; i.Name == "" {
i.Name = "proj:region:inst"
if c.FUSETempDir == "" {
c.FUSETempDir = filepath.Join(os.TempDir(), "csql-tmp")
}
return c
}
Expand Down Expand Up @@ -520,6 +527,10 @@ func TestNewCommandWithErrors(t *testing.T) {
desc: "using an invalid url for sqladmin-api-endpoint",
args: []string{"--sqladmin-api-endpoint", "https://user:abc{DEf1=ghi@example.com:5432/db?sslmode=require", "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")
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ require (
github.com/denisenkom/go-mssqldb v0.12.2
github.com/go-sql-driver/mysql v1.6.0
github.com/google/go-cmp v0.5.8
github.com/hanwen/go-fuse/v2 v2.1.0
github.com/jackc/pgx/v4 v4.17.0
github.com/spf13/cobra v1.5.0
go.opencensus.io v0.23.0
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,9 @@ 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/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 @@ -798,6 +801,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
91 changes: 91 additions & 0 deletions internal/proxy/fuse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// 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 Cloud SQL instance of the same name will be established.
For example, when you run one of the followg commands, the proxy will initiate a
connection to the corresponding Cloud SQL instance, given you have the correct
IAM permissions.
mysql -u root -S "/somedir/project:region:instance"
# or
psql "host=/somedir/project:region:instance dbname=mydb user=myuser"
For MySQL, the proxy will create a socket with the instance connection name
(e.g., project:region:instance) in this directory. For Postgres, the proxy will
create a directory with the instance connection 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 6cf4d25

Please sign in to comment.