ironsmith/project.go

240 lines
5.3 KiB
Go

// Copyright 2016 Tim Shannon. All rights reserved.
// Use of this source code is governed by the MIT license
// that can be found in the LICENSE file.
package main
import (
"encoding/json"
"log"
"os"
"path/filepath"
"sync"
"time"
"git.townsourced.com/ironsmith/datastore"
)
const (
enabledProjectDir = "enabled"
deletedProjectDir = "deleted"
)
//stages
const (
stageLoad = "load"
stageFetch = "fetch"
stageBuild = "build"
stageTest = "test"
stageRelease = "release"
)
const projectFilePoll = 30 * time.Second
// Project is an ironsmith project that contains how to fetch, build, test, and release a project
/*
The project lifecycle goes like this, each step calling the next if successful
(Load Project file) -> (Fetch) -> (Build) -> (Test) -> (Release) - > (Sleep for polling period) ->
(Reload Project File) -> (Fetch) -> etc...
Changes the project file will be reloaded on every poll / trigger
If a project file is deleted then the cycle will finish it's current poll and stop at the load phase
*/
type Project struct {
Name string `json:"name"` // name of the project
Fetch string `json:"fetch"` //Script to fetch the latest project code into the current directory
Build string `json:"build"` //Script to build the latest project code
Test string `json:"test"` //Script to test the latest project code
Release string `json:"release"` //Script to build the release of latest project code
Version string `json:"version"` //Script to generate the version num of the current build, should be indempotent
ReleaseFile string `json:"releaseFile"`
PollInterval string `json:"pollInterval"` // if not poll interval is specified, this project is trigger only
TriggerSecret string `json:"triggerSecret"` //secret to be included with a trigger call
filename string
poll time.Duration
ds *datastore.Store
stage string
version string
hash string
}
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",
Test: "go test ./...",
Release: "tar -czf release.tar.gz ironsmith",
Version: "git describe --tags --long",
ReleaseFile: "release.tar.gz",
PollInterval: "15m",
}
func prepTemplateProject() error {
filename := filepath.Join(projectDir, projectTemplateFilename)
_, err := os.Stat(filename)
if os.IsNotExist(err) {
vlog("Creating template project file in %s", filename)
f, err := os.Create(filename)
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = cerr
}
}()
if err != nil {
return err
}
data, err := json.MarshalIndent(projectTemplate, "", " ")
if err != nil {
return err
}
_, err = f.Write(data)
if err != nil {
return err
}
} else if err != nil {
return err
}
return nil
}
type projectList struct {
sync.RWMutex
data map[string]*Project
}
var projects = projectList{
data: make(map[string]*Project),
}
func (p *projectList) load() error {
vlog("Loading projects from the enabled definitions in %s\n", filepath.Join(projectDir, enabledProjectDir))
dir, err := os.Open(filepath.Join(projectDir, enabledProjectDir))
defer func() {
if cerr := dir.Close(); cerr != nil && err == nil {
err = cerr
}
}()
if err != nil {
return err
}
files, err := dir.Readdir(0)
if err != nil {
return err
}
for i := range files {
if !files[i].IsDir() && filepath.Ext(files[i].Name()) == ".json" {
p.add(files[i].Name())
}
}
time.AfterFunc(projectFilePoll, startProjectLoader)
return nil
}
func (p *projectList) exists(name string) bool {
p.RLock()
defer p.RUnlock()
_, ok := p.data[name]
return ok
}
func (p *projectList) add(name string) {
vlog("Adding project %s to the project list.\n", name)
p.Lock()
defer p.Unlock()
prj := &Project{
filename: name,
Name: name,
stage: stageLoad,
}
p.data[name] = prj
go func() {
prj.load()
}()
}
// removeMissing removes projects that are missing from the passed in list of names
func (p *projectList) removeMissing(names []string) {
p.Lock()
defer p.Unlock()
for i := range p.data {
found := false
for k := range names {
if names[k] == i {
found = true
}
}
if !found {
vlog("Removing project %s from the project list, because the project file was removed.\n",
i)
delete(p.data, i)
}
}
}
func (p *projectList) closeAll() {
p.RLock()
defer p.RUnlock()
for i := range p.data {
_ = p.data[i].ds.Close()
}
}
// startProjectLoader polls for new projects
func startProjectLoader() {
dir, err := os.Open(filepath.Join(projectDir, enabledProjectDir))
defer func() {
if cerr := dir.Close(); cerr != nil && err == nil {
err = cerr
}
}()
if err != nil {
log.Printf("Error in startProjectLoader opening the filepath %s: %s\n", projectDir, err)
return
}
files, err := dir.Readdir(0)
if err != nil {
log.Printf("Error in startProjectLoader reading the dir %s: %s\n", projectDir, err)
return
}
names := make([]string, len(files))
for i := range files {
if !files[i].IsDir() && filepath.Ext(files[i].Name()) == ".json" {
names[i] = files[i].Name()
if !projects.exists(files[i].Name()) {
projects.add(files[i].Name())
}
}
}
//check for removed projects
projects.removeMissing(names)
time.AfterFunc(projectFilePoll, startProjectLoader)
}