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 #135

Merged
merged 12 commits into from
Sep 19, 2022
13 changes: 9 additions & 4 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ on:

jobs:
integration:
# 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)
# 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)
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' }}"
runs-on: [self-hosted, linux, x64]
name: "integration tests (linux)"
Expand Down Expand Up @@ -79,6 +79,11 @@ jobs:
ALLOYDB_CONN_NAME:${{ secrets.GOOGLE_CLOUD_PROJECT }}/ALLOYDB_CONN_NAME
ALLOYDB_CLUSTER_PASS:${{ secrets.GOOGLE_CLOUD_PROJECT }}/ALLOYDB_CLUSTER_PASS

- name: Enable fuse config (Linux)
if: runner.os == 'Linux'
run: |
sed -i 's/#user_allow_other/user_allow_other/g' /etc/fuse.conf

- name: Run tests
env:
ALLOYDB_DB: 'postgres'
Expand All @@ -89,7 +94,7 @@ jobs:
shell: bash
run: |
go test -v -race -cover ./tests | tee test_results.txt

- name: Convert test output to XML
if: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && always() }}
run: |
Expand All @@ -105,7 +110,7 @@ jobs:
./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}

unit:
# 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)
# 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)
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' }}"
name: "unit tests"
runs-on: ${{ matrix.os }}
Expand Down Expand Up @@ -171,7 +176,7 @@ jobs:
curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L
chmod +x ./flakybot
./flakybot --repo ${{github.repository}} --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}}

- name: FlakyBot (Windows)
# only run flakybot on periodic (schedule) and continuous (push) events
if: ${{ (github.event_name == 'schedule' || github.event_name == 'push') && runner.os == 'Windows' && always() }}
Expand Down
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 @@ -175,6 +176,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 @@ -211,11 +217,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")
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ 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/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
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -633,6 +633,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 @@ -784,6 +787,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
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the README for AlloyDB.

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