Finished backend work

Added last of the web REST endpoints.
Added processing lock to ensure that only one project cycle is running
at a time.

Basically all that's left is the web front end
This commit is contained in:
Tim Shannon 2016-04-13 20:53:49 +00:00
parent d3850c24f8
commit 05eb182419
6 changed files with 127 additions and 21 deletions

View File

@ -27,6 +27,9 @@ Project life cycle:
// load is the beginning of the cycle. Loads / reloads the project file to make sure that the scripts are up-to-date
// call's fetch and triggers the next poll if one exists
func (p *Project) load() {
p.processing.Lock() // ensure only one cycle is running at a time per project
defer p.processing.Unlock()
p.setStage(stageLoad)
p.setVersion("")
@ -39,7 +42,9 @@ func (p *Project) load() {
// project has been deleted
// don't continue polling
// move project data to deleted folder with a timestamp
p.close()
if p.errHandled(p.close()) {
return
}
p.errHandled(os.Rename(p.dir(), filepath.Join(dataDir, deletedProjectDir,
strconv.FormatInt(time.Now().Unix(), 10), p.id())))
return
@ -57,17 +62,17 @@ func (p *Project) load() {
p.setData(new)
if p.errHandled(os.MkdirAll(p.dir(), 0777)) {
return
}
p.fetch()
p.setStage(stageWait)
//full cycle completed
if p.poll > 0 {
//start polling
go func() {
time.AfterFunc(p.poll, p.load)
}()
}
}

View File

@ -83,3 +83,44 @@ func (k TimeKey) UUID() string {
func (k TimeKey) Bytes() []byte {
return []byte(k[:])
}
// String returns the string representation of a timekey
func (k TimeKey) String() string {
return k.UUID()
}
// MarshalJSON implements JSON marshaler
func (k *TimeKey) MarshalJSON() ([]byte, error) {
return []byte(`"` + k.String() + `"`), nil
}
// UnmarshalJSON implements JSON unmarshaler
func (k *TimeKey) UnmarshalJSON(buf []byte) error {
// drop quotes
buf = buf[1 : len(buf)-1]
_, err := hex.Decode(k[0:4], buf[0:8])
if err != nil {
return err
}
_, err = hex.Decode(k[4:6], buf[9:13])
if err != nil {
return err
}
_, err = hex.Decode(k[6:8], buf[14:18])
if err != nil {
return err
}
_, err = hex.Decode(k[8:10], buf[19:23])
if err != nil {
return err
}
_, err = hex.Decode(k[10:], buf[24:])
if err != nil {
return err
}
return nil
}

View File

@ -53,11 +53,11 @@ func (ds *Store) AddRelease(version, fileName string, fileData []byte) error {
// ReleaseFile returns a specific file from a release for the given file key
func (ds *Store) ReleaseFile(fileKey TimeKey) ([]byte, error) {
var fileData []byte
err := ds.get(bucketFiles, fileKey.Bytes(), &fileData)
err := ds.get(bucketFiles, fileKey.Bytes(), fileData)
if err != nil {
return nil, err
}
return fileData, nil
}
@ -100,12 +100,15 @@ func (ds *Store) Releases() ([]*Release, error) {
// LastRelease lists the last release for a project
func (ds *Store) LastRelease() (*Release, error) {
var r *Release
r := &Release{}
err := ds.bolt.View(func(tx *bolt.Tx) error {
c := tx.Bucket([]byte(bucketReleases)).Cursor()
_, v := c.Last()
if v == nil {
return ErrNotFound
}
err := json.Unmarshal(v, r)
if err != nil {
@ -119,9 +122,5 @@ func (ds *Store) LastRelease() (*Release, error) {
return nil, err
}
if r == nil {
return nil, ErrNotFound
}
return r, nil
}

View File

@ -25,11 +25,12 @@ const (
//stages
const (
stageLoad = "load"
stageFetch = "fetch"
stageBuild = "build"
stageTest = "test"
stageRelease = "release"
stageLoad = "loading"
stageFetch = "fetching"
stageBuild = "building"
stageTest = "testing"
stageRelease = "releasing"
stageWait = "waiting"
)
const projectFilePoll = 30 * time.Second
@ -61,10 +62,12 @@ type Project struct {
poll time.Duration
ds *datastore.Store
stage string
status string
version string
hash string
sync.RWMutex
processing sync.Mutex
}
func (p *Project) errHandled(err error) bool {
@ -140,6 +143,11 @@ func (p *Project) open() error {
return nil
}
err := os.MkdirAll(p.dir(), 0777)
if err != nil {
return err
}
ds, err := datastore.Open(filepath.Join(p.dir(), p.id()+".ironsmith"))
if err != nil {
return err
@ -168,9 +176,9 @@ func (p *Project) setStage(stage string) {
defer p.Unlock()
if p.version != "" {
vlog("Entering %s stage for Project: %s Version: %s\n", p.stage, p.id(), p.version)
vlog("Entering %s stage for Project: %s Version: %s\n", stage, p.id(), p.version)
} else {
vlog("Entering %s stage for Project: %s\n", p.stage, p.id())
vlog("Entering %s stage for Project: %s\n", stage, p.id())
}
p.stage = stage
@ -181,6 +189,7 @@ type webProject struct {
Name string `json:"name"`
ReleaseVersion string `json:"releaseVersion"` //last successfully released version
LastVersion string `json:"lastVersion"` //last version success or otherwise
Stage string `json:"stage"` // current stage
}
func (p *Project) webData() (*webProject, error) {
@ -202,6 +211,7 @@ func (p *Project) webData() (*webProject, error) {
ID: p.id(),
LastVersion: last,
ReleaseVersion: release,
Stage: p.stage,
}
return d, nil
@ -302,7 +312,7 @@ const projectTemplateFilename = "template.project.json"
var projectTemplate = &Project{
Name: "Template Project",
Fetch: "git clone root@git.townsourced.com:tshannon/ironsmith.git .",
Build: "go build -o ironsmith",
Build: "go build -a -v -o ironsmith",
Test: "go test ./...",
Release: "tar -czf release.tar.gz ironsmith",
Version: "git describe --tags --long",

View File

@ -107,6 +107,14 @@ func routes() {
get: logGet,
})
webRoot.Handle("/release/", &methodHandler{
get: releaseGet,
})
webRoot.Handle("/trigger/", &methodHandler{
post: triggerPost,
})
}
func rootGet(w http.ResponseWriter, r *http.Request) {

View File

@ -166,6 +166,7 @@ func releaseGet(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Add("Content-disposition", `attachment; filename="`+last.FileName+`"`)
http.ServeContent(w, r, last.FileName, time.Time{}, bytes.NewReader(fileData))
return
}
@ -191,6 +192,8 @@ func releaseGet(w http.ResponseWriter, r *http.Request) {
if errHandled(err, w, r) {
return
}
w.Header().Add("Content-disposition", `attachment; filename="`+release.FileName+`"`)
http.ServeContent(w, r, release.FileName, time.Time{}, bytes.NewReader(fileData))
return
}
@ -200,3 +203,43 @@ func releaseGet(w http.ResponseWriter, r *http.Request) {
Data: release,
})
}
type triggerInput struct {
Secret string `json:"secret"`
}
/*trigger routes
/trigger/<project-id>
Triggers a project to start a cycle
*/
func triggerPost(w http.ResponseWriter, r *http.Request) {
prj, _, _ := splitPath(r.URL.Path)
if prj == "" {
four04(w, r)
return
}
project, ok := projects.get(prj)
if !ok {
four04(w, r)
return
}
input := &triggerInput{}
if errHandled(parseInput(r, input), w, r) {
return
}
if input.Secret != project.TriggerSecret {
errHandled(&Fail{
Message: "Invalid trigger secret for this project",
HTTPStatus: http.StatusUnauthorized,
}, w, r)
return
}
go func() {
project.load()
}()
}