Skip to content

Commit 83e6735

Browse files
authored
Merge pull request #68 from nitrous-io/download-deployment
Allow project to be deployed using a template and deployments to be downloaded
2 parents 675be43 + 4427ce3 commit 83e6735

File tree

20 files changed

+980
-81
lines changed

20 files changed

+980
-81
lines changed

apiserver/controllers/deployments/deployments.go

Lines changed: 192 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
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.
2738
func 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.
293472
func Rollback(c *gin.Context) {

0 commit comments

Comments
 (0)