77 "net/http"
88 "strconv"
99 "strings"
10+ "time"
1011
1112 log "github.com/Sirupsen/logrus"
1213 "github.com/gin-gonic/gin"
@@ -16,13 +17,23 @@ import (
1617 "github.com/nitrous-io/rise-server/apiserver/dbconn"
1718 "github.com/nitrous-io/rise-server/apiserver/models/deployment"
1819 "github.com/nitrous-io/rise-server/apiserver/models/rawbundle"
20+ "github.com/nitrous-io/rise-server/apiserver/models/template"
1921 "github.com/nitrous-io/rise-server/pkg/hasher"
2022 "github.com/nitrous-io/rise-server/pkg/job"
2123 "github.com/nitrous-io/rise-server/shared/messages"
2224 "github.com/nitrous-io/rise-server/shared/queues"
2325 "github.com/nitrous-io/rise-server/shared/s3client"
2426)
2527
28+ const (
29+ viaUnknown = iota
30+ viaPayload
31+ viaCachedBundle
32+ viaTemplate
33+ )
34+
35+ const presignExpiryDuration = 1 * time .Minute
36+
2637// Create deploys a project.
2738func Create (c * gin.Context ) {
2839 u := controllers .CurrentUser (c )
@@ -50,8 +61,21 @@ func Create(c *gin.Context) {
5061 depl .JsEnvVars = prevDepl .JsEnvVars
5162 }
5263
53- var archiveFormat string
64+ var (
65+ archiveFormat string
66+ strategy = viaUnknown
67+ )
68+
5469 if strings .HasPrefix (c .Request .Header .Get ("Content-Type" ), "multipart/form-data; boundary=" ) {
70+ strategy = viaPayload
71+ } else if c .PostForm ("bundle_checksum" ) != "" {
72+ strategy = viaCachedBundle
73+ } else if c .PostForm ("template_id" ) != "" {
74+ strategy = viaTemplate
75+ }
76+
77+ switch strategy {
78+ case viaPayload :
5579 reader , err := c .Request .MultipartReader ()
5680 if err != nil {
5781 c .JSON (http .StatusBadRequest , gin.H {
@@ -108,33 +132,35 @@ func Create(c *gin.Context) {
108132 controllers .InternalServerError (c , err )
109133 return
110134 }
111- fileType := http .DetectContentType (partHead )
112135
136+ mimeType := http .DetectContentType (partHead )
113137 var uploadKey string
114- if fileType == "application/zip" {
138+ switch mimeType {
139+ case "application/zip" :
115140 uploadKey = fmt .Sprintf ("deployments/%s/raw-bundle.zip" , depl .PrefixID ())
116141 archiveFormat = "zip"
117- } else if fileType == "application/x-gzip" {
142+ case "application/x-gzip" :
118143 uploadKey = fmt .Sprintf ("deployments/%s/raw-bundle.tar.gz" , depl .PrefixID ())
119144 archiveFormat = "tar.gz"
120- } else {
145+ default :
121146 c .JSON (http .StatusBadRequest , gin.H {
122147 "error" : "invalid_request" ,
123- "error_description" : "payload has invalid file " ,
148+ "error_description" : "payload is in an unsupported format " ,
124149 })
125150 return
126151 }
127152
128- hashReader := hasher .NewReader (br )
129- if err := s3client .Upload (uploadKey , hashReader , "" , "private" ); err != nil {
153+ hr := hasher .NewReader (br )
154+ if err := s3client .Upload (uploadKey , hr , "" , "private" ); err != nil {
130155 controllers .InternalServerError (c , err )
131156 return
132157 }
133158
134- bun := & rawbundle.RawBundle {}
135- bun .ProjectID = proj .ID
136- bun .Checksum = hashReader .Checksum ()
137- bun .UploadedPath = uploadKey
159+ bun := & rawbundle.RawBundle {
160+ ProjectID : proj .ID ,
161+ Checksum : hr .Checksum (),
162+ UploadedPath : uploadKey ,
163+ }
138164 if err := db .Create (bun ).Error ; err != nil {
139165 controllers .InternalServerError (c , err )
140166 return
@@ -144,7 +170,8 @@ func Create(c *gin.Context) {
144170 break
145171 }
146172 }
147- } else {
173+
174+ case viaCachedBundle :
148175 ver , err := proj .NextVersion (db )
149176 if err != nil {
150177 controllers .InternalServerError (c , err )
@@ -186,6 +213,81 @@ func Create(c *gin.Context) {
186213
187214 // Currently bundle from CLI is always tar.gz
188215 archiveFormat = "tar.gz"
216+
217+ case viaTemplate :
218+ templateID , err := strconv .ParseInt (c .PostForm ("template_id" ), 10 , 64 )
219+ if err != nil {
220+ c .JSON (422 , gin.H {
221+ "error" : "invalid_params" ,
222+ "errors" : map [string ]string {
223+ "template_id" : "is invalid" ,
224+ },
225+ })
226+ return
227+ }
228+
229+ tmpl := & template.Template {}
230+ if err := db .First (tmpl , templateID ).Error ; err != nil {
231+ c .JSON (422 , gin.H {
232+ "error" : "invalid_params" ,
233+ "errors" : map [string ]string {
234+ "template_id" : "is not that of a known template" ,
235+ },
236+ })
237+ return
238+ }
239+
240+ if strings .HasSuffix (tmpl .DownloadURL , ".tar.gz" ) {
241+ archiveFormat = "tar.gz"
242+ } else if strings .HasSuffix (tmpl .DownloadURL , ".zip" ) {
243+ archiveFormat = "zip"
244+ } else {
245+ c .JSON (422 , gin.H {
246+ "error" : "invalid_params" ,
247+ "errors" : map [string ]string {
248+ "template_id" : "is no longer valid" ,
249+ },
250+ })
251+ return
252+ }
253+
254+ ver , err := proj .NextVersion (db )
255+ if err != nil {
256+ controllers .InternalServerError (c , err )
257+ return
258+ }
259+
260+ depl .TemplateID = & tmpl .ID
261+ depl .Version = ver
262+ if err := db .Create (depl ).Error ; err != nil {
263+ controllers .InternalServerError (c , err )
264+ return
265+ }
266+
267+ bundlePath := "deployments/" + depl .PrefixID () + "/raw-bundle." + archiveFormat
268+ if err := s3client .Copy (tmpl .DownloadURL , bundlePath ); err != nil {
269+ log .Printf ("failed to make a copy of template %q to %q in S3, err: %v" , tmpl .DownloadURL , bundlePath , err )
270+ controllers .InternalServerError (c , err )
271+ return
272+ }
273+
274+ bun := & rawbundle.RawBundle {
275+ ProjectID : proj .ID ,
276+ UploadedPath : bundlePath ,
277+ }
278+ if err := db .Create (bun ).Error ; err != nil {
279+ controllers .InternalServerError (c , err )
280+ return
281+ }
282+
283+ depl .RawBundleID = & bun .ID
284+
285+ default :
286+ c .JSON (http .StatusBadRequest , gin.H {
287+ "error" : "invalid_request" ,
288+ "error_description" : "could not understand your request" ,
289+ })
290+ return
189291 }
190292
191293 if err := depl .UpdateState (db , deployment .StateUploaded ); err != nil {
@@ -288,6 +390,83 @@ func Show(c *gin.Context) {
288390 })
289391}
290392
393+ // Download allows users to download an (unoptimized) tarball of the files of a
394+ // deployment.
395+ func Download (c * gin.Context ) {
396+ deploymentID , err := strconv .ParseInt (c .Param ("id" ), 10 , 64 )
397+ if err != nil {
398+ c .JSON (http .StatusNotFound , gin.H {
399+ "error" : "not_found" ,
400+ "error_description" : "deployment could not be found" ,
401+ })
402+ return
403+ }
404+
405+ db , err := dbconn .DB ()
406+ if err != nil {
407+ controllers .InternalServerError (c , err )
408+ return
409+ }
410+
411+ depl := & deployment.Deployment {}
412+ if err := db .First (depl , deploymentID ).Error ; err != nil {
413+ if err == gorm .RecordNotFound {
414+ c .JSON (http .StatusNotFound , gin.H {
415+ "error" : "not_found" ,
416+ "error_description" : "deployment could not be found" ,
417+ })
418+ return
419+ }
420+ controllers .InternalServerError (c , err )
421+ return
422+ }
423+
424+ if depl .RawBundleID == nil {
425+ c .JSON (http .StatusNotFound , gin.H {
426+ "error" : "not_found" ,
427+ "error_description" : "deployment cannot be downloaded" ,
428+ })
429+ return
430+ }
431+
432+ bun := & rawbundle.RawBundle {}
433+ if err := db .First (bun , * depl .RawBundleID ).Error ; err != nil {
434+ if err == gorm .RecordNotFound {
435+ c .JSON (http .StatusGone , gin.H {
436+ "error" : "gone" ,
437+ "error_description" : "deployment can no longer be downloaded" ,
438+ })
439+ return
440+ }
441+ controllers .InternalServerError (c , err )
442+ return
443+ }
444+
445+ exists , err := s3client .Exists (bun .UploadedPath )
446+ if err != nil {
447+ log .Warnf ("failed to check existence of %q on S3, err: %v" , bun .UploadedPath , err )
448+ controllers .InternalServerError (c , err )
449+ return
450+ }
451+ if ! exists {
452+ log .Warnf ("deployment raw bundle %q does not exist in S3" , bun .UploadedPath )
453+ c .JSON (http .StatusGone , gin.H {
454+ "error" : "gone" ,
455+ "error_description" : "deployment can no longer be downloaded" ,
456+ })
457+ return
458+ }
459+
460+ url , err := s3client .PresignedURL (bun .UploadedPath , presignExpiryDuration )
461+ if err != nil {
462+ log .Printf ("error generating presigned URL to %q, err: %v" , bun .UploadedPath , err )
463+ controllers .InternalServerError (c , err )
464+ return
465+ }
466+
467+ c .Redirect (http .StatusFound , url )
468+ }
469+
291470// Rollback either rolls back a project to the previous deployment, or to a
292471// given version.
293472func Rollback (c * gin.Context ) {
0 commit comments