diff --git a/cycle.go b/cycle.go index 7e8535d..f46f4d2 100644 --- a/cycle.go +++ b/cycle.go @@ -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 - time.AfterFunc(p.poll, p.load) + go func() { + time.AfterFunc(p.poll, p.load) + }() } } diff --git a/datastore/key.go b/datastore/key.go index b5838e6..4b82b6b 100644 --- a/datastore/key.go +++ b/datastore/key.go @@ -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 +} diff --git a/datastore/releases.go b/datastore/releases.go index ffcb5de..fc481ee 100644 --- a/datastore/releases.go +++ b/datastore/releases.go @@ -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 } diff --git a/project.go b/project.go index 6e84ca4..0f16379 100644 --- a/project.go +++ b/project.go @@ -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", diff --git a/server.go b/server.go index 7052a13..7566c88 100644 --- a/server.go +++ b/server.go @@ -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) { diff --git a/webProject.go b/webProject.go index a0dcf25..3fbe501 100644 --- a/webProject.go +++ b/webProject.go @@ -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/ + 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() + }() +}