Skip to content

Commit

Permalink
Add mysqldump support
Browse files Browse the repository at this point in the history
[#130224029]
  • Loading branch information
andreasf committed Nov 28, 2016
1 parent 53ddd64 commit a0b5380
Show file tree
Hide file tree
Showing 6 changed files with 392 additions and 10 deletions.
46 changes: 45 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,35 @@ Cloud Foundry apps. Use it to

## Usage

### Geting a list of available databases
```bash
$ cf mysql -h
NAME:
mysql - Connect to a MySQL database service

USAGE:
Get a list of available databases:
cf mysql

Open a mysql client to a database:
cf mysql <service-name> [mysql args...]


$ cf mysqldump -h
NAME:
mysqldump - Dump a MySQL database

USAGE:
Get a list of available databases:
cf mysqldump

Dumping all tables in a database:
cf mysqldump <service-name> [mysqldump args...]

Dumping specific tables in a database:
cf mysqldump <service-name> [tables...] [mysqldump args...]
```

### Getting a list of available databases

Running the plugin without arguments should give a list of available MySQL databases:

Expand Down Expand Up @@ -71,6 +99,22 @@ $ echo "select 1 as foo, 2 as bar;" | cf mysql my-db --xml
</resultset>
```

### Dumping a database

Running `cf mysqldump` with a database name will dump the whole database:

```bash
$ cf mysqldump my-db --single-transaction > dump.sql
```

### Dumping individual tables

Passing table names in addition to the database name will just dump those tables:

```bash
$ cf mysqldump my-db table1 table2 --single-transaction > two-tables.sql
```

## Installing and uninstalling

Download a binary release or build yourself by running `go build`. Then, install the plugin with
Expand Down
53 changes: 53 additions & 0 deletions cfmysql/cfmysqlfakes/fake_mysql_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ type FakeMysqlRunner struct {
runMysqlReturns struct {
result1 error
}
RunMysqlDumpStub func(hostname string, port int, dbName string, username string, password string, args ...string) error
runMysqlDumpMutex sync.RWMutex
runMysqlDumpArgsForCall []struct {
hostname string
port int
dbName string
username string
password string
args []string
}
runMysqlDumpReturns struct {
result1 error
}
invocations map[string][][]interface{}
invocationsMutex sync.RWMutex
}
Expand Down Expand Up @@ -63,11 +76,51 @@ func (fake *FakeMysqlRunner) RunMysqlReturns(result1 error) {
}{result1}
}

func (fake *FakeMysqlRunner) RunMysqlDump(hostname string, port int, dbName string, username string, password string, args ...string) error {
fake.runMysqlDumpMutex.Lock()
fake.runMysqlDumpArgsForCall = append(fake.runMysqlDumpArgsForCall, struct {
hostname string
port int
dbName string
username string
password string
args []string
}{hostname, port, dbName, username, password, args})
fake.recordInvocation("RunMysqlDump", []interface{}{hostname, port, dbName, username, password, args})
fake.runMysqlDumpMutex.Unlock()
if fake.RunMysqlDumpStub != nil {
return fake.RunMysqlDumpStub(hostname, port, dbName, username, password, args...)
} else {
return fake.runMysqlDumpReturns.result1
}
}

func (fake *FakeMysqlRunner) RunMysqlDumpCallCount() int {
fake.runMysqlDumpMutex.RLock()
defer fake.runMysqlDumpMutex.RUnlock()
return len(fake.runMysqlDumpArgsForCall)
}

func (fake *FakeMysqlRunner) RunMysqlDumpArgsForCall(i int) (string, int, string, string, string, []string) {
fake.runMysqlDumpMutex.RLock()
defer fake.runMysqlDumpMutex.RUnlock()
return fake.runMysqlDumpArgsForCall[i].hostname, fake.runMysqlDumpArgsForCall[i].port, fake.runMysqlDumpArgsForCall[i].dbName, fake.runMysqlDumpArgsForCall[i].username, fake.runMysqlDumpArgsForCall[i].password, fake.runMysqlDumpArgsForCall[i].args
}

func (fake *FakeMysqlRunner) RunMysqlDumpReturns(result1 error) {
fake.RunMysqlDumpStub = nil
fake.runMysqlDumpReturns = struct {
result1 error
}{result1}
}

func (fake *FakeMysqlRunner) Invocations() map[string][][]interface{} {
fake.invocationsMutex.RLock()
defer fake.invocationsMutex.RUnlock()
fake.runMysqlMutex.RLock()
defer fake.runMysqlMutex.RUnlock()
fake.runMysqlDumpMutex.RLock()
defer fake.runMysqlDumpMutex.RUnlock()
return fake.invocations
}

Expand Down
38 changes: 38 additions & 0 deletions cfmysql/mysql_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import (
"code.cloudfoundry.org/cli/cf/errors"
"fmt"
"os"
"strings"
)

//go:generate counterfeiter . MysqlRunner
type MysqlRunner interface {
RunMysql(hostname string, port int, dbName string, username string, password string, args ...string) error
RunMysqlDump(hostname string, port int, dbName string, username string, password string, args ...string) error
}

type MysqlClientRunner struct {
Expand Down Expand Up @@ -40,6 +42,42 @@ func (self *MysqlClientRunner) RunMysql(hostname string, port int, dbName string
return nil
}

func (self *MysqlClientRunner) RunMysqlDump(hostname string, port int, dbName string, username string, password string, mysqlDumpArgs ...string) error {
path, err := self.ExecWrapper.LookPath("mysqldump")
if err != nil {
return errors.New("'mysqldump' not found in PATH")
}

tableArgs := []string{}
nonTableArgs := mysqlDumpArgs[0:]

for i, argument := range (mysqlDumpArgs) {
if strings.HasPrefix(argument, "-") {
break
}

tableArgs = append(tableArgs, argument)
nonTableArgs = mysqlDumpArgs[i + 1:]
}

args := []string{"-u", username, "-p" + password, "-h", hostname, "-P", strconv.Itoa(port)}
args = append(args, nonTableArgs...)
args = append(args, dbName)
args = append(args, tableArgs...)

cmd := exec.Command(path, args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

err = self.ExecWrapper.Run(cmd)
if err != nil {
return fmt.Errorf("Error running mysqldump: %s", err)
}

return nil
}

func (self *MysqlClientRunner) MakeMysqlCommand(hostname string, port int, dbName string, username string, password string) *exec.Cmd {
return exec.Command("mysql", "-u", "username", "-p" + password, "-h", "hostname", "-P", strconv.Itoa(port), dbName)
}
Expand Down
73 changes: 73 additions & 0 deletions cfmysql/mysql_runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ var _ = Describe("MysqlRunner", func() {
err := runner.RunMysql("hostname", 42, "dbname", "username", "password")

Expect(err).To(Equal(errors.New("'mysql' client not found in PATH")))
Expect(exec.LookPathArgsForCall(0)).To(Equal("mysql"))
})
})

Expand Down Expand Up @@ -81,4 +82,76 @@ var _ = Describe("MysqlRunner", func() {
})
})
})

Context("RunMysqlDump", func() {
var exec *cfmysqlfakes.FakeExec
var runner MysqlClientRunner

BeforeEach(func() {
exec = new(cfmysqlfakes.FakeExec)
runner = MysqlClientRunner{
ExecWrapper: exec,
}
})

Context("When mysqldump is not in PATH", func() {
It("Returns an error", func() {
exec.LookPathReturns("", errors.New("PC LOAD LETTER"))

err := runner.RunMysqlDump("hostname", 42, "dbname", "username", "password")

Expect(err).To(Equal(errors.New("'mysqldump' not found in PATH")))
Expect(exec.LookPathArgsForCall(0)).To(Equal("mysqldump"))
})
})

Context("When Run returns an error", func() {
It("Forwards the error", func() {
exec.LookPathReturns("/path/to/mysqldump", nil)
exec.RunReturns(errors.New("PC LOAD LETTER"))

err := runner.RunMysqlDump("hostname", 42, "dbname", "username", "password")

Expect(err).To(Equal(errors.New("Error running mysqldump: PC LOAD LETTER")))
})
})

Context("When mysqldump is in PATH", func() {
It("Calls mysqldump with the right arguments", func() {
exec.LookPathReturns("/path/to/mysqldump", nil)

err := runner.RunMysqlDump("hostname", 42, "dbname", "username", "password")

Expect(err).To(BeNil())
Expect(exec.LookPathCallCount()).To(Equal(1))
Expect(exec.RunCallCount()).To(Equal(1))

cmd := exec.RunArgsForCall(0)
Expect(cmd.Path).To(Equal("/path/to/mysqldump"))
Expect(cmd.Args).To(Equal([]string{"/path/to/mysqldump", "-u", "username", "-ppassword", "-h", "hostname", "-P", "42", "dbname"}))
Expect(cmd.Stdin).To(Equal(os.Stdin))
Expect(cmd.Stdout).To(Equal(os.Stdout))
Expect(cmd.Stderr).To(Equal(os.Stderr))
})
})

Context("When mysqldump is in PATH and additional arguments are passed", func() {
It("Calls mysqldump with the right arguments", func() {
exec.LookPathReturns("/path/to/mysqldump", nil)

err := runner.RunMysqlDump("hostname", 42, "dbname", "username", "password", "table1", "table2", "--foo", "bar", "--baz")

Expect(err).To(BeNil())
Expect(exec.LookPathCallCount()).To(Equal(1))
Expect(exec.RunCallCount()).To(Equal(1))

cmd := exec.RunArgsForCall(0)
Expect(cmd.Path).To(Equal("/path/to/mysqldump"))
Expect(cmd.Args).To(Equal([]string{"/path/to/mysqldump", "-u", "username", "-ppassword", "-h", "hostname", "-P", "42", "--foo", "bar", "--baz", "dbname", "table1", "table2"}))
Expect(cmd.Stdin).To(Equal(os.Stdin))
Expect(cmd.Stdout).To(Equal(os.Stdout))
Expect(cmd.Stderr).To(Equal(os.Stderr))
})
})
})
})
41 changes: 35 additions & 6 deletions cfmysql/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func (self *MysqlPlugin) GetMetadata() plugin.PluginMetadata {
Name: "mysql",
Version: plugin.VersionType{
Major: 1,
Minor: 0,
Minor: 3,
Build: 0,
},
MinCliVersion: plugin.VersionType{
Expand All @@ -41,21 +41,38 @@ func (self *MysqlPlugin) GetMetadata() plugin.PluginMetadata {
"cf mysql <service-name> [mysql args...]",
},
},
{
Name: "mysqldump",
HelpText: "Dump a MySQL database",
UsageDetails: plugin.Usage{
Usage: "Get a list of available databases:\n " +
"cf mysqldump\n\n " +
"Dumping all tables in a database:\n " +
"cf mysqldump <service-name> [mysqldump args...]\n\n " +
"Dumping specific tables in a database:\n " +
"cf mysqldump <service-name> [tables...] [mysqldump args...]",
},
},
},
}
}

func (self *MysqlPlugin) Run(cliConnection plugin.CliConnection, args []string) {
command := args[0]
mysqlArgs := []string{}
if len(args) > 2 {
mysqlArgs = args[2:]
}

switch command {
case "mysql":
fallthrough

case "mysqldump":
if len(args) > 1 {
dbName := args[1]

mysqlArgs := []string{}
if len(args) > 2 {
mysqlArgs = args[2:]
}

self.connectTo(cliConnection, command, dbName, mysqlArgs)
} else {
self.showServices(cliConnection, command)
Expand Down Expand Up @@ -103,7 +120,7 @@ func (self *MysqlPlugin) connectTo(cliConnection plugin.CliConnection, command s
tunnelPort := self.PortFinder.GetPort()
self.ApiClient.OpenSshTunnel(cliConnection, *service, startedApps[0].Name, tunnelPort)

err = self.MysqlRunner.RunMysql("127.0.0.1", tunnelPort, service.DbName, service.Username, service.Password, mysqlArgs...)
err = self.runClient(command, "127.0.0.1", tunnelPort, service.DbName, service.Username, service.Password, mysqlArgs...)
if err != nil {
fmt.Fprintf(self.Err, "FAILED\n%s", err)
self.setErrorExit()
Expand All @@ -119,6 +136,18 @@ func getServiceByName(services []MysqlService, dbName string) (*MysqlService, bo
return nil, false
}

func (self *MysqlPlugin) runClient(command string, hostname string, port int, dbName string, username string, password string, args ...string) error {
switch command {
case "mysql":
return self.MysqlRunner.RunMysql(hostname, port, dbName, username, password, args...)

case "mysqldump":
return self.MysqlRunner.RunMysqlDump(hostname, port, dbName, username, password, args...)
}

panic(fmt.Errorf("Command not implemented: %s", command))
}

func (self *MysqlPlugin) showServices(cliConnection plugin.CliConnection, command string) {
services, err := self.ApiClient.GetMysqlServices(cliConnection)
if err != nil {
Expand Down
Loading

0 comments on commit a0b5380

Please sign in to comment.