github.com/koron/hk@v0.0.0-20150303213137-b8aeaa3ab34c/pg_helper.go (about) 1 package main 2 3 import ( 4 "fmt" 5 "io" 6 "os" 7 "sort" 8 "strings" 9 10 "github.com/heroku/hk/Godeps/_workspace/src/github.com/bgentry/heroku-go" 11 "github.com/heroku/hk/Godeps/_workspace/src/github.com/mgutz/ansi" 12 "github.com/heroku/hk/postgresql" 13 ) 14 15 // the names of heroku postgres addons vary in dev environments 16 func hpgAddonName() string { 17 if e := os.Getenv("SHOGUN"); e != "" { 18 return "shogun-" + e 19 } 20 if e := os.Getenv("HEROKU_POSTGRESQL_ADDON_NAME"); e != "" { 21 return e 22 } 23 return "heroku-postgresql" 24 } 25 26 // addon options that Heroku Postgres needs resolved from database names to 27 // full postgres URLs 28 var hpgOptNames = []string{"fork", "follow", "rollback"} 29 30 // resolve addon options whose names are in hpgOptNames into their full URLs 31 func hpgAddonOptResolve(opts *map[string]string, appEnv map[string]string) error { 32 if opts != nil { 33 for _, k := range hpgOptNames { 34 val, ok := (*opts)[k] 35 if ok && !strings.HasPrefix(val, "postgres://") { 36 envName := dbNameToPgEnv(val) 37 url, exists := appEnv[envName] 38 if !exists { 39 return fmt.Errorf("could not resolve %s option %q to a %s addon", k, val, hpgAddonName()) 40 } 41 (*opts)[k] = url 42 } 43 } 44 } 45 return nil 46 } 47 48 func pgEnvToDBName(key string) string { 49 return strings.ToLower(strings.Replace(strings.TrimSuffix(key, "_URL"), "_", "-", -1)) 50 } 51 52 func dbNameToPgEnv(name string) string { 53 return ensureSuffix(ensurePrefix( 54 strings.ToUpper(strings.Replace(name, "-", "_", -1)), 55 strings.ToUpper(strings.Replace(hpgAddonName()+"_", "-", "_", -1)), 56 ), "_URL") 57 } 58 59 type pgAddonMap struct { 60 addonToEnv map[string][]string 61 appConf map[string]string 62 } 63 64 func (p *pgAddonMap) FindAddonFromValue(value string) (key string, ok bool) { 65 for addonName, envs := range p.addonToEnv { 66 for _, e := range envs { 67 if p.appConf[e] == value { 68 return addonName, true 69 } 70 } 71 } 72 return "", false 73 } 74 75 func (p *pgAddonMap) FindEnvsFromValue(value string) []string { 76 addonName, ok := p.FindAddonFromValue(value) 77 if !ok { 78 return []string{} 79 } 80 return p.addonToEnv[addonName] 81 } 82 83 func newPgAddonMap(addons []heroku.Addon, appConf map[string]string) pgAddonMap { 84 m := make(map[string][]string) 85 for _, addon := range addons { 86 if strings.HasPrefix(addon.Name, hpgAddonName()+"-") { 87 if len(addon.ConfigVars) > 0 { 88 m[addon.Name] = addon.ConfigVars 89 includesDbURL := false 90 for _, k := range addon.ConfigVars { 91 if k == "DATABASE_URL" { 92 includesDbURL = true 93 } 94 } 95 // add DATABASE_URL if it's not already included and the values match 96 if !includesDbURL && appConf["DATABASE_URL"] == appConf[addon.ConfigVars[0]] { 97 m[addon.Name] = append([]string{"DATABASE_URL"}, m[addon.Name]...) 98 } 99 } 100 } 101 } 102 return pgAddonMap{m, appConf} 103 } 104 105 // Fetches an app's addon list and config, and returns the postgres database 106 // info (and addon map) for the database specified by addonName. If addonName is 107 // "", default to whichever addon matches DATABASE_URL, if any. 108 func mustGetDBInfoAndAddonMap(addonName, appname string) (postgresql.DB, fullDBInfo, pgAddonMap) { 109 // fetch app's config concurrently in case we need to resolve DB names 110 var appConf map[string]string 111 confch := make(chan map[string]string, 1) 112 errch := make(chan error, 1) 113 go func(appname string) { 114 if config, err := client.ConfigVarInfo(appname); err != nil { 115 errch <- err 116 } else { 117 confch <- config 118 } 119 }(appname) 120 121 // list all addons 122 addons, err := client.AddonList(appname, nil) 123 must(err) 124 125 // we'll need this from a couple places below 126 var addonMap pgAddonMap 127 waitForAddonMap := func() { 128 select { 129 case appConf = <-confch: 130 addonMap = newPgAddonMap(addons, appConf) 131 case err := <-errch: 132 printFatal(err.Error()) 133 } 134 } 135 136 // locate specific addon 137 var addon *heroku.Addon 138 // default to whichever addon is DATABASE_URL if one isn't specified 139 if addonName == "" { 140 // block on getting the addon map since we need it to determine which addon 141 // is the DATABASE_URL 142 waitForAddonMap() 143 dbURL := appConf["DATABASE_URL"] 144 if dbURL == "" { 145 printFatal("app has no DATABASE_URL, please specify a database name") 146 } 147 var ok bool 148 addonName, ok = addonMap.FindAddonFromValue(dbURL) 149 if !ok { 150 printFatal("no addon matches DATABASE_URL") 151 } 152 } 153 for i := range addons { 154 if addons[i].Name == addonName { 155 addon = &addons[i] 156 break 157 } 158 } 159 if addon == nil { 160 printFatal("addon %s not found", addonName) 161 } 162 163 db := pgclient.NewDB(addon.ProviderId, addon.Plan.Name) 164 dbi, err := db.Info() 165 must(err) 166 167 if appConf == nil { 168 waitForAddonMap() // might not have this yet if addonName was "" to start 169 } 170 171 return db, fullDBInfo{Name: addonName, DBInfo: dbi}, addonMap 172 } 173 174 type fullDBInfo struct { 175 Name string 176 DBInfo postgresql.DBInfo 177 Parent *fullDBInfo 178 Children []*fullDBInfo 179 } 180 181 func (f *fullDBInfo) MaintenanceString() string { 182 valstr, _ := f.DBInfo.Info.GetString("Maintenance") 183 if valstr != "" && valstr != "not required" { 184 return " " + ansi.Color("!!", "red+b") + ansi.ColorCode("reset") 185 } 186 return "" 187 } 188 189 // fullDBInfosByName implements sort.Interface for []*fullDBInfo based on the 190 // Name field. 191 type fullDBInfosByName []*fullDBInfo 192 193 func (f fullDBInfosByName) Len() int { return len(f) } 194 func (f fullDBInfosByName) Swap(i, j int) { f[i], f[j] = f[j], f[i] } 195 func (f fullDBInfosByName) Less(i, j int) bool { return f[i].Name < f[j].Name } 196 197 func sortedDBInfoTree(dbinfos []*fullDBInfo, addonMap pgAddonMap) (result []*fullDBInfo) { 198 sort.Sort(fullDBInfosByName(dbinfos)) 199 // get all children organized under their parents 200 for _, info := range dbinfos { 201 parentName := getResolvedInfoValue(info.DBInfo, "Forked From", &addonMap) 202 if parentName == "" { 203 parentName = getResolvedInfoValue(info.DBInfo, "Following", &addonMap) 204 } 205 if parentName != "" { 206 for _, parent := range dbinfos { 207 if parent.Name == parentName { 208 parent.Children = append(parent.Children, info) 209 info.Parent = parent 210 break 211 } 212 } 213 } 214 } 215 // keep items that have no parent 216 for i := 0; i < len(dbinfos); i++ { 217 if dbinfos[i].Parent == nil { 218 result = append(result, dbinfos[i]) 219 } 220 } 221 return 222 } 223 224 func printDBTree(w io.Writer, dbinfos []*fullDBInfo, addonMap pgAddonMap) { 225 for _, info := range dbinfos { 226 name := info.Name 227 if info.Parent != nil { 228 name = printTreeElements(info) + name 229 } 230 dburlMarker := " " 231 if stringsIndex(addonMap.addonToEnv[info.Name], "DATABASE_URL") != -1 { 232 dburlMarker = "* " 233 } 234 status, _ := info.DBInfo.Info.GetString("Status") 235 listRec(w, 236 dburlMarker+name, 237 info.DBInfo.Plan, 238 strings.ToLower(status)+info.MaintenanceString(), 239 info.DBInfo.NumConnections, 240 ) 241 if len(info.Children) > 0 { 242 printDBTree(w, info.Children, addonMap) 243 } 244 } 245 } 246 247 const ( 248 treeMiddleBranch = "├─" 249 treeLastBranch = "└─" 250 treeForkSymbol = " ─┤" 251 treeFollowSymbol = "──>" 252 treeIndentMiddle = "│ " 253 treeIndentLast = " " 254 ) 255 256 func printTreeElements(info *fullDBInfo) string { 257 if info == nil || info.Parent == nil { 258 return "" 259 } 260 prefix := "" 261 for curInfo, p := info, info.Parent; p != nil; curInfo, p = p, p.Parent { 262 prefix = treePrefix(p, curInfo, prefix == "") + prefix 263 } 264 symbol := treeForkSymbol 265 if info.DBInfo.IsFollower() { 266 symbol = treeFollowSymbol 267 } 268 return prefix + symbol + " " 269 } 270 271 func treePrefix(parent, info *fullDBInfo, firstLevel bool) string { 272 if parent.Children[len(parent.Children)-1] == info { 273 // this is the parent's last child 274 if firstLevel { 275 return treeLastBranch 276 } 277 return treeIndentLast 278 } 279 if firstLevel { 280 return treeMiddleBranch 281 } 282 return treeIndentMiddle 283 } 284 285 func getResolvedInfoValue(dbi postgresql.DBInfo, key string, addonMap *pgAddonMap) string { 286 val, resolve := dbi.Info.GetString(key) 287 if val != "" && resolve { 288 if addonName, ok := addonMap.FindAddonFromValue(val); ok { 289 return addonName 290 } 291 } 292 return val 293 }