-
-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add `db open` command
- Loading branch information
Showing
10 changed files
with
494 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package cmd | ||
|
||
import ( | ||
"strings" | ||
|
||
"github.com/mitchellh/cli" | ||
"trellis-cli/trellis" | ||
) | ||
|
||
type DBCommand struct { | ||
UI cli.Ui | ||
Trellis *trellis.Trellis | ||
} | ||
|
||
func (c *DBCommand) Run(args []string) int { | ||
return cli.RunResultHelp | ||
} | ||
|
||
func (c *DBCommand) Synopsis() string { | ||
return "Commands for database management" | ||
} | ||
|
||
func (c *DBCommand) Help() string { | ||
helpText := ` | ||
Usage: trellis db <subcommand> [<args>] | ||
` | ||
|
||
return strings.TrimSpace(helpText) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
package cmd | ||
|
||
import ( | ||
"encoding/json" | ||
"flag" | ||
"fmt" | ||
"io/ioutil" | ||
"strings" | ||
|
||
"github.com/mitchellh/cli" | ||
"trellis-cli/templates" | ||
"trellis-cli/trellis" | ||
) | ||
|
||
const playbookPath = "dump_db_credentials.yml" | ||
const dumpDbCredentialsYmlTemplate = ` | ||
--- | ||
- name: 'Trellis CLI: Dump database credentials' | ||
hosts: web:&{{ env }} | ||
remote_user: "{{ web_user }}" | ||
gather_facts: false | ||
connection: local | ||
tasks: | ||
- name: Dump database credentials | ||
template: | ||
src: db_credentials.json.j2 | ||
dest: "{{ dest }}" | ||
mode: '0600' | ||
with_dict: "{{ wordpress_sites }}" | ||
when: item.key == site | ||
` | ||
|
||
const j2TemplatePath = "db_credentials.json.j2" | ||
const dbCredentialsJsonJ2Template = ` | ||
{ | ||
"web_user": "{{ web_user }}", | ||
"ansible_host": "{{ ansible_host }}", | ||
"ansible_port": {{ ansible_port | default(22) }}, | ||
"db_user": "{{ site_env.db_user }}", | ||
"db_password": "{{ site_env.db_password }}", | ||
"db_host": "{{ site_env.db_host }}", | ||
"db_name": "{{ site_env.db_name }}", | ||
"wp_env": "{{ site_env.wp_env }}" | ||
} | ||
` | ||
|
||
func NewDBOpenCommand(ui cli.Ui, trellis *trellis.Trellis, dbOpenerFactory *DBOpenerFactory) *DBOpenCommand { | ||
c := &DBOpenCommand{UI: ui, Trellis: trellis, dbOpenerFactory: dbOpenerFactory} | ||
c.init() | ||
return c | ||
} | ||
|
||
type DBOpenCommand struct { | ||
UI cli.Ui | ||
flags *flag.FlagSet | ||
app string | ||
Trellis *trellis.Trellis | ||
dbOpenerFactory *DBOpenerFactory | ||
} | ||
|
||
type DBCredentials struct { | ||
SSHUser string `json:"web_user"` | ||
SSHHost string `json:"ansible_host"` | ||
SSHPort int `json:"ansible_port"` | ||
DBUser string `json:"db_user"` | ||
DBPassword string `json:"db_password"` | ||
DBHost string `json:"db_host"` | ||
DBName string `json:"db_name"` | ||
WPEnv string `json:"wp_env"` | ||
} | ||
|
||
func (c *DBOpenCommand) init() { | ||
c.flags = flag.NewFlagSet("", flag.ContinueOnError) | ||
c.flags.Usage = func() { c.UI.Info(c.Help()) } | ||
c.flags.StringVar(&c.app, "app", "", "Database client to be used; Supported: tableplus, sequel-pro") | ||
} | ||
|
||
func (c *DBOpenCommand) Run(args []string) int { | ||
if err := c.Trellis.LoadProject(); err != nil { | ||
c.UI.Error(err.Error()) | ||
return 1 | ||
} | ||
|
||
if flagsParseErr := c.flags.Parse(args); flagsParseErr != nil { | ||
return 1 | ||
} | ||
|
||
args = c.flags.Args() | ||
|
||
commandArgumentValidator := &CommandArgumentValidator{required: 1, optional: 1} | ||
if commandArgumentErr := commandArgumentValidator.validate(args); commandArgumentErr != nil { | ||
c.UI.Error(commandArgumentErr.Error()) | ||
c.UI.Output(c.Help()) | ||
return 1 | ||
} | ||
|
||
environment := c.flags.Arg(0) | ||
environmentErr := c.Trellis.ValidateEnvironment(environment) | ||
if environmentErr != nil { | ||
c.UI.Error(environmentErr.Error()) | ||
return 1 | ||
} | ||
|
||
siteNameArg := c.flags.Arg(1) | ||
siteName, siteNameErr := c.Trellis.FindSiteNameFromEnvironment(environment, siteNameArg) | ||
if siteNameErr != nil { | ||
c.UI.Error(siteNameErr.Error()) | ||
return 1 | ||
} | ||
|
||
// Template JSON file for db credentials | ||
dbCredentialsJson, dbCredentialsErr := ioutil.TempFile("", "*.json") | ||
if dbCredentialsErr != nil { | ||
c.UI.Error(fmt.Sprintf("Error createing temporary db credentials JSON file: %s", dbCredentialsErr)) | ||
} | ||
defer deleteFile(dbCredentialsJson.Name()) | ||
|
||
// Template playbook files from package to Trellis | ||
writeFile(playbookPath, templates.TrimSpace(dumpDbCredentialsYmlTemplate)) | ||
defer deleteFile(playbookPath) | ||
writeFile(j2TemplatePath, templates.TrimSpace(dbCredentialsJsonJ2Template)) | ||
defer deleteFile(j2TemplatePath) | ||
|
||
// Run the playbook to generate dbCredentialsJson | ||
playbookCommand := execCommand("ansible-playbook", playbookPath, "-e", "env="+environment, "-e", "site="+siteName, "-e", "dest="+dbCredentialsJson.Name()) | ||
appendEnvironmentVariable(playbookCommand, "ANSIBLE_RETRY_FILES_ENABLED=false") | ||
logCmd(playbookCommand, c.UI, true) | ||
playbookErr := playbookCommand.Run() | ||
if playbookErr != nil { | ||
c.UI.Error(fmt.Sprintf("Error running ansible-playbook: %s", playbookErr)) | ||
return 1 | ||
} | ||
|
||
// Read dbCredentialsJson | ||
dbCredentialsByte, readErr := ioutil.ReadFile(dbCredentialsJson.Name()) | ||
if readErr != nil { | ||
c.UI.Error(fmt.Sprintf("Error reading db credentials JSON file: %s", readErr)) | ||
return 1 | ||
} | ||
var dbCredentials DBCredentials | ||
unmarshalErr := json.Unmarshal(dbCredentialsByte, &dbCredentials) | ||
if unmarshalErr != nil { | ||
c.UI.Error(fmt.Sprintf("Error unmarshaling db credentials JSON file: %s", unmarshalErr)) | ||
return 1 | ||
} | ||
|
||
// Open database with dbCredentialsJson and the app | ||
opener, newDBOpenerErr := c.dbOpenerFactory.make(c.app, c.UI) | ||
if newDBOpenerErr != nil { | ||
c.UI.Error(fmt.Sprintf("Error initializing new db opener object: %s", newDBOpenerErr)) | ||
return 1 | ||
} | ||
|
||
openErr := opener.open(dbCredentials) | ||
if openErr != nil { | ||
c.UI.Error(fmt.Sprintf("Error opening db: %s", openErr)) | ||
return 1 | ||
} | ||
|
||
return 0 | ||
} | ||
|
||
func (c *DBOpenCommand) Synopsis() string { | ||
return "Open database with GUI applications" | ||
} | ||
|
||
func (c *DBOpenCommand) Help() string { | ||
helpText := ` | ||
Usage: trellis db [options] ENVIRONMENT [SITE] | ||
Open database with GUI applications | ||
Open a site's production database with tableplus: | ||
$ trellis db open --app=tableplus production example.com | ||
Arguments: | ||
ENVIRONMENT Name of environment (ie: production) | ||
SITE Name of the site (ie: example.com); Optional when only single site exist in the environment | ||
Options: | ||
--app Database client to be open with; Supported: tableplus, sequel-pro | ||
-h, --help show this help | ||
` | ||
|
||
return strings.TrimSpace(helpText) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
|
||
"github.com/mitchellh/cli" | ||
) | ||
|
||
type DBOpenerFactory struct {} | ||
|
||
type DBOpener interface { | ||
open(c DBCredentials) (err error) | ||
} | ||
|
||
func (f *DBOpenerFactory) make(app string, ui cli.Ui) (o DBOpener, err error) { | ||
switch app { | ||
case "tableplus": | ||
return &DBOpenerTableplus{}, nil | ||
case "sequel-pro": | ||
return &DBOpenerSequelPro{ui: ui}, nil | ||
} | ||
|
||
return nil, fmt.Errorf("Error: %s is not supported", app) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package cmd | ||
|
||
import ( | ||
"strings" | ||
"testing" | ||
"reflect" | ||
|
||
"github.com/mitchellh/cli" | ||
) | ||
|
||
func TestMakeUnexpected(t *testing.T) { | ||
factory := &DBOpenerFactory{} | ||
|
||
_, actualErr := factory.make("unexpected-app", cli.NewMockUi()) | ||
|
||
actualErrorMessage := actualErr.Error() | ||
|
||
expected := "unexpected-app is not supported" | ||
if !strings.Contains(actualErrorMessage, expected) { | ||
t.Errorf("expected command %s to contains %q", actualErr, expected) | ||
} | ||
} | ||
|
||
func TestMakeSequelPro(t *testing.T) { | ||
factory := &DBOpenerFactory{} | ||
|
||
actual, actualErr := factory.make("sequel-pro", cli.NewMockUi()) | ||
|
||
if actualErr != nil { | ||
t.Errorf("expected error %s to be nil", actualErr) | ||
} | ||
|
||
actualType := reflect.TypeOf(actual) | ||
expectedType := reflect.TypeOf(&DBOpenerSequelPro{}) | ||
|
||
if actualType != expectedType { | ||
t.Errorf("expected return type %s to be %s", actualType, expectedType) | ||
} | ||
} | ||
|
||
func TestMakeTableplus(t *testing.T) { | ||
factory := &DBOpenerFactory{} | ||
|
||
actual, actualErr := factory.make("tableplus", cli.NewMockUi()) | ||
|
||
if actualErr != nil { | ||
t.Errorf("expected error %s to be nil", actualErr) | ||
} | ||
|
||
actualType := reflect.TypeOf(actual) | ||
expectedType := reflect.TypeOf(&DBOpenerTableplus{}) | ||
|
||
if actualType != expectedType { | ||
t.Errorf("expected return type %s to be %s", actualType, expectedType) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
package cmd | ||
|
||
import ( | ||
"fmt" | ||
|
||
"io/ioutil" | ||
"text/template" | ||
|
||
"github.com/mitchellh/cli" | ||
) | ||
|
||
type DBOpenerSequelPro struct { | ||
ui cli.Ui | ||
} | ||
|
||
const sequelProSpfTemplate = ` | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>ContentFilters</key> | ||
<dict/> | ||
<key>auto_connect</key> | ||
<true/> | ||
<key>data</key> | ||
<dict> | ||
<key>connection</key> | ||
<dict> | ||
<key>database</key> | ||
<string>{{.DBName}}</string> | ||
<key>host</key> | ||
<string>{{.DBHost}}</string> | ||
<key>user</key> | ||
<string>{{.DBUser}}</string> | ||
<key>password</key> | ||
<string>{{.DBPassword}}</string> | ||
<key>ssh_host</key> | ||
<string>{{.SSHHost}}</string> | ||
<key>ssh_port</key> | ||
<string>{{.SSHPort}}</string> | ||
<key>ssh_user</key> | ||
<string>{{.SSHUser}}</string> | ||
<key>type</key> | ||
<string>SPSSHTunnelConnection</string> | ||
</dict> | ||
</dict> | ||
<key>format</key> | ||
<string>connection</string> | ||
<key>queryFavorites</key> | ||
<array/> | ||
<key>queryHistory</key> | ||
<array/> | ||
</dict> | ||
</plist> | ||
` | ||
|
||
func (o *DBOpenerSequelPro) open(c DBCredentials) (err error) { | ||
sequelProSpf, sequelProSpfErr := ioutil.TempFile("", "*.spf") | ||
if sequelProSpfErr != nil { | ||
return fmt.Errorf("Error creating temporary SequelPro SPF file: %s", sequelProSpfErr) | ||
} | ||
// TODO: [Help Wanted] There is a chance that the SPF file got deleted before SequelPro establish db connection. | ||
// But we really want to delete this file because it contains db credentials in plain text. | ||
//defer deleteFile(sequelProSpf.Name()) | ||
|
||
tmpl, tmplErr := template.New("sequelProSpf").Parse(sequelProSpfTemplate) | ||
if tmplErr != nil { | ||
return fmt.Errorf("Error templating SequelPro SPF: %s", tmplErr) | ||
} | ||
|
||
tmplExecuteErr := tmpl.Execute(sequelProSpf, c) | ||
if tmplExecuteErr != nil { | ||
return fmt.Errorf("Error writing SequelPro SPF: %s", tmplExecuteErr) | ||
} | ||
|
||
open := execCommand("open", sequelProSpf.Name()) | ||
logCmd(open, o.ui, true) | ||
openErr := open.Run() | ||
if openErr != nil { | ||
return fmt.Errorf("Error opening database with Tableplus: %s", openErr) | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.