Skip to content

Commit 7ac7efa

Browse files
committed
implemented configuration system via js
implemented basic target execution
1 parent 8aafd94 commit 7ac7efa

File tree

9 files changed

+351
-8
lines changed

9 files changed

+351
-8
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ module github.com/Southclaws/wadsworth
33
require (
44
github.com/Southclaws/gitwatch v1.0.1
55
github.com/pkg/errors v0.8.0
6+
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d
7+
github.com/stretchr/testify v1.2.2
68
github.com/urfave/cli v1.20.0
79
go.uber.org/atomic v1.3.2 // indirect
810
go.uber.org/multierr v1.1.0 // indirect
911
go.uber.org/zap v1.9.1
12+
gopkg.in/sourcemap.v1 v1.0.5 // indirect
1013
)

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
3333
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
3434
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
3535
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
36+
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d h1:1VUlQbCfkoSGv7qP7Y+ro3ap1P1pPZxgdGVqiTVy5C4=
37+
github.com/robertkrimen/otto v0.0.0-20180617131154-15f95af6e78d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
3638
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
3739
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
3840
github.com/src-d/gcfg v1.3.0 h1:2BEDr8r0I0b8h/fOqwtxCEiq2HJu8n2JGZJQFGXWLjg=
@@ -59,6 +61,8 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
5961
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
6062
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
6163
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
64+
gopkg.in/sourcemap.v1 v1.0.5 h1:inv58fC9f9J3TK2Y2R1NPntXEn3/wjWHkonhIUODNTI=
65+
gopkg.in/sourcemap.v1 v1.0.5/go.mod h1:2RlvNNSMglmRrcvhfuzp4hQHwOtjxlbjX7UPY/GXb78=
6266
gopkg.in/src-d/go-billy.v4 v4.2.0 h1:VGbrP1EsYxtvVPEiHui+4//imr4E5MGEFLx66bQtusg=
6367
gopkg.in/src-d/go-billy.v4 v4.2.0/go.mod h1:ZHSF0JP+7oD97194otDUCD7Ofbk63+xFcfWP5bT6h+Q=
6468
gopkg.in/src-d/go-git-fixtures.v3 v3.1.0 h1:xjgkEQtv542nRaDzOALYfbFzcRwdt07Q/s2b82fq7AA=

