-
Notifications
You must be signed in to change notification settings - Fork 2
/
push.go
310 lines (243 loc) · 7.35 KB
/
push.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"os/exec"
"runtime"
"time"
"gopkg.in/alecthomas/kingpin.v2"
)
// PushCommand used for pushing an app during app development
type PushCommand struct {
appPath string // path to the App folder
}
type bundleMessage struct {
Widget string
Output string
}
type pushPollResponse struct {
Meta struct {
Status string
Messages []bundleMessage
}
Links struct {
Preview string
}
}
const (
pushTemplateURI = "%s/files/push/%s?sessionid=%s"
pollClientTimeout = 5 * time.Second
pollInterval = 5 * time.Second // how often to poll status URL
pollFinishedStatus = "FINISHED"
pollFailedStatus = "FAILED"
)
func configurePushCommand(app *kingpin.Application) {
cmd := &PushCommand{}
appCmd := app.Command("push", "Push the App in the specified folder.").
Action(cmd.push)
appCmd.Arg("appPath", "path to the App folder (default: current folder)").
Default(".").
ExistingDirVar(&cmd.appPath)
}
func (cmd *PushCommand) push(context *kingpin.ParseContext) error {
appPath := cmd.appPath
appPath, appName, appManifestFile, err := prepareAppUpload(cmd.appPath)
if err != nil {
log.Println("Could not prepare the app folder for uploading")
return err
}
zapFile, err := createZapPackage(appPath)
if err != nil {
log.Println("Could not create zap package.")
return err
}
sessionID, err := getSessionID(appPath)
if err != nil {
log.Println("Could not get the session id.")
return err
}
log.Printf("Run push for App '%s', path '%s'\n", appName, appPath)
rootURI := catalogURIs[targetEnv]
pushURI := fmt.Sprintf(pushTemplateURI, rootURI, appName, sessionID)
uploadURI, err := pushToCatalog(pushURI, appManifestFile)
if err != nil {
log.Println("Error during pushing the manifest to the App Catalog.")
return err
}
log.Println("Frontend upload url:", uploadURI)
pollURI, err := uploadToFrontend(uploadURI, zapFile, appName, sessionID)
log.Println("Frontend upload poll uri:", pollURI)
if err != nil {
log.Println("Error. during uploading package to the frontend")
return err
}
finished := false
var statusResponse pushPollResponse
timeout := time.Duration(pollClientTimeout)
client := http.Client{Timeout: timeout}
for !finished {
resp, err := client.Get(pollURI)
if err != nil {
log.Println("Error. during polling push to the frontend")
return err
}
err = json.NewDecoder(resp.Body).Decode(&statusResponse)
resp.Body.Close()
if err != nil {
log.Println("Error. during parsing poll status result")
bodyData, _ := ioutil.ReadAll(resp.Body)
if bodyData != nil {
log.Println(bodyData)
}
return err
}
log.Printf("Pushing to the website to the development environment, status: [%s]", statusResponse.Meta.Status)
if statusResponse.Meta.Status == pollFinishedStatus || statusResponse.Meta.Status == pollFailedStatus {
finished = true
break
}
time.Sleep(pollInterval)
}
log.Printf("Server output for the app bundling:")
for _, message := range statusResponse.Meta.Messages {
log.Printf("Widget: %s", message.Widget)
log.Printf("Output: %s", message.Output)
}
if statusResponse.Meta.Status == pollFinishedStatus {
log.Printf("App successfully pushed. The frontend for this development session is at %s", statusResponse.Links.Preview)
} else {
log.Printf("App push failed.")
}
openWebsite(statusResponse.Links.Preview)
return nil
}
func pushToCatalog(pushURI string, appManifestFile string) (uploadURI string, err error) {
// To the App Catalog we have to POST the manifest in a multipart HTTP form.
// When doing the push, it'll only contain a single file, the manifest.
files := map[string]string{
"manifest": appManifestFile,
}
if verbose {
log.Println("Posting the app manifest to the App Catalog overlay: " + pushURI)
}
request, err := createMultiFileUploadRequest(pushURI, files, nil)
if err != nil {
log.Println("Creating the HTTP request failed.")
return "", err
}
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
log.Println("Call to App Catalog failed.")
return "", err
}
responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println("Error reading response from App Catalog.")
return "", err
}
type PushResponse struct {
Links map[string]string `json:"links"`
Messages []string `json:"messages"`
}
var responseObject PushResponse
err = json.Unmarshal(responseBody, &responseObject)
if err != nil {
if verbose {
log.Println(err)
}
return "", err
}
log.Printf("App Catalog returned status code %v. Response details:\n", response.StatusCode)
for _, line := range responseObject.Messages {
log.Printf("\t%v\n", line)
}
if response.StatusCode == http.StatusOK {
log.Println("App has been pushed successfully.")
} else {
return "", fmt.Errorf("Push failed, App Catalog returned status code %v", response.StatusCode)
}
return responseObject.Links["upload"], nil
}
func uploadToFrontend(uploadURI string, zapFile string, appName string, sessionID string) (frontendURI string, err error) {
files := map[string]string{
"file": zapFile,
}
params := map[string]string{
"name": appName,
}
if verbose {
log.Println("Uploading the app to the Express frontend: " + uploadURI)
}
request, err := createMultiFileUploadRequest(uploadURI, files, params)
if err != nil {
log.Println("Creating the HTTP request failed.")
return "", err
}
client := &http.Client{}
response, err := client.Do(request)
if err != nil {
log.Println("Call to the Express frontend failed.")
return "", err
}
responseBody, err := ioutil.ReadAll(response.Body)
if err != nil {
log.Println("Error reading response from the fronted.")
return "", err
}
var responseObject map[string]map[string]string
err = json.Unmarshal(responseBody, &responseObject)
if err != nil {
if verbose {
log.Println(err)
}
return "", err
}
log.Printf("Express frontend returned status code %v.", response.StatusCode)
if response.StatusCode == http.StatusOK {
log.Println("The app has been uploaded to the frontend successfully.")
} else {
return "", fmt.Errorf("Uploading failed, the frontend returned status code %v", response.StatusCode)
}
// The frontend returns a link which can be used to poll the upload status.
// {
// "links": {
// "progress": "https://fireball-dev.travix.com/upload/progress?sessionId=123`"
// }
// }
return responseObject["links"]["progress"], nil
}
// getSessionID gets the current session id. If there is an existing one in the folder, it uses that, otherwise it creates a new one.
func getSessionID(appPath string) (string, error) {
settings, err := readDevelopmentSettings(appPath)
if err != nil {
settings, err = getDefaultDevelopmentSettings()
if err != nil {
log.Println("Couldn't create new development settings.")
return "", err
}
err = writeDevelopmentSettings(appPath, settings)
if err != nil {
log.Println("Could not save new development settings file.")
return "", err
}
}
return settings.SessionID, nil
}
func openWebsite(url string) error {
var err error
switch runtime.GOOS {
case "linux":
err = exec.Command("xdg-open", url).Start()
case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin":
err = exec.Command("open", url).Start()
default:
err = fmt.Errorf("unsupported platform")
}
return err
}