diff --git a/README.md b/README.md index 77deea7..79d7a17 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ development machines. To run the server, supply a configuration file: ```sh -$ ephemerald -c config.json +$ ephemerald -c config.yaml ``` Press Q to quit the server. @@ -61,53 +61,43 @@ Note: use Ctrl-C to stop the server wen not in `--ui tui` mode (`SIGINT`,`SIGQUI For example, to see only log messages (at debug level) use: ```sh -$ ephememerald --ui none --log-level debug --log-file /dev/stdout -c config.json +$ ephememerald --ui none --log-level debug --log-file /dev/stdout -c config.yaml ``` ## Configuration -Container pools are configured in a json file. Each pool has options for the container parameters and +Container pools are configured in a yaml (or json) file. Each pool has options for the container parameters and for lifecycle actions. The following configuration creates a single pool called "pg" which maintains five containers from the "postgres" image and exposes port 5432 to clients. See the [`params`](#params) and [`actions`](#lifecycle-actions) below for documentation on those fields. -```json -{ - "pools": { - "pg": { - "image": "postgres", - "size": 5, - "port": 5432, - "params": { - "username": "postgres", - "password": "", - "database": "postgres", - "url": "postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable" - }, - "actions": { - "healthcheck": { - "type": "postgres.ping", - "retries": 10, - "delay": "50ms" - }, - "initialize": { - "type": "exec", - "path": "make", - "args": ["db:migrate"], - "env": ["DATABASE_URL={{.Url}}"], - "timeout": "10s", - }, - "reset": { - "type": "postgres.truncate" - } - } - } - } -} +```yaml +pools: + pg: + image: postgres + size: 5 + port: 5432 + params: + username: postgres + database: postgres + url: postgres://{{.Username}}:@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable + actions: + healthcheck: + type: postgres.ping + retries: 10 + delay: 50ms + initialize: + type: exec + path: make + args: [ 'db:migrate' ] + env: [ 'DATABASE_URL={{.Url}}' ] + timeout: 10s + reset: + type: postgres.truncate ``` -See [example/config.json](_example/config.json) for a full working configuration. +See [example/config.yaml](_example/config.yaml) for a full working configuration. ### Params @@ -126,13 +116,10 @@ Database | The `database` field declared in `params` A `params` section for postgres may look like this: -```json -{ - "username": "postgres", - "password": "", - "database": "postgres", - "url": "postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable" -} +```yaml +username: postgres +database: postgres +url: postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable ``` ### Container @@ -233,12 +220,10 @@ args | `[]` | values to be escaped with positional arguments in `command`. Example: -```json -{ - "type": "postgres.exec", - "command": "INSERT INTO users (name) VALUES ($1)", - "args": "Robert'); DROP TABLE STUDENTS;--" -} +```yaml +type: postgres.exec +command: 'INSERT INTO users (name) VALUES ($1)' +args: "Robert'); DROP TABLE STUDENTS;--" ``` #### postgres.ping @@ -365,7 +350,7 @@ $ make server example Run the example server and client in separate terminals ```sh -$ ./ephemerald/ephemerald -f ./_example/config.json +$ ./ephemerald/ephemerald -c _example/config.yaml ``` ```sh @@ -389,7 +374,7 @@ Download the [latest release](https://github.com/boz/ephemerald/releases/latest) ```sh $ release="https://github.com/boz/ephemerald/releases/download/v0.3.1/ephemerald_Linux_x86_64.tar.gz" $ curl -L "$release" | tar -C /tmp -zxv -$ /tmp/ephemerald -c config.json +$ /tmp/ephemerald -c config.yaml ``` ### Homebrew diff --git a/_example/config.json b/_example/config.json deleted file mode 100644 index 1c0add2..0000000 --- a/_example/config.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "pools": { - "redis": { - "image": "redis", - "size": 5, - "port": 6379, - "params": { - "database": "0", - "url": "redis://{{.Hostname}}:{{.Port}}/{{.Database}}" - }, - "actions": { - "healthcheck": { - "type": "redis.ping" - }, - "reset": { - "type": "redis.truncate" - } - } - }, - "postgres": { - "image": "postgres", - "size": 5, - "port": 5432, - "params": { - "username": "postgres", - "password": "", - "database": "postgres", - "url": "postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable" - }, - "actions": { - "initialize": { - "type": "postgres.exec", - "query": "CREATE TABLE users (id serial PRIMARY KEY, name VARCHAR(255) NOT NULL, UNIQUE(name))" - }, - "healthcheck": { - "type": "postgres.ping" - }, - "reset": { - "type": "postgres.truncate" - } - } - } - } -} diff --git a/_example/config.yaml b/_example/config.yaml new file mode 100644 index 0000000..f32fabe --- /dev/null +++ b/_example/config.yaml @@ -0,0 +1,35 @@ +--- +pools: + redis: + image: redis + size: 5 + port: 6379 + params: + database: "0" + url: redis://{{.Hostname}}:{{.Port}}/{{.Database}} + actions: + healthcheck: + type: redis.ping + reset: + type: redis.truncate + postgres: + image: postgres + size: 5 + port: 5432 + params: + username: postgres + database: postgres + url: postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable + actions: + initialize: + type: postgres.exec + query: | + CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + UNIQUE(name) + ) + healthcheck: + type: postgres.ping + reset: + type: postgres.truncate diff --git a/_example/demo.json b/_example/demo.json deleted file mode 100644 index 3e05513..0000000 --- a/_example/demo.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "pools": { - "redis": { - "image": "redis", - "size": 5, - "port": 6379, - "params": { - "database": "0", - "url": "redis://{{.Hostname}}:{{.Port}}/{{.Database}}" - }, - "actions": { - "healthcheck": { - "type": "redis.ping" - }, - "reset": { - "type": "exec", - "path": "sleep", - "args": ["1"] - } - } - }, - "vault": { - "image": "vault", - "size": 5, - "port": 8200, - "container": { - "env": ["SKIP_SETCAP=1"] - }, - "params": { - "url": "http://{{.Hostname}}:{{.Port}}" - }, - "actions": { - "healthcheck": { - "type": "http.get" - } - } - }, - "postgres": { - "image": "postgres", - "size": 5, - "port": 5432, - "params": { - "username": "postgres", - "password": "", - "database": "postgres", - "url": "postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable" - }, - "actions": { - "initialize": { - "type": "postgres.exec", - "query": "CREATE TABLE users (id serial PRIMARY KEY, name VARCHAR(255) NOT NULL, UNIQUE(name))" - }, - "healthcheck": { - "type": "postgres.ping", - "retries": 20 - }, - "reset": { - "type": "postgres.truncate" - } - } - } - } -} diff --git a/_example/demo.yaml b/_example/demo.yaml new file mode 100644 index 0000000..3e4a0a2 --- /dev/null +++ b/_example/demo.yaml @@ -0,0 +1,50 @@ +--- +pools: + redis: + image: redis + size: 5 + port: 6379 + params: + database: '0' + url: redis://{{.Hostname}}:{{.Port}}/{{.Database}} + actions: + healthcheck: + type: redis.ping + reset: + type: exec + path: sleep + args: [ '1' ] + vault: + image: vault + size: 5 + port: 8200 + container: + env: + - SKIP_SETCAP=1 + params: + url: http://{{.Hostname}}:{{.Port}} + actions: + healthcheck: + type: http.get + postgres: + image: postgres + size: 5 + port: 5432 + params: + username: postgres + database: postgres + url: postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable + actions: + initialize: + type: postgres.exec + query: | + CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + UNIQUE(name) + ) + healthcheck: + type: postgres.ping + retries: 20 + reset: + type: postgres.truncate diff --git a/_example/ui.json b/_example/ui.json deleted file mode 100644 index 22d9036..0000000 --- a/_example/ui.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "pools": { - "redis": { - "image": "redis", - "size": 5, - "port": 6379, - "params": { - "database": "0", - "url": "redis://{{.Hostname}}:{{.Port}}/{{.Database}}" - }, - "actions": { - "healthcheck": { - "type": "redis.ping" - } - } - } - } -} diff --git a/_example/ui.yaml b/_example/ui.yaml new file mode 100644 index 0000000..228a4a1 --- /dev/null +++ b/_example/ui.yaml @@ -0,0 +1,12 @@ +--- +pools: + redis: + image: redis + size: 5 + port: 6379 + params: + database: '0' + url: redis://{{.Hostname}}:{{.Port}}/{{.Database}} + actions: + healthcheck: + type: redis.ping diff --git a/_testdata/pool.redis.json b/_testdata/pool.redis.json index 50dc349..bb2a306 100644 --- a/_testdata/pool.redis.json +++ b/_testdata/pool.redis.json @@ -1,6 +1,6 @@ { "size": 1, - "image": "redis" + "image": "redis", "port": 6379, "actions": { "healthcheck": { diff --git a/_testdata/pools.json b/_testdata/pools.json index fd6ddeb..111fa07 100644 --- a/_testdata/pools.json +++ b/_testdata/pools.json @@ -21,7 +21,7 @@ }, "redis": { "size": 1, - "image": "redis" + "image": "redis", "port": 6379, "params": { "database": "0", diff --git a/builtin/postgres/_testdata/pool.yaml b/builtin/postgres/_testdata/pool.yaml new file mode 100644 index 0000000..bce620e --- /dev/null +++ b/builtin/postgres/_testdata/pool.yaml @@ -0,0 +1,20 @@ +size: 1 +image: postgres +port: 5432 +params: + username: postgres + database: postgres + url: postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable +actions: + initialize: + type: postgres.exec + query: | + create table users ( + id serial primary key, + name varchar(255), + unique(name) + ) + healthcheck: + type: postgres.ping + reset: + type: postgres.truncate diff --git a/builtin/postgres/postgres_test.go b/builtin/postgres/postgres_test.go index 6024cf8..54cc561 100644 --- a/builtin/postgres/postgres_test.go +++ b/builtin/postgres/postgres_test.go @@ -11,21 +11,25 @@ import ( ) func TestActionsPingExec(t *testing.T) { - testutil.RunPoolFromFile(t, "pool.json", func(p params.Params) { - db, err := sql.Open("postgres", p.Url) - require.NoError(t, err) - defer db.Close() + files := []string{"pool.json", "pool.yaml"} - rows, err := db.Query("SELECT COUNT(*) FROM users") - require.NoError(t, err) - defer rows.Close() + for _, file := range files { + testutil.RunPoolFromFile(t, file, func(p params.Params) { + db, err := sql.Open("postgres", p.Url) + require.NoError(t, err, file) + defer db.Close() + + rows, err := db.Query("SELECT COUNT(*) FROM users") + require.NoError(t, err, file) + defer rows.Close() - require.True(t, rows.Next()) + require.True(t, rows.Next(), file) - var count int - require.NoError(t, rows.Scan(&count)) - require.Equal(t, 0, count) - }) + var count int + require.NoError(t, rows.Scan(&count), file) + require.Equal(t, 0, count, file) + }) + } } func TestActionTruncate(t *testing.T) { diff --git a/builtin/redis/_testdata/pool.yaml b/builtin/redis/_testdata/pool.yaml new file mode 100644 index 0000000..b204b01 --- /dev/null +++ b/builtin/redis/_testdata/pool.yaml @@ -0,0 +1,13 @@ +size: 1 +image: redis +port: 6379 +params: + database: "0" + url: redis://{{.Hostname}}:{{.Port}}/{{.Database}} +actions: + initialize: + type: redis.exec + healthcheck: + type: redis.ping + reset: + type: redis.truncate diff --git a/builtin/redis/redis_test.go b/builtin/redis/redis_test.go index 4912254..bece743 100644 --- a/builtin/redis/redis_test.go +++ b/builtin/redis/redis_test.go @@ -12,14 +12,18 @@ import ( ) func TestActionExec(t *testing.T) { - testutil.RunPoolFromFile(t, "pool.json", func(p params.Params) { - db, err := rredis.DialURL(p.Url) - require.NoError(t, err) - defer db.Close() + files := []string{"pool.json", "pool.yaml"} - _, err = db.Do("PING") - require.NoError(t, err) - }) + for _, file := range files { + testutil.RunPoolFromFile(t, file, func(p params.Params) { + db, err := rredis.DialURL(p.Url) + require.NoError(t, err, file) + defer db.Close() + + _, err = db.Do("PING") + require.NoError(t, err, file) + }) + } } func TestActionTruncate(t *testing.T) { diff --git a/config/_testdata/config.json b/config/_testdata/config.json index c63d851..111d6b4 100644 --- a/config/_testdata/config.json +++ b/config/_testdata/config.json @@ -1,23 +1,23 @@ { - "pools": { - "redis": { - "size": 10, - "image": "redis", - "port": 6379, + "pools": { + "redis": { + "size": 10, + "image": "redis", + "port": 6379, - "params": { - "database": "0", - "url": "redis://{{.Hostname}}:{{.Port}}/{{.Database}}" - }, + "params": { + "database": "0", + "url": "redis://{{.Hostname}}:{{.Port}}/{{.Database}}" + }, - "actions": { - "healthcheck": { - "type": "noop" - }, - "reset": { - "type": "noop" - } - } - } - } + "actions": { + "healthcheck": { + "type": "noop" + }, + "reset": { + "type": "noop" + } + } + } + } } diff --git a/config/_testdata/config.yaml b/config/_testdata/config.yaml new file mode 100644 index 0000000..19774a3 --- /dev/null +++ b/config/_testdata/config.yaml @@ -0,0 +1,13 @@ +pools: + redis: + size: 10 + image: redis + port: 6379 + params: + database: "0" + url: "redis://{{.Hostname}}:{{.Port}}/{{.Database}}" + actions: + healthcheck: + type: noop + reset: + type: noop diff --git a/config/config.go b/config/config.go index f09422c..8892b1a 100644 --- a/config/config.go +++ b/config/config.go @@ -6,12 +6,14 @@ import ( "io" "io/ioutil" "os" + "path" "github.com/Sirupsen/logrus" "github.com/boz/ephemerald/lifecycle" "github.com/boz/ephemerald/params" "github.com/boz/ephemerald/ui" "github.com/buger/jsonparser" + "github.com/ghodss/yaml" ) const ( @@ -32,13 +34,21 @@ type Config struct { uie ui.PoolEmitter } -func ReadFile(log logrus.FieldLogger, uie ui.Emitter, path string) ([]*Config, error) { - file, err := os.Open(path) +func ReadFile(log logrus.FieldLogger, uie ui.Emitter, fpath string) ([]*Config, error) { + file, err := os.Open(fpath) if err != nil { return []*Config{}, err } defer file.Close() - return Read(log, uie, file) + + switch path.Ext(fpath) { + case ".yml", ".yaml": + return ReadYAML(log, uie, file) + case ".json": + return Read(log, uie, file) + default: + return nil, fmt.Errorf("Unknown extension %v", path.Ext(fpath)) + } } func Read(log logrus.FieldLogger, uie ui.Emitter, r io.Reader) ([]*Config, error) { @@ -50,6 +60,20 @@ func Read(log logrus.FieldLogger, uie ui.Emitter, r io.Reader) ([]*Config, error return ParseAll(log, uie, buf) } +func ReadYAML(log logrus.FieldLogger, uie ui.Emitter, r io.Reader) ([]*Config, error) { + var configs []*Config + buf, err := ioutil.ReadAll(r) + if err != nil { + return configs, err + } + + buf, err = yaml.YAMLToJSON(buf) + if err != nil { + return configs, err + } + return ParseAll(log, uie, buf) +} + func ParseAll(log logrus.FieldLogger, uie ui.Emitter, buf []byte) ([]*Config, error) { var configs []*Config err := jsonparser.ObjectEach(buf, func(key []byte, buf []byte, dt jsonparser.ValueType, _ int) error { diff --git a/config/config_test.go b/config/config_test.go index 63ee22a..8f365bf 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,35 +1,39 @@ package config_test import ( - "io/ioutil" "testing" - "github.com/Sirupsen/logrus" "github.com/boz/ephemerald/config" "github.com/boz/ephemerald/testutil" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestParseAll(t *testing.T) { - buf, err := ioutil.ReadFile("_testdata/config.json") - require.NoError(t, err) +func TestRead(t *testing.T) { + doReadTest(t, "_testdata/config.json", "json") + doReadTest(t, "_testdata/config.yaml", "yaml") + doReadTest(t, "_testdata/config.yml", "yml") +} + +func doReadTest(t *testing.T, path string, msg string) { + log := testutil.Log() + uie := testutil.Emitter() + + configs, err := config.ReadFile(log, uie, "_testdata/config.json") + require.NoError(t, err, msg) - log := logrus.New() - configs, err := config.ParseAll(log, testutil.Emitter(), buf) - require.NoError(t, err) - require.Equal(t, 1, len(configs)) + require.Equal(t, 1, len(configs), msg) cfg := configs[0] - assert.Equal(t, "redis", cfg.Name) - assert.Equal(t, "redis", cfg.Image) - assert.Equal(t, 6379, cfg.Port) - assert.Equal(t, 10, cfg.Size) + assert.Equal(t, "redis", cfg.Name, msg) + assert.Equal(t, "redis", cfg.Image, msg) + assert.Equal(t, 6379, cfg.Port, msg) + assert.Equal(t, 10, cfg.Size, msg) m := cfg.Lifecycle.ForContainer(testutil.ContainerEmitter(), testutil.CID()) - assert.False(t, m.HasInitialize()) - assert.True(t, m.HasHealthcheck()) - assert.True(t, m.HasReset()) + assert.False(t, m.HasInitialize(), msg) + assert.True(t, m.HasHealthcheck(), msg) + assert.True(t, m.HasReset(), msg) } diff --git a/ephemerald/main.go b/ephemerald/main.go index 16427cc..c3b66ce 100644 --- a/ephemerald/main.go +++ b/ephemerald/main.go @@ -26,7 +26,7 @@ var ( configFile = kingpin.Flag("config", "config file").Short('c'). Required(). - File() + ExistingFile() logLevel = kingpin.Flag("log-level", "Log level (debug, info, warn, error). Default: info"). Default("info"). @@ -67,8 +67,7 @@ func main() { } kingpin.FatalIfError(err, "Can't start UI") - configs, err := config.Read(log, appui.Emitter(), *configFile) - (*configFile).Close() + configs, err := config.ReadFile(log, appui.Emitter(), *configFile) kingpin.FatalIfError(err, "invalid config file") pools, err := ephemerald.NewPoolSet(log, ctx, configs) diff --git a/lifecycle/_testdata/action.exec.yaml b/lifecycle/_testdata/action.exec.yaml new file mode 100644 index 0000000..efa7994 --- /dev/null +++ b/lifecycle/_testdata/action.exec.yaml @@ -0,0 +1,9 @@ +type: exec +path: echo +args: + - a + - b + - c +env: + - FOO=BAR +dir: / diff --git a/lifecycle/_testdata/action.http.get.yaml b/lifecycle/_testdata/action.http.get.yaml new file mode 100644 index 0000000..ebd1497 --- /dev/null +++ b/lifecycle/_testdata/action.http.get.yaml @@ -0,0 +1,2 @@ +type: http.get +url: http://www.google.com diff --git a/lifecycle/_testdata/action.tcp.connect.yaml b/lifecycle/_testdata/action.tcp.connect.yaml new file mode 100644 index 0000000..b190a90 --- /dev/null +++ b/lifecycle/_testdata/action.tcp.connect.yaml @@ -0,0 +1 @@ +type: tcp.connect diff --git a/lifecycle/_testdata/base.defaults.json b/lifecycle/_testdata/base.defaults.json new file mode 100644 index 0000000..1a67088 --- /dev/null +++ b/lifecycle/_testdata/base.defaults.json @@ -0,0 +1,4 @@ +{ + "type": "exec", + "path": "echo" +} diff --git a/lifecycle/_testdata/base.defaults.yaml b/lifecycle/_testdata/base.defaults.yaml new file mode 100644 index 0000000..d0a91a2 --- /dev/null +++ b/lifecycle/_testdata/base.defaults.yaml @@ -0,0 +1,2 @@ +type: exec +path: echo diff --git a/lifecycle/_testdata/base.override.json b/lifecycle/_testdata/base.override.json new file mode 100644 index 0000000..4d373a3 --- /dev/null +++ b/lifecycle/_testdata/base.override.json @@ -0,0 +1,7 @@ +{ + "type": "exec", + "retries": 10, + "timeout": "5ms", + "delay": "10s", + "path": "make" +} diff --git a/lifecycle/_testdata/base.override.yaml b/lifecycle/_testdata/base.override.yaml new file mode 100644 index 0000000..dc3efa5 --- /dev/null +++ b/lifecycle/_testdata/base.override.yaml @@ -0,0 +1,5 @@ +type: exec +retries: 10 +timeout: 5ms +delay: 10s +path: make diff --git a/lifecycle/_testdata/manager.full.yaml b/lifecycle/_testdata/manager.full.yaml new file mode 100644 index 0000000..6ac2b2b --- /dev/null +++ b/lifecycle/_testdata/manager.full.yaml @@ -0,0 +1,6 @@ +initialize: + type: noop +healthcheck: + type: noop +reset: + type: noop diff --git a/lifecycle/_testdata/manager.partial.yaml b/lifecycle/_testdata/manager.partial.yaml new file mode 100644 index 0000000..3dd717c --- /dev/null +++ b/lifecycle/_testdata/manager.partial.yaml @@ -0,0 +1,2 @@ +initialize: + type: noop diff --git a/lifecycle/action.go b/lifecycle/action.go index 80bca88..6b1ea61 100644 --- a/lifecycle/action.go +++ b/lifecycle/action.go @@ -51,7 +51,7 @@ type ActionPlugin interface { func ParseAction(buf []byte) (Action, error) { t, err := jsonparser.GetString(buf, "type") if err != nil { - return nil, err + return nil, parseError("type", err) } p, err := lookupPlugin(t) @@ -80,7 +80,7 @@ func (ac *ActionConfig) UnmarshalJSON(buf []byte) error { if other.Timeout != "" { val, err := time.ParseDuration(other.Timeout) if err != nil { - return err + return parseError("timeout", err) } ac.Timeout = val } @@ -88,7 +88,7 @@ func (ac *ActionConfig) UnmarshalJSON(buf []byte) error { if other.Delay != "" { val, err := time.ParseDuration(other.Delay) if err != nil { - return err + return parseError("delay", err) } ac.Delay = val } @@ -123,3 +123,7 @@ func (a *actionPlugin) Name() string { func (a *actionPlugin) ParseConfig(buf []byte) (Action, error) { return a.parseConfig(buf) } + +func parseError(field string, err error) error { + return fmt.Errorf("error parsing field '%v': %v", field, err) +} diff --git a/lifecycle/action_test.go b/lifecycle/action_test.go index 845cfc0..d3bb4fe 100644 --- a/lifecycle/action_test.go +++ b/lifecycle/action_test.go @@ -2,75 +2,62 @@ package lifecycle_test import ( "context" - "io/ioutil" - "os" - "path" "testing" "time" - "github.com/Sirupsen/logrus" "github.com/boz/ephemerald/lifecycle" "github.com/boz/ephemerald/params" + "github.com/boz/ephemerald/testutil" "github.com/stretchr/testify/require" ) func TestParseAction_override(t *testing.T) { + actions := map[string]lifecycle.Action{ + "json": actionFromFile(t, "base.override.json"), + "yaml": actionFromFile(t, "base.override.yaml"), + } - js := []byte(`{ - "type": "exec", - "retries": 10, - "timeout": "5ms", - "delay": "10s", - "path": "make" - }`) - - action, err := lifecycle.ParseAction(js) - require.NoError(t, err) - require.NotNil(t, action) - - require.Equal(t, 10, action.Config().Retries) - require.Equal(t, 5*time.Millisecond, action.Config().Timeout) - require.Equal(t, 10*time.Second, action.Config().Delay) + for ext, action := range actions { + require.NotNil(t, action, ext) + require.Equal(t, 10, action.Config().Retries, ext) + require.Equal(t, 5*time.Millisecond, action.Config().Timeout, ext) + require.Equal(t, 10*time.Second, action.Config().Delay, ext) + } } func TestParseAction_defaults(t *testing.T) { + actions := map[string]lifecycle.Action{ + "json": actionFromFile(t, "base.defaults.json"), + "yaml": actionFromFile(t, "base.defaults.yaml"), + } - js := []byte(`{ - "type": "exec", - "path": "echo" - }`) - - action, err := lifecycle.ParseAction(js) - require.NoError(t, err) - require.NotNil(t, action) - - require.Equal(t, lifecycle.ActionDefaultRetries, action.Config().Retries) - require.Equal(t, 5*time.Second, action.Config().Timeout) - require.Equal(t, lifecycle.ActionDefaultDelay, action.Config().Delay) + for ext, action := range actions { + require.Equal(t, lifecycle.ActionDefaultRetries, action.Config().Retries, ext) + require.Equal(t, 5*time.Second, action.Config().Timeout, ext) + require.Equal(t, lifecycle.ActionDefaultDelay, action.Config().Delay, ext) + } } func TestActionExec(t *testing.T) { runActionFromFile(t, "action.exec.json", "exec", params.Params{}, true, "exec") + runActionFromFile(t, "action.exec.yaml", "exec", params.Params{}, true, "exec") } func TestActionHttpPing(t *testing.T) { runActionFromFile(t, "action.http.get.json", "http.get", params.Params{}, true, "http.get") + runActionFromFile(t, "action.http.get.yaml", "http.get", params.Params{}, true, "http.get") } func TestActionTCPConnect(t *testing.T) { runActionFromFile(t, "action.tcp.connect.json", "tcp.connect", params.Params{Hostname: "google.com", Port: "80"}, true, "tcp.connect") + runActionFromFile(t, "action.tcp.connect.yaml", "tcp.connect", params.Params{Hostname: "google.com", Port: "80"}, true, "tcp.connect") } func actionFromFile(t *testing.T, name string) lifecycle.Action { - path := path.Join("_testdata", name) - file, err := os.Open(path) - require.NoError(t, err) - - buf, err := ioutil.ReadAll(file) - require.NoError(t, err) + buf := testutil.ReadJSON(t, name) action, err := lifecycle.ParseAction(buf) - require.NoError(t, err) + require.NoError(t, err, name) return action } @@ -89,7 +76,6 @@ func runActionFromFile(t *testing.T, name string, at string, p params.Params, ok } func actionEnv(t *testing.T) lifecycle.Env { - log := logrus.New() - log.Level = logrus.DebugLevel + log := testutil.Log() return lifecycle.NewEnv(context.Background(), log.WithField("test", t.Name())) } diff --git a/lifecycle/manager.go b/lifecycle/manager.go index e9d8423..32c2982 100644 --- a/lifecycle/manager.go +++ b/lifecycle/manager.go @@ -62,21 +62,21 @@ func (m *manager) ParseConfig(buf []byte) error { { action, err := m.parseAction(buf, "initialize") if err != nil { - return err + return parseError("initialize", err) } m.initializeAction = action } { action, err := m.parseAction(buf, "healthcheck") if err != nil { - return err + return parseError("healthcheck", err) } m.healthcheckAction = action } { action, err := m.parseAction(buf, "reset") if err != nil { - return err + return parseError("reset", err) } m.resetAction = action } diff --git a/lifecycle/manager_test.go b/lifecycle/manager_test.go index 0d6bef3..0bddc7c 100644 --- a/lifecycle/manager_test.go +++ b/lifecycle/manager_test.go @@ -1,10 +1,8 @@ package lifecycle_test import ( - "io/ioutil" "testing" - "github.com/Sirupsen/logrus" "github.com/boz/ephemerald/lifecycle" "github.com/boz/ephemerald/testutil" "github.com/stretchr/testify/assert" @@ -12,35 +10,39 @@ import ( ) func TestParseManager_full(t *testing.T) { - - buf, err := ioutil.ReadFile("_testdata/manager.full.json") - require.NoError(t, err) - - log := logrus.New() - m := lifecycle.NewManager(log) - - require.NoError(t, m.ParseConfig(buf)) - - cm := m.ForContainer(testutil.ContainerEmitter(), testutil.CID()) - - assert.True(t, cm.HasInitialize()) - assert.True(t, cm.HasHealthcheck()) - assert.True(t, cm.HasReset()) + ms := map[string]lifecycle.Manager{ + "json": managerFromFile(t, "manager.full.json"), + "yaml": managerFromFile(t, "manager.full.yaml"), + } + + for ext, m := range ms { + cm := m.ForContainer(testutil.ContainerEmitter(), testutil.CID()) + + assert.True(t, cm.HasInitialize(), ext) + assert.True(t, cm.HasHealthcheck(), ext) + assert.True(t, cm.HasReset(), ext) + } } func TestParseManager_partial(t *testing.T) { - buf, err := ioutil.ReadFile("_testdata/manager.partial.json") - require.NoError(t, err) + ms := map[string]lifecycle.Manager{ + "json": managerFromFile(t, "manager.partial.json"), + "yaml": managerFromFile(t, "manager.partial.yaml"), + } + + for ext, m := range ms { + cm := m.ForContainer(testutil.ContainerEmitter(), testutil.CID()) + assert.True(t, cm.HasInitialize(), ext) + assert.False(t, cm.HasHealthcheck(), ext) + assert.False(t, cm.HasReset(), ext) + } +} - log := logrus.New() +func managerFromFile(t *testing.T, fpath string) lifecycle.Manager { + buf := testutil.ReadJSON(t, fpath) + log := testutil.Log() m := lifecycle.NewManager(log) - require.NoError(t, m.ParseConfig(buf)) - - cm := m.ForContainer(testutil.ContainerEmitter(), testutil.CID()) - - assert.True(t, cm.HasInitialize()) - assert.False(t, cm.HasHealthcheck()) - assert.False(t, cm.HasReset()) + return m } diff --git a/net/_testdata/config.json b/net/_testdata/config.json deleted file mode 100644 index ffeb9eb..0000000 --- a/net/_testdata/config.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "pools": { - "redis": { - "size": 1, - "image": "redis", - "port": 6379, - "params": { - "database": "0", - "url": "redis://{{.Hostname}}:{{.Port}}/{{.Database}}" - }, - "actions": { - "healthcheck": { - "type": "redis.ping" - }, - "reset": { - "type": "redis.truncate" - } - } - } - } -} diff --git a/net/_testdata/config.yaml b/net/_testdata/config.yaml new file mode 100644 index 0000000..1f91910 --- /dev/null +++ b/net/_testdata/config.yaml @@ -0,0 +1,13 @@ +pools: + redis: + size: 1 + image: redis + port: 6379 + params: + database: "0" + url: redis://{{.Hostname}}:{{.Port}}/{{.Database}} + actions: + healthcheck: + type: redis.ping + reset: + type: redis.truncate diff --git a/net/net_test.go b/net/net_test.go index 5480d41..278af6e 100644 --- a/net/net_test.go +++ b/net/net_test.go @@ -27,7 +27,7 @@ func TestClientServer(t *testing.T) { ctx := context.Background() - configs, err := config.ReadFile(log, uie, "_testdata/config.json") + configs, err := config.ReadFile(log, uie, "_testdata/config.yaml") require.NoError(t, err) pools, err := ephemerald.NewPoolSet(log, ctx, configs) diff --git a/params/_testdata/config.params.yaml b/params/_testdata/config.params.yaml new file mode 100644 index 0000000..9641d01 --- /dev/null +++ b/params/_testdata/config.params.yaml @@ -0,0 +1,3 @@ +username: postgres +database: postgres +url: postgres://{{.Username}}:{{.Password}}@{{.Hostname}}:{{.Port}}/{{.Database}}?sslmode=disable diff --git a/params/params.go b/params/params.go index e4e71fd..31b7410 100644 --- a/params/params.go +++ b/params/params.go @@ -48,7 +48,7 @@ func (c Config) ParamsFor(id string, status types.ContainerJSON, port int) (Para p := Params{ Config: c, Id: id, - Port: tcpPortFor(status, port), + Port: TCPPortFor(status, port), } return p.ForHost(defaultHostname) } @@ -107,12 +107,14 @@ func (p Params) queryEscape() Params { } } -func tcpPortFor(status types.ContainerJSON, port int) string { - ports := tcpPortsFor(status) +// TODO: move these. + +func TCPPortFor(status types.ContainerJSON, port int) string { + ports := TCPPortsFor(status) return ports[strconv.Itoa(port)] } -func tcpPortsFor(status types.ContainerJSON) map[string]string { +func TCPPortsFor(status types.ContainerJSON) map[string]string { ports := make(map[string]string) if status.Config == nil { diff --git a/params/params_test.go b/params/params_test.go index 3d1576a..831b4ea 100644 --- a/params/params_test.go +++ b/params/params_test.go @@ -1,38 +1,40 @@ -package params +package params_test import ( "encoding/json" - "io/ioutil" - "os" "testing" + "github.com/boz/ephemerald/params" + "github.com/boz/ephemerald/testutil" "github.com/docker/docker/api/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParseConfig(t *testing.T) { - buf, err := ioutil.ReadFile("_testdata/config.params.json") - require.NoError(t, err) - cfg, err := ParseConfig(buf) - require.NoError(t, err) - - assert.Equal(t, "postgres", cfg.Username) - assert.Equal(t, "", cfg.Password) - assert.Equal(t, "postgres", cfg.Database) + bufs := map[string][]byte{ + "json": testutil.ReadJSON(t, "config.params.json"), + "yaml": testutil.ReadJSON(t, "config.params.yaml"), + } + + for ext, buf := range bufs { + cfg, err := params.ParseConfig(buf) + require.NoError(t, err, ext) + assert.Equal(t, "postgres", cfg.Username, ext) + assert.Equal(t, "", cfg.Password, ext) + assert.Equal(t, "postgres", cfg.Database, ext) + } } func TestTCPPorts(t *testing.T) { - file, err := os.Open("_testdata/inspect.postgres.json") - require.NoError(t, err) - defer file.Close() + buf := testutil.ReadJSON(t, "inspect.postgres.json") var status types.ContainerJSON - require.NoError(t, json.NewDecoder(file).Decode(&status)) + require.NoError(t, json.Unmarshal(buf, &status)) - ports := tcpPortsFor(status) + ports := params.TCPPortsFor(status) assert.Equal(t, 1, len(ports)) assert.Equal(t, "32768", ports["5432"]) diff --git a/poolset.go b/poolset.go index 6e68cd5..0e02671 100644 --- a/poolset.go +++ b/poolset.go @@ -18,6 +18,8 @@ type PoolSet interface { Stop() error } +// ugh. + type poolSet struct { pools map[string]Pool log logrus.FieldLogger diff --git a/testutil/testutil.go b/testutil/testutil.go index dff3050..dd32702 100644 --- a/testutil/testutil.go +++ b/testutil/testutil.go @@ -11,6 +11,7 @@ import ( "github.com/boz/ephemerald/config" "github.com/boz/ephemerald/params" "github.com/boz/ephemerald/ui" + "github.com/ghodss/yaml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -19,6 +20,12 @@ func CID() string { return strings.Repeat("A", 36) } +func Log() logrus.FieldLogger { + log := logrus.New() + log.Level = logrus.DebugLevel + return log +} + func Emitter() ui.Emitter { return ui.NewNoopEmitter() } @@ -47,13 +54,9 @@ func RunPoolFromFile(t *testing.T, path string, fn func(params.Params)) { func WithPoolFromFile(t *testing.T, basename string, fn func(ephemerald.Pool)) { - path := path.Join("_testdata", basename) + buf := ReadJSON(t, basename) - log := logrus.New() - log.Level = logrus.DebugLevel - - buf, err := ioutil.ReadFile(path) - require.NoError(t, err) + log := Log() config, err := config.Parse(log, Emitter(), t.Name(), buf) require.NoError(t, err) @@ -71,3 +74,13 @@ func WithPoolFromFile(t *testing.T, basename string, fn func(ephemerald.Pool)) { fn(pool) } } + +func ReadJSON(t *testing.T, fpath string) []byte { + buf, err := ioutil.ReadFile(path.Join("_testdata", fpath)) + require.NoError(t, err, fpath) + if path.Ext(fpath) == ".yaml" { + buf, err = yaml.YAMLToJSON(buf) + require.NoError(t, err, fpath) + } + return buf +}