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
17
cycle.go
17
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
|
// 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
|
// call's fetch and triggers the next poll if one exists
|
||||||
func (p *Project) load() {
|
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.setStage(stageLoad)
|
||||||
p.setVersion("")
|
p.setVersion("")
|
||||||
|
|
||||||
@ -39,7 +42,9 @@ func (p *Project) load() {
|
|||||||
// project has been deleted
|
// project has been deleted
|
||||||
// don't continue polling
|
// don't continue polling
|
||||||
// move project data to deleted folder with a timestamp
|
// 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,
|
p.errHandled(os.Rename(p.dir(), filepath.Join(dataDir, deletedProjectDir,
|
||||||
strconv.FormatInt(time.Now().Unix(), 10), p.id())))
|
strconv.FormatInt(time.Now().Unix(), 10), p.id())))
|
||||||
return
|
return
|
||||||
@ -57,17 +62,17 @@ func (p *Project) load() {
|
|||||||
|
|
||||||
p.setData(new)
|
p.setData(new)
|
||||||
|
|
||||||
if p.errHandled(os.MkdirAll(p.dir(), 0777)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.fetch()
|
p.fetch()
|
||||||
|
|
||||||
|
p.setStage(stageWait)
|
||||||
|
|
||||||
//full cycle completed
|
//full cycle completed
|
||||||
|
|
||||||
if p.poll > 0 {
|
if p.poll > 0 {
|
||||||
//start polling
|
//start polling
|
||||||
time.AfterFunc(p.poll, p.load)
|
go func() {
|
||||||
|
time.AfterFunc(p.poll, p.load)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,3 +83,44 @@ func (k TimeKey) UUID() string {
|
|||||||
func (k TimeKey) Bytes() []byte {
|
func (k TimeKey) Bytes() []byte {
|
||||||
return []byte(k[:])
|
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
|
// ReleaseFile returns a specific file from a release for the given file key
|
||||||
func (ds *Store) ReleaseFile(fileKey TimeKey) ([]byte, error) {
|
func (ds *Store) ReleaseFile(fileKey TimeKey) ([]byte, error) {
|
||||||
var fileData []byte
|
var fileData []byte
|
||||||
|
err := ds.get(bucketFiles, fileKey.Bytes(), fileData)
|
||||||
err := ds.get(bucketFiles, fileKey.Bytes(), &fileData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return fileData, nil
|
return fileData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,12 +100,15 @@ func (ds *Store) Releases() ([]*Release, error) {
|
|||||||
|
|
||||||
// LastRelease lists the last release for a project
|
// LastRelease lists the last release for a project
|
||||||
func (ds *Store) LastRelease() (*Release, error) {
|
func (ds *Store) LastRelease() (*Release, error) {
|
||||||
var r *Release
|
r := &Release{}
|
||||||
|
|
||||||
err := ds.bolt.View(func(tx *bolt.Tx) error {
|
err := ds.bolt.View(func(tx *bolt.Tx) error {
|
||||||
c := tx.Bucket([]byte(bucketReleases)).Cursor()
|
c := tx.Bucket([]byte(bucketReleases)).Cursor()
|
||||||
|
|
||||||
_, v := c.Last()
|
_, v := c.Last()
|
||||||
|
if v == nil {
|
||||||
|
return ErrNotFound
|
||||||
|
}
|
||||||
|
|
||||||
err := json.Unmarshal(v, r)
|
err := json.Unmarshal(v, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -119,9 +122,5 @@ func (ds *Store) LastRelease() (*Release, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if r == nil {
|
|
||||||
return nil, ErrNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
26
project.go
26
project.go
@ -25,11 +25,12 @@ const (
|
|||||||
|
|
||||||
//stages
|
//stages
|
||||||
const (
|
const (
|
||||||
stageLoad = "load"
|
stageLoad = "loading"
|
||||||
stageFetch = "fetch"
|
stageFetch = "fetching"
|
||||||
stageBuild = "build"
|
stageBuild = "building"
|
||||||
stageTest = "test"
|
stageTest = "testing"
|
||||||
stageRelease = "release"
|
stageRelease = "releasing"
|
||||||
|
stageWait = "waiting"
|
||||||
)
|
)
|
||||||
|
|
||||||
const projectFilePoll = 30 * time.Second
|
const projectFilePoll = 30 * time.Second
|
||||||
@ -61,10 +62,12 @@ type Project struct {
|
|||||||
poll time.Duration
|
poll time.Duration
|
||||||
ds *datastore.Store
|
ds *datastore.Store
|
||||||
stage string
|
stage string
|
||||||
|
status string
|
||||||
version string
|
version string
|
||||||
hash string
|
hash string
|
||||||
|
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
processing sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Project) errHandled(err error) bool {
|
func (p *Project) errHandled(err error) bool {
|
||||||
@ -140,6 +143,11 @@ func (p *Project) open() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err := os.MkdirAll(p.dir(), 0777)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
ds, err := datastore.Open(filepath.Join(p.dir(), p.id()+".ironsmith"))
|
ds, err := datastore.Open(filepath.Join(p.dir(), p.id()+".ironsmith"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -168,9 +176,9 @@ func (p *Project) setStage(stage string) {
|
|||||||
defer p.Unlock()
|
defer p.Unlock()
|
||||||
|
|
||||||
if p.version != "" {
|
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 {
|
} 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
|
p.stage = stage
|
||||||
@ -181,6 +189,7 @@ type webProject struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ReleaseVersion string `json:"releaseVersion"` //last successfully released version
|
ReleaseVersion string `json:"releaseVersion"` //last successfully released version
|
||||||
LastVersion string `json:"lastVersion"` //last version success or otherwise
|
LastVersion string `json:"lastVersion"` //last version success or otherwise
|
||||||
|
Stage string `json:"stage"` // current stage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Project) webData() (*webProject, error) {
|
func (p *Project) webData() (*webProject, error) {
|
||||||
@ -202,6 +211,7 @@ func (p *Project) webData() (*webProject, error) {
|
|||||||
ID: p.id(),
|
ID: p.id(),
|
||||||
LastVersion: last,
|
LastVersion: last,
|
||||||
ReleaseVersion: release,
|
ReleaseVersion: release,
|
||||||
|
Stage: p.stage,
|
||||||
}
|
}
|
||||||
|
|
||||||
return d, nil
|
return d, nil
|
||||||
@ -302,7 +312,7 @@ const projectTemplateFilename = "template.project.json"
|
|||||||
var projectTemplate = &Project{
|
var projectTemplate = &Project{
|
||||||
Name: "Template Project",
|
Name: "Template Project",
|
||||||
Fetch: "git clone root@git.townsourced.com:tshannon/ironsmith.git .",
|
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 ./...",
|
Test: "go test ./...",
|
||||||
Release: "tar -czf release.tar.gz ironsmith",
|
Release: "tar -czf release.tar.gz ironsmith",
|
||||||
Version: "git describe --tags --long",
|
Version: "git describe --tags --long",
|
||||||
|
@ -107,6 +107,14 @@ func routes() {
|
|||||||
get: logGet,
|
get: logGet,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
webRoot.Handle("/release/", &methodHandler{
|
||||||
|
get: releaseGet,
|
||||||
|
})
|
||||||
|
|
||||||
|
webRoot.Handle("/trigger/", &methodHandler{
|
||||||
|
post: triggerPost,
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func rootGet(w http.ResponseWriter, r *http.Request) {
|
func rootGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -166,6 +166,7 @@ func releaseGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-disposition", `attachment; filename="`+last.FileName+`"`)
|
||||||
http.ServeContent(w, r, last.FileName, time.Time{}, bytes.NewReader(fileData))
|
http.ServeContent(w, r, last.FileName, time.Time{}, bytes.NewReader(fileData))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -191,6 +192,8 @@ func releaseGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
if errHandled(err, w, r) {
|
if errHandled(err, w, r) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.Header().Add("Content-disposition", `attachment; filename="`+release.FileName+`"`)
|
||||||
http.ServeContent(w, r, release.FileName, time.Time{}, bytes.NewReader(fileData))
|
http.ServeContent(w, r, release.FileName, time.Time{}, bytes.NewReader(fileData))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -200,3 +203,43 @@ func releaseGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
Data: release,
|
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