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

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"log"
     7  	"math/big"
     8  	"os"
     9  	"sort"
    10  	"strings"
    11  	"text/tabwriter"
    12  
    13  	"github.com/heroku/hk/Godeps/_workspace/src/github.com/bgentry/heroku-go"
    14  )
    15  
    16  var cmdAddons = &Command{
    17  	Run:      runAddons,
    18  	Usage:    "addons [<service>:<plan>...]",
    19  	NeedsApp: true,
    20  	Category: "add-on",
    21  	Short:    "list addons",
    22  	Long: `
    23  Lists addons.
    24  
    25  Examples:
    26  
    27      $ hk addons
    28      heroku-postgresql-blue  heroku-postgresql:crane  Nov 19 12:40
    29      pgbackups               pgbackups:plus           Sep 30 15:43
    30  
    31      $ hk addons pgbackups
    32      pgbackups  pgbackups:plus  Sep 30 15:43
    33  `,
    34  }
    35  
    36  func runAddons(cmd *Command, names []string) {
    37  	appname := mustApp()
    38  	addons, err := client.AddonList(appname, nil)
    39  	must(err)
    40  
    41  	w := tabwriter.NewWriter(os.Stdout, 1, 2, 2, ' ', 0)
    42  	defer w.Flush()
    43  
    44  	for i, s := range names {
    45  		names[i] = strings.ToLower(s)
    46  	}
    47  	for _, a := range addons {
    48  		if len(names) == 0 || addonMatch(a, names) {
    49  			listAddon(w, a)
    50  		}
    51  	}
    52  }
    53  
    54  func addonMatch(a heroku.Addon, names []string) bool {
    55  	for _, name := range names {
    56  		if name == strings.ToLower(a.Name) {
    57  			return true
    58  		}
    59  		if name == strings.ToLower(a.Plan.Name) {
    60  			return true
    61  		}
    62  		if name == strings.ToLower(a.Id) {
    63  			return true
    64  		}
    65  	}
    66  	return false
    67  }
    68  
    69  func listAddon(w io.Writer, a heroku.Addon) {
    70  	name := a.Name
    71  	if name == "" {
    72  		name = "[unnamed]"
    73  	}
    74  	listRec(w,
    75  		name,
    76  		a.Plan.Name,
    77  		prettyTime{a.CreatedAt},
    78  	)
    79  }
    80  
    81  var cmdAddonAdd = &Command{
    82  	Run:      runAddonAdd,
    83  	Usage:    "addon-add <service>[:<plan>] [<config>=<value>...]",
    84  	NeedsApp: true,
    85  	Category: "add-on",
    86  	Short:    "add an addon",
    87  	Long: `
    88  Adds an addon to an app.
    89  
    90  Examples:
    91  
    92      $ hk addon-add heroku-postgresql
    93      Added heroku-postgresql:hobby-dev to myapp as heroku-postgresql-yellow.
    94  
    95      $ hk addon-add heroku-postgresql:standard-tengu
    96      Added heroku-postgresql:standard-tengu to myapp as heroku-postgresql-orange.
    97  `,
    98  }
    99  
   100  func runAddonAdd(cmd *Command, args []string) {
   101  	appname := mustApp()
   102  	if len(args) == 0 {
   103  		cmd.PrintUsage()
   104  		os.Exit(2)
   105  	}
   106  	plan := args[0]
   107  	var opts heroku.AddonCreateOpts
   108  	if len(args) > 1 {
   109  		config, err := parseAddonAddConfig(args[1:])
   110  		if err != nil {
   111  			log.Println(err)
   112  			os.Exit(2)
   113  		}
   114  		// if this is a postgres addon, resolve fork/follow/rollback args
   115  		provider, _ := splitProviderAndPlan(plan)
   116  		if provider == hpgAddonName() && config != nil {
   117  			for k := range *config {
   118  				if i := stringsIndex(hpgOptNames, k); i != -1 {
   119  					// contains an hpgOptNames key, we need to resolve these against envs
   120  					appEnv, err := client.ConfigVarInfo(appname)
   121  					must(err)
   122  					must(hpgAddonOptResolve(config, appEnv))
   123  					break
   124  				}
   125  			}
   126  		}
   127  		opts = heroku.AddonCreateOpts{Config: config}
   128  	}
   129  	addon, err := client.AddonCreate(appname, plan, &opts)
   130  	must(err)
   131  	log.Printf("Added %s to %s as %s.", addon.Plan.Name, appname, addon.Name)
   132  }
   133  
   134  func splitProviderAndPlan(providerAndPlan string) (provider string, plan string) {
   135  	parts := strings.Split(providerAndPlan, ":")
   136  	if len(parts) > 0 {
   137  		provider = parts[0]
   138  	}
   139  	if len(parts) > 1 {
   140  		plan = parts[1]
   141  	}
   142  	return
   143  }
   144  
   145  func parseAddonAddConfig(config []string) (*map[string]string, error) {
   146  	conf := make(map[string]string, len(config))
   147  	for _, kv := range config {
   148  		iEq := strings.IndexRune(kv, '=')
   149  		if iEq < 1 || len(kv) < iEq+2 {
   150  			return nil, fmt.Errorf("Invalid option '%s', must be of form 'key=value'", kv)
   151  		}
   152  		val := kv[iEq+1:]
   153  		if val[0] == '\'' {
   154  			val = strings.Trim(val, "'")
   155  		} else if val[0] == '"' {
   156  			val = strings.Trim(val, "\"")
   157  		}
   158  		conf[kv[:iEq]] = val
   159  	}
   160  	return &conf, nil
   161  }
   162  
   163  var cmdAddonDestroy = &Command{
   164  	Run:      runAddonDestroy,
   165  	Usage:    "addon-destroy <name>",
   166  	NeedsApp: true,
   167  	Category: "add-on",
   168  	Short:    "destroy an addon",
   169  	Long: `
   170  Removes an addon from an app, permanently destroying any data
   171  stored by that addon. The command will prompt for confirmation,
   172  or accept confirmation via stdin.
   173  
   174  Examples:
   175  
   176      $ hk addon-destroy heroku-postgresql-blue
   177      warning: This will destroy heroku-postgresql-blue on myapp. Please type "myapp" to continue:
   178      > myapp
   179      Destroyed heroku-postgresql-blue on myapp.
   180  
   181      $ echo myapp | hk addon-destroy redistogo
   182      Destroyed redistogo on myapp.
   183  `,
   184  }
   185  
   186  func runAddonDestroy(cmd *Command, args []string) {
   187  	appname := mustApp()
   188  	if len(args) != 1 {
   189  		cmd.PrintUsage()
   190  		os.Exit(2)
   191  	}
   192  	name := args[0]
   193  	if strings.IndexRune(name, ':') != -1 {
   194  		// specified an addon with plan name, unsupported in v3
   195  		log.Println("Please specify an addon name, not a plan name.")
   196  		cmd.PrintUsage()
   197  		os.Exit(2)
   198  	}
   199  
   200  	warning := "This will destroy %s on %s. Please type %q to continue:"
   201  	mustConfirm(fmt.Sprintf(warning, name, appname, appname), appname)
   202  
   203  	checkAddonError(client.AddonDelete(appname, name))
   204  	log.Printf("Destroyed %s on %s.", name, appname)
   205  }
   206  
   207  var cmdAddonOpen = &Command{
   208  	Run:      runAddonOpen,
   209  	Usage:    "addon-open <name>",
   210  	NeedsApp: true,
   211  	Category: "add-on",
   212  	Short:    "open an addon" + extra,
   213  	Long: `
   214  Open the addon's management page in your default web browser.
   215  
   216  Examples:
   217  
   218      $ hk addon-open heroku-postgresql-blue
   219  
   220      $ hk addon-open redistogo
   221  `,
   222  }
   223  
   224  func runAddonOpen(cmd *Command, args []string) {
   225  	appname := mustApp()
   226  	if len(args) != 1 {
   227  		cmd.PrintUsage()
   228  		os.Exit(2)
   229  	}
   230  	name := args[0]
   231  	// look up addon to make sure it exists and to get plan name
   232  	a, err := client.AddonInfo(appname, name)
   233  	checkAddonError(err)
   234  	must(openURL("https://addons-sso.heroku.com/apps/" + appname + "/addons/" + a.Plan.Name))
   235  }
   236  
   237  var cmdAddonPlan = &Command{
   238  	Run:      runAddonPlan,
   239  	Usage:    "addon-plan <name> <plan>",
   240  	NeedsApp: true,
   241  	Category: "add-on",
   242  	Short:    "change an addon's plan" + extra,
   243  	Long: `
   244  Change an addon's plan. Not all add-on providers support this
   245  
   246  Examples:
   247  
   248      $ hk addon-plan redistogo small
   249      Changed redistogo plan to small on myapp.
   250  `,
   251  }
   252  
   253  func runAddonPlan(cmd *Command, args []string) {
   254  	appname := mustApp()
   255  	if len(args) != 2 {
   256  		cmd.PrintUsage()
   257  		os.Exit(2)
   258  	}
   259  	name := args[0]
   260  	plan := args[1]
   261  
   262  	addon, err := client.AddonInfo(appname, name)
   263  	checkAddonError(err)
   264  
   265  	// assemble service:plan string
   266  	serviceAndPlan := strings.Split(addon.Plan.Name, ":")[0] + ":" + plan
   267  
   268  	a, err := client.AddonUpdate(appname, name, serviceAndPlan)
   269  	checkAddonError(err)
   270  	log.Printf("Changed %s plan to %s on %s.", a.Name, plan, appname)
   271  }
   272  
   273  func checkAddonError(err error) {
   274  	if err != nil {
   275  		if hkerr, ok := err.(heroku.Error); ok && hkerr.Id == "not_found" {
   276  			printFatal(err.Error() + " Choose an addon name from `hk addons`.")
   277  		} else {
   278  			printFatal(err.Error())
   279  		}
   280  		os.Exit(2)
   281  	}
   282  }
   283  
   284  var cmdAddonServices = &Command{
   285  	Run:      runAddonServices,
   286  	Usage:    "addon-services",
   287  	Category: "add-on",
   288  	Short:    "list addon services" + extra,
   289  	Long: `
   290  Lists available addon services.
   291  
   292  Examples:
   293  
   294      $ hk addon-services
   295      heroku-postgresql
   296      newrelic
   297      redisgreen
   298      ...
   299  `,
   300  }
   301  
   302  func runAddonServices(cmd *Command, args []string) {
   303  	if len(args) != 0 {
   304  		cmd.PrintUsage()
   305  		os.Exit(2)
   306  	}
   307  	services, err := client.AddonServiceList(nil)
   308  	must(err)
   309  
   310  	for _, s := range services {
   311  		fmt.Println(s.Name)
   312  	}
   313  }
   314  
   315  var cmdAddonPlans = &Command{
   316  	Run:      runAddonPlans,
   317  	Usage:    "addon-plans <service>",
   318  	Category: "add-on",
   319  	Short:    "list addon plans" + extra,
   320  	Long: `
   321  Lists available addon plans for an addon provider.
   322  
   323  Examples:
   324  
   325      $ hk addon-plans heroku-postgresql
   326      hobby-dev        $0/mo
   327      hobby-basic      $9/mo
   328      standard-yanari  $50/mo
   329      standard-tengu   $200/mo
   330      premium-yanari   $200/mo
   331      premium-tengu    $350/mo
   332      standard-ika     $750/mo
   333      premium-ika      $1200/mo
   334      ...
   335  `,
   336  }
   337  
   338  func runAddonPlans(cmd *Command, args []string) {
   339  	if len(args) != 1 {
   340  		cmd.PrintUsage()
   341  		os.Exit(2)
   342  	}
   343  	service := args[0]
   344  	plans, err := client.PlanList(service, nil)
   345  	must(err)
   346  
   347  	w := tabwriter.NewWriter(os.Stdout, 1, 2, 2, ' ', 0)
   348  	defer w.Flush()
   349  
   350  	sort.Sort(addonPlansByPrice(plans))
   351  	for _, p := range plans {
   352  		listRec(w,
   353  			strings.TrimPrefix(p.Name, service+":"),
   354  			addonPlanPriceString(p),
   355  		)
   356  	}
   357  }
   358  
   359  type addonPlansByPrice []heroku.Plan
   360  
   361  func (a addonPlansByPrice) Len() int           { return len(a) }
   362  func (a addonPlansByPrice) Swap(i, j int)      { a[i], a[j] = a[j], a[i] }
   363  func (a addonPlansByPrice) Less(i, j int) bool { return a[i].Price.Cents < a[j].Price.Cents }
   364  
   365  func addonPlanPriceString(p heroku.Plan) string {
   366  	r := big.NewRat(int64(p.Price.Cents), 100)
   367  	decimals := 2
   368  	if p.Price.Cents%100 == 0 {
   369  		decimals = 0
   370  	}
   371  	return "$" + r.FloatString(decimals) + "/" + shortenPriceUnit(p.Price.Unit)
   372  }
   373  
   374  func shortenPriceUnit(unit string) string {
   375  	switch unit {
   376  	case "month":
   377  		return "mo"
   378  	default:
   379  		return unit
   380  	}
   381  }