Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for FUSE #1381

Merged
merged 2 commits into from
Sep 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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{[email protected]: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