github.com/koron/hk@v0.0.0-20150303213137-b8aeaa3ab34c/pg.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"net/url"
     6  	"os"
     7  	"os/exec"
     8  	"strconv"
     9  	"strings"
    10  	"text/tabwriter"
    11  
    12  	"github.com/heroku/hk/Godeps/_workspace/src/github.com/bgentry/heroku-go"
    13  )
    14  
    15  var cmdPgList = &Command{
    16  	Run:      runPgList,
    17  	Usage:    "pg-list",
    18  	NeedsApp: true,
    19  	Category: "pg",
    20  	Short:    "list Heroku Postgres databases" + extra,
    21  	Long: `
    22  Pg-list shows the name, plan, state, and connection count for
    23  all Heroku Postgres databases on an app. Forks and followers are
    24  shown in a tree under the database they follow.
    25  
    26  The database configured as your DATABASE_URL is indicated with
    27  an asterisk (*). Exclamation marks (!!) indicate databases which
    28  are due for maintenance.
    29  
    30  Examples:
    31  
    32      $ hk pg-list
    33      * heroku-postgresql-crimson       crane  available  5
    34        └───> heroku-postgresql-copper  ronin  available  3
    35  
    36      $ hk pg-list
    37        heroku-postgresql-green              standard-tengu  available     3
    38      * heroku-postgresql-olive              standard-tengu  available     3
    39        ├───> heroku-postgresql-gray         standard-tengu  available !!  3
    40        ├─ ─┤ heroku-postgresql-rose         standard-tengu  available     3
    41        │     └───> heroku-postgresql-white  standard-tengu  available     3
    42        └─ ─┤ heroku-postgresql-teal         standard-tengu  available     3
    43  `,
    44  }
    45  
    46  func runPgList(cmd *Command, args []string) {
    47  	if len(args) != 0 {
    48  		cmd.PrintUsage()
    49  		os.Exit(2)
    50  	}
    51  	appname := mustApp()
    52  	// list all addons
    53  	addons, err := client.AddonList(appname, nil)
    54  	must(err)
    55  
    56  	// locate Heroku Postgres addons
    57  	hpgprefix := hpgAddonName() + "-"
    58  	hpgs := make(map[string]*heroku.Addon)
    59  	for i := range addons {
    60  		if strings.HasPrefix(addons[i].Name, hpgprefix) {
    61  			hpgs[addons[i].Name] = &addons[i]
    62  		}
    63  	}
    64  	if len(hpgs) == 0 {
    65  		return // no Heroku Postgres databases to list
    66  	}
    67  
    68  	// fetch app's config concurrently in case we need to resolve DB names
    69  	var appConf map[string]string
    70  	confch := make(chan map[string]string, 1)
    71  	errch := make(chan error, len(hpgs)+1)
    72  	go func(appname string) {
    73  		if config, err := client.ConfigVarInfo(appname); err != nil {
    74  			errch <- err
    75  		} else {
    76  			confch <- config
    77  		}
    78  	}(appname)
    79  
    80  	// fetch info for each database concurrently
    81  	var dbinfos []*fullDBInfo
    82  	dbinfoch := make(chan fullDBInfo, len(hpgs))
    83  	for name, addon := range hpgs {
    84  		go func(name string, addon *heroku.Addon) {
    85  			db := pgclient.NewDB(addon.ProviderId, addon.Plan.Name)
    86  			if dbinfo, err := db.Info(); err != nil {
    87  				errch <- err
    88  			} else {
    89  				dbinfoch <- fullDBInfo{Name: name, DBInfo: dbinfo}
    90  			}
    91  		}(name, addon)
    92  	}
    93  	// wait for db info repsonses and app config response
    94  	for i := 0; i < len(hpgs)+1; i++ {
    95  		select {
    96  		case err := <-errch:
    97  			printFatal(err.Error())
    98  		case dbinfo := <-dbinfoch:
    99  			dbinfos = append(dbinfos, &dbinfo)
   100  		case appConf = <-confch:
   101  		}
   102  	}
   103  
   104  	addonMap := newPgAddonMap(addons, appConf)
   105  	dbinfos = sortedDBInfoTree(dbinfos, addonMap)
   106  
   107  	w := tabwriter.NewWriter(os.Stdout, 1, 2, 2, ' ', 0)
   108  	defer w.Flush()
   109  	printDBTree(w, dbinfos, addonMap)
   110  }
   111  
   112  var cmdPgInfo = &Command{
   113  	Run:      runPgInfo,
   114  	Usage:    "pg-info [<dbname>]",
   115  	NeedsApp: true,
   116  	Category: "pg",
   117  	Short:    "show Heroku Postgres database info" + extra,
   118  	Long: `
   119  Pg-info shows general information about a Heroku Postgres
   120  database. If no dbname is provided, the command defaults to the
   121  app's primary database (DATABASE_URL).
   122  
   123  Examples:
   124  
   125      $ hk pg-info
   126      Name:         heroku-postgresql-crimson
   127      Env Vars:     DATABASE_URL, HEROKU_POSTGRESQL_CRIMSON_URL
   128      Plan:         Crane
   129      Status:       Available
   130      Data Size:    6.3 MB
   131      Tables:       3
   132      PG Version:   9.1.11
   133      Connections:  5
   134      Fork/Follow:  Available
   135      Rollback:     Unsupported
   136      Created:      2013-11-19 20:40 UTC
   137      Followers:    none
   138      Forks:        heroku-postgresql-copper
   139      Maintenance:  not required
   140  
   141      $ hk pg-info heroku-postgresql-crimson
   142      ...
   143  
   144      $ hk pg-info crimson
   145      ...
   146  `,
   147  }
   148  
   149  func runPgInfo(cmd *Command, args []string) {
   150  	if len(args) > 1 {
   151  		cmd.PrintUsage()
   152  		os.Exit(2)
   153  	}
   154  	appname := mustApp()
   155  	var addonName string
   156  	if len(args) > 0 {
   157  		addonName = ensurePrefix(args[0], hpgAddonName()+"-")
   158  	}
   159  
   160  	_, dbi, addonMap := mustGetDBInfoAndAddonMap(addonName, appname)
   161  	printPgInfo(addonName, dbi, &addonMap)
   162  }
   163  
   164  func printPgInfo(name string, dbi fullDBInfo, addonMap *pgAddonMap) {
   165  	w := tabwriter.NewWriter(os.Stdout, 1, 2, 2, ' ', 0)
   166  	defer w.Flush()
   167  
   168  	listRec(w, "Name:", dbi.Name)
   169  	envNames := strings.Join(addonMap.FindEnvsFromValue(dbi.DBInfo.ResourceURL), ", ")
   170  	listRec(w, "Env Vars:", envNames)
   171  
   172  	// List info items returned by PG API
   173  	for _, ie := range dbi.DBInfo.Info {
   174  		if len(ie.Values) == 0 {
   175  			listRec(w, ie.Name+":", "none")
   176  		} else {
   177  			for n, val := range ie.Values {
   178  				label := ie.Name + ":"
   179  				if n != 0 {
   180  					label = ""
   181  				}
   182  				// try to resolve the value to an addon name if PG API says we should
   183  				if ie.ResolveDBName {
   184  					valstr := val.(string)
   185  					if addonName, ok := addonMap.FindAddonFromValue(valstr); ok {
   186  						// resolved it to an addon name, print that instead
   187  						valstr = addonName
   188  					} else {
   189  						// Couldn't resolve to an addon name. Try to parse the URL so we
   190  						// can display only its Host and Path (without creds).
   191  						if u, err := url.Parse(valstr); err == nil && u.User != nil {
   192  							valstr = u.Host + u.Path
   193  						}
   194  					}
   195  					listRec(w, label, valstr)
   196  					continue
   197  				}
   198  				listRec(w, label, val)
   199  			}
   200  		}
   201  	}
   202  }
   203  
   204  var cmdPgUnfollow = &Command{
   205  	Run:      runPgUnfollow,
   206  	Usage:    "pg-unfollow <dbname>",
   207  	NeedsApp: true,
   208  	Category: "pg",
   209  	Short:    "stop a replica postgres database from following" + extra,
   210  	Long: `
   211  Pg-unfollow stops a Heroku Postgres database follower from
   212  following, turning it into a read/write database. The command
   213  will prompt for confirmation, or accept confirmation via stdin.
   214  
   215  Examples:
   216  
   217      $ hk pg-unfollow heroku-postgresql-blue
   218      warning: heroku-postgresql-blue on myapp will permanently stop following heroku-postgresql-red.
   219      warning: This cannot be undone. Please type "heroku-postgresql-blue" to continue:
   220      > heroku-postgresql-blue
   221      Unfollowed heroku-postgresql-blue on myapp.
   222  
   223      $ hk pg-unfollow blue
   224      warning: heroku-postgresql-blue on myapp will permanently stop following heroku-postgresql-red.
   225      warning: This cannot be undone. Please type "blue" to continue:
   226      > blue
   227      Unfollowed heroku-postgresql-blue on myapp.
   228  
   229      $ echo blue | hk pg-unfollow blue
   230      Unfollowed heroku-postgresql-blue on myapp.
   231  `,
   232  }
   233  
   234  func runPgUnfollow(cmd *Command, args []string) {
   235  	if len(args) != 1 {
   236  		cmd.PrintUsage()
   237  		os.Exit(2)
   238  	}
   239  	appname := mustApp()
   240  	addonName := ensurePrefix(args[0], hpgAddonName()+"-")
   241  
   242  	db, dbi, addonMap := mustGetDBInfoAndAddonMap(addonName, appname)
   243  	if !dbi.DBInfo.IsFollower() {
   244  		printFatal("%s is not following another database.", addonName)
   245  	}
   246  	parentName := getResolvedInfoValue(dbi.DBInfo, "Following", &addonMap)
   247  
   248  	printWarning("%s on %s will permanently stop following %s.", addonName, appname, parentName)
   249  	warning := fmt.Sprintf("This cannot be undone. Please type %q to continue:", args[0])
   250  	mustConfirm(warning, args[0])
   251  
   252  	must(db.Unfollow())
   253  	fmt.Printf("Unfollowed %s on %s.\n", addonName, appname)
   254  }
   255  
   256  var commandNamePsql string
   257  
   258  var cmdPsql = &Command{
   259  	Run:      runPsql,
   260  	Usage:    "psql [-c <command>] [<dbname>]",
   261  	NeedsApp: true,
   262  	Category: "pg",
   263  	Short:    "open a psql shell to a Heroku Postgres database" + extra,
   264  	Long: `
   265  Psql opens a PostgreSQL shell to a Heroku Postgres database
   266  using the locally-installed psql command.
   267  
   268  Examples:
   269  
   270      $ hk psql
   271      psql (9.3.1, server 9.1.11)
   272      SSL connection (cipher: DHE-RSA-AES256-SHA, bits: 256)
   273      Type "help" for help.
   274      
   275      d1234abcdefghi=>
   276  
   277      $ hk psql crimson
   278      ...
   279  
   280      $ hk psql heroku-postgresql-crimson
   281      ...
   282  `,
   283  }
   284  
   285  func init() {
   286  	cmdPsql.Flag.StringVarP(&commandNamePsql, "command", "c", "", "SQL command to run")
   287  }
   288  
   289  func runPsql(cmd *Command, args []string) {
   290  	if len(args) > 1 {
   291  		cmd.PrintUsage()
   292  		os.Exit(2)
   293  	}
   294  
   295  	configName := "DATABASE_URL"
   296  	if len(args) == 1 {
   297  		configName = dbNameToPgEnv(args[0])
   298  	}
   299  	appname := mustApp()
   300  
   301  	// Make sure psql is installed
   302  	if _, err := exec.LookPath("psql"); err != nil {
   303  		printFatal("Local psql command not found. For help installing psql, see http://devcenter.heroku.com/articles/local-postgresql")
   304  	}
   305  
   306  	// fetch app's config to get the URL
   307  	config, err := client.ConfigVarInfo(appname)
   308  	must(err)
   309  
   310  	// get URL
   311  	urlstr, exists := config[configName]
   312  	if !exists {
   313  		printFatal("Env %s not found", configName)
   314  	}
   315  	u, err := url.Parse(urlstr)
   316  	if err != nil {
   317  		printFatal("Invalid URL at env " + configName)
   318  	}
   319  
   320  	// handle custom port
   321  	hostname := u.Host
   322  	portnum := 5432
   323  	if colIndex := strings.Index(u.Host, ":"); colIndex != -1 {
   324  		hostname = u.Host[:colIndex]
   325  		portnum, err = strconv.Atoi(u.Host[colIndex+1:])
   326  		if err != nil {
   327  			printFatal("Invalid port in %s: %s", configName, u.Host[colIndex+1:])
   328  		}
   329  	}
   330  
   331  	if u.User == nil || u.User.Username() == "" {
   332  		printFatal("Missing credentials in %s", configName)
   333  	}
   334  
   335  	// construct and run psql command
   336  	psqlArgs := []string{
   337  		"psql",
   338  		"-U", u.User.Username(),
   339  		"-h", hostname,
   340  		"-p", strconv.Itoa(portnum),
   341  	}
   342  	if commandNamePsql != "" {
   343  		psqlArgs = append(psqlArgs, "-c")
   344  		psqlArgs = append(psqlArgs, commandNamePsql)
   345  	}
   346  	psqlArgs = append(psqlArgs, u.Path[1:])
   347  
   348  	pgenv := os.Environ()
   349  	pass, _ := u.User.Password()
   350  	pgenv = append(pgenv, "PGPASSWORD="+pass)
   351  	pgenv = append(pgenv, "PGSSLMODE=require")
   352  
   353  	if err := runCommand("psql", psqlArgs, pgenv); err != nil {
   354  		printFatal("Error running psql: %s", err)
   355  	}
   356  }