-
-
Notifications
You must be signed in to change notification settings - Fork 24
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
Add db open
command
#72
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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) | ||
} |
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) | ||
} |
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) | ||
} |
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) | ||
} | ||
} |
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you explain this more? Was it getting deleted by the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That is my theory. If we uncomment the I suspect sequel pro needs to read that spf file after launch, but golang deleted the spf too soon. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes some sense. You could verify by logging like this: func (o *DBOpenerSequelPro) open(c DBCredentials) (err error) {
# etc
open := execCommand("open", sequelProSpf.Name())
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
log.Printf("running sequel pro...")
err = cmd.Wait()
log.Printf("Command finished with error: %v", err)
} And then add a log printf to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confirmed my theroy - the SPF file is read twice:
Newer version of sequel pro shows this error message if SPF is deleted too early:
|
||
// 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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Every usage of
writeFile
deletes the file after. Maybe we should just make this a more specificwriteTempFile
function which also handles the defer delete?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How do we defer the
defer
?If we put
defer
insidewriteTempFile
, it deletes the files at the end ofwriteTempFile
instead of the end offunc (c *DBOpenCommand) Run(args []string) int
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes good call. We do this:
trellis-cli/trellis/testing.go
Lines 32 to 38 in 54aeb5d
Then it would be called as: