├── .gitignore ├── version.go ├── demo ├── share │ ├── config │ │ ├── template │ │ │ ├── client │ │ │ │ ├── service_ntp.json │ │ │ │ ├── check_loadavg.json │ │ │ │ └── agent_client.json │ │ │ └── server │ │ │ │ ├── service_ntp.json │ │ │ │ ├── check_loadavg.json │ │ │ │ ├── service_apache2.json │ │ │ │ └── agent_server.json │ │ ├── check │ │ │ └── loadavg.json │ │ └── service │ │ │ ├── ntp.json │ │ │ └── apache2.json │ └── bin │ │ ├── ntp │ │ ├── apache2 │ │ └── loadavg └── Dockerfile ├── fileconsul.go ├── command.go ├── LICENSE ├── fileconsul ├── consul_test.go ├── localfile_test.go ├── consul.go ├── remotefile.go ├── localfile.go └── remotefile_test.go ├── command ├── push_command.go ├── pull_command.go └── status_command.go └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .vagrant/* 4 | pkg/* 5 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | const Version string = "0.1.1" 4 | -------------------------------------------------------------------------------- /demo/share/config/template/client/service_ntp.json: -------------------------------------------------------------------------------- 1 | ../../service/ntp.json -------------------------------------------------------------------------------- /demo/share/config/template/server/service_ntp.json: -------------------------------------------------------------------------------- 1 | ../../service/ntp.json -------------------------------------------------------------------------------- /demo/share/config/template/client/check_loadavg.json: -------------------------------------------------------------------------------- 1 | ../../check/loadavg.json -------------------------------------------------------------------------------- /demo/share/config/template/server/check_loadavg.json: -------------------------------------------------------------------------------- 1 | ../../check/loadavg.json -------------------------------------------------------------------------------- /demo/share/config/template/server/service_apache2.json: -------------------------------------------------------------------------------- 1 | ../../service/apache2.json -------------------------------------------------------------------------------- /demo/share/bin/ntp: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | N=`ps aux | grep /usr/sbin/ntpd | grep -v grep | wc -l` 3 | if [ $N = 0 ]; then 4 | exit 1 5 | fi 6 | exit 0 7 | -------------------------------------------------------------------------------- /demo/share/bin/apache2: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | N=`ps aux | grep /usr/sbin/apache2 | grep -v grep | wc -l` 3 | if [ $N = 0 ]; then 4 | exit 1 5 | fi 6 | exit 0 7 | -------------------------------------------------------------------------------- /demo/share/bin/loadavg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | loadavg=`uptime | awk '{print $10}' | awk -F '.' '{print $1}'` 3 | if [ $(( loadavg )) -gt 0 ]; then 4 | exit 1 5 | fi 6 | exit 0 7 | -------------------------------------------------------------------------------- /demo/share/config/check/loadavg.json: -------------------------------------------------------------------------------- 1 | { 2 | "check": { 3 | "id": "loadavg", 4 | "name": "Check load average", 5 | "script": "/consul/share/bin/loadavg", 6 | "interval": "10s" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /demo/share/config/service/ntp.json: -------------------------------------------------------------------------------- 1 | { 2 | "service": { 3 | "name": "ntp", 4 | "tags": [], 5 | "check": { 6 | "script": "/consul/share/bin/ntp", 7 | "interval": "10s" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/share/config/service/apache2.json: -------------------------------------------------------------------------------- 1 | { 2 | "service": { 3 | "name": "apache2", 4 | "tags": [], 5 | "check": { 6 | "script": "/consul/share/bin/apache2", 7 | "interval": "10s" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /demo/share/config/template/client/agent_client.json: -------------------------------------------------------------------------------- 1 | { 2 | "datacenter": "dc1", 3 | "data_dir": "/tmp/consul", 4 | "watches": [ 5 | { 6 | "type": "keyprefix", 7 | "prefix": "fileconsul/", 8 | "handler": "fileconsul pull --basepath /consul/share" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /demo/share/config/template/server/agent_server.json: -------------------------------------------------------------------------------- 1 | { 2 | "datacenter": "dc1", 3 | "data_dir": "/tmp/consul", 4 | "server": true, 5 | "ui_dir": "/consul/ui", 6 | "watches": [ 7 | { 8 | "type": "keyprefix", 9 | "prefix": "fileconsul/", 10 | "handler": "fileconsul pull --basepath /consul/share" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /fileconsul.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/codegangsta/cli" 7 | ) 8 | 9 | func main() { 10 | app := cli.NewApp() 11 | app.Name = "fileconsul" 12 | app.Version = Version 13 | app.Usage = "Sharing files in a consul cluster." 14 | app.Author = "foostan" 15 | app.Email = "ks@fstn.jp" 16 | app.Commands = Commands 17 | 18 | app.Run(os.Args) 19 | } 20 | -------------------------------------------------------------------------------- /demo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:trusty 2 | 3 | MAINTAINER foostan ks@fstn.jp 4 | 5 | RUN apt-get update 6 | RUN apt-get install -y unzip wget curl 7 | RUN apt-get install -y apache2 ntp # install sample packages 8 | 9 | RUN mkdir /consul 10 | 11 | RUN cd /consul && \ 12 | wget https://dl.bintray.com/mitchellh/consul/0.4.0_linux_amd64.zip && \ 13 | unzip 0.4.0_linux_amd64.zip && \ 14 | mv consul /usr/local/bin 15 | 16 | RUN cd /consul && \ 17 | wget https://dl.bintray.com/mitchellh/consul/0.4.0_web_ui.zip && \ 18 | unzip 0.4.0_web_ui.zip && \ 19 | mv dist ui 20 | 21 | RUN cd /consul && \ 22 | wget https://dl.bintray.com/mitchellh/consul/0.4.0_linux_amd64.zip && \ 23 | wget http://dl.bintray.com/foostan/fileconsul/0.1.1_linux_amd64.zip && \ 24 | unzip 0.1.1_linux_amd64.zip && \ 25 | mv fileconsul /usr/local/bin 26 | 27 | ADD share /consul/share 28 | 29 | CMD ["/bin/bash"] 30 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | 6 | "github.com/foostan/fileconsul/command" 7 | ) 8 | 9 | var Commands = []cli.Command{ 10 | cli.Command{ 11 | Name: "status", 12 | Usage: "Show status of local files", 13 | Description: "Show the difference between local files and remote files that is stored in K/V Store of a consul cluster.", 14 | Flags: command.StatusFlags, 15 | Action: command.StatusCommand, 16 | }, 17 | cli.Command{ 18 | Name: "pull", 19 | Usage: "Pull files from a consul cluster", 20 | Description: "Pull remote files from K/V Store of a consul cluster.", 21 | Flags: command.PullFlags, 22 | Action: command.PullCommand, 23 | }, 24 | cli.Command{ 25 | Name: "push", 26 | Usage: "Push file to a consul cluster", 27 | Description: "Push remote files to K/V Store of a consul cluster.", 28 | Flags: command.PushFlags, 29 | Action: command.PushCommand, 30 | }, 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kosuke Adachi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /fileconsul/consul_test.go: -------------------------------------------------------------------------------- 1 | package fileconsul 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestConstructClient(t *testing.T) { 8 | _, err := NewClient(&ClientConfig{ 9 | ConsulAddr: "localhost:8500", 10 | ConsulDC: "dc1", 11 | }) 12 | if err != nil { 13 | t.Skipf("err: %v", err) 14 | } 15 | } 16 | 17 | func TestPutGetDeleteKV(t *testing.T) { 18 | client, err := NewClient(&ClientConfig{ 19 | ConsulAddr: "localhost:8500", 20 | ConsulDC: "dc1", 21 | }) 22 | if err != nil { 23 | t.Skipf("err: %v", err) 24 | } 25 | 26 | err = client.PutKV("foo/bar/bazz", []byte("123")) 27 | if err != nil { 28 | t.Skipf("err: %v", err) 29 | } 30 | 31 | _, err = client.GetKV("foo/bar/bazz") 32 | if err != nil { 33 | t.Skipf("err: %v", err) 34 | } 35 | 36 | _, err = client.ListKV("foo") 37 | if err != nil { 38 | t.Skipf("err: %v", err) 39 | } 40 | 41 | err = client.DeleteKV("foo/bar/bazz") 42 | if err != nil { 43 | t.Skipf("err: %v", err) 44 | } 45 | } 46 | 47 | func TestConsulAgentInfo(t *testing.T) { 48 | client, err := NewClient(&ClientConfig{ 49 | ConsulAddr: "localhost:8500", 50 | ConsulDC: "dc1", 51 | }) 52 | if err != nil { 53 | t.Skipf("err: %v", err) 54 | } 55 | 56 | _, err = client.ConsulAgentInfo() 57 | if err != nil { 58 | t.Skipf("err: %v", err) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /fileconsul/localfile_test.go: -------------------------------------------------------------------------------- 1 | package fileconsul 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestReadLFList(t *testing.T) { 8 | _, err := ReadLFList("..") 9 | if err != nil { 10 | t.Errorf("err: %v", err) 11 | } 12 | } 13 | 14 | func TestToRFList(t *testing.T) { 15 | lfList := LFList{ 16 | Localfile{Base: "/path/to/base", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 17 | Localfile{Base: "/path/to/base", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}, 18 | } 19 | 20 | ansRFList := RFList{ 21 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 22 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}, 23 | } 24 | 25 | rfList := lfList.ToRFList("fileconsul") 26 | 27 | if !rfList.Equal(ansRFList) { 28 | t.Fatalf("expected result is %s, but %s", ansRFList, rfList) 29 | } 30 | } 31 | 32 | func TestSaveRemove(t *testing.T) { 33 | lfList := LFList{ 34 | Localfile{Base: ".", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 35 | Localfile{Base: ".", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}, 36 | } 37 | 38 | err := lfList.Save() 39 | if err != nil { 40 | t.Errorf("err: %v", err) 41 | } 42 | 43 | err = lfList.Remove() 44 | if err != nil { 45 | t.Errorf("err: %v", err) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /fileconsul/consul.go: -------------------------------------------------------------------------------- 1 | package fileconsul 2 | 3 | import ( 4 | "time" 5 | 6 | consulapi "github.com/armon/consul-api" 7 | ) 8 | 9 | type Client struct { 10 | ConsulClient *consulapi.Client 11 | } 12 | 13 | type ClientConfig struct { 14 | ConsulAddr string 15 | ConsulDC string 16 | Timeout time.Duration 17 | } 18 | 19 | type ConsulAgentInfo struct { 20 | Name string 21 | Port float64 22 | Addr string 23 | Status float64 24 | } 25 | 26 | func NewClient(config *ClientConfig) (*Client, error) { 27 | kvConfig := consulapi.DefaultConfig() 28 | kvConfig.Address = config.ConsulAddr 29 | kvConfig.Datacenter = config.ConsulDC 30 | 31 | consulClient, err := consulapi.NewClient(kvConfig) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return &Client{ 37 | ConsulClient: consulClient, 38 | }, nil 39 | } 40 | 41 | func (c *Client) GetKV(key string) (*consulapi.KVPair, error) { 42 | pair, _, err := c.ConsulClient.KV().Get(key, nil) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return pair, nil 48 | } 49 | 50 | func (c *Client) ListKV(prefix string) (consulapi.KVPairs, error) { 51 | pairs, _, err := c.ConsulClient.KV().List(prefix, nil) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | return pairs, nil 57 | } 58 | 59 | func (c *Client) PutKV(prefix string, value []byte) error { 60 | p := &consulapi.KVPair{Key: prefix, Value: value} 61 | _, err := c.ConsulClient.KV().Put(p, nil) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (c *Client) DeleteKV(prefix string) error { 70 | _, err := c.ConsulClient.KV().DeleteTree(prefix, nil) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (c *Client) ConsulAgentInfo() (*ConsulAgentInfo, error) { 79 | info, err := c.ConsulClient.Agent().Self() 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | return &ConsulAgentInfo{ 85 | Name: info["Member"]["Name"].(string), 86 | Port: info["Member"]["Port"].(float64), 87 | Addr: info["Member"]["Addr"].(string), 88 | Status: info["Member"]["Status"].(float64), 89 | }, nil 90 | } 91 | -------------------------------------------------------------------------------- /command/push_command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "log" 5 | "fmt" 6 | "path/filepath" 7 | 8 | "github.com/codegangsta/cli" 9 | 10 | . "github.com/foostan/fileconsul/fileconsul" 11 | ) 12 | 13 | var PushFlags = []cli.Flag{ 14 | cli.StringFlag{ 15 | Name: "addr", 16 | Value: "localhost:8500", 17 | Usage: "consul HTTP API address with port", 18 | }, 19 | cli.StringFlag{ 20 | Name: "dc", 21 | Value: "dc1", 22 | Usage: "consul datacenter, uses local if blank", 23 | }, 24 | cli.StringFlag{ 25 | Name: "prefix", 26 | Value: "fileconsul", 27 | Usage: "reading file status from Consul's K/V store with the given prefix", 28 | }, 29 | cli.StringFlag{ 30 | Name: "basepath", 31 | Value: ".", 32 | Usage: "base directory path of target files", 33 | }, 34 | } 35 | 36 | func PushCommand(c *cli.Context) { 37 | addr := c.String("addr") 38 | dc := c.String("dc") 39 | prefix := c.String("prefix") 40 | basepath := c.String("basepath") 41 | 42 | client, err := NewClient(&ClientConfig{ 43 | ConsulAddr: addr, 44 | ConsulDC: dc, 45 | }) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | lfList, err := ReadLFList(basepath) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | rfList, err := client.ReadRFList(prefix) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | lfrfList := lfList.ToRFList(prefix) 61 | rfDiff := lfrfList.Diff(rfList) 62 | 63 | for _, remotefile := range rfDiff.Add { 64 | fmt.Println("add remote file:\t" + filepath.Join(basepath, remotefile.Path)) 65 | err = client.PutKV(filepath.Join(prefix, remotefile.Path), remotefile.Data) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | } 70 | for _, remotefile := range rfDiff.New { 71 | fmt.Println("modify remote file:\t" + filepath.Join(basepath, remotefile.Path)) 72 | err = client.PutKV(filepath.Join(prefix, remotefile.Path), remotefile.Data) 73 | if err != nil { 74 | log.Fatal(err) 75 | } 76 | } 77 | for _, remotefile := range rfDiff.Del { 78 | fmt.Println("delete remote file:\t" + filepath.Join(basepath, remotefile.Path)) 79 | err = client.DeleteKV(filepath.Join(prefix, remotefile.Path)) 80 | if err != nil { 81 | log.Fatal(err) 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /command/pull_command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path/filepath" 7 | 8 | "github.com/codegangsta/cli" 9 | 10 | . "github.com/foostan/fileconsul/fileconsul" 11 | ) 12 | 13 | var PullFlags = []cli.Flag{ 14 | cli.StringFlag{ 15 | Name: "addr", 16 | Value: "localhost:8500", 17 | Usage: "consul HTTP API address with port", 18 | }, 19 | cli.StringFlag{ 20 | Name: "dc", 21 | Value: "dc1", 22 | Usage: "consul datacenter, uses local if blank", 23 | }, 24 | cli.StringFlag{ 25 | Name: "prefix", 26 | Value: "fileconsul", 27 | Usage: "reading file status from Consul's K/V store with the given prefix", 28 | }, 29 | cli.StringFlag{ 30 | Name: "basepath", 31 | Value: ".", 32 | Usage: "base directory path of target files", 33 | }, 34 | } 35 | 36 | func PullCommand(c *cli.Context) { 37 | addr := c.String("addr") 38 | dc := c.String("dc") 39 | prefix := c.String("prefix") 40 | basepath := c.String("basepath") 41 | 42 | client, err := NewClient(&ClientConfig{ 43 | ConsulAddr: addr, 44 | ConsulDC: dc, 45 | }) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | lfList, err := ReadLFList(basepath) 51 | if err != nil { 52 | log.Fatal(err) 53 | } 54 | 55 | rfList, err := client.ReadRFList(prefix) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | 60 | lfrfList := lfList.ToRFList(prefix) 61 | rfDiff := rfList.Diff(lfrfList) 62 | 63 | switch { 64 | case rfList.Empty(): 65 | fmt.Println("There are no remote files. Skip synchronizing.") 66 | case lfrfList.Equal(rfList): 67 | fmt.Println("Already up-to-date.") 68 | default: 69 | fmt.Println("Synchronize remote files:") 70 | 71 | for _, remotefile := range rfDiff.Add { 72 | localfile := remotefile.ToLocalfile(basepath) 73 | err := localfile.Save() 74 | if err != nil { 75 | log.Fatal(err) 76 | } 77 | 78 | fmt.Println("\tadd local file:\t" + filepath.Join(basepath, remotefile.Path)) 79 | } 80 | 81 | for _, remotefile := range rfDiff.New { 82 | localfile := remotefile.ToLocalfile(basepath) 83 | err := localfile.Save() 84 | if err != nil { 85 | log.Fatal(err) 86 | } 87 | 88 | fmt.Println("\tmodify local file:\t" + filepath.Join(basepath, remotefile.Path)) 89 | } 90 | 91 | for _, remotefile := range rfDiff.Del { 92 | localfile := remotefile.ToLocalfile(basepath) 93 | err := localfile.Remove() 94 | if err != nil { 95 | log.Fatal(err) 96 | } 97 | 98 | fmt.Println("\tdelete local file:\t" + filepath.Join(basepath, remotefile.Path)) 99 | } 100 | 101 | fmt.Println("Already up-to-date.") 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /command/status_command.go: -------------------------------------------------------------------------------- 1 | package command 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path/filepath" 7 | 8 | "github.com/codegangsta/cli" 9 | 10 | . "github.com/foostan/fileconsul/fileconsul" 11 | ) 12 | 13 | var StatusFlags = []cli.Flag{ 14 | cli.StringFlag{ 15 | Name: "addr", 16 | Value: "localhost:8500", 17 | Usage: "consul HTTP API address with port", 18 | }, 19 | cli.StringFlag{ 20 | Name: "dc", 21 | Value: "dc1", 22 | Usage: "consul datacenter, uses local if blank", 23 | }, 24 | cli.StringFlag{ 25 | Name: "prefix", 26 | Value: "fileconsul", 27 | Usage: "reading file status from Consul's K/V store with the given prefix", 28 | }, 29 | cli.StringFlag{ 30 | Name: "basepath", 31 | Value: ".", 32 | Usage: "base directory path of target files", 33 | }, 34 | } 35 | 36 | func StatusCommand(c *cli.Context) { 37 | args := c.Args() 38 | pattern := args.First() 39 | 40 | addr := c.String("addr") 41 | dc := c.String("dc") 42 | prefix := c.String("prefix") 43 | basepath := c.String("basepath") 44 | 45 | client, err := NewClient(&ClientConfig{ 46 | ConsulAddr: addr, 47 | ConsulDC: dc, 48 | }) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | lfList, err := ReadLFList(basepath) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | rfList, err := client.ReadRFList(prefix) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | lfrfList := lfList.ToRFList(prefix) 64 | 65 | if pattern != "" { 66 | rfList, err = rfList.Filter(pattern) 67 | if err != nil { 68 | log.Fatal(err) 69 | } 70 | 71 | lfrfList, err = lfrfList.Filter(pattern) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | } 76 | 77 | rfDiff := lfrfList.Diff(rfList) 78 | switch { 79 | case lfrfList.Equal(rfList): 80 | default: 81 | fmt.Println("Changes to be pushed:\n (use \"fileconsul push [command options]\" to synchronize local files)") 82 | 83 | for _, remotefile := range rfDiff.Add { 84 | println("\tadd remote file:\t" + filepath.Join(basepath, remotefile.Path)) 85 | } 86 | 87 | for _, remotefile := range rfDiff.Del { 88 | println("\tdelete remote file:\t" + filepath.Join(basepath, remotefile.Path)) 89 | } 90 | 91 | for _, remotefile := range rfDiff.New { 92 | println("\tmodify remote file:\t" + filepath.Join(basepath, remotefile.Path)) 93 | } 94 | } 95 | 96 | rfDiff = rfList.Diff(lfrfList) 97 | switch { 98 | case lfrfList.Equal(rfList): 99 | case rfList.Empty(): 100 | default: 101 | fmt.Println("Changes to be pulled:\n (use \"fileconsul pull [command options]\" to synchronize remote files)") 102 | 103 | for _, remotefile := range rfDiff.Add { 104 | println("\tadd local file:\t" + filepath.Join(basepath, remotefile.Path)) 105 | } 106 | 107 | for _, remotefile := range rfDiff.Del { 108 | println("\tdelete local file:\t" + filepath.Join(basepath, remotefile.Path)) 109 | } 110 | 111 | for _, remotefile := range rfDiff.New { 112 | println("\tmodify local file:\t" + filepath.Join(basepath, remotefile.Path)) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /fileconsul/remotefile.go: -------------------------------------------------------------------------------- 1 | package fileconsul 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "crypto/md5" 7 | "strings" 8 | "os" 9 | ) 10 | 11 | type Remotefile struct { 12 | Prefix string 13 | Path string 14 | Hash string 15 | Data []byte 16 | } 17 | 18 | type RFList []Remotefile 19 | 20 | type RFDiff struct { 21 | Add RFList 22 | Del RFList 23 | New RFList 24 | Old RFList 25 | Eq RFList 26 | } 27 | 28 | func (a *Remotefile) EqFile(b Remotefile) bool { 29 | if a.Path == b.Path { 30 | return true 31 | } 32 | 33 | return false 34 | } 35 | 36 | func (a *Remotefile) EqVer(b Remotefile) bool { 37 | if a.EqFile(b) && a.Hash == b.Hash { 38 | return true 39 | } 40 | 41 | return false 42 | } 43 | 44 | func (a *Remotefile) NeVer(b Remotefile) bool { 45 | if a.EqFile(b) && a.Hash != b.Hash { 46 | return true 47 | } 48 | 49 | return false 50 | } 51 | 52 | func (rfListA *RFList) Include(b Remotefile) (bool, Remotefile) { 53 | for _, a := range *rfListA { 54 | if a.EqFile(b) { 55 | return true, a 56 | } 57 | } 58 | return false, Remotefile{} 59 | } 60 | 61 | func (rfListA *RFList) Diff(rfListB RFList) *RFDiff { 62 | add := make([]Remotefile, 0) 63 | del := make([]Remotefile, 0) 64 | new := make([]Remotefile, 0) 65 | old := make([]Remotefile, 0) 66 | eq := make([]Remotefile, 0) 67 | 68 | for _, b := range rfListB { 69 | ok, a := rfListA.Include(b) 70 | if ok { 71 | switch { 72 | case a.EqVer(b): 73 | eq = append(eq, a) 74 | case a.NeVer(b): 75 | new = append(new, a) 76 | } 77 | } else { 78 | del = append(del, b) 79 | } 80 | } 81 | 82 | for _, a := range *rfListA { 83 | ok, b := rfListB.Include(a) 84 | if ok { 85 | switch { 86 | case b.EqVer(a): 87 | // skip 88 | case b.NeVer(a): 89 | old = append(old, b) 90 | } 91 | } else { 92 | add = append(add, a) 93 | } 94 | } 95 | 96 | return &RFDiff{ 97 | Add: add, 98 | Del: del, 99 | New: new, 100 | Old: old, 101 | Eq: eq, 102 | } 103 | } 104 | 105 | func (rfListA *RFList) Equal(rfListB RFList) bool { 106 | rfDiff := rfListA.Diff(rfListB) 107 | if len(rfDiff.Add) == 0 && 108 | len(rfDiff.Del) == 0 && 109 | len(rfDiff.New) == 0 && 110 | len(rfDiff.Old) == 0 { 111 | return true 112 | } 113 | 114 | return false 115 | } 116 | 117 | func (rfList *RFList) Empty() bool { 118 | return len(*rfList) <= 0 119 | } 120 | 121 | func (client *Client) ReadRFList(prefix string) (RFList, error) { 122 | kvpairs, err := client.ListKV(prefix) 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | rfList := make([]Remotefile, 0) 128 | for _, kvpair := range kvpairs { 129 | relPath, err := filepath.Rel(prefix, kvpair.Key) 130 | if err != nil { 131 | return nil, fmt.Errorf("Invalid path '%s': %s", kvpair.Key, err) 132 | } 133 | 134 | hash := fmt.Sprintf("%x", md5.Sum(kvpair.Value)) 135 | 136 | rfList = append(rfList, Remotefile{Prefix: prefix, Path: relPath, Hash: hash, Data: kvpair.Value}) 137 | } 138 | 139 | return rfList, nil 140 | } 141 | 142 | func (remotefile *Remotefile) ToLocalfile(base string) Localfile { 143 | return Localfile{ 144 | Base: base, 145 | Path: remotefile.Path, 146 | Hash: remotefile.Hash, 147 | Data: remotefile.Data, 148 | } 149 | } 150 | 151 | func (rfList *RFList) ToLFList(base string) LFList { 152 | lfList := make([]Localfile, 0) 153 | for _, remotefile := range *rfList { 154 | lfList = append(lfList, remotefile.ToLocalfile(base)) 155 | } 156 | return lfList 157 | } 158 | 159 | func (rfList *RFList) Filter(pattern string) (RFList, error) { 160 | newRFList := make([]Remotefile, 0) 161 | for _, remotefile := range *rfList { 162 | match, err := matchPath(pattern, remotefile.Path) 163 | if err != nil { 164 | return nil, err 165 | } 166 | if match { 167 | newRFList = append(newRFList, remotefile) 168 | } 169 | } 170 | 171 | return newRFList, nil 172 | } 173 | 174 | func matchPath(pattern string, path string) (bool, error) { 175 | patternList := strings.Split(pattern, string(os.PathSeparator)) 176 | pathList := strings.Split(path, string(os.PathSeparator)) 177 | if len(patternList) > len(pathList) { 178 | return false, nil 179 | } 180 | 181 | for i := 0; i < len(patternList); i++ { 182 | match, err := filepath.Match(patternList[i], pathList[i]) 183 | if err != nil { 184 | return false, err 185 | } 186 | if !match { 187 | return false, nil 188 | } 189 | } 190 | 191 | return true, nil 192 | } 193 | -------------------------------------------------------------------------------- /fileconsul/localfile.go: -------------------------------------------------------------------------------- 1 | package fileconsul 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | type Localfile struct { 14 | Base string 15 | Path string 16 | Hash string 17 | Data []byte 18 | } 19 | 20 | type LFList []Localfile 21 | 22 | func ReadLFList(basepath string) (LFList, error) { 23 | lfList := make([]Localfile, 0) 24 | searchPaths := []string{basepath} 25 | 26 | for len(searchPaths) > 0 { 27 | path := searchPaths[len(searchPaths)-1] 28 | searchPaths = searchPaths[:len(searchPaths)-1] 29 | 30 | f, err := os.Open(path) 31 | if err != nil { 32 | return nil, fmt.Errorf("Error reading '%s': %s", path, err) 33 | } 34 | 35 | fi, err := f.Stat() 36 | if err != nil { 37 | f.Close() 38 | return nil, fmt.Errorf("Error reading '%s': %s", path, err) 39 | } 40 | 41 | if fi.IsDir() { 42 | contents, err := f.Readdir(-1) 43 | if err != nil { 44 | return nil, fmt.Errorf("Error reading '%s': %s", path, err) 45 | } 46 | 47 | for _, fi := range contents { 48 | subpath := filepath.Join(path, fi.Name()) 49 | searchPaths = append(searchPaths, subpath) 50 | } 51 | } else { 52 | data := make([]byte, fi.Size()) 53 | _, err := f.Read(data) 54 | if err != nil { 55 | return nil, fmt.Errorf("Error reading '%s': %s", path, err) 56 | } 57 | 58 | relPath, err := filepath.Rel(basepath, path) 59 | if err != nil { 60 | return nil, fmt.Errorf("Invalid path '%s': %s", path, err) 61 | } 62 | 63 | hash := fmt.Sprintf("%x", md5.Sum(data)) 64 | 65 | lfList = append(lfList, Localfile{Base: basepath, Path: relPath, Hash: hash, Data: data}) 66 | } 67 | 68 | f.Close() 69 | } 70 | 71 | return lfList, nil 72 | } 73 | 74 | func (localfile *Localfile) ToRemotefile(prefix string) Remotefile { 75 | return Remotefile{ 76 | Prefix: prefix, 77 | Path: localfile.Path, 78 | Hash: localfile.Hash, 79 | Data: localfile.Data, 80 | } 81 | } 82 | 83 | func (lfList *LFList) ToRFList(prefix string) RFList { 84 | rfList := make([]Remotefile, 0) 85 | for _, localfile := range *lfList { 86 | rfList = append(rfList, localfile.ToRemotefile(prefix)) 87 | } 88 | return rfList 89 | } 90 | 91 | func (localfile *Localfile) Save() error { 92 | // temporally creating 93 | tmpfile, err := randstr(32) 94 | if err != nil { 95 | return fmt.Errorf("Error while generating rand string : %s", err) 96 | } 97 | err = ioutil.WriteFile(tmpfile, localfile.Data, os.FileMode(0644)) 98 | if err != nil { 99 | return fmt.Errorf("Error while creating tmpfile '%s' : %s", tmpfile, err) 100 | } 101 | 102 | // atomically moving 103 | path := filepath.Join(localfile.Base, localfile.Path) 104 | err = os.MkdirAll(filepath.Dir(path), os.FileMode(0755)) 105 | if err != nil { 106 | return fmt.Errorf("Error while creating '%s' : %s", path, err) 107 | } 108 | 109 | err = os.Rename(tmpfile, path) 110 | if err != nil { 111 | return fmt.Errorf("Error while moving '%s' to '%s' : %s", tmpfile, path, err) 112 | } 113 | 114 | defer os.RemoveAll(filepath.Join(localfile.Base, tmpfile)) 115 | 116 | return nil 117 | } 118 | 119 | func (lfList *LFList) Save() error { 120 | for _, localfile := range *lfList { 121 | err := localfile.Save() 122 | if err != nil { 123 | return err 124 | } 125 | } 126 | 127 | return nil 128 | } 129 | 130 | func (localfile *Localfile) Remove() error { 131 | path := filepath.Join(localfile.Base, localfile.Path) 132 | err := os.RemoveAll(path) 133 | if err != nil { 134 | return fmt.Errorf("Error while removing '%s' : %s", path, err) 135 | } 136 | 137 | err = RemoveAllEmpDir(filepath.Dir(path)) 138 | if err != nil { 139 | return fmt.Errorf("Error while removing '%s' : %s", path, err) 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (lfList *LFList) Remove() error { 146 | for _, localfile := range *lfList { 147 | err := localfile.Remove() 148 | if err != nil { 149 | return nil 150 | } 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func RemoveAllEmpDir(path string) error { 157 | files, err := ioutil.ReadDir(path) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | if len(files) == 0 { 163 | err := os.Remove(path) 164 | if err != nil { 165 | return fmt.Errorf("Error while removing '%s' : %s", path, err) 166 | } 167 | 168 | return RemoveAllEmpDir(filepath.Dir(path)) 169 | } 170 | 171 | return nil 172 | } 173 | 174 | func randstr(size int) (string, error) { 175 | rb := make([]byte, size) 176 | _, err := rand.Read(rb) 177 | if err != nil { 178 | return "", err 179 | } 180 | 181 | return base64.URLEncoding.EncodeToString(rb), nil 182 | } 183 | -------------------------------------------------------------------------------- /fileconsul/remotefile_test.go: -------------------------------------------------------------------------------- 1 | package fileconsul 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestDiff(t *testing.T) { 8 | NewRFList := RFList{ 9 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 10 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}, 11 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample3", Hash: "d35f70211135de265bc7c66df4dd3605", Data: []byte("sample3")}, 12 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample4", Hash: "247f4201f214ff279da3a24570d642f1", Data: []byte("sample4")}, 13 | } 14 | 15 | OldRFList := RFList{ 16 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 17 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 18 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample5", Hash: "828974c6c954abd2ada226a48c7d6090", Data: []byte("sample5")}, 19 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample6", Hash: "1d1756986764035547f4a1e1a106d7d1", Data: []byte("sample6")}, 20 | } 21 | 22 | rfDiff := NewRFList.Diff(OldRFList) 23 | 24 | addMfDiff := RFList{ 25 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample3", Hash: "d35f70211135de265bc7c66df4dd3605", Data: []byte("sample3")}, 26 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample4", Hash: "247f4201f214ff279da3a24570d642f1", Data: []byte("sample4")}} 27 | if len(rfDiff.Add) != len(addMfDiff) { 28 | t.Fatalf("expected result is %s, but %s", addMfDiff, rfDiff.Add) 29 | } 30 | for i := 0; i < len(addMfDiff); i++ { 31 | if !addMfDiff[i].EqVer(rfDiff.Add[i]) { 32 | t.Fatalf("expected result is %s, but %s", addMfDiff[i], rfDiff.Add[i]) 33 | } 34 | } 35 | 36 | delMfDiff := RFList{ 37 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample5", Hash: "828974c6c954abd2ada226a48c7d6090", Data: []byte("sample5")}, 38 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample6", Hash: "1d1756986764035547f4a1e1a106d7d1", Data: []byte("sample6")}} 39 | if len(rfDiff.Del) != len(delMfDiff) { 40 | t.Fatalf("expected result is %s, but %s", delMfDiff, rfDiff.Del) 41 | } 42 | for i := 0; i < len(delMfDiff); i++ { 43 | if !delMfDiff[i].EqVer(rfDiff.Del[i]) { 44 | t.Fatalf("expected result is %s, but %s", delMfDiff[i], rfDiff.Del[i]) 45 | } 46 | } 47 | 48 | newMfDiff := RFList{ 49 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}} 50 | if len(rfDiff.New) != len(newMfDiff) { 51 | t.Fatalf("expected result is %s, but %s", newMfDiff, rfDiff.New) 52 | } 53 | for i := 0; i < len(newMfDiff); i++ { 54 | if !newMfDiff[i].EqVer(rfDiff.New[i]) { 55 | t.Fatalf("expected result is %s, but %s", newMfDiff[i], rfDiff.New[i]) 56 | } 57 | } 58 | 59 | oldMfDiff := RFList{ 60 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}} 61 | if len(rfDiff.Old) != len(oldMfDiff) { 62 | t.Fatalf("expected result is %s, but %s", oldMfDiff, rfDiff.Old) 63 | } 64 | for i := 0; i < len(oldMfDiff); i++ { 65 | if !oldMfDiff[i].EqVer(rfDiff.Old[i]) { 66 | t.Fatalf("expected result is %s, but %s", oldMfDiff[i], rfDiff.Old[i]) 67 | } 68 | } 69 | 70 | eqMfDiff := RFList{ 71 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}} 72 | if len(rfDiff.Eq) != len(eqMfDiff) { 73 | t.Fatalf("expected result is %s, but %s", eqMfDiff, rfDiff.Eq) 74 | } 75 | for i := 0; i < len(eqMfDiff); i++ { 76 | if !eqMfDiff[i].EqVer(rfDiff.Eq[i]) { 77 | t.Fatalf("expected result is %s, but %s", eqMfDiff[i], rfDiff.Eq[i]) 78 | } 79 | } 80 | } 81 | 82 | func TestReadRFList(t *testing.T) { 83 | client, err := NewClient(&ClientConfig{ 84 | ConsulAddr: "localhost:8500", 85 | ConsulDC: "dc1", 86 | }) 87 | if err != nil { 88 | t.Fatalf("err: %v", err) 89 | } 90 | 91 | _, err = client.ReadRFList("fileconsul") 92 | if err != nil { 93 | t.Skipf("err: %v", err) 94 | } 95 | } 96 | 97 | func TestToLFList(t *testing.T) { 98 | rfList := RFList{ 99 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 100 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}, 101 | } 102 | 103 | lfList := rfList.ToLFList("/path/to/base") 104 | ansRFList := lfList.ToRFList("fileconsul") 105 | 106 | if !rfList.Equal(ansRFList) { 107 | t.Fatalf("expected result is %s, but %s", ansRFList, lfList) 108 | } 109 | } 110 | 111 | func TestFilter(t *testing.T){ 112 | rfList := RFList{ 113 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 114 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}, 115 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample3", Hash: "d35f70211135de265bc7c66df4dd3605", Data: []byte("sample3")}, 116 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample4", Hash: "247f4201f214ff279da3a24570d642f1", Data: []byte("sample4")}, 117 | } 118 | 119 | ans1 := RFList{ 120 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 121 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}, 122 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample3", Hash: "d35f70211135de265bc7c66df4dd3605", Data: []byte("sample3")}, 123 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample4", Hash: "247f4201f214ff279da3a24570d642f1", Data: []byte("sample4")}, 124 | } 125 | res1, err := rfList.Filter("/path") 126 | if err != nil { 127 | t.Fatalf("err: %v", err) 128 | } 129 | if !ans1.Equal(res1) { 130 | t.Fatalf("expected result is %s, but %s", ans1, res1) 131 | } 132 | 133 | ans2 := RFList{ 134 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample1", Hash: "ac46374a846d97e22f917b6863f690ad", Data: []byte("sample1")}, 135 | } 136 | res2, err := rfList.Filter("/path/to/sample1") 137 | if err != nil { 138 | t.Fatalf("err: %v", err) 139 | } 140 | if !ans2.Equal(res2) { 141 | t.Fatalf("expected result is %s, but %s", ans2, res2) 142 | } 143 | 144 | ans3 := RFList{ 145 | Remotefile{Prefix: "fileconsul", Path: "/path/to/sample2", Hash: "656b38f3402a1e8b4211fac826efd433", Data: []byte("sample2")}, 146 | } 147 | res3, err := rfList.Filter("/*/sample*") 148 | if err != nil { 149 | t.Fatalf("err: %v", err) 150 | } 151 | if !ans3.Equal(res3) { 152 | t.Fatalf("expected result is %s, but %s", ans3, res3) 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fileconsul 2 | [ ![Download](https://api.bintray.com/packages/foostan/fileconsul/fileconsul/images/download.png) ](https://bintray.com/foostan/fileconsul/fileconsul/_latestVersion) 3 | 4 | Fileconsul is sharing files(configuration file, Service/Check definition scripts, handler scripts and more) in a consul cluster. 5 | 6 | ## Usage 7 | Run `fileconsul -h` to see the usage help: 8 | 9 | ``` 10 | $ fileconsul -h 11 | NAME: 12 | fileconsul - Sharing files in a consul cluster. 13 | 14 | USAGE: 15 | fileconsul [global options] command [command options] [arguments...] 16 | 17 | VERSION: 18 | 0.1.0 19 | 20 | COMMANDS: 21 | status Show status of local files 22 | pull Pull files from a consul cluster 23 | push Push file to a consul cluster 24 | help, h Shows a list of commands or help for one command 25 | 26 | GLOBAL OPTIONS: 27 | --version, -v print the version 28 | --help, -h show help 29 | ``` 30 | 31 | ### Status Command 32 | Run `fileconsul status -h` to see the usage help: 33 | 34 | ``` 35 | $ fileconsul status -h 36 | NAME: 37 | status - Show status of local files 38 | 39 | USAGE: 40 | command status [command options] [arguments...] 41 | 42 | DESCRIPTION: 43 | Show the difference between local files and remote files that is stored in K/V Store of a consul cluster. 44 | 45 | OPTIONS: 46 | --addr 'localhost:8500' consul HTTP API address with port 47 | --dc 'dc1' consul datacenter, uses local if blank 48 | --prefix 'fileconsul' reading file status from Consul's K/V store with the given prefix 49 | --basepath '.' base directory path of target files 50 | ``` 51 | 52 | ### Pull Command 53 | Run `fileconsul pull -h` to see the usage help: 54 | 55 | ``` 56 | $ fileconsul pull -h 57 | NAME: 58 | pull - Pull files from a consul cluster 59 | 60 | USAGE: 61 | command pull [command options] [arguments...] 62 | 63 | DESCRIPTION: 64 | Pull remote files from K/V Store of a consul cluster. 65 | 66 | OPTIONS: 67 | --addr 'localhost:8500' consul HTTP API address with port 68 | --dc 'dc1' consul datacenter, uses local if blank 69 | --prefix 'fileconsul' reading file status from Consul's K/V store with the given prefix 70 | --basepath '.' base directory path of target files 71 | ``` 72 | 73 | ### Push Command 74 | Run `fileconsul push -h` to see the usage help: 75 | 76 | ``` 77 | $ fileconsul push -h 78 | NAME: 79 | push - Push file to a consul cluster 80 | 81 | USAGE: 82 | command push [command options] [arguments...] 83 | 84 | DESCRIPTION: 85 | Push remote files to K/V Store of a consul cluster. 86 | 87 | OPTIONS: 88 | --addr 'localhost:8500' consul HTTP API address with port 89 | --dc 'dc1' consul datacenter, uses local if blank 90 | --prefix 'fileconsul' reading file status from Consul's K/V store with the given prefix 91 | --basepath '.' base directory path of target files 92 | ``` 93 | 94 | ## Example 95 | Use the demo environment for fileconsul 96 | ### Setup the demo environment used by Docker 97 | ``` 98 | $ git clone https://github.com/foostan/fileconsul.git 99 | $ cd fileconsul/demo 100 | $ docker build -t fileconsul . 101 | ``` 102 | 103 | ### Run containers and a consul agent 104 | #### Consul server 105 | 106 | ``` 107 | $ docker run -h server -i -t fileconsul 108 | root@server:/# consul agent -data-dir=/tmp/consul -server -bootstrap-expect 1 & 109 | ==> WARNING: BootstrapExpect Mode is specified as 1; this is the same as Bootstrap mode. 110 | ==> WARNING: Bootstrap mode enabled! Do not enable unless necessary 111 | ==> WARNING: It is highly recommended to set GOMAXPROCS higher than 1 112 | ==> Starting Consul agent... 113 | ==> Starting Consul agent RPC... 114 | ==> Consul agent running! 115 | Node name: 'server' 116 | Datacenter: 'dc1' 117 | Server: true (bootstrap: true) 118 | Client Addr: 127.0.0.1 (HTTP: 8500, DNS: 8600, RPC: 8400) 119 | Cluster Addr: 172.17.0.6 (LAN: 8301, WAN: 8302) 120 | Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false 121 | 122 | --- omitted below --- 123 | ``` 124 | 125 | #### Consul client 126 | 127 | ``` 128 | $ docker run -h client -i -t fileconsul 129 | root@client:/# consul agent -data-dir=/tmp/consul -join 172.17.0.6 & 130 | ==> WARNING: It is highly recommended to set GOMAXPROCS higher than 1 131 | ==> Starting Consul agent... 132 | ==> Starting Consul agent RPC... 133 | ==> Joining cluster... 134 | Join completed. Synced with 1 initial agents 135 | ==> Consul agent running! 136 | Node name: 'client' 137 | Datacenter: 'dc1' 138 | Server: false (bootstrap: false) 139 | Client Addr: 127.0.0.1 (HTTP: 8500, DNS: 8600, RPC: 8400) 140 | Cluster Addr: 172.17.0.5 (LAN: 8301, WAN: 8302) 141 | Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false 142 | 143 | --- omitted below --- 144 | ``` 145 | 146 | ### Use fileconsul 147 | 148 | #### Check status and push on the consul server 149 | ``` 150 | root@server:/# cd /consul/share/ 151 | root@server:/consul/share# fileconsul status 152 | Changes to be pushed: 153 | (use "fileconsul push [command options]" to synchronize local files) 154 | add remote file: bin/ntp 155 | add remote file: bin/apache2 156 | add remote file: bin/loadavg 157 | add remote file: config/service/apache2.json 158 | add remote file: config/service/ntp.json 159 | add remote file: config/template/server/check_loadavg.json 160 | add remote file: config/template/server/service_apache2.json 161 | add remote file: config/template/server/agent_server.json 162 | add remote file: config/template/server/service_ntp.json 163 | add remote file: config/template/client/check_loadavg.json 164 | add remote file: config/template/client/agent_client.json 165 | add remote file: config/template/client/service_ntp.json 166 | add remote file: config/check/loadavg.json 167 | root@server:/consul/share# fileconsul push 168 | push new file: bin/ntp 169 | push new file: bin/apache2 170 | push new file: bin/loadavg 171 | push new file: config/service/apache2.json 172 | push new file: config/service/ntp.json 173 | push new file: config/template/server/check_loadavg.json 174 | push new file: config/template/server/service_apache2.json 175 | push new file: config/template/server/agent_server.json 176 | push new file: config/template/server/service_ntp.json 177 | push new file: config/template/client/check_loadavg.json 178 | push new file: config/template/client/agent_client.json 179 | push new file: config/template/client/service_ntp.json 180 | push new file: config/check/loadavg.json 181 | ``` 182 | 183 | #### Edit a file and push on the consul client 184 | ``` 185 | root@server:/# cd /consul/share/ 186 | root@client:/consul/share# vi bin/apache2 # edit a file 187 | root@client:/consul/share# fileconsul status 188 | Changes to be pushed: 189 | (use "fileconsul push [command options]" to synchronize local files) 190 | modify remote file: bin/apache2 191 | Changes to be pulled: 192 | (use "fileconsul pull [command options]" to synchronize remote files) 193 | modify local file: bin/apache2 194 | root@client:/consul/share# fileconsul push 195 | push modified file: bin/apache2 196 | ``` 197 | 198 | #### Check status and pull on the consul server 199 | ``` 200 | root@server:/consul/share# fileconsul status 201 | Changes to be pushed: 202 | (use "fileconsul push [command options]" to synchronize local files) 203 | modify remote file: bin/apache2 204 | Changes to be pulled: 205 | (use "fileconsul pull [command options]" to synchronize remote files) 206 | modify local file: bin/apache2 207 | root@server:/consul/share# fileconsul pull 208 | Synchronize remote files: 209 | modified: bin/apache2 210 | Already up-to-date. 211 | ``` 212 | 213 | ## Contributing 214 | 215 | 1. Fork it ( https://github.com/[my-github-username]/fileconsul/fork ) 216 | 2. Create your feature branch (`git checkout -b my-new-feature`) 217 | 3. Commit your changes (`git commit -am 'Add some feature'`) 218 | 4. Push to the branch (`git push origin my-new-feature`) 219 | 5. Create a new Pull Request 220 | --------------------------------------------------------------------------------