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 }