Skip to content
This repository was archived by the owner on Jan 16, 2021. It is now read-only.

Commit b0b2ed5

Browse files
committed
add native support for server code on heroku
- adds native support for running server code on heroku - also some reorganization around webhook registration
1 parent c8e4ab8 commit b0b2ed5

26 files changed

+1722
-68
lines changed

add.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"github.com/ParsePlatform/parse-cli/herokucmd"
45
"github.com/ParsePlatform/parse-cli/parsecli"
56
"github.com/ParsePlatform/parse-cli/parsecmd"
67
"github.com/facebookgo/stackerr"
@@ -26,6 +27,12 @@ func (a *addCmd) addSelectedApp(
2627
return stackerr.New("invalid parse app config passed.")
2728
}
2829
return parsecmd.AddSelectedParseApp(name, parseAppConfig, args, a.MakeDefault, a.verbose, e)
30+
case parsecli.HerokuFormat:
31+
herokuAppConfig, ok := appConfig.(*parsecli.HerokuAppConfig)
32+
if !ok {
33+
return stackerr.New("invalid heroku app config passed.")
34+
}
35+
return herokucmd.AddSelectedHerokuApp(name, herokuAppConfig, args, a.MakeDefault, a.verbose, e)
2936
}
3037

3138
return stackerr.Newf("Unknown project type: %d.", e.Type)
@@ -54,7 +61,7 @@ func (a *addCmd) selectApp(e *parsecli.Env, appName string) (*parsecli.App, erro
5461

5562
func (a *addCmd) run(e *parsecli.Env, args []string) error {
5663

57-
if err := a.apps.Login.AuthUser(e); err != nil {
64+
if err := a.apps.Login.AuthUser(e, false); err != nil {
5865
return err
5966
}
6067
var appName string
@@ -69,7 +76,19 @@ func (a *addCmd) run(e *parsecli.Env, args []string) error {
6976
if err != nil {
7077
return err
7178
}
72-
appConfig := parsecmd.GetParseAppConfig(app)
79+
80+
var appConfig parsecli.AppConfig
81+
switch e.Type {
82+
case parsecli.LegacyParseFormat, parsecli.ParseFormat:
83+
appConfig = parsecmd.GetParseAppConfig(app)
84+
85+
case parsecli.HerokuFormat:
86+
_, appConfig, err = herokucmd.GetLinkedHerokuAppConfig(app, e)
87+
if err != nil {
88+
return err
89+
}
90+
}
91+
7392
return a.addSelectedApp(app.Name, appConfig, args, e)
7493
}
7594

heroku_commands.go

+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"time"
7+
8+
"github.com/ParsePlatform/parse-cli/herokucmd"
9+
"github.com/ParsePlatform/parse-cli/parsecli"
10+
"github.com/ParsePlatform/parse-cli/webhooks"
11+
"github.com/spf13/cobra"
12+
)
13+
14+
func herokuRootCmd(e *parsecli.Env) ([]string, *cobra.Command) {
15+
c := &cobra.Command{
16+
Use: "parse",
17+
Long: fmt.Sprintf(
18+
`Parse Command Line Tool
19+
20+
Tools to help you set up server code on Heroku
21+
Version %v
22+
Copyright %d Parse, Inc.
23+
https://www.parse.com`,
24+
parsecli.Version,
25+
time.Now().Year(),
26+
),
27+
Run: func(cmd *cobra.Command, args []string) {
28+
cmd.Help()
29+
},
30+
}
31+
32+
c.AddCommand(NewAddCmd(e))
33+
c.AddCommand(NewConfigureCmd(e))
34+
c.AddCommand(NewDefaultCmd(e))
35+
c.AddCommand(herokucmd.NewDownloadCmd(e))
36+
c.AddCommand(herokucmd.NewDeployCmd(e))
37+
c.AddCommand(webhooks.NewFunctionHooksCmd(e))
38+
c.AddCommand(NewListCmd(e))
39+
c.AddCommand(herokucmd.NewLogsCmd(e))
40+
c.AddCommand(NewNewCmd(e))
41+
c.AddCommand(herokucmd.NewReleasesCmd(e))
42+
c.AddCommand(herokucmd.NewRollbackCmd(e))
43+
c.AddCommand(webhooks.NewTriggerHooksCmd(e))
44+
c.AddCommand(NewUpdateCmd(e))
45+
c.AddCommand(NewVersionCmd(e))
46+
47+
if len(os.Args) <= 1 {
48+
return nil, c
49+
}
50+
commands := []string{"help"}
51+
for _, command := range c.Commands() {
52+
commands = append(commands, command.Name())
53+
}
54+
55+
args := make([]string, len(os.Args)-1)
56+
copy(args, os.Args[1:])
57+
58+
if message := parsecli.MakeCorrections(commands, args); message != "" {
59+
fmt.Fprintln(e.Out, message)
60+
}
61+
c.SetArgs(args)
62+
63+
return args, c
64+
}

herokucmd/add.go

+215
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package herokucmd
2+
3+
import (
4+
"fmt"
5+
"math/rand"
6+
"strconv"
7+
"strings"
8+
"text/tabwriter"
9+
"time"
10+
11+
"github.com/ParsePlatform/parse-cli/parsecli"
12+
"github.com/facebookgo/stackerr"
13+
"github.com/skratchdot/open-golang/open"
14+
)
15+
16+
const linkHerokuURL = "https://parse.com/account/edit"
17+
18+
func selectHerokuApp(apps nameIDs, e *parsecli.Env) (*nameID, error) {
19+
fmt.Fprintf(e.Out, "Please select from the following Heroku apps: (Enter a number between 1 and %d)\n", len(apps))
20+
for i, app := range apps {
21+
w := new(tabwriter.Writer)
22+
w.Init(e.Out, 0, 8, 0, '\t', 0)
23+
fmt.Fprintf(w, "%d: %s\t\t(%s)\n", i+1, app.name, app.id)
24+
if err := w.Flush(); err != nil {
25+
return nil, stackerr.Wrap(err)
26+
}
27+
}
28+
fmt.Fprintf(e.Out, "Selection: ")
29+
var selection string
30+
fmt.Fscanf(e.In, "%s\n", &selection)
31+
32+
n, err := strconv.Atoi(selection)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
lapps := len(apps)
38+
if n <= 0 || n > lapps {
39+
return nil, stackerr.Newf("Invalid selection: can only be in range 1..%d", lapps)
40+
}
41+
return &apps[n-1], nil
42+
}
43+
44+
func getRandomAppName(app *parsecli.App) string {
45+
rand.Seed(time.Now().Unix())
46+
construct := fmt.Sprintf(
47+
"%s-%s-%d",
48+
strings.ToLower(app.Name),
49+
strings.ToLower(app.ApplicationID[:6]),
50+
rand.Intn(10000),
51+
)
52+
l := len(construct)
53+
if l > 25 {
54+
l = 25
55+
}
56+
if construct[l-1] == '-' { // ensure no dash at the end
57+
l -= -1
58+
}
59+
return construct[:l]
60+
}
61+
62+
func GetLinkedHerokuAppConfig(app *parsecli.App, e *parsecli.Env) (bool, *parsecli.HerokuAppConfig, error) {
63+
h := &herokuLink{parseAppID: app.ApplicationID}
64+
apps, err := h.getAppLinks(e)
65+
if err != nil {
66+
return false, nil, err
67+
}
68+
if len(apps) == 0 {
69+
randomName := getRandomAppName(app)
70+
appName := randomName
71+
for i := 0; i < 3; i++ {
72+
if appName == randomName {
73+
fmt.Fprintf(e.Out,
74+
`Let's create a new Heroku app in which server code will be run.
75+
The Heroku app will be named: %q
76+
Note that this can be changed later using Heroku API or Dashboard.
77+
78+
`,
79+
randomName,
80+
)
81+
} else {
82+
appName = h.getHerokuAppName(e)
83+
}
84+
85+
id, err := h.createNewLink(e, appName)
86+
if err == nil {
87+
return true, &parsecli.HerokuAppConfig{
88+
ParseAppID: app.ApplicationID,
89+
HerokuAppID: id,
90+
}, nil
91+
}
92+
if i == 2 {
93+
return false, nil, err
94+
}
95+
96+
switch {
97+
case stackerr.HasUnderlying(err, stackerr.MatcherFunc(herokuAppNameTaken)):
98+
fmt.Fprintf(e.Err, "App name %s already taken.\nPlease try again...\n\n", appName)
99+
appName = ""
100+
101+
case stackerr.HasUnderlying(err, stackerr.MatcherFunc(herokuAccountNotLinked)):
102+
fmt.Fprintf(e.Err, `Looks like you have not yet linked your Heroku Account to your Parse account.
103+
Trying to open %q in the browser.
104+
Please click "Link Heroku" button at the bottom.
105+
`,
106+
linkHerokuURL,
107+
)
108+
appName = randomName
109+
err := open.Run(linkHerokuURL)
110+
if err != nil {
111+
fmt.Fprintf(e.Err,
112+
`Sorry, we couldn’t open the browser for you. Go to
113+
%q
114+
and click the "Link Heroku" button to link your Heroku account to Parse.
115+
`,
116+
linkHerokuURL,
117+
)
118+
}
119+
120+
fmt.Fprintf(e.Out, "Press ENTER when you are done linking your Heroku account to Parse: ")
121+
var discard string
122+
fmt.Fscanf(e.In, "%s\n", &discard)
123+
124+
default:
125+
return false, nil, err
126+
}
127+
}
128+
}
129+
130+
if len(apps) == 1 {
131+
return false, &parsecli.HerokuAppConfig{
132+
ParseAppID: app.ApplicationID,
133+
HerokuAppID: apps[0].id,
134+
}, nil
135+
}
136+
137+
// NOTE: this part of code will not be used for now
138+
for r := 0; r < 3; r++ {
139+
selected, err := selectHerokuApp(apps, e)
140+
if err != nil {
141+
fmt.Fprintf(e.Err, "error: %s.\nPlease try again...\n", parsecli.ErrorString(e, err))
142+
continue
143+
}
144+
if selected.id != "" {
145+
return false, &parsecli.HerokuAppConfig{
146+
ParseAppID: app.ApplicationID,
147+
HerokuAppID: selected.id,
148+
}, nil
149+
}
150+
id, err := h.createNewLink(e, selected.name)
151+
if err != nil {
152+
return false, nil, err
153+
}
154+
return false, &parsecli.HerokuAppConfig{
155+
ParseAppID: app.ApplicationID,
156+
HerokuAppID: id,
157+
}, nil
158+
}
159+
return false, nil, stackerr.New("failed to selected an heroku app in 3 attempts")
160+
}
161+
162+
func AddSelectedHerokuApp(
163+
appName string,
164+
appConfig *parsecli.HerokuAppConfig,
165+
args []string,
166+
makeDefault, verbose bool,
167+
e *parsecli.Env,
168+
) error {
169+
config, err := parsecli.ConfigFromDir(e.Root)
170+
if err != nil {
171+
return err
172+
}
173+
herokuConfig, ok := config.(*parsecli.HerokuConfig)
174+
if !ok {
175+
return stackerr.New("Invalid Heroku config.")
176+
}
177+
178+
// add app to config
179+
if _, ok := herokuConfig.Applications[appName]; ok {
180+
return stackerr.Newf("App %s has already been added", appName)
181+
}
182+
183+
herokuConfig.Applications[appName] = appConfig
184+
185+
if len(args) > 0 && args[0] != "" {
186+
alias := args[0]
187+
aliasConfig, ok := herokuConfig.Applications[alias]
188+
if !ok {
189+
herokuConfig.Applications[alias] = &parsecli.HerokuAppConfig{Link: appName}
190+
}
191+
if ok && aliasConfig.GetLink() != "" {
192+
fmt.Fprintf(e.Out, "Overwriting alias: %q to point to %q\n", alias, appName)
193+
herokuConfig.Applications[alias] = &parsecli.HerokuAppConfig{Link: appName}
194+
}
195+
}
196+
197+
if makeDefault {
198+
if _, ok := herokuConfig.Applications[parsecli.DefaultKey]; ok {
199+
return stackerr.New(`Default key already set. To override default, use command "parse default"`)
200+
}
201+
herokuConfig.Applications[parsecli.DefaultKey] = &parsecli.HerokuAppConfig{Link: appName}
202+
}
203+
204+
if err := parsecli.StoreConfig(e, herokuConfig); err != nil {
205+
return err
206+
}
207+
if verbose {
208+
fmt.Fprintf(e.Out, "Written config for %q\n", appName)
209+
if makeDefault {
210+
fmt.Fprintf(e.Out, "Set %q as default\n", appName)
211+
}
212+
}
213+
214+
return nil
215+
}

0 commit comments

Comments
 (0)