Skip to content

Commit 6cf4d25

Browse files
authored
feat: add support for FUSE (#1381)
* 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.
1 parent 4fd5b86 commit 6cf4d25

File tree

14 files changed

+957
-41
lines changed

14 files changed

+957
-41
lines changed

cmd/root.go

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"net/url"
2525
"os"
2626
"os/signal"
27+
"path/filepath"
2728
"strconv"
2829
"strings"
2930
"syscall"
@@ -241,7 +242,15 @@ func NewCommand(opts ...Option) *Command {
241242
cmd.PersistentFlags().StringVar(&c.conf.APIEndpointURL, "sqladmin-api-endpoint", "",
242243
"API endpoint for all Cloud SQL Admin API requests. (default: https://sqladmin.googleapis.com)")
243244
cmd.PersistentFlags().StringVar(&c.conf.QuotaProject, "quota-project", "",
244-
`Specifies the project for Cloud SQL Admin API quota tracking. Must have "serviceusage.service.use" IAM permission.`)
245+
`Specifies the project to use for Cloud SQL Admin API quota tracking.
246+
The IAM principal must have the "serviceusage.services.use" permission
247+
for the given project. See https://cloud.google.com/service-usage/docs/overview and
248+
https://cloud.google.com/storage/docs/requester-pays`)
249+
cmd.PersistentFlags().StringVar(&c.conf.FUSEDir, "fuse", "",
250+
"Mount a directory at the path using FUSE to access Cloud SQL instances.")
251+
cmd.PersistentFlags().StringVar(&c.conf.FUSETempDir, "fuse-tmp-dir",
252+
filepath.Join(os.TempDir(), "csql-tmp"),
253+
"Temp dir for Unix sockets created with FUSE")
245254

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

261270
func parseConfig(cmd *Command, conf *proxy.Config, args []string) error {
262-
// If no instance connection names were provided, error.
263-
if len(args) == 0 {
271+
// If no instance connection names were provided AND FUSE isn't enabled,
272+
// error.
273+
if len(args) == 0 && conf.FUSEDir == "" {
264274
return newBadCommandError("missing instance_connection_name (e.g., project:region:instance)")
265275
}
266276

277+
if conf.FUSEDir != "" {
278+
if err := proxy.SupportsFUSE(); err != nil {
279+
return newBadCommandError(
280+
fmt.Sprintf("--fuse is not supported: %v", err),
281+
)
282+
}
283+
}
284+
285+
if len(args) == 0 && conf.FUSEDir == "" && conf.FUSETempDir != "" {
286+
return newBadCommandError("cannot specify --fuse-tmp-dir without --fuse")
287+
}
288+
267289
userHasSet := func(f string) bool {
268290
return cmd.PersistentFlags().Lookup(f).Changed
269291
}

cmd/root_linux_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"os"
19+
"path/filepath"
20+
"testing"
21+
22+
"github.com/spf13/cobra"
23+
)
24+
25+
func TestNewCommandArgumentsOnLinux(t *testing.T) {
26+
defaultTmp := filepath.Join(os.TempDir(), "csql-tmp")
27+
tcs := []struct {
28+
desc string
29+
args []string
30+
wantDir string
31+
wantTempDir string
32+
}{
33+
{
34+
desc: "using the fuse flag",
35+
args: []string{"--fuse", "/cloudsql"},
36+
wantDir: "/cloudsql",
37+
wantTempDir: defaultTmp,
38+
},
39+
{
40+
desc: "using the fuse temporary directory flag",
41+
args: []string{"--fuse", "/cloudsql", "--fuse-tmp-dir", "/mycooldir"},
42+
wantDir: "/cloudsql",
43+
wantTempDir: "/mycooldir",
44+
},
45+
}
46+
47+
for _, tc := range tcs {
48+
t.Run(tc.desc, func(t *testing.T) {
49+
c := NewCommand()
50+
// Keep the test output quiet
51+
c.SilenceUsage = true
52+
c.SilenceErrors = true
53+
// Disable execute behavior
54+
c.RunE = func(*cobra.Command, []string) error {
55+
return nil
56+
}
57+
c.SetArgs(tc.args)
58+
59+
err := c.Execute()
60+
if err != nil {
61+
t.Fatalf("want error = nil, got = %v", err)
62+
}
63+
64+
if got, want := c.conf.FUSEDir, tc.wantDir; got != want {
65+
t.Fatalf("FUSEDir: want = %v, got = %v", want, got)
66+
}
67+
68+
if got, want := c.conf.FUSETempDir, tc.wantTempDir; got != want {
69+
t.Fatalf("FUSEDir: want = %v, got = %v", want, got)
70+
}
71+
})
72+
}
73+
}

cmd/root_test.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"errors"
2020
"net"
2121
"net/http"
22+
"os"
23+
"path/filepath"
2224
"sync"
2325
"testing"
2426
"time"
@@ -41,11 +43,16 @@ func TestNewCommandArguments(t *testing.T) {
4143
if c.Addr == "" {
4244
c.Addr = "127.0.0.1"
4345
}
44-
if c.Instances == nil {
45-
c.Instances = []proxy.InstanceConnConfig{{}}
46+
if c.FUSEDir == "" {
47+
if c.Instances == nil {
48+
c.Instances = []proxy.InstanceConnConfig{{}}
49+
}
50+
if i := &c.Instances[0]; i.Name == "" {
51+
i.Name = "proj:region:inst"
52+
}
4653
}
47-
if i := &c.Instances[0]; i.Name == "" {
48-
i.Name = "proj:region:inst"
54+
if c.FUSETempDir == "" {
55+
c.FUSETempDir = filepath.Join(os.TempDir(), "csql-tmp")
4956
}
5057
return c
5158
}
@@ -520,6 +527,10 @@ func TestNewCommandWithErrors(t *testing.T) {
520527
desc: "using an invalid url for sqladmin-api-endpoint",
521528
args: []string{"--sqladmin-api-endpoint", "https://user:abc{[email protected]:5432/db?sslmode=require", "proj:region:inst"},
522529
},
530+
{
531+
desc: "using fuse-tmp-dir without fuse",
532+
args: []string{"--fuse-tmp-dir", "/mydir"},
533+
},
523534
}
524535

525536
for _, tc := range tcs {

cmd/root_windows_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cmd
16+
17+
import (
18+
"testing"
19+
20+
"github.com/spf13/cobra"
21+
)
22+
23+
func TestWindowsDoesNotSupportFUSE(t *testing.T) {
24+
c := NewCommand()
25+
// Keep the test output quiet
26+
c.SilenceUsage = true
27+
c.SilenceErrors = true
28+
// Disable execute behavior
29+
c.RunE = func(*cobra.Command, []string) error { return nil }
30+
c.SetArgs([]string{"--fuse"})
31+
32+
err := c.Execute()
33+
if err == nil {
34+
t.Fatal("want error != nil, got = nil")
35+
}
36+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/denisenkom/go-mssqldb v0.12.2
1010
github.com/go-sql-driver/mysql v1.6.0
1111
github.com/google/go-cmp v0.5.8
12+
github.com/hanwen/go-fuse/v2 v2.1.0
1213
github.com/jackc/pgx/v4 v4.17.0
1314
github.com/spf13/cobra v1.5.0
1415
go.opencensus.io v0.23.0

go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,9 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
643643
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
644644
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
645645
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
646+
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
647+
github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek=
648+
github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
646649
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
647650
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
648651
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
@@ -798,6 +801,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
798801
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
799802
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
800803
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
804+
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
805+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
801806
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
802807
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
803808
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=

internal/proxy/fuse.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package proxy
16+
17+
import (
18+
"context"
19+
"syscall"
20+
21+
"github.com/hanwen/go-fuse/v2/fs"
22+
"github.com/hanwen/go-fuse/v2/fuse"
23+
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
24+
)
25+
26+
// symlink implements a symbolic link, returning the underlying path when
27+
// Readlink is called.
28+
type symlink struct {
29+
fs.Inode
30+
path string
31+
}
32+
33+
// Readlink implements fs.NodeReadlinker and returns the symlink's path.
34+
func (s *symlink) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
35+
return []byte(s.path), fs.OK
36+
}
37+
38+
// readme represents a static read-only text file.
39+
type readme struct {
40+
fs.Inode
41+
}
42+
43+
const readmeText = `
44+
When applications attempt to open files in this directory, a remote connection
45+
to the Cloud SQL instance of the same name will be established.
46+
47+
For example, when you run one of the followg commands, the proxy will initiate a
48+
connection to the corresponding Cloud SQL instance, given you have the correct
49+
IAM permissions.
50+
51+
mysql -u root -S "/somedir/project:region:instance"
52+
53+
# or
54+
55+
psql "host=/somedir/project:region:instance dbname=mydb user=myuser"
56+
57+
For MySQL, the proxy will create a socket with the instance connection name
58+
(e.g., project:region:instance) in this directory. For Postgres, the proxy will
59+
create a directory with the instance connection name, and create a socket inside
60+
that directory with the special Postgres name: .s.PGSQL.5432.
61+
62+
Listing the contents of this directory will show all instances with active
63+
connections.
64+
`
65+
66+
// Getattr implements fs.NodeGetattrer and indicates that this file is a regular
67+
// file.
68+
func (*readme) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
69+
*out = fuse.AttrOut{Attr: fuse.Attr{
70+
Mode: 0444 | syscall.S_IFREG,
71+
Size: uint64(len(readmeText)),
72+
}}
73+
return fs.OK
74+
}
75+
76+
// Read implements fs.NodeReader and supports incremental reads.
77+
func (*readme) Read(ctx context.Context, f fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
78+
end := int(off) + len(dest)
79+
if end > len(readmeText) {
80+
end = len(readmeText)
81+
}
82+
return fuse.ReadResultData([]byte(readmeText[off:end])), fs.OK
83+
}
84+
85+
// Open implements fs.NodeOpener and supports opening the README as a read-only
86+
// file.
87+
func (*readme) Open(ctx context.Context, mode uint32) (fs.FileHandle, uint32, syscall.Errno) {
88+
df := nodefs.NewDataFile([]byte(readmeText))
89+
rf := nodefs.NewReadOnlyFile(df)
90+
return rf, 0, fs.OK
91+
}

internal/proxy/fuse_darwin.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package proxy
16+
17+
import (
18+
"errors"
19+
"os"
20+
)
21+
22+
const (
23+
macfusePath = "/Library/Filesystems/macfuse.fs/Contents/Resources/mount_macfuse"
24+
osxfusePath = "/Library/Filesystems/osxfuse.fs/Contents/Resources/mount_osxfuse"
25+
)
26+
27+
// SupportsFUSE checks if macfuse or osxfuse are installed on the host by
28+
// looking for both in their known installation location.
29+
func SupportsFUSE() error {
30+
// This code follows the same strategy as hanwen/go-fuse.
31+
// See https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_darwin.go#L121-L124.
32+
33+
// check for macfuse first (newer version of osxfuse)
34+
if _, err := os.Stat(macfusePath); err != nil {
35+
// if that fails, check for osxfuse next
36+
if _, err := os.Stat(osxfusePath); err != nil {
37+
return errors.New("failed to find osxfuse or macfuse: verify FUSE installation and try again (see https://osxfuse.github.io).")
38+
}
39+
}
40+
return nil
41+
}

internal/proxy/fuse_linux.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2022 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package proxy
16+
17+
import (
18+
"errors"
19+
"os/exec"
20+
)
21+
22+
// SupportsFUSE checks if the fusermount binary is present in the PATH or a well
23+
// known location.
24+
func SupportsFUSE() error {
25+
// This code follows the same strategy found in hanwen/go-fuse.
26+
// See https://github.com/hanwen/go-fuse/blob/0f728ba15b38579efefc3dc47821882ca18ffea7/fuse/mount_linux.go#L184-L198.
27+
if _, err := exec.LookPath("fusermount"); err != nil {
28+
if _, err := exec.LookPath("/bin/fusermount"); err != nil {
29+
return errors.New("fusermount binary not found in PATH or /bin")
30+
}
31+
}
32+
return nil
33+
}

0 commit comments

Comments
 (0)