Skip to content

Commit e383f58

Browse files
authored
feat: add support for FUSE (GoogleCloudPlatform#135)
* feat: add support for FUSE This is a port of GoogleCloudPlatform/cloud-sql-proxy#1381 and GoogleCloudPlatform/cloud-sql-proxy#1400. Fixes GoogleCloudPlatform#132
1 parent 1a3d46b commit e383f58

18 files changed

+1056
-55
lines changed

.github/workflows/tests.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ on:
2525

2626
jobs:
2727
integration:
28-
# run job on proper workflow event triggers (skip job for pull_request event from forks and only run pull_request_target for "tests: run" label)
28+
# run job on proper workflow event triggers (skip job for pull_request event from forks and only run pull_request_target for "tests: run" label)
2929
if: "${{ (github.event.action != 'labeled' && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) || github.event.label.name == 'tests: run' }}"
3030
runs-on: [self-hosted, linux, x64]
3131
name: "integration tests (linux)"
@@ -89,7 +89,7 @@ jobs:
8989
shell: bash
9090
run: |
9191
go test -v -race -cover ./tests | tee test_results.txt
92-
92+
9393
- name: Convert test output to XML
9494
if: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && always() }}
9595
run: |
@@ -105,7 +105,7 @@ jobs:
105105
./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
106106
107107
unit:
108-
# run job on proper workflow event triggers (skip job for pull_request event from forks and only run pull_request_target for "tests: run" label)
108+
# run job on proper workflow event triggers (skip job for pull_request event from forks and only run pull_request_target for "tests: run" label)
109109
if: "${{ (github.event.action != 'labeled' && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name) || github.event.label.name == 'tests: run' }}"
110110
name: "unit tests"
111111
runs-on: ${{ matrix.os }}
@@ -171,7 +171,7 @@ jobs:
171171
curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L
172172
chmod +x ./flakybot
173173
./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}
174-
174+
175175
- name: FlakyBot (Windows)
176176
# only run flakybot on periodic (schedule) and continuous (push) events
177177
if: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && runner.os == 'Windows' && always() }}

cmd/root.go

Lines changed: 21 additions & 2 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"
@@ -175,6 +176,11 @@ down when the number of open connections reaches 0 or when
175176
the maximum time has passed. Defaults to 0s.`)
176177
cmd.PersistentFlags().StringVar(&c.conf.APIEndpointURL, "alloydbadmin-api-endpoint", "https://alloydb.googleapis.com/v1beta",
177178
"When set, the proxy uses this host as the base API path.")
179+
cmd.PersistentFlags().StringVar(&c.conf.FUSEDir, "fuse", "",
180+
"Mount a directory at the path using FUSE to access Cloud SQL instances.")
181+
cmd.PersistentFlags().StringVar(&c.conf.FUSETempDir, "fuse-tmp-dir",
182+
filepath.Join(os.TempDir(), "csql-tmp"),
183+
"Temp dir for Unix sockets created with FUSE")
178184

179185
cmd.PersistentFlags().StringVar(&c.telemetryProject, "telemetry-project", "",
180186
"Enable Cloud Monitoring and Cloud Trace integration with the provided project ID.")
@@ -211,11 +217,24 @@ only. Uses the port specified by the http-port flag.`)
211217
}
212218

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

