diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2b8a943 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: go +sudo: required +go: +- 1.6.3 +services: +- docker +before_install: +- docker pull progrium/consul +- docker pull golang:1.6.3 +install: +- sudo service docker restart ; sleep 10 +- docker run -d -p 127.0.0.1:8500:8500 --name consul progrium/consul -server -bootstrap +- true +before_script: +- sleep 10 +- make .docker-test +script: +- make .docker-build + +deploy: + provider: releases + skip_cleanup: true + api_key: + secure: VKajzP3u6dZh15/OV7tCWmoKsblg0k/B3dy9M3fU1wIZ/A+wttMZtCaqNzI65q94MBpu+9i8Yqb7apQMGz+scYtj5VmJ6FJRiczIDZeZ6pHbVlNudsCkPbjCXhC2f3MWgTCXf4W7Mw1MHket38WKnBTt3AIozeZfIMF5Vks7qUqqPO8U/sSnFn+ByONdFZmhkJzav8R2hfjqXC515tg31pnYpEloUKohvZGen8GG34cRp0VGNRq7P919Kpo5MLSC0HrXrUqtPxpMvdagnGGNQ4P+NdP1+JULzCep80r5TDsTE16AJxUKLarc11mtLsCWtMgD58bcZiwr2zI+9eKdDMrRBRO0wAjl1VwO/8xs2rgByBMNOfkYP6mcO6KgLEN96+UjhjxMTEjaMo/Z7Kze+ikPGq4h02h843BIwp8A4QovVGnV1bwLJoVVhpnJVU0lR365EYQ1o1Cj38CWRCe7gJcy9q6OBfCYK/e5BpUDSZZ+8QVCLFXLG5u4m2ARUI6aj/7xRnmeg/XHI9753Rmv0eoDihI7WJ2hKKXHQyJOthWN4qMLJ/3noeAO7jirYyEdtP3VwgunXCALNLIDu1Mx7Hq4KE+YVmq03nMnB70MOiY079rPJl4v7O+IacA2Xxw4Tjc0/gKQSolVgSaoheY1OfVd1qaEya0IIOz71Rmc99k= + file: build/voletc-*.tgz + file_glob: true + on: + tag: true + repo: ipkg/voletc \ No newline at end of file diff --git a/Makefile b/Makefile index e3b440b..3b587d3 100644 --- a/Makefile +++ b/Makefile @@ -3,8 +3,6 @@ NAMESPACE = ipkg NAME = voletc VERSION = $(shell grep "const VERSION" version.go | cut -d "\"" -f 2) -INST_PKG_NAME = $(NAME)-$(VERSION)-$(shell go env GOOS).tgz - PROJPATH = github.com/$(NAMESPACE)/$(NAME) BRANCH = $(shell git rev-parse --abbrev-ref HEAD) @@ -31,51 +29,63 @@ export VOLETC_INSTALL clean: rm -rf ./build rm -f ./coverage.out + rm -rf ./testrun go clean -i ./... - rm -f $(NAME)-installer -.PHONY: test -test: clean - docker run -d -p 127.0.0.1:8500:8500 --name consul progrium/consul -server -bootstrap - @sleep 3; - go test -coverprofile=coverage.out ./...; docker rm -f consul +# docker run -d -p 127.0.0.1:8500:8500 --name consul progrium/consul -server -bootstrap +.PHONY: test +test: + go test -coverprofile=coverage.out ./... .PHONY: deps deps: go get -d -v ./... -build: clean voletc.conf install.sh - [ -d ./build ] || mkdir ./build - CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags="-X main.branch=${BRANCH} -X main.commit=${COMMIT} -X main.buildtime=${BUILDTIME} -w" . - mv ${NAME} ./build/ +.linux-build: voletc.conf + GOOS=linux CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags="-X main.branch=${BRANCH} -X main.commit=${COMMIT} -X main.buildtime=${BUILDTIME} -w" -o ./build/linux/$(NAME) . -.docker-build: - docker run --rm -v $(shell pwd):/go/src/${PROJPATH} -w /go/src/${PROJPATH} golang:1.6.3 make clean deps build +.darwin-build: + if [ -e ./build/darwin ]; then rm -rf ./build/darwin; fi + mkdir -p ./build/darwin -# Assemble image -.docker-image: - docker build --no-cache -t $(NAMESPACE)/$(NAME):$(VERSION) . - -# Complete docker build -.PHONY: docker -docker: .docker-build .docker-image + GOOS=darwin CGO_ENABLED=0 go build -a -tags netgo -installsuffix netgo -ldflags="-X main.branch=${BRANCH} -X main.commit=${COMMIT} -X main.buildtime=${BUILDTIME} -w" -o ./build/darwin/$(NAME) . .PHONY: install.sh install.sh: - [ -d ./build ] || mkdir ./build - cd ./build && echo $${VOLETC_INSTALL} > install.sh - chmod +x ./build/install.sh + cd ./build/linux && echo $${VOLETC_INSTALL} > install.sh + chmod +x ./build/linux/install.sh + cp ./build/linux/install.sh ./build/darwin/ .PHONY: voletc.conf voletc.conf: - [ -d ./build ] || mkdir ./build - cd ./build && echo $${VOLETC_STARTUP} > voletc.conf + rm -rf ./build/linux + mkdir -p ./build/linux + cd ./build/linux && echo $${VOLETC_STARTUP} > voletc.conf +# Should be run after make all .PHONY: installer -installer: build - sea ./build/ $(NAME)-installer voletc ./install.sh - tar -czvf $(INST_PKG_NAME) $(NAME)-installer - mv $(NAME)-installer ./build/ - mv $(INST_PKG_NAME) ./build/ - \ No newline at end of file +installer: + sea ./build/linux/ $(NAME)-installer voletc ./install.sh + mv $(NAME)-installer ./build/linux + cd ./build/linux && tar -czvf $(NAME)-$(VERSION)-linux.tgz $(NAME)-installer && mv $(NAME)-$(VERSION)-linux.tgz ../ + + sea ./build/darwin/ $(NAME)-installer voletc ./install.sh + mv $(NAME)-installer ./build/darwin + cd ./build/darwin && tar -czvf $(NAME)-$(VERSION)-darwin.tgz $(NAME)-installer && mv $(NAME)-$(VERSION)-darwin.tgz ../ + +all: .darwin-build .linux-build install.sh + +.docker-test: + docker run --link consul:consul --rm -v $(shell pwd):/go/src/${PROJPATH} -w /go/src/${PROJPATH} golang:1.6.3 make clean deps test + +.docker-build: + docker run --rm -v $(shell pwd):/go/src/${PROJPATH} -w /go/src/${PROJPATH} golang:1.6.3 make clean deps all + +# Assemble image +.docker-image: + docker build --no-cache -t $(NAMESPACE)/$(NAME):$(VERSION) . + +# Complete docker build +.PHONY: docker +docker: .docker-build .docker-image \ No newline at end of file diff --git a/README.md b/README.md index 94668e8..51cc071 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# voletc +# voletc [![Build Status](https://travis-ci.org/ipkg/voletc.svg?branch=master)](https://travis-ci.org/ipkg/voletc) [![Release](https://img.shields.io/github/release/ipkg/voletc.svg)](https://github.com/ipkg/voletc/releases) + voletc (pronounced vol etc) is a Docker Volume Plugin that allows to create volumes containing application configurations that can be accessed on any of your docker nodes. Once created the application no longer needs to worry about obtaining application specific configurations. The mounted volume will contain all of config file/s based on the templates and keys you've specified during volume creation. diff --git a/appconfig.go b/appconfig.go index 63c60ea..6d69f0e 100644 --- a/appconfig.go +++ b/appconfig.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io/ioutil" - "log" "strings" ) @@ -66,7 +65,7 @@ func (ac *AppConfig) AddTemplate(t *Template) error { // add template keys for k, _ := range keys { if _, ok := ac.Keys[k]; !ok { - ac.Keys[k] = []byte{} + ac.Keys[k] = nil } } } @@ -74,8 +73,13 @@ func (ac *AppConfig) AddTemplate(t *Template) error { return nil } -func (c *AppConfig) HasKeys() bool { - return len(c.Keys) > 0 +func (c *AppConfig) HasMappedKeys() bool { + for _, v := range c.Keys { + if v != nil { + return true + } + } + return false } func (c *AppConfig) Metadata() map[string]interface{} { @@ -115,10 +119,7 @@ func (a *AppConfig) Commit() error { func (a *AppConfig) Generate(basedir string) error { err := a.Load() if err == nil { - keys := a.Keys.ToString() - log.Println("Keys from store:", keys) - for _, t := range a.Templates { rendered, err := t.Render(keys) if err == nil { @@ -146,6 +147,7 @@ func (a *AppConfig) Set(data map[string][]byte) error { k := strings.TrimPrefix(key, a.getOpaque("")) switch { + case strings.HasPrefix(k, "templates"): if t := NewTemplateFromKey(k); t != nil { t.SetBody(v) @@ -157,6 +159,7 @@ func (a *AppConfig) Set(data map[string][]byte) error { } } + return nil } diff --git a/cli.go b/cli.go index c0a87fa..8d8e763 100644 --- a/cli.go +++ b/cli.go @@ -6,6 +6,8 @@ import ( "fmt" //"io/ioutil" //"log" + "bufio" + "os" //"path/filepath" "strings" ) @@ -20,15 +22,9 @@ var ( dataPrefix = flag.String("prefix", driverName, "Path prefix to store data under") backendUri = flag.String("uri", defaultConsulUri, "Backend uri") listenAddr = flag.String("b", "127.0.0.1:8989", "Service bind address") - //showVersion = flag.Bool("version", false, "Show version") - - // Use for template operations - //tfile = flag.String("t", "", "Template file") - //appName = flag.String("a", "", "Name of app ( -- )") - //render = flag.Bool("r", false, "Render template") - //commitConf = flag.Bool("commit", false, "Commit app config to backend") - - dryrun = false + // Set when cli is parsed + dryrun = false + answerYes = new(bool) ) type cli struct { @@ -74,6 +70,59 @@ func (c *cli) Run(args []string) (bool, error) { } } + case "rm": + if len(args) < 2 || args[1] == "" { + err = errInvalidConfName + break + } + + var vol *AppConfig + if vol, err = c.ve.Get(args[1]); err == nil { + printDataStructue(vol) + parseCliKeyValues(args[2:]) + + if !*answerYes { + reader := bufio.NewReader(os.Stdin) + fmt.Printf("Are you sure you want to destroy '%s' [y/n]? : ", vol.QualifiedName()) + ans, _ := reader.ReadString('\n') + ans = strings.TrimSuffix(ans, "\n") + + if strings.ToLower(ans) != "y" && strings.ToLower(ans) != "yes" { + break + } + } + + fmt.Printf("Destroying volume (%s)...\n", vol.QualifiedName()) + err = vol.Destroy() + + } + + case "edit": + if len(args) < 2 || args[1] == "" { + err = errInvalidConfName + break + } + + var vol *AppConfig + if vol, err = c.ve.Get(args[1]); err == nil { + + if len(args[2:]) < 1 { + err = fmt.Errorf("no data provided") + break + } + + ckvs := parseCliKeyValues(args[2:]) + var reqOpts map[string][]byte + if reqOpts, err = parseCreateReqOptions(ckvs); err == nil { + vol.Set(reqOpts) + if !dryrun { + err = vol.Commit() + } + printDataStructue(vol) + } + + } + case "create": if len(args) < 2 || args[1] == "" { err = errInvalidConfName @@ -136,9 +185,14 @@ func parseCliKeyValues(arr []string) map[string]string { for _, s := range arr { // Treat keys starting with - specially. if strings.HasPrefix(s, "-") { - if strings.HasSuffix(s, "-dryrun") { + switch { + case strings.HasSuffix(s, "-dryrun"): dryrun = true + + case strings.HasSuffix(s, "-y"): + *answerYes = true } + continue } @@ -157,8 +211,7 @@ func printDataStructue(v interface{}) { fmt.Printf("%s\n", b) } -func printUsage() { - fmt.Println(` +var usageHeader = ` Usage: voletc [options] [name] [key=value] [key=value] @@ -169,18 +222,24 @@ Commands: ls List volumes create Create new volume + edit Edit volume configurations info Show volume info + rm Destroy volume i.e. remove all keys render Show rendered volume templates version Show version Options: -`) - flag.PrintDefaults() - fmt.Println(`Rules: +` + +var usageFooter = `Rules: - Volume names: -- - Template keys: template:= - File paths must begin with '/' or './' in order to be recognized. -`) - //os.Exit(0) +` + +func printUsage() { + fmt.Println(usageHeader) + flag.PrintDefaults() + fmt.Println(usageFooter) } diff --git a/cli_test.go b/cli_test.go index be55a90..60a60f1 100644 --- a/cli_test.go +++ b/cli_test.go @@ -11,7 +11,7 @@ func Test_cli(t *testing.T) { t.Fatal(err) } - printUsage() + //printUsage() if _, err := cl.Run([]string{"create", "test2-0.1.0-dev", "db/name=dbname", "template:config.json=./testdata/config.json"}); err != nil { @@ -23,18 +23,36 @@ func Test_cli(t *testing.T) { t.Fatal(err) } if _, err := cl.Run([]string{"info", "test3-0.1.0-dev"}); err == nil { - t.Fatal("info should fail") + t.Log("should fail") + t.Fail() } if _, err := cl.Run([]string{"info", "test2-0.1.0-dev"}); err != nil { - t.Fatal(err) + t.Log(err) + t.Fail() } if _, err := cl.Run([]string{"render", "test2-0.1.0-dev"}); err != nil { - t.Fatal(err) + t.Log(err) + t.Fail() } if _, err = cl.Run([]string{"ls"}); err != nil { + t.Log(err) + t.Fail() + } + + if _, err := cl.Run([]string{"edit", "test2-0.1.0-dev", + "db/username=dbuser,db/password=dbpasswd"}); err != nil { + t.Log(err) + t.Fail() + } + if _, err := cl.Run([]string{"edit"}); err == nil { + t.Log("should fail") + t.Fail() + } + + if _, err := cl.Run([]string{"rm", "test2-0.1.0-dev", "-y"}); err != nil { t.Fatal(err) } diff --git a/docs/CLI.md b/docs/CLI.md new file mode 100644 index 0000000..963f7cd --- /dev/null +++ b/docs/CLI.md @@ -0,0 +1,23 @@ +# Command Line + + +Volume names must be in the format of `--`. + +- **uri**: URI to backend +- **dir**: Data directory to store under +- **prefix**: Key and path prefix for backend and folder structure + +### Create volume + + voletc create template:= = [-dryrun] + +- **-dryrun**: Do not actually create the volume + +### Volume information + + voletc info + +### List volumes + + voletc ls + diff --git a/driver.go b/driver.go index a083a62..5103d2f 100644 --- a/driver.go +++ b/driver.go @@ -67,6 +67,12 @@ func NewVolumeDriver(cfg *DriverConfig) (*MyVolumeDriver, error) { func (m *MyVolumeDriver) Create(req volume.Request) volume.Response { log.Printf("[Create] Request: %+v\n", req) // Create kv structure on backend. + + _, err := m.ve.Get(req.Name) + if err == nil { + return volume.Response{Err: "exists: " + req.Name} + } + c, err := NewAppConfigFromName(req.Name, m.be) if err != nil { return volume.Response{Err: err.Error()} @@ -177,9 +183,12 @@ func (m *MyVolumeDriver) Mount(req volume.MountRequest) volume.Response { } dpath := m.cfg.MountBaseDir + c.getOpaque(c.Env) - os.MkdirAll(dpath, 0777) - if err = c.Generate(dpath); err != nil { + if err = os.MkdirAll(dpath, 0777); err == nil { + err = c.Generate(dpath) + } + + if err != nil { return volume.Response{Err: err.Error()} } diff --git a/driver_test.go b/driver_test.go index 35260dc..7385d32 100644 --- a/driver_test.go +++ b/driver_test.go @@ -15,11 +15,11 @@ var ( testAppCfg = &AppConfig{Name: "test", Version: "0.1.1", Env: "dev"} testName = testAppCfg.QualifiedName() - testConsulUri = defaultConsulUri + testConsulUri = "consul://consul:8500" ) func init() { - testDrvCfg = NewDriverConfig(testConsulUri, "/tmp/testrun", "test-driver") + testDrvCfg = NewDriverConfig(testConsulUri, "./testrun", "test-driver") var err error if testDriver, err = NewVolumeDriver(testDrvCfg); err != nil { @@ -27,7 +27,7 @@ func init() { } } -func Test_VolumeDriver_Create_Error(t *testing.T) { +func Test_VolumeDriver_Error_Create(t *testing.T) { req := volume.Request{ Name: testName, Options: map[string]string{ @@ -66,9 +66,16 @@ func Test_VolumeDriver_Create(t *testing.T) { t.Log("no templates") t.Fail() } - if len(c.Templates[0].Body) < 1 { - t.Log("no template data") - t.Fail() + + if v, ok := c.Keys["n1/k1"]; !ok || string(v) != "v1" { + t.Fatal("missing keys") + } + + if !t.Failed() { + if len(c.Templates[0].Body) < 1 { + t.Log("no template data") + t.Fail() + } } } @@ -147,10 +154,9 @@ func Test_VolumeDriver_Mount(t *testing.T) { t.Fatal(r3.Err) } - b, err := ioutil.ReadFile(testDriver.cfg.MountBaseDir + testAppCfg.getOpaque(testAppCfg.Env) + "/inline.json") + b, err := ioutil.ReadFile(testDriver.cfg.MountBaseDir + testAppCfg.getOpaque(testAppCfg.Env+"/inline.json")) if err != nil { - t.Log(err) - t.Fail() + t.Fatal(err) } if string(b) != `{"key": "v1"}` { diff --git a/main.go b/main.go index bfc2a49..873dbcb 100644 --- a/main.go +++ b/main.go @@ -26,11 +26,11 @@ func init() { log.Fatal(err) } - eor, err := cl.Run(flag.Args()) + extOnRet, err := cl.Run(flag.Args()) if err != nil { - fmt.Println(err) + fmt.Println("Volume", err) os.Exit(1) - } else if eor { + } else if extOnRet { os.Exit(0) } } diff --git a/version.go b/version.go index bee3cc1..29cddab 100644 --- a/version.go +++ b/version.go @@ -4,7 +4,7 @@ import ( "fmt" ) -const VERSION string = "0.1.5" +const VERSION string = "0.1.6" var ( branch string diff --git a/voletc.go b/voletc.go index b799399..5cc2dff 100644 --- a/voletc.go +++ b/voletc.go @@ -15,7 +15,7 @@ func (ve *VolEtc) Get(name string) (*AppConfig, error) { return nil, err } // doesn't really exist ??? - if !acfg.HasKeys() { + if !acfg.HasMappedKeys() { return nil, fmt.Errorf("not found: '%s'", name) } @@ -45,7 +45,7 @@ func (ve *VolEtc) List() (map[string]*AppConfig, error) { return nil, err } // doesn't really exist ??? - if !acfg.HasKeys() { + if !acfg.HasMappedKeys() { continue }