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 }