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:
parent
d3850c24f8
commit
05eb182419
15
cycle.go
15
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
|
||||
go func() {
|
||||
time.AfterFunc(p.poll, p.load)
|
||||
}()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
26
project.go
26
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",
|
||||
|
@ -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) {
|
||||
|
@ -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()
|
||||
}()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user