main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ this repository has new commits, Wadsworth will automatically reconfigure.`,
9090
signal.Notify(s, os.Interrupt)
9191

9292
select {
93+
case <-ctx.Done():
94+
err = ctx.Err()
9395
case sig := <-s:
9496
err = errors.New(sig.String())
9597
case err = <-errs:

service/config/config.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package config
2+
3+
import (
4+
"encoding/json"
5+
"io/ioutil"
6+
"path/filepath"
7+
8+
"github.com/pkg/errors"
9+
"github.com/robertkrimen/otto"
10+
11+
"github.com/Southclaws/wadsworth/service/task"
12+
)
13+
14+
// State represents a desired system state
15+
type State struct {
16+
Targets task.Targets `json:"targets"`
17+
}
18+
19+
// ConfigFromDirectory searches a directory for configuration files and
20+
// constructs a desired state from the declarations.
21+
func ConfigFromDirectory(dir string) (state State, err error) {
22+
files, err := ioutil.ReadDir(dir)
23+
if err != nil {
24+
err = errors.Wrap(err, "failed to read config directory")
25+
return
26+
}
27+
28+
sources := []string{}
29+
30+
for _, file := range files {
31+
if file.IsDir() {
32+
continue
33+
}
34+
35+
if filepath.Ext(file.Name()) == ".js" {
36+
sources = append(sources, fileToString(filepath.Join(dir, file.Name())))
37+
}
38+
}
39+
40+
cb := configBuilder{
41+
vm: otto.New(),
42+
state: new(State),
43+
scripts: sources,
44+
}
45+
46+
err = cb.construct()
47+
if err != nil {
48+
return
49+
}
50+
51+
state = *cb.state
52+
return
53+
}
54+
55+
type configBuilder struct {
56+
vm *otto.Otto
57+
state *State
58+
scripts []string
59+
}
60+
61+
func (cb *configBuilder) construct() (err error) {
62+
//nolint:errcheck
63+
cb.vm.Run(`'use strict';
64+
var STATE = {
65+
targets: []
66+
};
67+
68+
function T(t) {
69+
if(t.name === undefined) { throw "target name undefined"; }
70+
if(t.url === undefined) { throw "target url undefined"; }
71+
if(t.up === undefined) { throw "target up undefined"; }
72+
// if(t.down === undefined) { }
73+
// if(t.env) { }
74+
// if(t.initial_run) { }
75+
// if(t.shutdown_command) { }
76+
77+
STATE.targets.push(t)
78+
}
79+
`)
80+
81+
for _, s := range cb.scripts {
82+
err = cb.applyFileTargets(s)
83+
if err != nil {
84+
return
85+
}
86+
}
87+
88+
stateJsonObj, err := cb.vm.Run(`JSON.stringify(STATE)`)
89+
if err != nil {
90+
return errors.Wrap(err, "failed to stringify STATE object")
91+
}
92+
stateJson, err := stateJsonObj.ToString()
93+
if err != nil {
94+
return errors.Wrap(err, "failed to get string representation of STATE")
95+
}
96+
err = json.Unmarshal([]byte(stateJson), cb.state)
97+
98+
return
99+
}
100+
101+
func (cb *configBuilder) applyFileTargets(script string) (err error) {
102+
_, err = cb.vm.Run(script)
103+
if err != nil {
104+
return
105+
}
106+
107+
return
108+
}
109+
110+
func fileToString(path string) (contents string) {
111+
b, err := ioutil.ReadFile(path)
112+
if err != nil {
113+
return
114+
}
115+
return string(b)
116+
}

service/config/config_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/robertkrimen/otto"
7+
"github.com/stretchr/testify/assert"
8+
9+
"github.com/Southclaws/wadsworth/service/task"
10+
)
11+
12+
func Test_applyFileTargets(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
script string
16+
wantTargets task.Targets
17+
wantErr bool
18+
}{
19+
{"one", `T({
20+
name: "name",
21+
url: "../test.local",
22+
up: ["echo", "hello world"]
23+
})`, Targets{
24+
{
25+
Name: "name",
26+
RepoURL: "../test.local",
27+
Up: []string{"echo", "hello world"},
28+
},
29+
}, false},
30+
{"variable", `
31+
var url = "https://github.com/Southclaws/";
32+
33+
T({name: "1", url: url + "project1", up: ["sleep"]});
34+
T({name: "2", url: url + "project2", up: ["sleep"]});
35+
T({name: "3", url: url + "project3", up: ["sleep"]});
36+
37+
console.log("done!");
38+
`, Targets{
39+
{Name: "1", RepoURL: "https://github.com/Southclaws/project1", Up: []string{"sleep"}},
40+
{Name: "2", RepoURL: "https://github.com/Southclaws/project2", Up: []string{"sleep"}},
41+
{Name: "3", RepoURL: "https://github.com/Southclaws/project3", Up: []string{"sleep"}},
42+
}, false},
43+
{"envmap", `
44+
var url = "https://github.com/Southclaws/";
45+
46+
T({name: "1", url: url + "project1", up: ["sleep"], env: {PASSWORD: "nope"}});
47+
T({name: "2", url: url + "project2", up: ["sleep"], env: {PASSWORD: "nope"}});
48+
T({name: "3", url: url + "project3", up: ["sleep"], env: {PASSWORD: "nope"}});
49+
50+
console.log("done!");
51+
`, Targets{
52+
{Name: "1", RepoURL: "https://github.com/Southclaws/project1", Up: []string{"sleep"}, Env: map[string]string{"PASSWORD": "nope"}},
53+
{Name: "2", RepoURL: "https://github.com/Southclaws/project2", Up: []string{"sleep"}, Env: map[string]string{"PASSWORD": "nope"}},
54+
{Name: "3", RepoURL: "https://github.com/Southclaws/project3", Up: []string{"sleep"}, Env: map[string]string{"PASSWORD": "nope"}},
55+
}, false},
56+
{"badtype", `T({name: "name", url: "../test.local", up: 1.23})`, Targets{{}}, true},
57+
{"missingkey", `T({name: "name", url: "../test.local"})`, Targets{{}}, true},
58+
}
59+
60+
for _, tt := range tests {
61+
t.Run(tt.name, func(t *testing.T) {
62+
cb := configBuilder{
63+
vm: otto.New(),
64+
state: new(State),
65+
scripts: []string{tt.script},
66+
}
67+
68+
err := cb.construct()
69+
if tt.wantErr {
70+
assert.Error(t, err)
71+
return
72+
}
73+
74+
assert.NoError(t, err)
75+
assert.Equal(t, tt.wantTargets, cb.state.Targets)
76+
})
77+
}
78+
}

service/handler.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package service
2+
3+
import (
4+
"github.com/Southclaws/gitwatch"
5+
"github.com/pkg/errors"
6+
)
7+
8+
// handle takes an event from gitwatch and runs the event's triggers
9+
func (app *App) handle(e gitwatch.Event) (err error) {
10+
target, exists := app.targets[e.URL]
11+
if !exists {
12+
return errors.Errorf("attempt to handle event for unknown target %s at %s", e.URL, e.Path)
13+
}
14+
15+
return target.Execute(e.Path, nil, false)
16+
}

service/reconfigure.go

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import (
44
"github.com/Southclaws/gitwatch"
55
"github.com/pkg/errors"
66
"go.uber.org/zap"
7+
8+
"github.com/Southclaws/wadsworth/service/config"
9+
"github.com/Southclaws/wadsworth/service/task"
710
)
811

912
// reconfigure will close the configuration watcher and target watcher (unless
@@ -31,14 +34,52 @@ func (app *App) reconfigure() (err error) {
3134
return errors.Wrap(err, "failed to watch config target")
3235
}
3336
go app.configWatcher.Run() //nolint:errcheck - no worthwhile errors returned
34-
zap.L().Debug("created new watcher, awaiting initial event")
37+
zap.L().Debug("created new config watcher, awaiting setup")
3538

3639
<-app.configWatcher.InitialDone
37-
zap.L().Debug("initial event received")
40+
zap.L().Debug("config initial setup done")
41+
42+
path, err := gitwatch.GetRepoPath(app.config.Directory, app.config.Target)
43+
if err != nil {
44+
return
45+
}
46+
47+
// TODO: if this fails, log an error and fall back to the old state
48+
state, err := config.ConfigFromDirectory(path)
49+
if err != nil {
50+
return errors.Wrap(err, "failed to construct config from repo")
51+
}
52+
zap.L().Debug("constructed desired state", zap.Int("targets", len(state.Targets)))
53+
54+
if app.targetsWatcher != nil {
55+
app.targetsWatcher.Close()
56+
}
57+
58+
// TODO: diff what changed, run the `down` command for those that were removed
59+
60+
app.targets = make(map[string]task.Target)
61+
targets := make([]string, len(state.Targets))
62+
for i, t := range state.Targets {
63+
zap.L().Debug("assigned target", zap.String("url", t.RepoURL))
64+
targets[i] = t.RepoURL
65+
app.targets[t.RepoURL] = t
66+
}
67+
68+
app.targetsWatcher, err = gitwatch.New(
69+
app.ctx,
70+
targets,
71+
app.config.CheckInterval,
72+
app.config.Directory,
73+
nil,
74+
true)
75+
if err != nil {
76+
return errors.Wrap(err, "failed to watch targets")
77+
}
78+
go app.targetsWatcher.Run() //nolint:errcheck - no worthwhile errors returned
79+
zap.L().Debug("created targets watcher, awaiting setup")
3880

39-
// read config from repo
40-
// recreate targets gitwatch
41-
// diff?
81+
<-app.targetsWatcher.InitialDone
82+
zap.L().Debug("targets initial setup done")
4283

4384
return
4485
}

service/service.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import (
66

77
"github.com/Southclaws/gitwatch"
88
"go.uber.org/zap"
9+
10+
"github.com/Southclaws/wadsworth/service/task"
911
)
1012

1113
// App stores application state
1214
type App struct {
1315
config Config
1416
configWatcher *gitwatch.Session
15-
targets []string
17+
targets map[string]task.Target
1618
targetsWatcher *gitwatch.Session
1719
ctx context.Context
1820
cancel context.CancelFunc
@@ -25,11 +27,11 @@ type Config struct {
2527
}
2628

2729
// Initialise prepares an instance of the app to run
28-
func Initialise(ctx context.Context, config Config) (app *App, err error) {
30+
func Initialise(ctx context.Context, c Config) (app *App, err error) {
2931
app = new(App)
3032

3133
app.ctx, app.cancel = context.WithCancel(ctx)
32-
app.config = config
34+
app.config = c
3335

3436
err = app.reconfigure()
3537
if err != nil {
@@ -45,6 +47,9 @@ func (app *App) Start() (final error) {
4547
select {
4648
case <-app.configWatcher.Events:
4749
err = app.reconfigure()
50+
51+
case e := <-app.targetsWatcher.Events:
52+
err = app.handle(e)
4853
}
4954
return
5055
}

0 commit comments

Comments
 (0)