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  }