226+
if conf.FUSEDir != "" {
227+
if err := proxy.SupportsFUSE(); err != nil {
228+
return newBadCommandError(
229+
fmt.Sprintf("--fuse is not supported: %v", err),
230+
)
231+
}
232+
}
233+
234+
if len(args) == 0 && conf.FUSEDir == "" && conf.FUSETempDir != "" {
235+
return newBadCommandError("cannot specify --fuse-tmp-dir without --fuse")
236+
}
237+
219238
userHasSet := func(f string) bool {
220239
return cmd.PersistentFlags().Lookup(f).Changed
221240
}

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
@@ -20,6 +20,8 @@ import (
2020
"fmt"
2121
"net"
2222
"net/http"
23+
"os"
24+
"path/filepath"
2325
"sync"
2426
"testing"
2527
"time"
@@ -41,11 +43,16 @@ func TestNewCommandArguments(t *testing.T) {
4143
if c.Port == 0 {
4244
c.Port = 5432
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 = "projects/proj/locations/region/clusters/clust/instances/inst"
52+
}
4653
}
47-
if i := &c.Instances[0]; i.Name == "" {
48-
i.Name = "projects/proj/locations/region/clusters/clust/instances/inst"
54+
if c.FUSETempDir == "" {
55+
c.FUSETempDir = filepath.Join(os.TempDir(), "csql-tmp")
4956
}
5057
if c.APIEndpointURL == "" {
5158
c.APIEndpointURL = "https://alloydb.googleapis.com/v1beta"
@@ -354,6 +361,10 @@ func TestNewCommandWithErrors(t *testing.T) {
354361
desc: "using an invalid url for host flag",
355362
args: []string{"--host", "https://invalid:url[/]", "proj:region:inst"},
356363
},
364+
{
365+
desc: "using fuse-tmp-dir without fuse",
366+
args: []string{"--fuse-tmp-dir", "/mydir"},
367+
},
357368
}
358369

359370
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
@@ -7,6 +7,7 @@ require (
77
contrib.go.opencensus.io/exporter/prometheus v0.4.2
88
contrib.go.opencensus.io/exporter/stackdriver v0.13.13
99
github.com/google/go-cmp v0.5.8
10+
github.com/hanwen/go-fuse/v2 v2.1.0
1011
github.com/spf13/cobra v1.5.0
1112
go.opencensus.io v0.23.0
1213
go.uber.org/zap v1.23.0

go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,9 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
633633
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
634634
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
635635
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks=
636+
github.com/hanwen/go-fuse v1.0.0/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
637+
github.com/hanwen/go-fuse/v2 v2.1.0 h1:+32ffteETaLYClUj0a3aHjZ1hOPxxaNEHiZiujuDaek=
638+
github.com/hanwen/go-fuse/v2 v2.1.0/go.mod h1:oRyA5eK+pvJyv5otpO/DgccS8y/RvYMaO00GgRLGryc=
636639
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
637640
github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0=
638641
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
@@ -784,6 +787,8 @@ github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
784787
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
785788
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
786789
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
790+
github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
791+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
787792
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
788793
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
789794
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=

internal/proxy/fuse.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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+
//go:build !windows
16+
// +build !windows
17+
18+
package proxy
19+
20+
import (
21+
"context"
22+
"syscall"
23+
24+
"github.com/hanwen/go-fuse/v2/fs"
25+
"github.com/hanwen/go-fuse/v2/fuse"
26+
"github.com/hanwen/go-fuse/v2/fuse/nodefs"
27+
)
28+
29+
// symlink implements a symbolic link, returning the underlying path when
30+
// Readlink is called.
31+
type symlink struct {
32+
fs.Inode
33+
path string
34+
}
35+
36+
// Readlink implements fs.NodeReadlinker and returns the symlink's path.
37+
func (s *symlink) Readlink(ctx context.Context) ([]byte, syscall.Errno) {
38+
return []byte(s.path), fs.OK
39+
}
40+
41+
// readme represents a static read-only text file.
42+
type readme struct {
43+
fs.Inode
44+
}
45+
46+
const readmeText = `
47+
When applications attempt to open files in this directory, a remote connection
48+
to the AlloyDB instance of the same name will be established.
49+
50+
For example, when you run one of the following commands, the proxy will initiate
51+
a connection to the corresponding Cloud SQL instance, given you have the correct
52+
IAM permissions.
53+
54+
psql "host=/somedir/project.region.cluster.instance dbname=mydb user=myuser"
55+
56+
The proxy will create a directory with the instance short name, and create a
57+
socket inside that directory with the special Postgres name: .s.PGSQL.5432.
58+
59+
Listing the contents of this directory will show all instances with active
60+
connections.
61+
`
62+
63+
// Getattr implements fs.NodeGetattrer and indicates that this file is a regular
64+
// file.
65+
func (*readme) Getattr(ctx context.Context, f fs.FileHandle, out *fuse.AttrOut) syscall.Errno {
66+
*out = fuse.AttrOut{Attr: fuse.Attr{
67+
Mode: 0444 | syscall.S_IFREG,
68+
Size: uint64(len(readmeText)),
69+
}}
70+
return fs.OK
71+
}
72+
73+
// Read implements fs.NodeReader and supports incremental reads.
74+
func (*readme) Read(ctx context.Context, f fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) {
75+
end := int(off) + len(dest)
76+
if end > len(readmeText) {
77+
end = len(readmeText)
78+
}
79+
return fuse.ReadResultData([]byte(readmeText[off:end])), fs.OK
80+
}
81+
82+
// Open implements fs.NodeOpener and supports opening the README as a read-only
83+
// file.
84+
func (*readme) Open(ctx context.Context, mode uint32) (fs.FileHandle, uint32, syscall.Errno) {
85+
df := nodefs.NewDataFile([]byte(readmeText))
86+
rf := nodefs.NewReadOnlyFile(df)
87+
return rf, 0, fs.OK
88+
}

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+
}

0 commit comments

Comments
 (0)