├── .gitignore ├── .travis.yml ├── API.md ├── Gopkg.lock ├── Gopkg.toml ├── LICENSE ├── README.md ├── client ├── client.go └── client_test.go ├── cmd └── floe │ ├── integ_test.go │ └── main.go ├── config.yml ├── config ├── config.go ├── config_test.go ├── flow.go ├── flow_graph.go ├── flow_graph_test.go ├── flow_test.go ├── node.go ├── node_test.go └── nodetype │ ├── data.go │ ├── exec.go │ ├── exec_test.go │ ├── fetch.go │ ├── fetch_test.go │ ├── git.go │ ├── node_type.go │ ├── opts.go │ ├── opts_test.go │ └── timer.go ├── dev ├── config.yml ├── docker │ ├── .gitignore │ ├── Dockerfile │ └── README.md ├── env.sh ├── floe.yml ├── start.sh └── upgrade.sh ├── event ├── event.go └── event_test.go ├── exe ├── git │ ├── git.go │ ├── git_integ_test.go │ └── git_test.go ├── run.go └── run_test.go ├── floe.yml ├── hub ├── hub_exec.go ├── hub_pend.go ├── hub_setup.go ├── hub_test.go ├── runs.go ├── setup.go ├── timers.go ├── timers_integ_test.go └── timers_test.go ├── log └── log.go ├── path ├── path.go └── path_test.go ├── server ├── bindata.go ├── generate.go ├── handler_auth.go ├── handler_conf.go ├── handler_flows.go ├── handler_runs.go ├── middleware.go ├── push │ ├── data.go │ └── push.go ├── server.go ├── session.go └── ws.go ├── start.sh ├── store ├── store.go └── store_test.go ├── vendor ├── github.com │ ├── cavaliercoder │ │ └── grab │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── Makefile │ │ │ ├── README.md │ │ │ ├── client.go │ │ │ ├── doc.go │ │ │ ├── error.go │ │ │ ├── grab.go │ │ │ ├── request.go │ │ │ ├── response.go │ │ │ ├── states.wsd │ │ │ ├── transfer.go │ │ │ └── util.go │ ├── elazarl │ │ └── go-bindata-assetfs │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── assetfs.go │ │ │ └── doc.go │ ├── julienschmidt │ │ └── httprouter │ │ │ ├── .travis.yml │ │ │ ├── LICENSE │ │ │ ├── README.md │ │ │ ├── params_go17.go │ │ │ ├── params_legacy.go │ │ │ ├── path.go │ │ │ ├── router.go │ │ │ └── tree.go │ └── mitchellh │ │ └── mapstructure │ │ ├── .travis.yml │ │ ├── LICENSE │ │ ├── README.md │ │ ├── decode_hooks.go │ │ ├── error.go │ │ └── mapstructure.go ├── golang.org │ └── x │ │ └── net │ │ ├── AUTHORS │ │ ├── CONTRIBUTORS │ │ ├── LICENSE │ │ ├── PATENTS │ │ └── websocket │ │ ├── client.go │ │ ├── dial.go │ │ ├── hybi.go │ │ ├── server.go │ │ └── websocket.go └── gopkg.in │ └── yaml.v2 │ ├── .travis.yml │ ├── LICENSE │ ├── LICENSE.libyaml │ ├── README.md │ ├── apic.go │ ├── decode.go │ ├── emitterc.go │ ├── encode.go │ ├── parserc.go │ ├── readerc.go │ ├── resolve.go │ ├── scannerc.go │ ├── sorter.go │ ├── writerc.go │ ├── yaml.go │ ├── yamlh.go │ └── yamlprivateh.go └── webapp ├── css ├── main.css └── normalize.css ├── font ├── floe.woff └── floe.woff2 ├── img ├── floe.png └── gear.svg ├── index.html └── js ├── main.js ├── page ├── dash.js ├── flow-single.js ├── flow.js ├── header.js ├── login.js └── settings.js ├── panel ├── controller.js ├── event.js ├── expander.js ├── form.js ├── panel.js ├── rest.js ├── store.js └── util.js ├── vendor ├── dot.js └── rlite.js └── ws.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | .vscode 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | bin/ 26 | Notes.md 27 | 28 | workspace 29 | workspace_old 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.10" 4 | install: true -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | 2 | http://127.0.0.1:8080/build/api/config 3 | Cookie:floe-sesh=547dd44e09ac2835 4 | 5 | ### 6 | 7 | http://127.0.0.1:8080/build/api/flows 8 | Cookie:floe-sesh=547dd44e09ac2835 9 | 10 | ### 11 | 12 | http://127.0.0.1:8080/build/api/runs/archive 13 | Cookie:floe-sesh=547dd44e09ac2835 14 | 15 | ### 16 | 17 | POST http://127.0.0.1:8080/build/api/login HTTP/1.1 18 | content-type: application/json 19 | 20 | { 21 | "user": "admin", 22 | "password": "password" 23 | } 24 | 25 | events... 26 | 27 | add-pend 28 | activate 29 | remove-pend -------------------------------------------------------------------------------- /Gopkg.lock: -------------------------------------------------------------------------------- 1 | # This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. 2 | 3 | 4 | [[projects]] 5 | branch = "master" 6 | name = "github.com/cavaliercoder/grab" 7 | packages = ["."] 8 | revision = "94177710b7005a1bc795e6805f2f7481f4120189" 9 | 10 | [[projects]] 11 | branch = "master" 12 | name = "github.com/elazarl/go-bindata-assetfs" 13 | packages = ["."] 14 | revision = "38087fe4dafb822e541b3f7955075cc1c30bd294" 15 | 16 | [[projects]] 17 | name = "github.com/julienschmidt/httprouter" 18 | packages = ["."] 19 | revision = "d1898390779332322e6b5ca5011da4bf249bb056" 20 | 21 | [[projects]] 22 | branch = "master" 23 | name = "github.com/mitchellh/mapstructure" 24 | packages = ["."] 25 | revision = "00c29f56e2386353d58c599509e8dc3801b0d716" 26 | 27 | [[projects]] 28 | name = "golang.org/x/net" 29 | packages = ["websocket"] 30 | revision = "ae05321a78c1401cec22ba7bc203b597ea372496" 31 | 32 | [[projects]] 33 | name = "gopkg.in/yaml.v2" 34 | packages = ["."] 35 | revision = "a5b47d31c556af34a302ce5d659e6fea44d90de0" 36 | 37 | [solve-meta] 38 | analyzer-name = "dep" 39 | analyzer-version = 1 40 | inputs-digest = "b1801d504bd18f81e013a219d71c85db7dbf3678ec47c7a1f59ee95154d81c42" 41 | solver-name = "gps-cdcl" 42 | solver-version = 1 43 | -------------------------------------------------------------------------------- /Gopkg.toml: -------------------------------------------------------------------------------- 1 | # Gopkg.toml example 2 | # 3 | # Refer to https://golang.github.io/dep/docs/Gopkg.toml.html 4 | # for detailed Gopkg.toml documentation. 5 | # 6 | # required = ["github.com/user/thing/cmd/thing"] 7 | # ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] 8 | # 9 | # [[constraint]] 10 | # name = "github.com/user/project" 11 | # version = "1.0.0" 12 | # 13 | # [[constraint]] 14 | # name = "github.com/user/project2" 15 | # branch = "dev" 16 | # source = "github.com/myfork/project2" 17 | # 18 | # [[override]] 19 | # name = "github.com/x/y" 20 | # version = "2.4.0" 21 | # 22 | # [prune] 23 | # non-go = false 24 | # go-tests = true 25 | # unused-packages = true 26 | 27 | 28 | [[constraint]] 29 | branch = "master" 30 | name = "github.com/elazarl/go-bindata-assetfs" 31 | 32 | [[constraint]] 33 | branch = "master" 34 | name = "github.com/mitchellh/mapstructure" 35 | 36 | [prune] 37 | go-tests = true 38 | unused-packages = true 39 | 40 | [[constraint]] 41 | branch = "master" 42 | name = "github.com/cavaliercoder/grab" 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dan Mullineux 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. -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "testing" 4 | 5 | func TestTagsMatch(t *testing.T) { 6 | fix := []struct { 7 | ht []string 8 | it []string 9 | match bool 10 | }{ 11 | { 12 | ht: []string{"tag1", "tag2"}, 13 | it: []string{"tag1", "tag2"}, 14 | match: true, 15 | }, 16 | { 17 | ht: []string{"tag1", "tag2"}, 18 | it: []string{"tag1"}, 19 | match: true, 20 | }, 21 | { 22 | ht: []string{"tag1", "tag2"}, 23 | it: []string{"tag1", "tag3"}, 24 | match: false, 25 | }, 26 | { 27 | ht: []string{"tag1", "tag2"}, 28 | it: []string{"tag1", "tag2", "tag3"}, 29 | match: false, 30 | }, 31 | { 32 | ht: []string{"tag1", "tag2"}, 33 | it: []string{}, 34 | match: true, 35 | }, 36 | { 37 | ht: []string{}, 38 | it: []string{}, 39 | match: true, 40 | }, 41 | { 42 | ht: []string{}, 43 | it: []string{"tag1", "tag2"}, 44 | match: false, 45 | }, 46 | } 47 | 48 | for i, f := range fix { 49 | hc := HostConfig{ 50 | Tags: f.ht, 51 | } 52 | if hc.TagsMatch(f.it) != f.match { 53 | t.Errorf("%d expected %v, got the opposite", i, f.match) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /cmd/floe/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | 10 | "github.com/floeit/floe/config" 11 | "github.com/floeit/floe/event" 12 | "github.com/floeit/floe/hub" 13 | "github.com/floeit/floe/log" 14 | "github.com/floeit/floe/path" 15 | "github.com/floeit/floe/server" 16 | "github.com/floeit/floe/store" 17 | ) 18 | 19 | func main() { 20 | c := srvConf{} 21 | flag.StringVar(&c.ConfFile, "conf", "config.yml", "the host config yaml") 22 | flag.StringVar(&c.HostName, "host_name", "h1", "a short host name to use in id creation and routing") 23 | flag.StringVar(&c.AdminToken, "admin", "", "admin token to share in a cluster to confirm it's a p2p call") 24 | flag.StringVar(&c.Tags, "tags", "master", "host tags") 25 | 26 | flag.StringVar(&c.PubBind, "pub_bind", ":443", "what to bind the public server to") 27 | flag.StringVar(&c.PubCert, "pub_cert", "", "public certificate path") 28 | flag.StringVar(&c.PubKey, "pub_key", "", "key path for the public endpoint") 29 | 30 | flag.StringVar(&c.PrvBind, "prv_bind", "", "what to bind the private server to") 31 | flag.StringVar(&c.PrvCert, "prv_cert", "", "private certificate path") 32 | flag.StringVar(&c.PrvKey, "prv_key", "", "key path for the private endpoint") 33 | 34 | flag.BoolVar(&c.WebDev, "dev", false, "set to true to use local webapp folder during development") 35 | 36 | flag.Parse() 37 | 38 | cfg, err := ioutil.ReadFile(c.ConfFile) 39 | if err != nil { 40 | log.Error(err) 41 | os.Exit(1) 42 | } 43 | 44 | log.Error(start(c, cfg, nil)) 45 | } 46 | 47 | type srvConf struct { 48 | server.Conf 49 | 50 | ConfFile string // the path to the main config file 51 | HostName string // the name of this host 52 | AdminToken string // the token to use to verify nodes in the cluster 53 | Tags string // tags for this server to be matched against tags specified in the flows 54 | 55 | WebDev bool // use local file system for web assets 56 | } 57 | 58 | // %store_root%/store "~/.floe/store" 59 | // %workspace_root%/spaces "~/.floe/spaces" 60 | 61 | func start(sc srvConf, conf []byte, addr chan string) error { 62 | 63 | c, err := config.ParseYAML(conf) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | var s store.Store 69 | switch c.Common.StoreType { 70 | case "", "memory": 71 | s = store.NewMemStore() 72 | case "local": 73 | root, err := path.Expand(c.Common.StoreRoot) 74 | if err != nil { 75 | return err 76 | } 77 | s, err = store.NewLocalStore(filepath.Join(root, "store")) 78 | if err != nil { 79 | return err 80 | } 81 | default: 82 | return fmt.Errorf("%s is not a supported store", c.Common.StoreType) 83 | } 84 | // TODO - implement other stores e.g. s3 85 | 86 | q := &event.Queue{} 87 | hub := hub.New(sc.HostName, sc.Tags, sc.AdminToken, c, s, q) 88 | server.AdminToken = sc.AdminToken 89 | 90 | server.LaunchWeb(sc.Conf, c.Common.BaseURL, hub, q, addr, sc.WebDev) 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | common: 2 | base-url: "/build/api" 3 | store-type: local 4 | hosts: 5 | - http://127.0.0.1:8080 6 | # - http://127.0.0.1:8090 7 | 8 | flows: 9 | - id: build-project # the name of this flow 10 | name: Build Project 11 | ver: 1 12 | reuse-space: false # reuse the workspace (false) - if true /single used 13 | resource-tags: [couchbase, nic] # resource labels that any other flows cant share 14 | host-tags: [linux, go, couch] # all these tags must match the tags on any host for it to be able to run there 15 | 16 | triggers: # external events to subscribe to 17 | - name: push # name of this trigger 18 | type: git-push # the type of this trigger 19 | opts: 20 | url: blah.blah # which url to monitor 21 | 22 | - name: start 23 | type: data 24 | opts: 25 | form: 26 | title: Start 27 | fields: 28 | - id: ref 29 | prompt: Ref (branch or hash) 30 | type: text 31 | value: master 32 | - id: from-ref 33 | prompt: Ref to merge (branch or hash) 34 | type: text 35 | 36 | tasks: 37 | - name: Checkout # the name of this node 38 | type: git-merge # the task type 39 | listen: trigger.good # the event tag that triggers this node 40 | good: [0] # define what the good statuses are, default [0] 41 | ignore-fail: false # if true only emit good 42 | opts: 43 | sub-dir: src/github.com/floeit 44 | url: git@github.com:floeit/floe.git 45 | 46 | - name: Echo 47 | type: exec 48 | listen: task.checkout.good 49 | opts: 50 | cmd: "echo" 51 | args: ["dan is a goon"] 52 | 53 | - name: Build 54 | type: exec 55 | listen: task.checkout.good 56 | opts: 57 | cmd: "ls" # the command to execute 58 | args: ["-lrt", "/"] 59 | 60 | - name: Test 61 | type: exec # execute a command 62 | listen: task.build.good 63 | opts: 64 | shell: "sleep 10" # the command to execute 65 | 66 | - name: Sign Off 67 | type: data 68 | listen: task.build.good # for a data node this event has to have occured before the data node can accept data 69 | opts: 70 | form: 71 | title: Sign off Manual Testing 72 | fields: 73 | - id: tests_passed 74 | prompt: Did the manual testing pass? 75 | type: bool 76 | - id: to_hash 77 | prompt: To Branch (or hash) 78 | type: string 79 | 80 | - name: Wait For Tests and Sign Off 81 | class: merge 82 | id: signed 83 | type: all 84 | wait: [task.echo.good, task.test.good, task.sign-off.good] 85 | 86 | - name: Final Task 87 | type: exec # execute a command 88 | listen: merge.signed.good 89 | opts: 90 | cmd: "ls" # the command to execute 91 | 92 | - name: complete 93 | listen: task.final-task.good 94 | type: end # getting here means the flow was a success 'end' is the special definitive end event 95 | 96 | - id: danmux 97 | ver: 1 98 | 99 | triggers: 100 | - name: start 101 | type: data 102 | opts: 103 | url: git@github.com:danmux/danmux-hugo.git 104 | form: 105 | title: Start 106 | fields: 107 | - id: branch 108 | prompt: Branch 109 | type: text 110 | 111 | - name: Every 5 Minutes 112 | type: timer # fires every period seconds 113 | opts: 114 | period: 300 # how often to fire in seconds 115 | url: git@github.com:danmux/danmux-hugo.git 116 | branch: master 117 | 118 | - name: Commits 119 | type: poll-git 120 | opts: 121 | period: 10 # check every 10 seconds 122 | url: git@github.com:danmux/danmux-hugo.git # the repo to check 123 | refs: "refs/heads/*" # the refs pattern to match 124 | exclude-refs: "refs/heads/master" 125 | 126 | flow-file: floe.yml # get the actual flow from this file -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "gopkg.in/yaml.v2" 7 | 8 | nt "github.com/floeit/floe/config/nodetype" 9 | "github.com/floeit/floe/log" 10 | ) 11 | 12 | // Config is the set of nodes and rules 13 | type Config struct { 14 | Common commonConfig 15 | // the list of flow configurations 16 | Flows []*Flow 17 | } 18 | 19 | // Defaults ensures some sensible defaults have been set up 20 | func (c *Config) Defaults() { 21 | if c.Common.StoreType == "" { 22 | c.Common.StoreType = "memory" 23 | } 24 | if c.Common.WorkspaceRoot == "" { 25 | c.Common.WorkspaceRoot = "~/.floe" 26 | } 27 | if c.Common.StoreRoot == "" { 28 | c.Common.StoreRoot = c.Common.WorkspaceRoot 29 | } 30 | } 31 | 32 | type commonConfig struct { 33 | // all other floe Hosts 34 | Hosts []string 35 | // the api base url - in case hosting on a sub domain 36 | BaseURL string `yaml:"base-url"` 37 | 38 | // WorkspaceRoot is the local disk root directory to store workspace output. 39 | WorkspaceRoot string `yaml:"workspace-root"` 40 | // StoreType define which type of store to use 41 | StoreType string `yaml:"store-type"` // memory, local, ec2, git // TODO 42 | // Store Root is local file location, ec2 bucket path, github url 43 | StoreRoot string `yaml:"store-root"` 44 | 45 | GitKey string `yaml:"git-key"` // path to the git key to use 46 | 47 | // StoreCredentials is a string in some format or other to provide needed credentials for 48 | // specific store type. 49 | // StoreCredentials string `yaml:"store-credentials"` 50 | } 51 | 52 | // FoundFlow is a struct containing a Flow and trigger that matched this flow. 53 | // It can be used to decide on the best host to use to run this Flow. 54 | type FoundFlow struct { 55 | // Ref constructed from the Flow 56 | Ref FlowRef 57 | // Matched is whatever node cause this flow to be found. It is either the trigger node that 58 | // matched the criteria to have found the flow and node, or a list of nodes that matched the 59 | // event that 60 | Matched *node 61 | // the full Flow definition 62 | *Flow 63 | } 64 | 65 | // FindFlowsByTriggers finds all flows where its subs match the given params 66 | func (c *Config) FindFlowsByTriggers(triggerType string, flow FlowRef, opts nt.Opts) map[FlowRef]FoundFlow { 67 | res := map[FlowRef]FoundFlow{} 68 | for _, f := range c.Flows { 69 | // if a flow is specified it has to match 70 | if flow.NonZero() { 71 | log.Debugf("config - comparing flow:<%s> to config flow:<%s-%d>", flow, f.ID, f.Ver) 72 | if f.ID != flow.ID || f.Ver != flow.Ver { 73 | continue 74 | } 75 | } 76 | log.Debugf("config - checking flow: <%s-%d> with %d triggers", f.ID, f.Ver, len(f.Triggers)) 77 | // match on other stuff 78 | ns := f.matchTriggers(triggerType, &opts) 79 | // found some matching nodes for this flow 80 | if len(ns) > 0 { 81 | log.Debugf("config - found flow: <%s-%d> with %d matching triggers for %s", f.ID, f.Ver, len(ns), triggerType) 82 | if len(ns) > 1 { 83 | log.Warning("config - triggered flow has too many matching triggers, using first", f.ID, f.Ver, len(ns)) 84 | } 85 | // make sure this flow is in the results 86 | fr := ns[0].FlowRef() 87 | ff, ok := res[fr] 88 | if !ok { 89 | ff = FoundFlow{ 90 | Ref: fr, 91 | Flow: f, 92 | } 93 | } 94 | ff.Matched = ns[0] // there should only really be one hence use the first one 95 | res[fr] = ff 96 | } else { 97 | log.Debugf("config - flow: <%s-%d> no trigger match for %s", f.ID, f.Ver, triggerType) 98 | } 99 | } 100 | return res 101 | } 102 | 103 | // Flow returns the flow config matching the id and version 104 | func (c *Config) Flow(fRef FlowRef) *Flow { 105 | for _, f := range c.Flows { 106 | if f.ID == fRef.ID && f.Ver == fRef.Ver { 107 | return f 108 | } 109 | } 110 | return nil 111 | } 112 | 113 | // LatestFlow returns the flow config matching the id with the highest version 114 | func (c *Config) LatestFlow(id string) *Flow { 115 | var latest *Flow 116 | highestVer := 0 117 | for _, f := range c.Flows { 118 | if f.ID != id { 119 | continue 120 | } 121 | if f.Ver > highestVer { 122 | latest = f 123 | } 124 | } 125 | return latest 126 | } 127 | 128 | // zero sets up all the default values 129 | func (c *Config) zero() error { 130 | for i, f := range c.Flows { 131 | if err := f.zero(); err != nil { 132 | return fmt.Errorf("flow %d - %v", i, err) 133 | } 134 | } 135 | return nil 136 | } 137 | 138 | // ParseYAML takes a YAML input as a byte array and returns a Config object 139 | // or an error 140 | func ParseYAML(in []byte) (*Config, error) { 141 | c := &Config{} 142 | err := yaml.Unmarshal(in, &c) 143 | if err != nil { 144 | return c, err 145 | } 146 | c.Defaults() 147 | err = c.zero() 148 | return c, err 149 | } 150 | -------------------------------------------------------------------------------- /config/flow_graph.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | ) 8 | 9 | type levelNode struct { 10 | level int 11 | node *node 12 | kids []*levelNode 13 | } 14 | 15 | // Graph returns an array representing levels in the flow 0 being the trigger events 16 | // and the end events having the highest number 17 | func (f *Flow) Graph() (lvs [][]string, problems []string) { 18 | 19 | lvs = append(lvs, []string{}, []string{}) 20 | 21 | l0 := map[string]*levelNode{} 22 | l1 := map[string]*levelNode{} 23 | 24 | nodes := map[string]*levelNode{} 25 | 26 | // level 0 is triggers 27 | for _, t := range f.Triggers { 28 | if _, ok := nodes[t.ID]; ok { 29 | problems = append(problems, fmt.Sprintf("duplicate trigger id: %s", t.ID)) 30 | continue 31 | } 32 | ln := &levelNode{ 33 | node: t, 34 | level: 0, 35 | } 36 | nodes[t.ID] = ln 37 | l0[ln.node.ID] = ln 38 | lvs[0] = append(lvs[0], t.ID) 39 | } 40 | for _, t := range f.Tasks { 41 | if _, ok := nodes[t.ID]; ok { 42 | problems = append(problems, fmt.Sprintf("duplicate node id: %s", t.ID)) 43 | continue 44 | } 45 | ln := &levelNode{ 46 | node: t, 47 | level: 1, 48 | } 49 | nodes[t.ID] = ln 50 | if t.Listen == "trigger.good" { // trigger listeners are always level 1 51 | l1[ln.node.ID] = ln 52 | lvs[1] = append(lvs[1], t.ID) 53 | } 54 | } 55 | 56 | // find all events listened to - then all potential emitters of those events 57 | tagsAndListener := map[string][]*levelNode{} 58 | for _, ln := range nodes { 59 | switch ln.node.Class { 60 | case NcMerge: 61 | for _, t := range ln.node.Wait { 62 | a := tagsAndListener[t] 63 | a = append(a, ln) 64 | tagsAndListener[t] = a 65 | } 66 | case NcTask: 67 | t := ln.node.Listen 68 | a := tagsAndListener[t] 69 | a = append(a, ln) 70 | tagsAndListener[t] = a 71 | } 72 | } 73 | 74 | // for all events being listened to find the parents 75 | for t, ns := range tagsAndListener { 76 | if t == "trigger.good" { // trigger listeners are always level 1 77 | for _, n := range ns { 78 | n.level = 1 79 | } 80 | continue 81 | } 82 | parts := strings.Split(t, ".") 83 | if len(parts) != 3 { 84 | problems = append(problems, fmt.Sprintf("nodes are listening to invalid event: %s", t)) 85 | continue 86 | } 87 | id := parts[1] 88 | parent, ok := nodes[id] 89 | if !ok { 90 | problems = append(problems, fmt.Sprintf("nodes are listening to invalid event: %s, that does not match a node", t)) 91 | continue 92 | } 93 | parent.kids = append(parent.kids, ns...) 94 | } 95 | 96 | // starting from level 1 traverse its tree adding nodes to the correct level 97 | for _, n := range l1 { 98 | fillLevels(n) 99 | } 100 | 101 | // group them by levels 102 | lm := map[int]map[string]bool{} 103 | for _, n := range l1 { 104 | addToLevels(n, lm) 105 | } 106 | 107 | // convert to slice of slice 108 | for l := 2; l < len(lm)+2; l++ { 109 | lv := []string{} 110 | for k := range lm[l] { 111 | lv = append(lv, k) 112 | } 113 | 114 | sort.Slice(lv, func(i, j int) bool { 115 | return lv[i] < lv[j] 116 | }) 117 | 118 | // TODO sort within the level to some repeatable order 119 | lvs = append(lvs, lv) 120 | } 121 | 122 | return lvs, problems 123 | } 124 | 125 | func fillLevels(n *levelNode) { 126 | for _, kn := range n.kids { 127 | // if this node is now at a greater level than it was 128 | // boos its level and all its kids 129 | if kn.level < n.level+1 { 130 | kn.level = n.level + 1 131 | } 132 | fillLevels(kn) 133 | } 134 | } 135 | 136 | func addToLevels(n *levelNode, levs map[int]map[string]bool) { 137 | for _, kn := range n.kids { 138 | l, ok := levs[kn.level] 139 | if !ok { 140 | l = map[string]bool{} 141 | } 142 | l[kn.node.ID] = true 143 | levs[kn.level] = l 144 | addToLevels(kn, levs) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /config/flow_graph_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGraph(t *testing.T) { 8 | c, err := ParseYAML(in) 9 | if err != nil { 10 | t.Fatal(err) 11 | } 12 | graph, p := c.Flows[0].Graph() 13 | if len(p) != 0 { 14 | t.Error("something went wrong with the graph") 15 | } 16 | if len(graph) != 6 { 17 | t.Error("graph is the wrong length", len(graph)) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /config/node_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | nt "github.com/floeit/floe/config/nodetype" 7 | ) 8 | 9 | func TestNodeExec(t *testing.T) { 10 | output := make(chan string) 11 | captured := make(chan bool) 12 | cl := 0 13 | go func() { 14 | for l := range output { 15 | println(l) 16 | cl++ 17 | } 18 | captured <- true 19 | }() 20 | n := &node{ 21 | // what flow is this node attached to 22 | Class: "task", 23 | Type: "exec", 24 | Opts: nt.Opts{ 25 | "shell": "export", 26 | "env": []string{"DAN=fart"}, 27 | }, 28 | } 29 | 30 | status, _, err := n.Execute(&nt.Workspace{}, nt.Opts{}, output) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | if status != 0 { 35 | t.Error("wrong status", status) 36 | } 37 | close(output) 38 | <-captured 39 | } 40 | -------------------------------------------------------------------------------- /config/nodetype/data.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | import ( 4 | "github.com/mitchellh/mapstructure" 5 | ) 6 | 7 | type data struct{} 8 | 9 | func (d data) Match(qs, as Opts) bool { 10 | return true 11 | } 12 | 13 | type dataOpts struct { 14 | Values map[string]string `json:"values"` 15 | Form form `json:"form"` 16 | } 17 | 18 | type form struct { 19 | Title string `json:"title"` 20 | Fields []field `json:"fields"` 21 | } 22 | 23 | type field struct { 24 | ID string `json:"id"` 25 | Prompt string `json:"prompt"` 26 | Type string `json:"type"` 27 | Value string `json:"value"` 28 | } 29 | 30 | // Execute on data nodes fill in the opts, validate the form, and decide if the node can be considered 31 | // good or bad. 32 | // returns status 0 = form requirements met, 1 = an error (error will be set), 2 = needs more data 33 | func (d data) Execute(ws *Workspace, in Opts, output chan string) (int, Opts, error) { 34 | do := dataOpts{} 35 | 36 | err := mapstructure.Decode(in, &do) 37 | if err != nil { 38 | return 1, nil, err 39 | } 40 | 41 | rCode := 0 42 | for i, f := range do.Form.Fields { 43 | if v, ok := do.Values[f.ID]; ok { 44 | f.Value = v 45 | do.Form.Fields[i] = f 46 | // TODO add and validate mandatory fields 47 | // TODO look for single field representing good or bad 48 | } else { 49 | rCode = 2 50 | } 51 | } 52 | 53 | out := map[string]interface{}{ 54 | "form": do.Form, 55 | "values": do.Values, 56 | } 57 | 58 | return rCode, out, nil 59 | } 60 | -------------------------------------------------------------------------------- /config/nodetype/exec.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | 10 | "github.com/floeit/floe/exe" 11 | "github.com/floeit/floe/log" 12 | ) 13 | 14 | const ( 15 | shortRel = "./" 16 | wsSub = "{{ws}}" 17 | ) 18 | 19 | // exec node executes an external task 20 | type exec struct { 21 | Cmd string 22 | Shell string 23 | Args []string 24 | SubDir string `json:"sub-dir"` 25 | Env []string 26 | } 27 | 28 | func (e exec) Match(ol, or Opts) bool { 29 | return true 30 | } 31 | 32 | func (e exec) cmdAndArgs() (cmd string, args []string) { 33 | cmd = e.Cmd 34 | shell := false 35 | if cmd == "" { 36 | cmd = e.Shell 37 | shell = true 38 | } 39 | args = e.Args 40 | if len(args) == 00 { 41 | p := strings.Split(cmd, " ") 42 | if len(p) > 1 { 43 | cmd = p[0] 44 | args = p[1:] 45 | } 46 | } 47 | if shell { 48 | args = []string{"-c", fmt.Sprintf(`%s %s`, cmd, strings.Join(args, " "))} 49 | cmd = "bash" 50 | } 51 | return cmd, args 52 | } 53 | 54 | func (e exec) Execute(ws *Workspace, in Opts, output chan string) (int, Opts, error) { 55 | err := decode(in, &e) 56 | if err != nil { 57 | return 255, nil, err 58 | } 59 | 60 | cmd, args := e.cmdAndArgs() 61 | 62 | if cmd == "" { 63 | return 255, nil, fmt.Errorf("missing cmd or shell option") 64 | } 65 | 66 | // expand the workspace var and any env vars for the vars, command and args 67 | e.Env = expandEnvOpts(e.Env, ws.BasePath) 68 | for i, arg := range args { 69 | args[i] = expandEnv(arg, ws.BasePath) 70 | } 71 | cmd = expandWs(cmd, ws.BasePath) 72 | // use any cmd on the new env path, rather than current path 73 | cmd = useEnvPathCmd(cmd, e.Env) 74 | 75 | // add in the env var path to the workspace so scripts can use it 76 | e.Env = append(e.Env, "FLOEWS="+ws.BasePath) 77 | 78 | status := doRun(filepath.Join(ws.BasePath, e.SubDir), e.Env, output, cmd, args...) 79 | 80 | return status, Opts{}, nil 81 | } 82 | 83 | func doRun(dir string, env []string, output chan string, cmd string, args ...string) int { 84 | stop := make(chan bool) 85 | out := make(chan string) 86 | 87 | output <- "in dir: " + dir + "\n" 88 | go func() { 89 | for o := range out { 90 | output <- o 91 | } 92 | stop <- true 93 | }() 94 | 95 | status := exe.Run(log.Log{}, out, env, dir, cmd, args...) 96 | 97 | // wait for output to complete 98 | <-stop 99 | 100 | if status != 0 { 101 | output <- fmt.Sprintf("\nexited with status: %d", status) 102 | } 103 | 104 | return status 105 | } 106 | 107 | // expand the workspace template item with the actual workspace 108 | func expandEnvOpts(es []string, path string) []string { 109 | ne := make([]string, len(es)) 110 | for i, e := range es { 111 | ne[i] = expandEnv(e, path) 112 | } 113 | return ne 114 | } 115 | 116 | func expandEnv(e string, path string) string { 117 | parts := strings.Split(e, "=") 118 | if len(parts) == 2 { 119 | return parts[0] + "=" + expandEnvWsDot(parts[1], path) 120 | } 121 | return expandEnvWsDot(e, path) 122 | } 123 | 124 | func expandEnvWsDot(e string, path string) string { 125 | if len(e) == 0 { 126 | return e 127 | } 128 | // relative values substitution 129 | // any "." on its own or anything starting ./ but not ./.. 130 | if len(e) == 1 && e[0] == '.' { 131 | e = wsSub 132 | } else if strings.HasPrefix(e, shortRel) && !strings.HasPrefix(e, "./...") { 133 | fmt.Printf("replacing in <%s>\n", e) 134 | e = strings.Replace(e, shortRel, wsSub+"/", 1) 135 | } 136 | 137 | return expandWs(e, path) 138 | } 139 | 140 | func expandWs(e string, path string) string { 141 | // avoid any //{{ws}} 142 | e = strings.Replace(e, "/"+wsSub, wsSub, -1) 143 | return os.ExpandEnv(strings.Replace(e, wsSub, path, -1)) 144 | } 145 | 146 | func useEnvPathCmd(cmd string, env []string) string { 147 | // find path 148 | for _, e := range env { 149 | parts := strings.Split(e, "=") 150 | if len(parts) == 2 && parts[0] == "PATH" { 151 | c := lookPath(cmd, parts[1]) 152 | if c != "" { 153 | return c 154 | } 155 | return cmd 156 | } 157 | } 158 | return cmd 159 | } 160 | 161 | // ErrNotFound is the error resulting if a path search failed to find an executable file. 162 | var ErrNotFound = errors.New("executable file not found in $PATH") 163 | 164 | func findExecutable(file string) error { 165 | d, err := os.Stat(file) 166 | if err != nil { 167 | return err 168 | } 169 | if m := d.Mode(); !m.IsDir() && m&0111 != 0 { 170 | return nil 171 | } 172 | return os.ErrPermission 173 | } 174 | 175 | func lookPath(file string, path string) string { 176 | if strings.Contains(file, "/") { 177 | err := findExecutable(file) 178 | if err == nil { 179 | return file 180 | } 181 | return "" 182 | } 183 | for _, dir := range filepath.SplitList(path) { 184 | if dir == "" { 185 | // Unix shell semantics: path element "" means "." 186 | dir = "." 187 | } 188 | path := filepath.Join(dir, file) 189 | if err := findExecutable(path); err == nil { 190 | return path 191 | } 192 | } 193 | return "" 194 | } 195 | -------------------------------------------------------------------------------- /config/nodetype/exec_test.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | import ( 4 | "io/ioutil" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestExec(t *testing.T) { 10 | e := exec{} 11 | op := make(chan string) 12 | go func() { 13 | for l := range op { 14 | println(l) 15 | } 16 | }() 17 | 18 | e.Execute(&Workspace{}, Opts{ 19 | "cmd": "echo foo", 20 | }, op) 21 | 22 | e.Execute(&Workspace{}, Opts{ 23 | "shell": "export", 24 | }, op) 25 | 26 | close(op) 27 | } 28 | 29 | func TestCmdAndArgs(t *testing.T) { 30 | e := exec{ 31 | Cmd: "foo bar", 32 | } 33 | cmd, arg := e.cmdAndArgs() 34 | if cmd != "foo" { 35 | t.Error("cmd should be foo") 36 | } 37 | if arg[0] != "bar" { 38 | t.Error("arg should be bar") 39 | } 40 | 41 | e = exec{ 42 | Shell: "foo bar", 43 | } 44 | cmd, arg = e.cmdAndArgs() 45 | if cmd != "bash" { 46 | t.Error("cmd should be bash", cmd) 47 | } 48 | if arg[0] != "-c" || arg[1] != "foo bar" { 49 | t.Error("arg should be '-c' 'foo bar'", arg) 50 | } 51 | } 52 | 53 | func TestEnvVars(t *testing.T) { 54 | opts := Opts{ 55 | "shell": "export", 56 | "env": []string{"DAN=fart"}, 57 | } 58 | testNode(t, "exe env vars", exec{}, opts, []string{`DAN="fart"`, `FLOEWS="`}) 59 | } 60 | 61 | func testNode(t *testing.T, msg string, nt NodeType, opts Opts, expected []string) bool { 62 | op := make(chan string) 63 | var out []string 64 | captured := make(chan bool) 65 | go func() { 66 | for l := range op { 67 | out = append(out, l) 68 | } 69 | captured <- true 70 | }() 71 | 72 | tmp, err := ioutil.TempDir("", "floe-test") 73 | if err != nil { 74 | t.Fatal("can't create tmp dir") 75 | } 76 | tmpBase, err := ioutil.TempDir("", "floe-test") 77 | if err != nil { 78 | t.Fatal("can't create tmp dir") 79 | } 80 | 81 | nt.Execute(&Workspace{ 82 | BasePath: tmpBase, 83 | FetchCache: tmp, 84 | }, opts, op) 85 | 86 | close(op) 87 | 88 | <-captured 89 | 90 | prob := false 91 | for _, x := range expected { 92 | found := false 93 | for _, l := range out { 94 | if strings.Contains(l, x) { 95 | found = true 96 | break 97 | } 98 | } 99 | if !found { 100 | prob = true 101 | t.Error(msg, "did not find expected:", x) 102 | } 103 | } 104 | // output the output if there was a problem 105 | t.Log("cache is at:", tmp) 106 | for _, o := range out { 107 | t.Log(o) 108 | } 109 | 110 | return prob 111 | } 112 | 113 | func TestExpandEnvOpts(t *testing.T) { 114 | t.Parallel() 115 | 116 | fxs := []struct { 117 | in string 118 | exp string 119 | }{ 120 | { 121 | in: "OOF={{ws}}/oof", 122 | exp: "OOF=/base/path/oof", 123 | }, 124 | { 125 | in: "./go", 126 | exp: "/base/path/go", 127 | }, 128 | { 129 | in: "OOF=./oof", 130 | exp: "OOF=/base/path/oof", 131 | }, 132 | { 133 | in: "/go{{ws}}", 134 | exp: "/go/base/path", 135 | }, 136 | { 137 | in: "/go/{{ws}}", 138 | exp: "/go/base/path", 139 | }, 140 | { 141 | in: "./...", 142 | exp: "./...", 143 | }, 144 | { 145 | in: "./../", 146 | exp: "/base/path/../", 147 | }, 148 | } 149 | 150 | for i, fx := range fxs { 151 | got := expandEnv(fx.in, "/base/path") 152 | if got != fx.exp { 153 | t.Errorf("%d, failed, in: %s, got: %s, wanted: %s", i, fx.in, got, fx.exp) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /config/nodetype/fetch.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "crypto/sha256" 7 | "encoding/hex" 8 | "fmt" 9 | "hash" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "github.com/cavaliercoder/grab" 16 | ) 17 | 18 | type fetchOpts struct { 19 | URL string `json:"url"` // url of the file to download 20 | Checksum string `json:"checksum"` // the checksum (and typically filename) 21 | ChecksumAlgo string `json:"checksum-algo"` // the checksum algorithm 22 | Location string `json:"location"` // where to download 23 | } 24 | 25 | // fetch downloads stuff if it is not in the cache 26 | type fetch struct{} 27 | 28 | func (g fetch) Match(ol, or Opts) bool { 29 | return true 30 | } 31 | 32 | func (g fetch) Execute(ws *Workspace, in Opts, output chan string) (int, Opts, error) { 33 | 34 | fop := fetchOpts{} 35 | err := decode(in, &fop) 36 | if err != nil { 37 | return 255, nil, err 38 | } 39 | 40 | if fop.URL == "" { 41 | return 255, nil, fmt.Errorf("problem getting fetch url option") 42 | } 43 | if fop.Checksum == "" { 44 | output <- "(N.B. fetch without a checksum can not be trusted)" 45 | } 46 | 47 | client := grab.NewClient() 48 | req, err := grab.NewRequest(ws.FetchCache, fop.URL) 49 | if err != nil { 50 | output <- fmt.Sprintf("Error setting up the download %v", err) 51 | return 255, nil, err 52 | } 53 | 54 | // set up any checksum 55 | if len(fop.Checksum) > 0 { 56 | // is it in the sum filename format e.g. ba411cafee2f0f702572369da0b765e2 bodhi-4.1.0-64.iso 57 | parts := strings.Split(fop.Checksum, " ") 58 | if len(parts) > 1 { 59 | fop.Checksum = parts[0] 60 | } 61 | checksum, err := hex.DecodeString(fop.Checksum) 62 | if err != nil { 63 | output <- fmt.Sprintf("Error decoding hex checksum: %s", fop.Checksum) 64 | return 255, nil, err 65 | } 66 | 67 | var h hash.Hash 68 | switch fop.ChecksumAlgo { 69 | case "sha256": 70 | h = sha256.New() 71 | case "sha1": 72 | h = sha1.New() 73 | case "md5": 74 | h = md5.New() 75 | } 76 | req.SetChecksum(h, checksum, true) 77 | } 78 | 79 | started := time.Now() 80 | // start download 81 | output <- fmt.Sprintf("Downloading %v...", req.URL()) 82 | resp := client.Do(req) 83 | output <- fmt.Sprintf(" %v", resp.HTTPResponse.Status) 84 | 85 | // start UI loop 86 | t := time.NewTicker(300 * time.Millisecond) 87 | defer t.Stop() 88 | 89 | Loop: 90 | for { 91 | select { 92 | case <-t.C: 93 | output <- fmt.Sprintf(" %v / %v bytes (%.2f%%)", resp.BytesComplete(), resp.Size, 100*resp.Progress()) 94 | case <-resp.Done: 95 | break Loop 96 | } 97 | } 98 | // check for errors, emit it and bail 99 | if err := resp.Err(); err != nil { 100 | output <- fmt.Sprintf("Download failed: %v", err) 101 | return 255, nil, err 102 | } 103 | output <- fmt.Sprintf(" %v / %v bytes (%.2f%%) in %v", resp.BytesComplete(), resp.Size, 100*resp.Progress(), time.Since(started)) 104 | output <- fmt.Sprintf("Download saved to %v", resp.Filename) 105 | 106 | // if no location was given to link it to then link it to the root of the workspace 107 | // this will be used to link to the file in the cache 108 | if fop.Location == "" { 109 | fop.Location = filepath.Join(wsSub, filepath.Base(resp.Filename)) 110 | } else if fop.Location[0] != filepath.Separator { // relative paths are relative to the workspace 111 | filepath.Join(wsSub, fop.Location) 112 | } 113 | // if the location is a folder (ends in '/') and not a file name then add the filename 114 | if fop.Location[len(fop.Location)-1] == filepath.Separator { 115 | fop.Location = filepath.Join(fop.Location, filepath.Base(resp.Filename)) 116 | } 117 | fop.Location = expandEnv(fop.Location, ws.BasePath) 118 | os.Remove(fop.Location) 119 | err = os.Link(resp.Filename, fop.Location) 120 | if err != nil { 121 | return 255, nil, err 122 | } 123 | output <- fmt.Sprintf("Download linked to %v", fop.Location) 124 | 125 | return 0, nil, nil 126 | } 127 | -------------------------------------------------------------------------------- /config/nodetype/fetch_test.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "testing" 8 | ) 9 | 10 | func TestFetch(t *testing.T) { 11 | portCh := make(chan int) 12 | 13 | go serveFiles(portCh) 14 | 15 | port := <-portCh 16 | 17 | success := []string{`(100.00%)`, `Downloading`, "200 OK"} 18 | fail := []string{"404", "Not Found"} 19 | fixtures := []struct { 20 | url string 21 | algo string 22 | checksum string 23 | expected []string 24 | }{ 25 | { // simple dl 26 | url: fmt.Sprintf("http://127.0.0.1:%d/get-file.txt", port), 27 | expected: success, 28 | }, 29 | { // dl with sha256 check 30 | url: fmt.Sprintf("http://127.0.0.1:%d/get-file.txt", port), 31 | algo: "sha256", 32 | checksum: "864d6473d56d235de9ffb9d404e76f23e4d596ce77eae5b7ce5106f454fa7ee4 get-file.txt", 33 | expected: success, 34 | }, 35 | { // dl with sha1 check 36 | url: fmt.Sprintf("http://127.0.0.1:%d/get-file.txt", port), 37 | algo: "sha1", 38 | checksum: "bb3357153aa8e2c0b22fef75a7f21969abb7c2b4", 39 | expected: success, 40 | }, 41 | { // dl with sha256 check 42 | url: fmt.Sprintf("http://127.0.0.1:%d/get-file.txt", port), 43 | algo: "md5", 44 | checksum: "f35ff35df6efc82e474e97eaf10e7ff6", 45 | expected: success, 46 | }, 47 | { // good dl bad checksum 48 | url: fmt.Sprintf("http://127.0.0.1:%d/get-file.txt", port), 49 | algo: "sha256", 50 | checksum: "load-of bollox", 51 | expected: []string{"Error", "hex", "checksum"}, 52 | }, 53 | { // bad dl 54 | url: fmt.Sprintf("http://127.0.0.1:%d/wont_be_found", port), 55 | expected: fail, 56 | }, 57 | { // good external check 58 | url: "https://dl.google.com/go/go1.10.2.src.tar.gz", 59 | algo: "sha256", 60 | checksum: "6264609c6b9cd8ed8e02ca84605d727ce1898d74efa79841660b2e3e985a98bd go1.10.2.src.tar.gz", 61 | expected: success, 62 | }, 63 | } 64 | 65 | for i, fx := range fixtures { 66 | opts := Opts{ 67 | "url": fx.url, 68 | "checksum": fx.checksum, 69 | "checksum-algo": fx.algo, 70 | } 71 | testNode(t, fmt.Sprintf("fetch test: %d", i), fetch{}, opts, fx.expected) 72 | } 73 | } 74 | 75 | // simple local server that returns a bit of content 76 | func serveFiles(portChan chan int) { 77 | listener, err := net.Listen("tcp", ":0") 78 | if err != nil { 79 | panic(err) 80 | } 81 | mux := http.NewServeMux() 82 | mux.HandleFunc("/get-file.txt", func(w http.ResponseWriter, r *http.Request) { 83 | message := "this is a file with known hashes" 84 | w.Write([]byte(message)) 85 | }) 86 | portChan <- listener.Addr().(*net.TCPAddr).Port 87 | http.Serve(listener, mux) 88 | } 89 | -------------------------------------------------------------------------------- /config/nodetype/git.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | 7 | "github.com/floeit/floe/log" 8 | ) 9 | 10 | type gitOpts struct { 11 | URL string `json:"url"` // the repo URL 12 | SubDir string `json:"sub-dir"` // the sub dir to check into 13 | Branch string `json:"branch"` // what to checkout 14 | Hash string `json:"hash"` // the exact hash for repeatability 15 | FromBranch string `json:"from-branch"` // what to checkout and rebase onto Ref 16 | KeyFile string `json:"key-file"` // what key file to use 17 | } 18 | 19 | // gitMerge is an executable node that checks out a hash and then 20 | // checks out another - and then merges into it from the other 21 | type gitMerge struct{} 22 | 23 | func (g gitMerge) Match(ol, or Opts) bool { 24 | return true 25 | } 26 | 27 | func (g gitMerge) Execute(ws *Workspace, in Opts, output chan string) (int, Opts, error) { 28 | 29 | gop := gitOpts{} 30 | err := decode(in, &gop) 31 | if err != nil { 32 | return 255, nil, err 33 | } 34 | 35 | if gop.URL == "" { 36 | return 255, nil, fmt.Errorf("problem getting git url option") 37 | } 38 | if gop.Branch == "" { 39 | return 255, nil, fmt.Errorf("problem getting ref option") 40 | } 41 | if gop.FromBranch == "" { 42 | return 255, nil, fmt.Errorf("problem getting from ref option") 43 | } 44 | 45 | output <- "git checkout: " + gop.URL + " merge into: " + gop.Branch + " from: " + gop.FromBranch 46 | 47 | log.Debug("GIT merge ", gop.URL, " merge into: ", gop.Branch, " from: ", gop.FromBranch) 48 | return 0, nil, nil 49 | } 50 | 51 | // gitCheckout checks out a has from a url 52 | type gitCheckout struct{} 53 | 54 | func (g gitCheckout) Match(ol, or Opts) bool { 55 | return true 56 | } 57 | 58 | func (g gitCheckout) Execute(ws *Workspace, in Opts, output chan string) (int, Opts, error) { 59 | gop := gitOpts{} 60 | err := decode(in, &gop) 61 | if err != nil { 62 | return 255, nil, err 63 | } 64 | if gop.Branch == "" { 65 | return 255, nil, fmt.Errorf("problem getting branch option") 66 | } 67 | if gop.URL == "" { 68 | return 255, nil, fmt.Errorf("problem getting git url option") 69 | } 70 | 71 | log.Debug("GIT clone ", gop.URL, "into:", gop.Branch, "into:", gop.SubDir) 72 | 73 | // for testing 74 | if gop.URL == "git@github.com:floeit/floe-test.git" { 75 | output <- "in dir: /Users/Dan/.flow/spaces/danmux/ws/h1-12/src/github.com/floeit" 76 | output <- "git clone --branch master --depth 1 git@github.com:floeit/floe-test.git" 77 | output <- "Cloning into 'floe'..." 78 | return 0, nil, nil 79 | } 80 | var env []string 81 | if gop.KeyFile != "" { 82 | env = []string{fmt.Sprintf(`GIT_SSH_COMMAND=ssh -i %s`, gop.KeyFile)} 83 | } 84 | // git clone --branch mytag0.1 --depth 1 https://example.com/my/repo.git 85 | args := []string{"clone", "--branch", gop.Branch, "--depth", "1", gop.URL} 86 | status := doRun(filepath.Join(ws.BasePath, gop.SubDir), env, output, "git", args...) 87 | 88 | return status, nil, nil 89 | } 90 | -------------------------------------------------------------------------------- /config/nodetype/node_type.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | import "github.com/mitchellh/mapstructure" 4 | 5 | // NType are the node types 6 | type NType string 7 | 8 | // NType reserved node types 9 | const ( 10 | NtEnd NType = "end" // the special end node 11 | NtData NType = "data" 12 | NtTimer NType = "timer" 13 | NtExec NType = "exec" 14 | NtFetch NType = "fetch" 15 | NtGitMerge NType = "git-merge" 16 | NtGitCheckout NType = "git-checkout" 17 | ) 18 | 19 | // NodeType is the interface for a node. All implementations on NodeType are stateless 20 | // THe Execute method must be a pure(ish) function operating on in and returning an out Opts 21 | type NodeType interface { 22 | Match(Opts, Opts) bool 23 | Execute(ws *Workspace, in Opts, output chan string) (int, Opts, error) 24 | } 25 | 26 | var nts = map[NType]NodeType{ 27 | NtData: data{}, 28 | NtTimer: timer{}, 29 | NtExec: exec{}, 30 | NtFetch: fetch{}, 31 | NtGitMerge: gitMerge{}, 32 | NtGitCheckout: gitCheckout{}, 33 | } 34 | 35 | // GetNodeType returns the node from the given the type and opts 36 | func GetNodeType(ty string) NodeType { 37 | return nts[NType(ty)] 38 | } 39 | 40 | func decode(input interface{}, output interface{}) error { 41 | 42 | decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ 43 | Metadata: nil, 44 | Result: output, 45 | TagName: "json", 46 | }) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | return decoder.Decode(input) 52 | } 53 | -------------------------------------------------------------------------------- /config/nodetype/opts.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | // Workspace is anything specific to a workspace for a single run or any locations common between runs 4 | type Workspace struct { 5 | BasePath string // The root path for this workspace 6 | FetchCache string // The host level cache of downloaded files (not per workspace, but handy to have listed in this struct) 7 | } 8 | 9 | // Opts are the options on the node type that will be compared to those on the event 10 | type Opts map[string]interface{} 11 | 12 | func (o Opts) string(key string) (string, bool) { 13 | si, ok := o[key] 14 | if !ok { 15 | return "", false 16 | } 17 | s, ok := si.(string) 18 | if !ok { 19 | return "", false 20 | } 21 | return s, true 22 | } 23 | 24 | func (o Opts) int(key string) (int, bool) { 25 | si, ok := o[key] 26 | if !ok { 27 | return 0, false 28 | } 29 | s, ok := si.(int) 30 | if !ok { 31 | fs, fok := si.(float64) 32 | if !fok { 33 | return 0, false 34 | } 35 | return int(fs), true 36 | } 37 | return s, true 38 | } 39 | 40 | func (o Opts) cmpString(key string, or Opts) bool { 41 | sl, ok := o.string(key) 42 | if !ok { 43 | return false 44 | } 45 | sr, ok := or.string(key) 46 | if !ok { 47 | return false 48 | } 49 | return sl == sr 50 | } 51 | 52 | // MergeOpts merges l and r into a new Opts struct, r taking precedence 53 | func MergeOpts(l, r Opts) Opts { 54 | o := Opts{} 55 | for k, v := range l { 56 | o[k] = v 57 | } 58 | for k, v := range r { 59 | // most arrays will be a full replacement, but environment variables get appended 60 | if k == "env" { 61 | if v1, ok := o[k]; ok { 62 | o[k] = appendIfArr(v1, v) 63 | continue 64 | } 65 | } 66 | o[k] = v 67 | } 68 | return o 69 | } 70 | 71 | // appendIfArr appends r to l, if they are both []string else any one of them that is []string 72 | // is returned, else nil is returned 73 | func appendIfArr(l interface{}, r interface{}) []interface{} { 74 | la, lok := l.([]interface{}) 75 | ra, rok := r.([]interface{}) 76 | 77 | if !lok && !rok { 78 | return nil 79 | } 80 | if !lok && rok { 81 | return ra 82 | } 83 | if lok && !rok { 84 | return la 85 | } 86 | return append(la, ra...) 87 | } 88 | 89 | // Fixup allows the receiver to be able to be rendered as json 90 | func (o *Opts) Fixup() { 91 | for k, v := range *o { 92 | (*o)[k] = yamlToJSON(v) 93 | } 94 | } 95 | 96 | // yamlToJSON takes the generic Yaml maps with interface keys 97 | // and converts them into the json string based keys 98 | func yamlToJSON(in interface{}) interface{} { 99 | // TODO - consider mapstructure 100 | if m, ok := in.(map[interface{}]interface{}); ok { 101 | o := map[string]interface{}{} 102 | for k, v := range m { 103 | o[k.(string)] = yamlToJSON(v) 104 | } 105 | return o 106 | } 107 | if m, ok := in.([]interface{}); ok { 108 | o := make([]interface{}, len(m)) 109 | for i, v := range m { 110 | o[i] = yamlToJSON(v) 111 | } 112 | return o 113 | } 114 | 115 | return in 116 | } 117 | -------------------------------------------------------------------------------- /config/nodetype/opts_test.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestMergeOpts(t *testing.T) { 8 | l := Opts{ 9 | "foo": "bar", 10 | "env": []interface{}{ 11 | "lar0", 12 | "lar1", 13 | "cmn", 14 | }, 15 | } 16 | r := Opts{ 17 | "baz": "boo", 18 | "env": []interface{}{ 19 | "rar0", 20 | "rar1", 21 | "cmn", 22 | }, 23 | } 24 | o := MergeOpts(l, r) 25 | envs, ok := o["env"] 26 | if !ok { 27 | t.Fatal("no env") 28 | } 29 | e, ok := envs.([]interface{}) 30 | if !ok { 31 | t.Fatal("env not slice") 32 | } 33 | if len(e) != 6 { 34 | t.Fatal("opts env merge failed", len(e)) 35 | } 36 | if e[4].(string) != "rar1" { 37 | t.Error("right env not appended") 38 | } 39 | 40 | l = Opts{ 41 | "foo": "bar", 42 | } 43 | r = Opts{ 44 | "baz": "boo", 45 | "env": []interface{}{ 46 | "rar0", 47 | }, 48 | } 49 | o = MergeOpts(l, r) 50 | _, ok = o["env"] 51 | if !ok { 52 | t.Fatal("no env when it did not exist") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /config/nodetype/timer.go: -------------------------------------------------------------------------------- 1 | package nodetype 2 | 3 | type timer struct{} 4 | 5 | func (d timer) Match(qs, as Opts) bool { 6 | qp, ok := qs.int("period") 7 | if !ok { 8 | return false 9 | } 10 | 11 | ap, ok := as.int("period") 12 | if !ok { 13 | return false 14 | } 15 | 16 | return qp == ap 17 | } 18 | 19 | // Execute 20 | func (d timer) Execute(ws *Workspace, in Opts, output chan string) (int, Opts, error) { 21 | 22 | return 0, in, nil 23 | } 24 | -------------------------------------------------------------------------------- /dev/config.yml: -------------------------------------------------------------------------------- 1 | common: 2 | base-url: "/build/api" 3 | store-type: local 4 | workspace-root: /home/ubuntu/floews 5 | git-key: "/home/ubuntu/.ssh/id_floedemo_rsa" 6 | hosts: 7 | - http://127.0.0.1:8080 # ourself only 8 | 9 | flows: 10 | - id: floe 11 | name: Floe CI 12 | ver: 1 13 | 14 | triggers: 15 | - name: start 16 | type: data 17 | opts: 18 | url: git@github.com:floeit/floe.git 19 | form: 20 | title: Start 21 | fields: 22 | - id: branch 23 | prompt: Branch 24 | type: text 25 | 26 | - name: Master Daily 27 | type: timer # fires every period seconds 28 | opts: 29 | period: 86400 # how often to fire in seconds 30 | url: git@github.com:floeit/floe.git 31 | branch: master 32 | 33 | - name: Commits 34 | type: poll-git 35 | opts: 36 | period: 10 # check every 10 seconds 37 | url: git@github.com:floeit/floe.git # the repo to check 38 | refs: "refs/heads/*" # the refs pattern to match 39 | 40 | flow-file: /home/ubuntu/floe/floe.yml # get the actual flow from this file 41 | 42 | 43 | -------------------------------------------------------------------------------- /dev/docker/.gitignore: -------------------------------------------------------------------------------- 1 | *_rsa* -------------------------------------------------------------------------------- /dev/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:trusty 2 | 3 | # gcc for cgo 4 | RUN apt-get update && apt-get install -y --no-install-recommends \ 5 | g++ \ 6 | gcc \ 7 | libc6-dev \ 8 | make \ 9 | pkg-config \ 10 | curl \ 11 | git \ 12 | openssh-client \ 13 | openssh-server \ 14 | && rm -rf /var/lib/apt/lists/* 15 | 16 | ENV GOLANG_VERSION 1.10.2 17 | 18 | # Install Go 19 | RUN set -eux; \ 20 | mkdir -p /usr/local/go; \ 21 | curl --insecure https://dl.google.com/go/go${GOLANG_VERSION}.linux-amd64.tar.gz | tar xvzf - -C /usr/local/go --strip-components=1 22 | 23 | # add the deployment private key 24 | COPY deploy_rsa /root/.ssh/id_rsa 25 | RUN echo " IdentityFile ~/.ssh/id_rsa" >> /etc/ssh/ssh_config 26 | 27 | # Set environment variables. 28 | ENV PATH /usr/local/go/bin:$PATH 29 | 30 | WORKDIR /root 31 | -------------------------------------------------------------------------------- /dev/docker/README.md: -------------------------------------------------------------------------------- 1 | Testing on Docker 2 | ================= 3 | 4 | To test your flow in isolation from your host tooling and environment its a good plan to test in a container. 5 | 6 | From this docker folder (`cd dev/docker`) - generate your key pair 7 | 8 | ``` 9 | ssh-keygen -N "" -t rsa -b 4096 -C "build@floe.it" -f deploy_rsa 10 | ``` 11 | 12 | Add the public key deploy_rsa.pub to the github repo deploy key (TODO link) 13 | 14 | Rebuild the image (still from this dev folder) ... 15 | 16 | `docker build -t golang:latest .` 17 | 18 | and shell onto it... 19 | 20 | `docker run -it golang /bin/bash` 21 | 22 | Do your ting... 23 | 24 | -------------------------------------------------------------------------------- /dev/env.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | result=${PWD} 4 | export GOPATH=$result 5 | export PATH=$PATH:$result/bin 6 | -------------------------------------------------------------------------------- /dev/floe.yml: -------------------------------------------------------------------------------- 1 | id: floe 2 | host-tags: [linux, go, couch] # all these tags must match the tags on any host for it to be able to run there 3 | env: 4 | - GOPATH=./ 5 | - GOROOT=./go 6 | - PATH=./go/bin:$PATH 7 | ver: 1 8 | 9 | tasks: 10 | - name: Checkout 11 | listen: trigger.good 12 | type: git-checkout 13 | opts: 14 | url: git@github.com:floeit/floe.git 15 | sub-dir: src/github.com/floeit 16 | 17 | - name: Download Go 18 | listen: task.checkout.good 19 | type: fetch 20 | opts: 21 | url: "https://dl.google.com/go/go1.10.2.linux-amd64.tar.gz" 22 | checksum: "4b677d698c65370afa33757b6954ade60347aaca310ea92a63ed717d7cb0c2ff" 23 | checksum-algo: "sha256" 24 | 25 | - name: Expand Go 26 | listen: task.download-go.good 27 | type: exec 28 | opts: 29 | cmd: "tar -xf go1.10.2.linux-amd64.tar.gz" 30 | 31 | - name: Go Version 32 | listen: task.expand-go.good 33 | type: exec 34 | opts: 35 | shell: "go version" 36 | 37 | - name: Build 38 | listen: task.go-version.good 39 | type: exec 40 | opts: 41 | cmd: "go install ./..." 42 | sub-dir: src/github.com/floeit/floe 43 | 44 | - name: Tests 45 | listen: task.build.good 46 | type: exec 47 | opts: 48 | cmd: "go test ./..." 49 | sub-dir: src/github.com/floeit/floe 50 | 51 | - name: done 52 | listen: task.tests.good 53 | type: end 54 | -------------------------------------------------------------------------------- /dev/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | # example startup commands for the demo.floe.it server. 5 | # for your own use change the certs or remove them, change the public ip. 6 | sudo ./floe \ 7 | -tags=linux,go,couch \ 8 | -admin=123456 \ 9 | -host_name=h1 \ 10 | -pub_bind=172.31.22.66:443 \ 11 | -conf=config.yml \ 12 | -pub_cert=/etc/letsencrypt/live/demo.floe.it/fullchain.pem \ 13 | -pub_key=/etc/letsencrypt/live/demo.floe.it/privkey.pem \ 14 | -prv_bind=127.0.0.1:8080 15 | -------------------------------------------------------------------------------- /dev/upgrade.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | cd floe 5 | rm floe 6 | wget https://s3-eu-west-1.amazonaws.com/floe-deploys/floe 7 | chmod +x floe 8 | sudo supervisorctl stop floe 9 | sleep 2 10 | sudo supervisorctl start floe -------------------------------------------------------------------------------- /event/event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/floeit/floe/config" 9 | nt "github.com/floeit/floe/config/nodetype" 10 | "github.com/floeit/floe/log" 11 | ) 12 | 13 | const sysPrefix = "sys." // all internal events that nodes can not see 14 | 15 | // HostedIDRef is any ID unique within the scope of the host that created it. 16 | type HostedIDRef struct { 17 | HostID string 18 | ID int64 19 | } 20 | 21 | func (h HostedIDRef) String() string { 22 | if h.ID == 0 { 23 | return "na" 24 | } 25 | return fmt.Sprintf("%s-%d", h.HostID, h.ID) 26 | } 27 | 28 | // Equal returns true if h and g are considered equal 29 | func (h HostedIDRef) Equal(g HostedIDRef) bool { 30 | return h.HostID == g.HostID && h.ID == g.ID 31 | } 32 | 33 | // Equals compares receiver with param rh 34 | func (h HostedIDRef) Equals(rh HostedIDRef) bool { 35 | return h.HostID == rh.HostID && h.ID == rh.ID 36 | } 37 | 38 | // RunRef uniquely identifies and routes a particular run across the whole cluster 39 | type RunRef struct { 40 | // FlowRef identifies the flow that this reference relates to 41 | FlowRef config.FlowRef 42 | 43 | // Run identifies the host and id that this run was initiated by. 44 | // This is a cluster unique reference, which may not refer to the node that is 45 | // executing the Run (that will be defined by ExecHost) 46 | Run HostedIDRef 47 | 48 | // ExecHost is the host that is actually executing, or executed this event, 49 | // use in conjunction with Run to find the active and archived run 50 | ExecHost string 51 | } 52 | 53 | func (r RunRef) String() string { 54 | return fmt.Sprintf("runref_%s_%s", r.FlowRef, r.Run) 55 | } 56 | 57 | // Equal returns true ir r and s are considered to refer to the same thing 58 | func (r RunRef) Equal(s RunRef) bool { 59 | return r.FlowRef.Equal(s.FlowRef) && r.Run.Equal(s.Run) 60 | } 61 | 62 | // Adopted means that this RunRef has been added to a pending list and been assigned a 63 | // unique run ID 64 | func (r RunRef) Adopted() bool { 65 | if r.Run.ID == 0 { 66 | return false 67 | } 68 | return true 69 | } 70 | 71 | // Observer defines the interface for observers. 72 | type Observer interface { 73 | Notify(e Event) 74 | } 75 | 76 | // Event defines a moment in time thing occurring 77 | type Event struct { 78 | // RunRef if this event is in the scope of a specific run 79 | // if nil then is a general event that could be routed to triggers 80 | RunRef RunRef 81 | 82 | // SourceNode is the Ref of the node in the context of a RunRef 83 | SourceNode config.NodeRef 84 | 85 | // Tag is the label that helps route the event. 86 | // it will match node.Type for trigger nodes, and node.Listen for task and merge nodes. 87 | Tag string 88 | 89 | // Good specifically when this is classed as a good event 90 | Good bool 91 | 92 | // Unique and ordered event ID within a Run. An ID greater than another 93 | // ID must have happened after it within the context of the RunRef. 94 | // A flow initiating trigger will have ID 1. 95 | ID int64 96 | 97 | // Opts - some optional data in the event 98 | Opts nt.Opts 99 | } 100 | 101 | // copy makes a copy without sharing the underlying Opts aps. 102 | // Any pointers in the opts map (there should not be) will share memory 103 | func (e Event) copy() Event { 104 | newE := e 105 | // break the common map memory link 106 | newE.Opts = nt.Opts{} 107 | for k, v := range e.Opts { 108 | newE.Opts[k] = v 109 | } 110 | return newE 111 | } 112 | 113 | // SetGood sets this event as a good event 114 | func (e *Event) SetGood() { 115 | e.Good = true 116 | e.Tag = fmt.Sprintf("%s.%s.good", e.SourceNode.Class, e.SourceNode.ID) 117 | } 118 | 119 | // IsSystem returns true if the event is a internal system event 120 | func (e *Event) IsSystem() bool { 121 | if len(e.Tag) < 3 { 122 | return false 123 | } 124 | return strings.HasPrefix(e.Tag, sysPrefix) 125 | } 126 | 127 | // Queue is not strictly a queue, it just distributes all events to the observers 128 | type Queue struct { 129 | sync.RWMutex 130 | 131 | idCounter int64 132 | // observers are any entities that care about events emitted from the queue 133 | observers []Observer 134 | } 135 | 136 | // Register registers an observer to this q 137 | func (q *Queue) Register(o Observer) { 138 | q.observers = append(q.observers, o) 139 | } 140 | 141 | // Publish sends an event to all the observers 142 | func (q *Queue) Publish(e Event) { 143 | q.Lock() 144 | // grab the next event ID 145 | q.idCounter++ 146 | e.ID = q.idCounter 147 | if e.Opts == nil { 148 | e.Opts = nt.Opts{} 149 | } 150 | q.Unlock() 151 | 152 | // node updates can be noisy - an event is issued for every line of output 153 | // if e.Tag != "sys.node.update" { 154 | // for helpfulness indicate if this event was issued by an already adopted flow 155 | isTrig := " (trigger)" 156 | if e.RunRef.Adopted() { 157 | isTrig = "" 158 | } 159 | log.Debugf("<%s-ev:%d> - queue publish type:<%s>%s from: %s", e.RunRef, e.ID, e.Tag, isTrig, e.SourceNode) 160 | // } 161 | 162 | // and notify all observers - in background goroutines 163 | for _, o := range q.observers { 164 | // send separate copies to each observer to avoid any races 165 | go o.Notify(e.copy()) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /event/event_test.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "testing" 4 | 5 | type listener struct { 6 | what func(e Event) 7 | } 8 | 9 | func (l *listener) Notify(e Event) { 10 | l.what(e) 11 | } 12 | 13 | func TestQueue(t *testing.T) { 14 | q := Queue{} 15 | fired := false 16 | done := make(chan bool) 17 | l := &listener{ 18 | what: func(e Event) { 19 | fired = true 20 | if e.ID != 1 { 21 | t.Error("event ID wrong", e.ID) 22 | } 23 | if e.RunRef.Run.String() != "h1-12" { 24 | t.Error("bad hosted ref", e.RunRef.Run.String()) 25 | } 26 | done <- true 27 | }, 28 | } 29 | 30 | q.Register(l) 31 | 32 | e := Event{ 33 | RunRef: RunRef{ 34 | Run: HostedIDRef{ 35 | HostID: "h1", 36 | ID: 12, 37 | }, 38 | }, 39 | } 40 | 41 | q.Publish(e) 42 | <-done 43 | if !fired { 44 | t.Error("notify not fired") 45 | } 46 | } 47 | 48 | 49 | func TestIsSystem(t *testing.T) { 50 | fix := []struct{ 51 | tag string 52 | is bool 53 | }{ 54 | {"", false}, 55 | {"fo", false}, 56 | {"system.foo", false}, 57 | {"sys.bar", true}, 58 | } 59 | for i, f := range fix { 60 | e := Event{ 61 | Tag: f.tag, 62 | } 63 | if e.IsSystem() != f.is { 64 | t.Errorf("%d - tag %s should have had IsSystem: %v", i, f.tag, f.is) 65 | } 66 | } 67 | } 68 | 69 | // The following benchmarks illustrate the relative performances of 70 | // passing th reference by value or as a pointer. 71 | // as of 2017/11 there is 1ns in it... 72 | // BenchmarkRunRefPBR-8 500000000 3.15 ns/op 73 | // BenchmarkRunRefPBV-8 300000000 4.32 ns/op 74 | func BenchmarkRunRefPBP(b *testing.B) { 75 | 76 | notnop := func (r *RunRef) { 77 | r.ExecHost = "" // hopefully avoid any optimisers? 78 | } 79 | 80 | r := &RunRef{} 81 | for i := 0; i < b.N; i++ { 82 | notnop(r) 83 | } 84 | } 85 | 86 | func BenchmarkRunRefPBV(b *testing.B) { 87 | 88 | notnop := func (r RunRef) { 89 | r.ExecHost = "" // hopefully avoid any optimisers? 90 | } 91 | 92 | r := RunRef{} 93 | for i := 0; i < b.N; i++ { 94 | notnop(r) 95 | } 96 | } -------------------------------------------------------------------------------- /exe/git/git.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | "time" 8 | 9 | "github.com/floeit/floe/exe" 10 | ) 11 | 12 | type logger interface { 13 | Info(...interface{}) 14 | Debug(...interface{}) 15 | Error(...interface{}) 16 | } 17 | 18 | // Ref contains details of a git reference 19 | type Ref struct { 20 | Name string 21 | Type string 22 | Hash string 23 | Change time.Time 24 | } 25 | 26 | // Hashes stores the result of a GitLS 27 | type Hashes struct { 28 | RepoURL string 29 | Hashes map[string]Ref 30 | } 31 | 32 | // Ls list a remote repo 33 | func Ls(log logger, url, pattern, exclude, gitKey string) (*Hashes, bool) { 34 | if pattern == "" { 35 | pattern = "refs/*" 36 | } 37 | 38 | var env []string 39 | if gitKey != "" { 40 | env = []string{fmt.Sprintf(`GIT_SSH_COMMAND=ssh -i %s`, gitKey)} 41 | } 42 | 43 | gitOut, status := exe.RunOutput(log, env, "", "git", "ls-remote", "--refs", url, pattern) 44 | if status != 0 { 45 | return nil, false 46 | } 47 | latestHash := &Hashes{ 48 | RepoURL: url, 49 | } 50 | 51 | // drop the command and blank line 52 | parseGitResponse(gitOut[2:], latestHash, exclude) 53 | return latestHash, true 54 | } 55 | 56 | func parseGitResponse(lines []string, hashes *Hashes, exclude string) { 57 | exclude = strings.TrimSpace(exclude) 58 | excl, _ := regexp.Compile(exclude) 59 | 60 | // map the lines by branch 61 | hashes.Hashes = map[string]Ref{} 62 | now := time.Now().UTC() 63 | for _, l := range lines { // from 2 onwards 1 = command 0 = empty 64 | sl := strings.Fields(l) 65 | 66 | if len(sl) < 2 { 67 | continue 68 | } 69 | 70 | dp := strings.Split(sl[1], "/") 71 | if len(dp) < 3 || dp[0] != "refs" { 72 | continue 73 | } 74 | 75 | if exclude != "" && excl.MatchString(sl[1]) { 76 | continue 77 | } 78 | 79 | ty := "branch" 80 | switch { 81 | case strings.HasPrefix(dp[1], "pull"): 82 | ty = "pull" 83 | case strings.HasPrefix(dp[1], "tag"): 84 | ty = "tag" 85 | } 86 | name := dp[2] 87 | name = strings.TrimSuffix(name, "^{}") 88 | hashes.Hashes[sl[1]] = Ref{ 89 | Name: name, 90 | Type: ty, 91 | Hash: sl[0], 92 | Change: now, 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /exe/git/git_integ_test.go: -------------------------------------------------------------------------------- 1 | // +build integration_test 2 | 3 | package git 4 | 5 | import ( 6 | "testing" 7 | ) 8 | 9 | func TestGitLs(t *testing.T) { 10 | output, ok := Ls(&tLog{t: t}, "git@github.com:floeit/floe.git", "", "") 11 | if !ok { 12 | t.Error("git ls failed") 13 | } 14 | if len(output.Hashes) == 0 { 15 | t.Error("got no hashes") 16 | } 17 | for k, v := range output.Hashes { 18 | t.Log(k, v) 19 | } 20 | 21 | output, ok = Ls(&tLog{t: t}, "git@github.com:floeit/floe.git", "master", "") 22 | if !ok { 23 | t.Error("git ls failed") 24 | } 25 | if len(output.Hashes) != 1 { 26 | t.Error("got more than one master") 27 | } 28 | } 29 | 30 | type tLog struct { 31 | t *testing.T 32 | } 33 | 34 | func (l *tLog) Info(args ...interface{}) { 35 | l.t.Log("INFO", args) 36 | } 37 | func (l *tLog) Debug(args ...interface{}) { 38 | l.t.Log("DEBUG", args) 39 | } 40 | func (l *tLog) Error(args ...interface{}) { 41 | l.t.Log("ERROR", args) 42 | } 43 | func (l *tLog) Infof(format string, args ...interface{}) { 44 | l.t.Logf("INFO - "+format, args...) 45 | } 46 | -------------------------------------------------------------------------------- /exe/git/git_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | ) 7 | 8 | func TestParse(t *testing.T) { 9 | 10 | stashAndGit := `8b650c4565361eab3ba0cd21295c169e55087257 refs/heads/reverted_certs 11 | a6767754ef433da45f8096d06ba1eb7c8de16e3f refs/heads/hoombe 12 | 9906c112ddd51934593fe5f97cba8bbc86e916b2 refs/heads/hoom-nag-message 13 | 5793e9c730a1b464539e304ba61f7e647622ba40 refs/heads/foo-1531910961902 14 | 5fe9cc9bb4bb6da45c112b390219a58bcb0543d6 refs/pull-requests/6638/from 15 | c47f2ec90df29fc4502d7364b635a1454636f2e4 refs/pull-requests/6649/from 16 | d433e9bb255c529450881d18dca62506b525bff2 refs/pull-requests/6649/merge 17 | 855b90cb574bd0945c4e7aa35189cf431abc3cd0 refs/pull-requests/6650/from 18 | bd8d2a6589ec23e455d35689207f3a5319c5f62a refs/tags/v_4.6.911 19 | 461f5ead415c6fa457dcfdc774855cdc9ecc1e3e refs/tags/v_5.6.912 20 | d046e4898b8df7e45fc1e63905a4a950a96452bf refs/tags/_4.6.913 21 | 0db621b7f0cf8dd4545f930f000d8d2f41c65607 refs/tags/ver_4.6.92 22 | 0db621b7f0cf8dd4545f930f000d8d2f41c65607 refs/tags/v_4.6.92^{} 23 | ef0f5274afae6d4f36ee29fa61d4398ad8a6567c HEAD 24 | ef0f5274afae6d4f36ee29fa61d4398ad8a6567c refs/heads/master 25 | fbf6240b17cd4aeedd070b6b5461395602708ace refs/heads/poll-git-changes 26 | 97a574fa05056609f5746afaae42e083477e06cc refs/pull/1/head 27 | 68aebba3d722f158eb59b3cd3f573bd2cf152bba refs/pull/2/head 28 | fbf6240b17cd4aeedd070b6b5461395602708ace refs/pull/3/head 29 | 978ae2def696424956ee03f367233ee20cedc8ad refs/tags/v0.1 30 | ` 31 | 32 | st := strings.Split(stashAndGit, "\n") 33 | h := Hashes{} 34 | parseGitResponse(st, &h, "poll.*") 35 | 36 | if len(h.Hashes) != len(st)-3 { 37 | t.Fatal("HEAD and poll should be ignored") 38 | } 39 | 40 | exp := map[string]Ref{ 41 | // stash ones 42 | "refs/heads/hoom-nag-message": Ref{Name: "hoom-nag-message", Type: "branch", Hash: "9906c112ddd51934593fe5f97cba8bbc86e916b2"}, 43 | "refs/pull-requests/6650/from": Ref{Name: "6650", Type: "pull", Hash: "855b90cb574bd0945c4e7aa35189cf431abc3cd0"}, 44 | "refs/tags/ver_4.6.92": Ref{Name: "ver_4.6.92", Type: "tag", Hash: "0db621b7f0cf8dd4545f930f000d8d2f41c65607"}, 45 | 46 | // git hub ones 47 | "refs/heads/master": Ref{Name: "master", Type: "branch", Hash: "ef0f5274afae6d4f36ee29fa61d4398ad8a6567c"}, 48 | "refs/pull/2/head": Ref{Name: "2", Type: "pull", Hash: "68aebba3d722f158eb59b3cd3f573bd2cf152bba"}, 49 | "refs/tags/v0.1": Ref{Name: "v0.1", Type: "tag", Hash: "978ae2def696424956ee03f367233ee20cedc8ad"}, 50 | } 51 | 52 | for n, ex := range exp { 53 | got := h.Hashes[n] 54 | if ex.Name != got.Name || ex.Hash != got.Hash || ex.Type != got.Type { 55 | t.Errorf("[%s] - parsing failed n:%s h:%s t:%s != n:%s h:%s t:%s", n, ex.Name, ex.Hash, ex.Type, got.Name, got.Hash, got.Type) 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /exe/run.go: -------------------------------------------------------------------------------- 1 | package exe 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | "syscall" 10 | ) 11 | 12 | type logger interface { 13 | Info(...interface{}) 14 | Debug(...interface{}) 15 | Error(...interface{}) 16 | } 17 | 18 | // RunOutput executes the command in a bash process capturing the output and 19 | // returning it in the string slice 20 | func RunOutput(log logger, env []string, wd, cmd string, args ...string) ([]string, int) { 21 | var output []string 22 | 23 | out := make(chan string) 24 | rangeDone := make(chan bool) 25 | go func() { 26 | for t := range out { 27 | output = append(output, t) 28 | } 29 | rangeDone <- true 30 | }() 31 | 32 | status := Run(log, out, env, wd, cmd, args...) 33 | 34 | <-rangeDone 35 | 36 | return output, status 37 | } 38 | 39 | // Run executes the command in a bash process 40 | func Run(log logger, out chan string, env []string, wd, cmd string, args ...string) int { 41 | 42 | log.Info("Exec Cmd:", cmd, "Args:", args) 43 | 44 | if wd != "" { 45 | // make sure working directory is in place 46 | if err := os.MkdirAll(wd, 0700); err != nil { 47 | log.Error(err) 48 | return 1 49 | } 50 | } 51 | 52 | eCmd := exec.Command(cmd, args...) 53 | 54 | eCmd.Env = os.Environ() 55 | eCmd.Env = append(eCmd.Env, env...) 56 | 57 | // this is mandatory 58 | eCmd.Dir = wd 59 | log.Info("In working directory:", eCmd.Dir) 60 | log.Info("Env vars:", eCmd.Env) 61 | 62 | out <- cmd + " " + strings.Join(args, " ") 63 | out <- "" 64 | 65 | // safely aggregate both to a single reader 66 | pr, pw := io.Pipe() 67 | eCmd.Stdout = pw 68 | eCmd.Stderr = pw 69 | 70 | // start scanning from the common pipe 71 | scanDone := make(chan bool) 72 | go func() { 73 | scanner := bufio.NewScanner(pr) 74 | for scanner.Scan() { 75 | out <- scanner.Text() 76 | } 77 | if e := scanner.Err(); e != nil { 78 | out <- "scanning output failed with: " + e.Error() 79 | } 80 | scanDone <- true 81 | }() 82 | 83 | log.Debug("Exec starting") 84 | err := eCmd.Start() 85 | if err != nil { 86 | log.Error("start failed", err) 87 | out <- err.Error() 88 | out <- "" 89 | close(out) 90 | return 1 91 | } 92 | 93 | log.Debug("Exec waiting") 94 | err = eCmd.Wait() 95 | 96 | // close the writer pipe 97 | e := pw.Close() 98 | if e != nil { 99 | panic("not sure how this particular close could error" + err.Error()) 100 | } 101 | 102 | // wait to be sure scanner is fully complete 103 | <-scanDone 104 | close(out) 105 | 106 | log.Debug("exec cmd complete") 107 | 108 | if err != nil { 109 | log.Error("Command failed:", err) 110 | exitCode := 1 111 | if msg, ok := err.(*exec.ExitError); ok { 112 | if status, ok := msg.Sys().(syscall.WaitStatus); ok { 113 | exitCode = status.ExitStatus() 114 | log.Info("exit status: ", exitCode) 115 | } 116 | } 117 | // we prefer to return 0 for good or one for bad 118 | return exitCode 119 | } 120 | 121 | log.Info("Executing command succeeded") 122 | return 0 123 | } 124 | -------------------------------------------------------------------------------- /exe/run_test.go: -------------------------------------------------------------------------------- 1 | package exe 2 | 3 | import ( 4 | "bufio" 5 | "io" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | func TestRun(t *testing.T) { 11 | t.Parallel() 12 | 13 | var output []string 14 | out := make(chan string) 15 | rangeDone := make(chan bool) 16 | go func() { 17 | for t := range out { 18 | output = append(output, t) 19 | } 20 | rangeDone <- true 21 | }() 22 | 23 | status := Run(&tLog{t: t}, out, nil, ".", "echo", "hello world") 24 | 25 | if status != 0 { 26 | t.Error("echo failed", status) 27 | } 28 | <-rangeDone 29 | 30 | if output[2] != "hello world" { 31 | t.Error("bad output", output) 32 | } 33 | 34 | // confirm bad command fails no command found 35 | out = make(chan string, 100) 36 | status = Run(&tLog{t: t}, out, nil, "", "echop", `hello world`) 37 | if status != 1 { 38 | t.Error("status should have been 1", status) 39 | } 40 | } 41 | 42 | func TestRunOutput(t *testing.T) { 43 | t.Parallel() 44 | 45 | out, status := RunOutput(&tLog{t: t}, nil, "", "bash", "-c", `echo "hello world"`) 46 | if status != 0 { 47 | t.Error("echo failed", status) 48 | } 49 | for _, l := range out { 50 | t.Log(">>", l) 51 | } 52 | if out[2] != "hello world" { 53 | t.Errorf("bad output >%s<", out[2]) 54 | } 55 | } 56 | 57 | func TestRunLongOutput(t *testing.T) { 58 | t.Parallel() 59 | 60 | out, status := RunOutput(&tLog{t: t}, nil, "", "bash", "-c", `for i in {1..50}; do echo "hello line number $i"; done`) 61 | if status != 0 { 62 | t.Error("echo failed", status) 63 | } 64 | 65 | for _, o := range out { 66 | t.Log(o) 67 | } 68 | if len(out) != 52 { 69 | t.Errorf("bad output: %d", len(out)) 70 | } 71 | } 72 | 73 | type tLog struct { 74 | t *testing.T 75 | } 76 | 77 | func (l *tLog) Info(args ...interface{}) { 78 | args = append([]interface{}{"INFO"}, args...) 79 | l.t.Log(args...) 80 | } 81 | func (l *tLog) Debug(args ...interface{}) { 82 | args = append([]interface{}{"DEBUG"}, args...) 83 | l.t.Log(args...) 84 | } 85 | func (l *tLog) Error(args ...interface{}) { 86 | args = append([]interface{}{"ERROR"}, args...) 87 | l.t.Log(args...) 88 | } 89 | func (l *tLog) Infof(format string, args ...interface{}) { 90 | l.t.Logf("INFO - "+format, args...) 91 | } 92 | 93 | func TestPlay(t *testing.T) { 94 | 95 | eCmd := exec.Command("bash", "-c", "export") 96 | 97 | pr, pw := io.Pipe() 98 | eCmd.Stdout = pw 99 | eCmd.Stderr = pw 100 | 101 | var output []string 102 | scanDone := make(chan bool) 103 | go func() { 104 | scanner := bufio.NewScanner(pr) 105 | for scanner.Scan() { 106 | t := scanner.Text() 107 | output = append(output, t) 108 | } 109 | if e := scanner.Err(); e != nil { 110 | output = append(output, "scanning output failed with: "+e.Error()) 111 | } 112 | scanDone <- true 113 | }() 114 | 115 | err := eCmd.Start() 116 | if err != nil { 117 | t.Error(err) 118 | return 119 | } 120 | 121 | err = eCmd.Wait() 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | 126 | pr.Close() 127 | <-scanDone 128 | println(len(output)) 129 | } 130 | -------------------------------------------------------------------------------- /floe.yml: -------------------------------------------------------------------------------- 1 | id: danmux 2 | env: 3 | - GOPATH={{ws}} 4 | # - PATH=$PATH:{{ws}}/bin 5 | ver: 1 6 | 7 | tasks: 8 | - name: Checkout 9 | listen: trigger.good 10 | type: git-checkout 11 | opts: 12 | sub-dir: src/github.com/floeit 13 | 14 | - name: Go Version 15 | listen: trigger.good 16 | type: exec 17 | opts: 18 | cmd: "go version" 19 | 20 | - name: Wait 21 | class: merge 22 | wait: 23 | - task.go-version.good 24 | - task.checkout.good 25 | 26 | - name: done 27 | listen: task.wait.good 28 | type: end -------------------------------------------------------------------------------- /hub/setup.go: -------------------------------------------------------------------------------- 1 | package hub 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | nt "github.com/floeit/floe/config/nodetype" 8 | "github.com/floeit/floe/event" 9 | ) 10 | 11 | // enforceWS make sure there is a matching file system location and returns the workspace object 12 | // shared will use the 'single' workspace 13 | func (h *Hub) enforceWS(runRef event.RunRef, single bool) (*nt.Workspace, error) { 14 | ws, err := h.getWorkspace(runRef, single) 15 | if err != nil { 16 | return nil, err 17 | } 18 | err = os.RemoveAll(ws.BasePath) 19 | if err != nil { 20 | return nil, err 21 | } 22 | err = os.MkdirAll(ws.BasePath, 0700) 23 | return ws, err 24 | } 25 | 26 | // getWorkspace returns the appropriate Workspace struct for this flow 27 | func (h *Hub) getWorkspace(runRef event.RunRef, single bool) (*nt.Workspace, error) { 28 | path := filepath.Join(h.config.Common.WorkspaceRoot, "spaces", runRef.FlowRef.ID) 29 | if single { 30 | path = filepath.Join(path, "ws", "single") 31 | } else { 32 | path = filepath.Join(path, "ws", runRef.Run.String()) 33 | } 34 | // setup the workspace config 35 | return &nt.Workspace{ 36 | BasePath: path, 37 | FetchCache: h.cachePath, 38 | }, nil 39 | } 40 | -------------------------------------------------------------------------------- /hub/timers.go: -------------------------------------------------------------------------------- 1 | package hub 2 | 3 | import ( 4 | "path/filepath" 5 | "sync" 6 | "time" 7 | 8 | "github.com/floeit/floe/config" 9 | nt "github.com/floeit/floe/config/nodetype" 10 | "github.com/floeit/floe/event" 11 | "github.com/floeit/floe/exe/git" 12 | "github.com/floeit/floe/log" 13 | "github.com/floeit/floe/store" 14 | ) 15 | 16 | type timerTrigger func(*event.Queue, *timer) 17 | 18 | type timer struct { 19 | flow config.FlowRef 20 | nodeID string 21 | period int // time between triggers in seconds 22 | next time.Time // computed next time to run 23 | trigger timerTrigger // the function to fire 24 | opts nt.Opts 25 | } 26 | 27 | type timers struct { 28 | mu sync.RWMutex 29 | list map[string]*timer 30 | } 31 | 32 | func newTimers(q *event.Queue) *timers { 33 | t := &timers{ 34 | list: map[string]*timer{}, 35 | } 36 | 37 | go func() { 38 | for now := range time.Tick(time.Second) { 39 | t.mu.RLock() 40 | for name, tim := range t.list { 41 | if !now.After(tim.next) { 42 | continue 43 | } 44 | tim.next = now.Add(time.Duration(tim.period) * time.Second) 45 | log.Debugf("<%s> - timer trigger", name) 46 | tim.trigger(q, tim) 47 | } 48 | t.mu.RUnlock() 49 | } 50 | }() 51 | return t 52 | } 53 | 54 | func (t *timers) register(flow config.FlowRef, nodeID string, opts nt.Opts, trigger timerTrigger) { 55 | period, ok := opts["period"].(int) 56 | if !ok { 57 | period = 10 58 | } 59 | t.mu.Lock() 60 | t.list[flow.String()+"-"+nodeID] = &timer{ 61 | flow: flow, 62 | nodeID: nodeID, 63 | period: period, 64 | next: time.Now().UTC().Add(time.Duration(period) * time.Second), 65 | trigger: trigger, 66 | opts: opts, 67 | } 68 | t.mu.Unlock() 69 | } 70 | 71 | func sendTriggerEvent(q *event.Queue, flowRef config.FlowRef, nodeID, typ string, opts nt.Opts) { 72 | log.Debugf("<%s> - from %s trigger <%s> added to pending", flowRef, typ, nodeID) 73 | q.Publish(event.Event{ 74 | RunRef: event.RunRef{ 75 | FlowRef: flowRef, 76 | }, 77 | Tag: "inbound." + typ, 78 | SourceNode: config.NodeRef{ 79 | Class: "trigger", 80 | ID: nodeID, 81 | }, 82 | Opts: opts, 83 | }) 84 | } 85 | 86 | func startFlowTrigger(q *event.Queue, tim *timer) { 87 | sendTriggerEvent(q, tim.flow, tim.nodeID, "timer", tim.opts) 88 | } 89 | 90 | const pollStoreRoot = "refs" 91 | 92 | type repoPoller struct { 93 | store store.Store 94 | nodeID string 95 | url string 96 | refs string 97 | exclude string 98 | gitKey string 99 | } 100 | 101 | func newRepoPoller(store store.Store, nodeID, gitKey string, opts nt.Opts) *repoPoller { 102 | rp := &repoPoller{ 103 | store: store, 104 | nodeID: nodeID, 105 | gitKey: gitKey, 106 | } 107 | 108 | rp.url, _ = opts["url"].(string) 109 | rp.refs, _ = opts["refs"].(string) 110 | rp.exclude, _ = opts["exclude"].(string) 111 | 112 | if rp.url == "" { 113 | return nil 114 | } 115 | if rp.refs == "" { 116 | rp.refs = "refs/*" 117 | } 118 | 119 | return rp 120 | } 121 | 122 | func (r *repoPoller) timer(q *event.Queue, tim *timer) { 123 | prev, err := r.loadRefs(tim.flow.ID) 124 | if err != nil { 125 | log.Errorf("<%s> - could not load previous refs: %s", tim.flow, err) 126 | } 127 | 128 | new, ok := git.Ls(log.Log{}, r.url, r.refs, r.exclude, r.gitKey) 129 | if !ok { 130 | log.Errorf("<%s> - could not get new refs: %s", tim.flow, err) 131 | } 132 | 133 | err = r.saveRefs(tim.flow.ID, *new) 134 | if err != nil { 135 | log.Errorf("<%s> - could not save refs: %s", tim.flow, err) 136 | } 137 | 138 | changes := changedRefs(prev, *new) 139 | 140 | // start a pending flow for each changed hash 141 | for _, ref := range changes.Hashes { 142 | opts := nt.Opts{ 143 | "url": r.url, 144 | "branch": ref.Name, 145 | "hash": ref.Hash, 146 | } 147 | log.Debugf("<%s> - found changed branch: <%s>", tim.flow, ref.Name) 148 | sendTriggerEvent(q, tim.flow, r.nodeID, "poll-git", opts) 149 | } 150 | } 151 | 152 | func changedRefs(old, new git.Hashes) (changed git.Hashes) { 153 | if old.RepoURL != new.RepoURL { 154 | return changed 155 | } 156 | changed = git.Hashes{ 157 | RepoURL: old.RepoURL, 158 | Hashes: map[string]git.Ref{}, 159 | } 160 | for key, n := range new.Hashes { 161 | o, ok := old.Hashes[key] 162 | if !ok { 163 | changed.Hashes[key] = n 164 | continue 165 | } 166 | if n.Hash != o.Hash { 167 | changed.Hashes[key] = n 168 | } 169 | } 170 | return changed 171 | } 172 | 173 | func (r *repoPoller) loadRefs(flowID string) (git.Hashes, error) { 174 | h := git.Hashes{} 175 | err := r.store.Load(repoKey(flowID, r.nodeID), &h) 176 | return h, err 177 | } 178 | 179 | func (r *repoPoller) saveRefs(flowID string, h git.Hashes) error { 180 | return r.store.Save(repoKey(flowID, r.nodeID), h) 181 | } 182 | 183 | func repoKey(flowID, nodeID string) string { 184 | return filepath.Join(pollStoreRoot, flowID, nodeID) 185 | } 186 | -------------------------------------------------------------------------------- /hub/timers_integ_test.go: -------------------------------------------------------------------------------- 1 | // +build integration_test 2 | 3 | package hub 4 | 5 | import ( 6 | "testing" 7 | "time" 8 | 9 | "github.com/floeit/floe/event" 10 | "github.com/floeit/floe/exe/git" 11 | 12 | "github.com/floeit/floe/config" 13 | nt "github.com/floeit/floe/config/nodetype" 14 | "github.com/floeit/floe/store" 15 | ) 16 | 17 | func TestRepoPoller(t *testing.T) { 18 | t.Parallel() 19 | 20 | s := store.NewMemStore() 21 | o := nt.Opts{ 22 | "url": "git@github.com:floeit/floe.git", 23 | "refs": "*", 24 | } 25 | tim := &timer{ 26 | flow: config.FlowRef{ 27 | ID: "testflo", 28 | Ver: 1, 29 | }, 30 | } 31 | q := &event.Queue{} 32 | c, err := config.ParseYAML(trigFlow) 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | hub := &Hub{ 38 | config: *c, 39 | runs: newRunStore(s), 40 | queue: q, 41 | } 42 | q.Register(hub) 43 | 44 | // make an observer that signals a chanel 45 | got := make(chan bool, 1) 46 | f := func(e event.Event) { 47 | if e.Tag == "sys.state" { 48 | got <- true 49 | } 50 | } 51 | q.Register(obs(f)) 52 | 53 | p := newRepoPoller(s, "nodeID", o) 54 | 55 | // first call sets up the refs 56 | p.timer(nil, tim) 57 | 58 | // second call should produce no diffs 59 | p.timer(nil, tim) 60 | 61 | h := git.Hashes{} 62 | err = s.Load("refs/testflo/nodeID", &h) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | master, ok := h.Hashes["refs/heads/master"] 68 | if !ok { 69 | t.Fatal("master not found") 70 | } 71 | hashFromRepo := master.Hash 72 | master.Hash = "difference" 73 | h.Hashes["refs/heads/master"] = master 74 | err = s.Save("refs/flowID/nodeID", h) 75 | if err != nil { 76 | t.Fatal(err) 77 | } 78 | 79 | // this one will launch a pending flow 80 | p.timer(q, tim) 81 | 82 | select { 83 | case <-time.After(time.Second * 10): 84 | t.Fatal("no event") 85 | case <-got: 86 | } 87 | 88 | pend := pending{} 89 | err = s.Load("pending-list", &pend) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | if len(pend.Pends) != 1 { 94 | t.Fatal("should have got one pending") 95 | } 96 | if pend.Pends[0].Opts["hash"].(string) != hashFromRepo { 97 | t.Error("got bad hash ref") 98 | } 99 | if pend.Pends[0].Opts["branch"].(string) != "master" { 100 | t.Error("got bad branch") 101 | } 102 | } 103 | 104 | var trigFlow = []byte(` 105 | flows: 106 | - id: testflo 107 | ver: 1 108 | 109 | triggers: 110 | - name: Commits 111 | type: poll-git 112 | opts: 113 | period: 10 # check every 10 seconds 114 | url: git@github.com:danmux/danmux-hugo.git # the repo to check 115 | refs: "refs/heads/*" # the refs pattern to match 116 | exclude-refs: "refs/heads/master" 117 | `) 118 | -------------------------------------------------------------------------------- /hub/timers_test.go: -------------------------------------------------------------------------------- 1 | package hub 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/floeit/floe/exe/git" 8 | 9 | "github.com/floeit/floe/config" 10 | nt "github.com/floeit/floe/config/nodetype" 11 | "github.com/floeit/floe/event" 12 | ) 13 | 14 | type obs func(e event.Event) 15 | 16 | func (o obs) Notify(e event.Event) { 17 | o(e) 18 | } 19 | 20 | func TestTimers(t *testing.T) { 21 | t.Parallel() 22 | 23 | q := &event.Queue{} 24 | 25 | // make an observer that signals a chanel 26 | got := make(chan bool, 1) 27 | f := func(e event.Event) { 28 | if e.Tag == "inbound.timer" { 29 | got <- true 30 | } 31 | } 32 | q.Register(obs(f)) 33 | 34 | ts := newTimers(q) 35 | 36 | ts.register(config.FlowRef{ 37 | ID: "test-flow", 38 | Ver: 1, 39 | }, "test-node", nt.Opts{ 40 | "period": 1, 41 | }, startFlowTrigger) 42 | 43 | select { 44 | case <-time.After(time.Second * 2): 45 | t.Fatal("no event") 46 | case <-got: 47 | } 48 | } 49 | 50 | func TestDiffRefs(t *testing.T) { 51 | old := git.Hashes{ 52 | RepoURL: "foo", 53 | } 54 | new := git.Hashes{ 55 | RepoURL: "bar", 56 | } 57 | diff := changedRefs(old, new) 58 | if len(diff.Hashes) != 0 { 59 | t.Error("diff of different repos must be zero") 60 | } 61 | 62 | old = git.Hashes{ 63 | RepoURL: "foo", 64 | Hashes: map[string]git.Ref{ 65 | "a": git.Ref{ 66 | Hash: "aaa", 67 | }, 68 | "b": git.Ref{ 69 | Hash: "bbb", 70 | }, 71 | }, 72 | } 73 | new = git.Hashes{ 74 | RepoURL: "foo", 75 | Hashes: map[string]git.Ref{}, 76 | } 77 | 78 | // 2 old 0 new 79 | diff = changedRefs(old, new) 80 | if len(diff.Hashes) != 0 { 81 | t.Error("changes when no new ones should be zero") 82 | } 83 | 84 | // 2 new ones 85 | diff = changedRefs(new, old) 86 | if len(diff.Hashes) != 2 { 87 | t.Error("2 new ones should produce 2 changes", len(diff.Hashes)) 88 | } 89 | 90 | // 2 the same one new 91 | new.Hashes["a"] = git.Ref{Hash: "aaa"} 92 | new.Hashes["b"] = git.Ref{Hash: "bbb"} 93 | new.Hashes["c"] = git.Ref{Hash: "ccc"} 94 | diff = changedRefs(old, new) 95 | if len(diff.Hashes) != 1 { 96 | t.Error("changes with 2 the same and 1 new should be 1", len(diff.Hashes)) 97 | } 98 | 99 | // 1 the same 1 changed and one new 100 | new.Hashes["a"] = git.Ref{Hash: "aaa"} 101 | new.Hashes["b"] = git.Ref{Hash: "bbc"} 102 | new.Hashes["c"] = git.Ref{Hash: "ccc"} 103 | diff = changedRefs(old, new) 104 | if len(diff.Hashes) != 2 { 105 | t.Error("changes with 1 the same, 1 changed and 1 new should be 2", len(diff.Hashes)) 106 | } 107 | if diff.RepoURL != "foo" { 108 | t.Error("repo url wrong", diff.RepoURL) 109 | } 110 | if diff.Hashes["b"].Hash != "bbc" { 111 | t.Error("got wrong changed hash", diff.Hashes["b"].Hash) 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /log/log.go: -------------------------------------------------------------------------------- 1 | // TODO clean this up 2 | package log 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | "runtime" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | const ( 16 | dbg = "[D]" 17 | inf = "[I]" 18 | err = "[E]" 19 | war = "[W]" 20 | pref = "" 21 | form = log.Ldate | log.Lmicroseconds 22 | 23 | lErr = 3 24 | lWar = 4 25 | lInf = 6 26 | lDbg = 7 27 | ) 28 | 29 | var ( 30 | logger *log.Logger 31 | logbuf bytes.Buffer 32 | level int = 7 33 | mu sync.Mutex 34 | ) 35 | 36 | // 3 = error 37 | // 4 = warning 38 | // 7 = debug 39 | 40 | func init() { 41 | NewStdErrLogger() 42 | } 43 | 44 | func badLevel(l int) bool { 45 | mu.Lock() 46 | b := level < l 47 | mu.Unlock() 48 | return b 49 | } 50 | 51 | func SetLevel(l int) { 52 | mu.Lock() 53 | level = l 54 | mu.Unlock() 55 | } 56 | 57 | func NewStdErrLogger() { 58 | logger = log.New(os.Stderr, pref, form) 59 | } 60 | 61 | func NewCaptureLogger() { 62 | logger = log.New(&logbuf, pref, form) 63 | } 64 | 65 | func NewSilentLogger() { 66 | logger = log.New(ioutil.Discard, pref, form) 67 | } 68 | 69 | func PrintLog() { 70 | fmt.Print(&logbuf) 71 | } 72 | 73 | func prefix(level string, args ...interface{}) []interface{} { 74 | _, file, line, ok := runtime.Caller(2) 75 | if !ok { 76 | file = "???" 77 | line = 0 78 | } else { 79 | bits := strings.Split(file, "/") 80 | if len(bits) > 2 { 81 | file = bits[len(bits)-2] + "/" + bits[len(bits)-1] 82 | } 83 | } 84 | a := []interface{}{level, fmt.Sprintf("(%s:%d)", file, line)} 85 | a = append(a, args...) 86 | return a 87 | } 88 | 89 | func Debug(args ...interface{}) { 90 | if badLevel(lDbg) { 91 | return 92 | } 93 | args = prefix(dbg, args...) 94 | logger.Println(args...) 95 | } 96 | 97 | func Debugf(format string, args ...interface{}) { 98 | if badLevel(lDbg) { 99 | return 100 | } 101 | args = []interface{}{fmt.Sprintf(format, args...)} 102 | args = prefix(dbg, args...) 103 | logger.Println(args...) 104 | } 105 | 106 | func Info(args ...interface{}) { 107 | if badLevel(lInf) { 108 | return 109 | } 110 | args = prefix(inf, args...) 111 | logger.Println(args...) 112 | } 113 | 114 | func Infof(format string, args ...interface{}) { 115 | if badLevel(lInf) { 116 | return 117 | } 118 | args = []interface{}{fmt.Sprintf(format, args...)} 119 | args = prefix(inf, args...) 120 | logger.Println(args...) 121 | } 122 | 123 | func Warning(args ...interface{}) { 124 | if badLevel(lWar) { 125 | return 126 | } 127 | args = prefix(war, args...) 128 | logger.Println(args...) 129 | } 130 | 131 | func Error(args ...interface{}) { 132 | if badLevel(lErr) { 133 | return 134 | } 135 | args = prefix(err, args...) 136 | logger.Println(args...) 137 | } 138 | 139 | func Errorf(format string, args ...interface{}) { 140 | if badLevel(lErr) { 141 | return 142 | } 143 | args = []interface{}{fmt.Sprintf(format, args...)} 144 | args = prefix(err, args...) 145 | logger.Println(args...) 146 | } 147 | 148 | func Fatal(args ...interface{}) { 149 | if badLevel(lErr) { 150 | return 151 | } 152 | args = prefix(err, args...) 153 | logger.Println(args...) 154 | os.Exit(255) 155 | } 156 | 157 | type Log struct{} 158 | 159 | func (l Log) Info(vals ...interface{}) { 160 | Info(vals...) 161 | } 162 | func (l Log) Debug(vals ...interface{}) { 163 | Debug(vals...) 164 | } 165 | func (l Log) Error(vals ...interface{}) { 166 | Error(vals...) 167 | } 168 | -------------------------------------------------------------------------------- /path/path.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "strings" 9 | ) 10 | 11 | // Expand expands the tilde and %tmp abbreviations 12 | func Expand(w string) (string, error) { 13 | // make sure the path does not have a trailing separator 14 | w = filepath.Clean(w) 15 | 16 | // cant use root or v small paths 17 | if len(w) < 2 { 18 | return "", errors.New("path too short") 19 | } 20 | 21 | b := strings.Split(w, "/") 22 | r := "" 23 | if b[0] == "" { 24 | r = string(filepath.Separator) 25 | } 26 | 27 | hd := os.Getenv("HOME") 28 | 29 | // expand ~ 30 | if b[0] == "~" { 31 | if b[1] == "" { // disallow "~/" 32 | return "", errors.New("root of user folder not allowed") 33 | } 34 | if hd == "" { 35 | return "", errors.New("~ not expanded as HOME env var not set") 36 | } 37 | b[0] = hd 38 | } 39 | // replace %tmp with a temp folder 40 | if b[0] == "%tmp" { 41 | tmp, err := ioutil.TempDir("", "floe") 42 | if err != nil { 43 | return "", err 44 | } 45 | b[0] = tmp 46 | } 47 | 48 | return r + filepath.Join(b...), nil 49 | } 50 | -------------------------------------------------------------------------------- /path/path_test.go: -------------------------------------------------------------------------------- 1 | package path 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestExpandPath(t *testing.T) { 10 | t.Parallel() 11 | 12 | hd := os.Getenv("HOME") 13 | 14 | fix := []struct { 15 | in string 16 | out string 17 | e bool 18 | }{ 19 | {in: "~/", out: "", e: true}, // too short 20 | {in: "~", out: "", e: true}, // too short 21 | {in: "~/test", out: hd + "/test"}, // sub ~ 22 | {in: "/test/~", out: "/test/~"}, // dont ~ 23 | {in: "test/foo", out: "test/foo"}, 24 | {in: "/test/foo", out: "/test/foo"}, 25 | } 26 | for i, f := range fix { 27 | ep, err := Expand(f.in) 28 | if (err == nil && f.e) || (err != nil && !f.e) { 29 | t.Errorf("test %d expected error mismatch", i) 30 | } 31 | if ep != f.out { 32 | t.Errorf("test %d failed, wanted: %s got: %s", i, f.out, ep) 33 | } 34 | } 35 | ep, _ := Expand("%tmp/test/bar") 36 | fPos := strings.Index(ep, "/floe") 37 | if fPos < 3 { 38 | t.Error("tmp expansion failed", ep) 39 | } 40 | if strings.Index(ep, "/test/bar") < fPos { 41 | t.Error("tmp expansion prefix... isn't ", ep) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /server/generate.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | //go:generate go-bindata-assetfs -pkg=server -prefix=../ ../webapp/... 4 | -------------------------------------------------------------------------------- /server/handler_auth.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "net/http" 4 | 5 | func loginHandler(rw http.ResponseWriter, r *http.Request, ctx *context) (int, string, renderable) { 6 | v := struct { 7 | User string 8 | Password string 9 | }{} 10 | 11 | if ok, code, msg := decodeBody(rw, r, &v); !ok { 12 | return code, msg, nil 13 | } 14 | 15 | token := login(v.User, v.Password) 16 | if token == "" { 17 | return rUnauth, "username or password were wrong", nil 18 | } 19 | 20 | setCookie(rw, token) 21 | 22 | // authenticated if we got here 23 | return rOK, "", struct { 24 | User string 25 | Role string 26 | Token string 27 | }{v.User, "ADMIN", token} 28 | } 29 | 30 | func logoutHandler(rw http.ResponseWriter, r *http.Request, ctx *context) (int, string, renderable) { 31 | if ctx.sesh == nil { 32 | return rBad, "token not supplied", nil 33 | } 34 | logout(ctx.sesh.token) 35 | return rOK, "logged out", nil 36 | } 37 | -------------------------------------------------------------------------------- /server/handler_conf.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/floeit/floe/client" 7 | ) 8 | 9 | // hostConfig is the publishable config of a host 10 | type hostConfig struct { 11 | HostID string 12 | Online bool 13 | Tags []string 14 | } 15 | 16 | // the /config endpoint 17 | func confHandler(rw http.ResponseWriter, r *http.Request, ctx *context) (int, string, renderable) { 18 | cnf := struct { 19 | Config hostConfig 20 | AllHosts map[string]client.HostConfig 21 | }{ 22 | Config: hostConfig{ 23 | HostID: ctx.hub.HostID(), 24 | Online: true, // TODO consider the option to pretend to be offline 25 | Tags: ctx.hub.Tags(), 26 | }, 27 | AllHosts: ctx.hub.AllHosts(), 28 | } 29 | 30 | return rOK, "OK", cnf 31 | } 32 | -------------------------------------------------------------------------------- /server/handler_flows.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/floeit/floe/client" 7 | "github.com/floeit/floe/config" 8 | "github.com/floeit/floe/hub" 9 | ) 10 | 11 | func hndAllFlows(rw http.ResponseWriter, r *http.Request, ctx *context) (int, string, renderable) { 12 | return rOK, "", ctx.hub.Config() 13 | } 14 | 15 | // hndFlow returns the latest config and run summaries from all clients for this flow 16 | func hndFlow(rw http.ResponseWriter, r *http.Request, ctx *context) (int, string, renderable) { 17 | id := ctx.ps.ByName("id") 18 | 19 | // get the latest config 20 | conf := ctx.hub.Config() 21 | latest := conf.LatestFlow(id) 22 | if latest == nil { 23 | return rNotFound, "not found", nil 24 | } 25 | 26 | // and run summaries from all hosts 27 | summaries := ctx.hub.AllClientRuns(id) 28 | 29 | response := struct { 30 | Config *config.Flow 31 | Runs client.RunSummaries 32 | }{ 33 | Config: latest, 34 | Runs: summaries, 35 | } 36 | 37 | return rOK, "", response 38 | } 39 | 40 | // hndP2PExecFlow is the handler for the internal call to execute the flow on this node 41 | func hndP2PExecFlow(rw http.ResponseWriter, r *http.Request, ctx *context) (int, string, renderable) { 42 | 43 | pend := hub.Pend{} 44 | if ok, code, msg := decodeBody(rw, r, &pend); !ok { 45 | return code, msg, nil 46 | } 47 | 48 | ok, err := ctx.hub.ExecutePending(pend) 49 | if err != nil { 50 | return rErr, err.Error(), nil 51 | } 52 | if !ok { 53 | return rConflict, "host has resource conflicting active flows", nil 54 | } 55 | 56 | return rOK, "started", nil 57 | } 58 | -------------------------------------------------------------------------------- /server/push/data.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | 9 | "github.com/julienschmidt/httprouter" 10 | 11 | "github.com/floeit/floe/config" 12 | nt "github.com/floeit/floe/config/nodetype" 13 | "github.com/floeit/floe/event" 14 | "github.com/floeit/floe/log" 15 | ) 16 | 17 | // Data is the push data endpoint handler 18 | type Data struct{} 19 | 20 | // RequiresAuth - decides if it needs a token. 21 | func (d Data) RequiresAuth() bool { 22 | return true 23 | } 24 | 25 | // PostHandler handles POST requests 26 | func (d Data) PostHandler(queue *event.Queue) httprouter.Handle { 27 | return func(w http.ResponseWriter, req *http.Request, par httprouter.Params) { 28 | log.Debug("got data push request") 29 | type form struct { 30 | ID string 31 | Values nt.Opts 32 | } 33 | o := struct { 34 | Ref config.FlowRef 35 | Run string 36 | Form form 37 | }{} 38 | 39 | if !decodeJSONBody(w, req, &o) { 40 | return 41 | } 42 | 43 | rr := event.RunRef{ 44 | FlowRef: o.Ref, 45 | } 46 | 47 | sourceNode := config.NodeRef{ 48 | Class: "trigger", 49 | ID: o.Form.ID, 50 | } 51 | 52 | // if a run is given then it is data targetting a data input node 53 | if o.Run != "" { 54 | ps := strings.Split(o.Run, "-") 55 | if len(ps) == 2 { 56 | id, err := strconv.ParseInt(ps[1], 10, 64) 57 | if err != nil { 58 | log.Error("could not parse run id", err) 59 | } else { 60 | rr.Run.HostID = ps[0] 61 | rr.Run.ID = id 62 | } 63 | } 64 | } 65 | 66 | // add a data event - including a specific targeted Run if given 67 | queue.Publish(event.Event{ 68 | RunRef: rr, 69 | Tag: "inbound.data", // "inbound" is checked before launching a pending, and data will become the type 70 | SourceNode: sourceNode, 71 | Opts: o.Form.Values, 72 | }) 73 | 74 | jsonResp(w, http.StatusOK, "OK", nil) 75 | } 76 | } 77 | 78 | func (d Data) GetHandler(queue *event.Queue) httprouter.Handle { 79 | return func(w http.ResponseWriter, req *http.Request, par httprouter.Params) { 80 | jsonResp(w, http.StatusOK, "OK", nil) 81 | } 82 | } 83 | 84 | func jsonResp(w http.ResponseWriter, code int, msg string, pl interface{}) { 85 | r := struct { 86 | Message string 87 | Payload interface{} 88 | }{ 89 | Message: msg, 90 | Payload: pl, 91 | } 92 | b, err := json.MarshalIndent(r, "", " ") 93 | if err != nil { 94 | log.Debug(err) 95 | log.Debugf("%#v", pl) 96 | w.WriteHeader(http.StatusInternalServerError) 97 | w.Write([]byte(`{"Message": "marshal failed", "Payload": "` + err.Error() + `"}`)) 98 | return 99 | } 100 | 101 | w.WriteHeader(code) 102 | w.Write(b) 103 | } 104 | 105 | func decodeJSONBody(rw http.ResponseWriter, r *http.Request, v interface{}) bool { 106 | defer r.Body.Close() 107 | dec := json.NewDecoder(r.Body) 108 | if err := dec.Decode(v); err != nil { 109 | jsonResp(rw, http.StatusBadRequest, "decoding json failed", err.Error()) 110 | return false 111 | } 112 | return true 113 | } 114 | -------------------------------------------------------------------------------- /server/push/push.go: -------------------------------------------------------------------------------- 1 | package push 2 | 3 | import ( 4 | "github.com/julienschmidt/httprouter" 5 | 6 | "github.com/floeit/floe/event" 7 | ) 8 | 9 | // Push defines the http push handlers that can send events to a queue 10 | // the handler funcs returns a handler closed on the event queue 11 | type Push interface { 12 | PostHandler(queue *event.Queue) httprouter.Handle 13 | GetHandler(queue *event.Queue) httprouter.Handle 14 | RequiresAuth() bool // this trigger expects to be authenticated with the server 15 | } 16 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | "github.com/julienschmidt/httprouter" 8 | 9 | "github.com/floeit/floe/event" 10 | "github.com/floeit/floe/hub" 11 | "github.com/floeit/floe/log" 12 | "github.com/floeit/floe/server/push" 13 | ) 14 | 15 | const rootPath = "/build/api" 16 | 17 | type Conf struct { 18 | PubBind string 19 | PubCert string 20 | PubKey string 21 | 22 | PrvBind string 23 | PrvCert string 24 | PrvKey string 25 | } 26 | 27 | // LaunchWeb sets up all the http routes runs the server and launches the trigger flows 28 | // rp is the root path. Returns the address it binds to. 29 | // If webDev is true then the web files will be served from the filesystem, rather than the compiled in assets 30 | func LaunchWeb(conf Conf, rp string, hub *hub.Hub, q *event.Queue, addrChan chan string, webDev bool) { 31 | if rp == "" { 32 | rp = rootPath 33 | } 34 | r := httprouter.New() 35 | r.HandleMethodNotAllowed = false 36 | r.NotFound = notFoundHandler{} 37 | r.PanicHandler = panicHandler 38 | 39 | h := handler{hub: hub} 40 | 41 | // --- authentication --- 42 | r.POST(rp+"/login", h.mw(loginHandler, false)) 43 | r.POST(rp+"/logout", h.mw(logoutHandler, true)) 44 | 45 | // --- api --- 46 | r.GET(rp+"/flows", h.mw(hndAllFlows, true)) // list all the flows configs 47 | r.GET(rp+"/flows/:id", h.mw(hndFlow, true)) // return highest version of the flow config and run summaries from the cluster 48 | r.GET(rp+"/flows/:id/runs/:rid", h.mw(hndRun, true)) // returns the identified run detail (may be on another host) 49 | 50 | // --- push endpoints --- 51 | h.setupPushes(rp+"/push/", r, hub) 52 | 53 | // --- p2p api --- 54 | r.POST(rp+"/p2p/flows/exec", h.mw(hndP2PExecFlow, true)) // internal api to pass a pending todo to activate it on this host 55 | r.GET(rp+"/p2p/flows/:id/runs", h.mw(hndP2PRuns, true)) // all summary runs from this host for this flow id 56 | r.GET(rp+"/p2p/flows/:id/runs/:rid", h.mw(hndP2PRun, true)) // detailed run info from this host for this flow id and run id 57 | r.GET(rp+"/p2p/config", h.mw(confHandler, true)) // return host config and what it knows about other hosts 58 | 59 | // --- static files for the spa --- 60 | if webDev { // local development mode 61 | serveFiles(r, "/static/*filepath", http.Dir("webapp")) 62 | r.GET("/app/*filepath", zipper(singleFile("webapp/index.html"))) 63 | } else { // release mode 64 | serveFiles(r, "/static/*filepath", assetFS()) 65 | r.GET("/app/*filepath", zipper(assetFile("webapp/index.html"))) 66 | } 67 | 68 | // serveFiles(r, "/static/img/*filepath", http.Dir("webapp/img")) 69 | // serveFiles(r, "/static/js/*filepath", http.Dir("webapp/js")) 70 | // serveFiles(r, "/static/font/*filepath", http.Dir("webapp/font")) 71 | 72 | // ws endpoint 73 | wsh := newWsHub() 74 | q.Register(wsh) 75 | r.GET("/ws", wsh.getWsHandler(&h)) 76 | 77 | // --- CORS --- 78 | r.OPTIONS(rp+"/*all", h.mw(nil, false)) // catch all options 79 | 80 | /* 81 | r.GET(rp+"/flows/:flid", h.mw(floeHandler, true)) 82 | r.POST(rp+"/flows/:flid/exec", h.mw(execHandler, true)) 83 | r.POST(rp+"/flows/:flid/stop", h.mw(stopHandler, true)) 84 | r.GET(rp+"/flows/:flid/run/:agentid/:runid", h.mw(runHandler, true)) // get the current progress of a run for an agent and run 85 | 86 | // --- web socket connection --- 87 | r.GET(rp+"/msg", wsHandler) 88 | 89 | 90 | 91 | // --- the web page stuff --- 92 | r.GET("/build/", indexHandler) 93 | serveFiles(r, "/build/css/*filepath", http.Dir("public/build/css")) 94 | serveFiles(r, "/build/fonts/*filepath", http.Dir("public/build/fonts")) 95 | serveFiles(r, "/build/img/*filepath", http.Dir("public/build/img")) 96 | serveFiles(r, "/build/js/*filepath", http.Dir("public/build/js")) 97 | 98 | */ 99 | 100 | // start the private server if one is configured differently to the public server 101 | if conf.PrvBind != conf.PubBind && conf.PrvBind != "" { 102 | log.Debug("private server listen on:", conf.PrvBind) 103 | go launch(conf.PrvBind, conf.PrvCert, conf.PrvKey, r, nil) 104 | } 105 | 106 | // start the public server 107 | log.Debug("pub server listen on:", conf.PubBind) 108 | launch(conf.PubBind, conf.PubCert, conf.PubKey, r, addrChan) 109 | } 110 | 111 | func launch(bind, cert, key string, r http.Handler, addrChan chan string) { 112 | log.Debug("attempting to listen on:", bind) 113 | 114 | listener, err := net.Listen("tcp", bind) 115 | if err != nil { 116 | log.Fatal(err) 117 | } 118 | address := listener.Addr().(*net.TCPAddr).String() 119 | 120 | // in separate go routine message the passed in chan with the server address 121 | if addrChan != nil { 122 | go func() { 123 | addrChan <- address 124 | }() 125 | } 126 | 127 | log.Debug("starting on:", address) 128 | 129 | if cert != "" { 130 | log.Debug("using https") 131 | log.Fatal(http.ServeTLS(listener, r, cert, key)) 132 | } else { 133 | log.Debug("using http") 134 | log.Fatal(http.Serve(listener, r)) 135 | } 136 | } 137 | 138 | func singleFile(path string) httprouter.Handle { 139 | return func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { 140 | http.ServeFile(rw, r, path) 141 | } 142 | } 143 | 144 | func assetFile(path string) httprouter.Handle { 145 | b := MustAsset(path) 146 | return func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { 147 | rw.Header().Set("Content-Type", "text/html; charset=utf-8") 148 | rw.Write(b) 149 | } 150 | } 151 | 152 | // pushes is the map of all trigger types that can be triggered via the trigger endpoints. 153 | // This map will be used to attach these pushes types to the http server. 154 | // The key here will be used as the sub path to route to this trigger. 155 | var pushes = map[string]push.Push{ 156 | "data": push.Data{}, 157 | } 158 | -------------------------------------------------------------------------------- /server/session.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "crypto/rand" 5 | "fmt" 6 | "time" 7 | ) 8 | 9 | const seshLifetime = time.Hour * 240 // 10 days 10 | 11 | type session struct { 12 | token string 13 | lastActive time.Time 14 | user string 15 | } 16 | 17 | var tokens = map[string]*session{} 18 | 19 | func goodToken(token string) *session { 20 | t, ok := tokens[token] 21 | if !ok { 22 | return nil 23 | } 24 | 25 | // expired? 26 | now := time.Now() 27 | d := now.Sub(t.lastActive) 28 | if d > seshLifetime { 29 | return nil 30 | } 31 | 32 | // update last active in token store 33 | t.lastActive = now 34 | t.token = token 35 | 36 | return t 37 | } 38 | 39 | // TODO - inject an authenticator 40 | func login(user, pass string) string { 41 | if user == "admin" && pass == "password" { 42 | b := make([]byte, 8) 43 | rand.Read(b) 44 | token := fmt.Sprintf("%x", b) 45 | tokens[token] = &session{ 46 | lastActive: time.Now(), 47 | user: user, 48 | } 49 | return token 50 | } 51 | return "" 52 | } 53 | 54 | func logout(token string) { 55 | delete(tokens, token) 56 | } 57 | -------------------------------------------------------------------------------- /server/ws.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "sync" 9 | 10 | "github.com/julienschmidt/httprouter" 11 | "golang.org/x/net/websocket" 12 | 13 | "github.com/floeit/floe/event" 14 | "github.com/floeit/floe/log" 15 | ) 16 | 17 | type wsHub struct { 18 | sync.RWMutex 19 | cons map[*websocket.Conn]bool 20 | } 21 | 22 | func newWsHub() *wsHub { 23 | return &wsHub{ 24 | cons: map[*websocket.Conn]bool{}, 25 | } 26 | } 27 | 28 | func (w *wsHub) Notify(e event.Event) { 29 | w.RLock() 30 | defer w.RUnlock() 31 | 32 | b, err := json.Marshal(e) 33 | if err != nil { 34 | log.Error("json encoding event failed:", err) 35 | return 36 | } 37 | 38 | for ws := range w.cons { 39 | m, err := ws.Write(b) 40 | if err != nil { 41 | log.Fatal(err) 42 | } 43 | if m != len(b) { 44 | log.Errorf("ws write did not send full event (%d out of %d):", m, len(b)) 45 | } 46 | } 47 | } 48 | 49 | func (w *wsHub) add(ws *websocket.Conn) { 50 | w.Lock() 51 | defer w.Unlock() 52 | 53 | log.Debug("ws - adding new client") 54 | 55 | w.cons[ws] = true 56 | } 57 | 58 | func (w *wsHub) remove(ws *websocket.Conn) { 59 | w.Lock() 60 | defer w.Unlock() 61 | 62 | delete(w.cons, ws) 63 | } 64 | 65 | func (w *wsHub) getWsHandler(h *handler) httprouter.Handle { 66 | return func(rw http.ResponseWriter, r *http.Request, ps httprouter.Params) { 67 | sesh := authRequest(rw, r) 68 | if sesh == nil { 69 | return 70 | } 71 | h := websocket.Handler(w.handler) 72 | h.ServeHTTP(rw, r) 73 | } 74 | } 75 | 76 | func (w *wsHub) handler(ws *websocket.Conn) { 77 | w.add(ws) 78 | defer func() { 79 | w.remove(ws) 80 | }() 81 | 82 | for { 83 | msg := make([]byte, 512) 84 | n, err := ws.Read(msg) 85 | if err != nil { 86 | // normal client close 87 | if err == io.EOF { 88 | log.Debug("websocket - client closed") 89 | } else { 90 | log.Error("websocket - got an error", err) 91 | } 92 | err = ws.Close() 93 | if err != nil { 94 | log.Error("websocket - close error", err) 95 | } 96 | return 97 | } 98 | 99 | fmt.Printf("TODO - something with this - Receive: %s\n", msg[:n]) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | 2 | floe -tags=linux,go,couch -admin=123456 -host_name=h1 -pub_bind=127.0.0.1:8080 >> h1.log 2>&1 & 3 | floe -tags=linux,go,couch -admin=123456 -host_name=h2 -pub_bind=127.0.0.1:8090 >> h2.log 2>&1& 4 | 5 | 6 | -------------------------------------------------------------------------------- /store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "reflect" 10 | "sync" 11 | 12 | "github.com/floeit/floe/path" 13 | ) 14 | 15 | // Store links events to the config rules 16 | type Store interface { 17 | Save(key string, data interface{}) error 18 | Load(key string, thing interface{}) error 19 | // Event(event.Event) 20 | } 21 | 22 | // MemStore is a simple in memory key value store 23 | type MemStore struct { 24 | sync.RWMutex 25 | stuff map[string]interface{} 26 | } 27 | 28 | // NewMemStore returns an initialised MemStore 29 | func NewMemStore() *MemStore { 30 | return &MemStore{ 31 | stuff: map[string]interface{}{}, 32 | } 33 | } 34 | 35 | // Save saves the data at the key 36 | func (m *MemStore) Save(key string, data interface{}) error { 37 | m.Lock() 38 | defer m.Unlock() 39 | m.stuff[key] = data 40 | return nil 41 | } 42 | 43 | // Load loads data from the key 44 | func (m *MemStore) Load(key string, thing interface{}) error { 45 | m.RLock() 46 | defer m.RUnlock() 47 | d, ok := m.stuff[key] 48 | if !ok { 49 | return nil 50 | } 51 | // set the val of the pointer with the stored val 52 | val := reflect.Indirect(reflect.ValueOf(thing)) 53 | sval := reflect.ValueOf(d) 54 | if val.Type() != sval.Type() { 55 | return errors.New("can not set mismatched types") 56 | } 57 | val.Set(sval) 58 | 59 | return nil 60 | } 61 | 62 | // LocalStore is a local disk store 63 | type LocalStore struct { 64 | sync.RWMutex 65 | root string 66 | stuff map[string]interface{} 67 | } 68 | 69 | // NewLocalStore returns a local store based at the root directory 70 | func NewLocalStore(root string) (*LocalStore, error) { 71 | r, err := path.Expand(root) 72 | if err != nil { 73 | return nil, err 74 | } 75 | err = os.MkdirAll(r, 0700) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | return &LocalStore{ 81 | root: r, 82 | stuff: map[string]interface{}{}, 83 | }, nil 84 | } 85 | 86 | // Save saves the data at the key 87 | func (m *LocalStore) Save(key string, data interface{}) error { 88 | m.Lock() 89 | defer m.Unlock() 90 | b, err := json.Marshal(data) 91 | if err != nil { 92 | return err 93 | } 94 | keyPath := filepath.Join(m.root, key) + ".json" 95 | err = ioutil.WriteFile(keyPath, b, 0644) 96 | if err != nil { 97 | if _, ok := err.(*os.PathError); !ok { 98 | return err 99 | } 100 | err = os.MkdirAll(filepath.Dir(keyPath), 0700) 101 | if err != nil { 102 | return err 103 | } 104 | return ioutil.WriteFile(keyPath, b, 0644) 105 | } 106 | return nil 107 | } 108 | 109 | // Load loads data from the key 110 | func (m *LocalStore) Load(key string, thing interface{}) error { 111 | m.RLock() 112 | defer m.RUnlock() 113 | keyPath := filepath.Join(m.root, key) + ".json" 114 | b, err := ioutil.ReadFile(keyPath) 115 | if err != nil { 116 | if _, ok := err.(*os.PathError); ok { // file not found is ok 117 | return nil 118 | } 119 | return err 120 | } 121 | return json.Unmarshal(b, thing) 122 | } 123 | -------------------------------------------------------------------------------- /store/store_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import "testing" 4 | 5 | func TestLocalStore(t *testing.T) { 6 | ls, err := NewLocalStore("%tmp") 7 | if err != nil { 8 | t.Fatal(err) 9 | } 10 | testStore("local", t, ls) 11 | 12 | ms := NewMemStore() 13 | testStore("mem", t, ms) 14 | } 15 | 16 | func testStore(which string, t *testing.T, s Store) { 17 | key := "foo/bar" 18 | val := "test data" 19 | err := s.Save(key, val) 20 | if err != nil { 21 | t.Fatal(which, err) 22 | } 23 | 24 | var loadVal string 25 | err = s.Load(key, &loadVal) 26 | if err != nil { 27 | t.Fatal(which, err) 28 | } 29 | if loadVal != val { 30 | t.Errorf("%s bad load <%s> <%s>", which, loadVal, val) 31 | } 32 | 33 | var failVal int 34 | err = s.Load(key, &failVal) 35 | if err == nil { 36 | t.Fatal("mismatched val should have failed") 37 | } 38 | t.Log(err) 39 | } 40 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.10.x 5 | - 1.9.x 6 | - 1.8.x 7 | - 1.7.x 8 | 9 | script: make check 10 | 11 | env: 12 | - GOARCH=amd64 13 | - GOARCH=386 14 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Ryan Armstrong. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/Makefile: -------------------------------------------------------------------------------- 1 | GO = go 2 | GOGET = $(GO) get -u 3 | 4 | all: check lint 5 | 6 | check: 7 | $(GO) test -cover -race ./... 8 | 9 | install: 10 | $(GO) install -v ./... 11 | 12 | clean: 13 | $(GO) clean -x ./... 14 | rm -rvf ./.test* 15 | 16 | lint: 17 | gofmt -l -e -s . || : 18 | go vet . || : 19 | golint . || : 20 | gocyclo -over 15 . || : 21 | misspell ./* || : 22 | 23 | deps: 24 | $(GOGET) github.com/golang/lint/golint 25 | $(GOGET) github.com/fzipp/gocyclo 26 | $(GOGET) github.com/client9/misspell/cmd/misspell 27 | 28 | .PHONY: all check install clean lint deps 29 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/README.md: -------------------------------------------------------------------------------- 1 | # grab 2 | 3 | [![GoDoc](https://godoc.org/github.com/cavaliercoder/grab?status.svg)](https://godoc.org/github.com/cavaliercoder/grab) [![Build Status](https://travis-ci.org/cavaliercoder/grab.svg)](https://travis-ci.org/cavaliercoder/grab) [![Go Report Card](https://goreportcard.com/badge/github.com/cavaliercoder/grab)](https://goreportcard.com/report/github.com/cavaliercoder/grab) 4 | 5 | *Downloading the internet, one goroutine at a time!* 6 | 7 | $ go get github.com/cavaliercoder/grab 8 | 9 | Grab is a Go package for downloading files from the internet with the following 10 | rad features: 11 | 12 | * Monitor download progress concurrently 13 | * Auto-resume incomplete downloads 14 | * Guess filename from content header or URL path 15 | * Safely cancel downloads using context.Context 16 | * Validate downloads using checksums 17 | * Download batches of files concurrently 18 | 19 | Requires Go v1.7+ 20 | 21 | ## Older versions 22 | 23 | If you are using an older version of Go, or require previous versions of the 24 | Grab API, you can import older version of this package, thanks to gpkg.in. 25 | Please see all GitHub tags for available versions. 26 | 27 | $ go get gopkg.in/cavaliercoder/grab.v1 28 | 29 | 30 | ## Example 31 | 32 | The following example downloads a PDF copy of the free eBook, "An Introduction 33 | to Programming in Go" and periodically prints the download progress until it is 34 | complete. 35 | 36 | The second time you run the example, it will auto-resume the previous download 37 | and exit sooner. 38 | 39 | ```go 40 | package main 41 | 42 | import ( 43 | "fmt" 44 | "os" 45 | "time" 46 | 47 | "github.com/cavaliercoder/grab" 48 | ) 49 | 50 | func main() { 51 | // create client 52 | client := grab.NewClient() 53 | req, _ := grab.NewRequest(".", "http://www.golang-book.com/public/pdf/gobook.pdf") 54 | 55 | // start download 56 | fmt.Printf("Downloading %v...\n", req.URL()) 57 | resp := client.Do(req) 58 | fmt.Printf(" %v\n", resp.HTTPResponse.Status) 59 | 60 | // start UI loop 61 | t := time.NewTicker(500 * time.Millisecond) 62 | defer t.Stop() 63 | 64 | Loop: 65 | for { 66 | select { 67 | case <-t.C: 68 | fmt.Printf(" transferred %v / %v bytes (%.2f%%)\n", 69 | resp.BytesComplete(), 70 | resp.Size, 71 | 100*resp.Progress()) 72 | 73 | case <-resp.Done: 74 | // download is complete 75 | break Loop 76 | } 77 | } 78 | 79 | // check for errors 80 | if err := resp.Err(); err != nil { 81 | fmt.Fprintf(os.Stderr, "Download failed: %v\n", err) 82 | os.Exit(1) 83 | } 84 | 85 | fmt.Printf("Download saved to ./%v \n", resp.Filename) 86 | 87 | // Output: 88 | // Downloading http://www.golang-book.com/public/pdf/gobook.pdf... 89 | // 200 OK 90 | // transferred 42970 / 2893557 bytes (1.49%) 91 | // transferred 1207474 / 2893557 bytes (41.73%) 92 | // transferred 2758210 / 2893557 bytes (95.32%) 93 | // Download saved to ./gobook.pdf 94 | } 95 | ``` 96 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package grab provides a HTTP download manager implementation. 3 | 4 | Get is the most simple way to download a file: 5 | 6 | resp, err := grab.Get("/tmp", "http://example.com/example.zip") 7 | // ... 8 | 9 | Get will download the given URL and save it to the given destination directory. 10 | The destination filename will be determined automatically by grab using 11 | Content-Disposition headers returned by the remote server, or by inspecting the 12 | requested URL path. 13 | 14 | An empty destination string or "." means the transfer will be stored in the 15 | current working directory. 16 | 17 | If a destination file already exists, grab will assume it is a complete or 18 | partially complete download of the requested file. If the remote server supports 19 | resuming interrupted downloads, grab will resume downloading from the end of the 20 | partial file. If the server does not support resumed downloads, the file will be 21 | retransferred in its entirety. If the file is already complete, grab will return 22 | successfully. 23 | 24 | For control over the HTTP client, destination path, auto-resume, checksum 25 | validation and other settings, create a Client: 26 | 27 | client := grab.NewClient() 28 | client.HTTPClient.Transport.DisableCompression = true 29 | 30 | req, err := grab.NewRequest("/tmp", "http://example.com/example.zip") 31 | // ... 32 | req.NoResume = true 33 | req.HTTPRequest.Header.Set("Authorization", "Basic YWxhZGRpbjpvcGVuc2VzYW1l") 34 | 35 | resp := client.Do(req) 36 | // ... 37 | 38 | You can monitor the progress of downloads while they are transferring: 39 | 40 | client := grab.NewClient() 41 | req, err := grab.NewRequest("", "http://example.com/example.zip") 42 | // ... 43 | resp := client.Do(req) 44 | 45 | t := time.NewTicker(time.Second) 46 | defer t.Stop() 47 | 48 | for { 49 | select { 50 | case <-t.C: 51 | fmt.Printf("%.02f%% complete\n", resp.Progress()) 52 | 53 | case <-resp.Done: 54 | if err := resp.Err(); err != nil { 55 | // ... 56 | } 57 | 58 | // ... 59 | return 60 | } 61 | } 62 | */ 63 | package grab 64 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/error.go: -------------------------------------------------------------------------------- 1 | package grab 2 | 3 | import "errors" 4 | 5 | var ( 6 | // ErrBadStatusCode indicates that the server response had a status code that 7 | // was not in the 200-299 range. 8 | ErrBadStatusCode = errors.New("server returned a non-2XX status code") 9 | 10 | // ErrBadLength indicates that the server response or an existing file does 11 | // not match the expected content length. 12 | ErrBadLength = errors.New("bad content length") 13 | 14 | // ErrBadChecksum indicates that a downloaded file failed to pass checksum 15 | // validation. 16 | ErrBadChecksum = errors.New("checksum mismatch") 17 | 18 | // ErrNoFilename indicates that a reasonable filename could not be 19 | // automatically determined using the URL or response headers from a server. 20 | ErrNoFilename = errors.New("no filename could be determined") 21 | 22 | // ErrNoTimestamp indicates that a timestamp could not be automatically 23 | // determined using the reponse headers from the remote server. 24 | ErrNoTimestamp = errors.New("no timestamp could be determined for the remote file") 25 | 26 | // ErrFileExists indicates that the destination path already exists. 27 | ErrFileExists = errors.New("file exists") 28 | ) 29 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/grab.go: -------------------------------------------------------------------------------- 1 | package grab 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // Get sends a HTTP request and downloads the content of the requested URL to 9 | // the given destination file path. The caller is blocked until the download is 10 | // completed, successfully or otherwise. 11 | // 12 | // An error is returned if caused by client policy (such as CheckRedirect), or 13 | // if there was an HTTP protocol or IO error. 14 | // 15 | // For non-blocking calls or control over HTTP client headers, redirect policy, 16 | // and other settings, create a Client instead. 17 | func Get(dst, urlStr string) (*Response, error) { 18 | req, err := NewRequest(dst, urlStr) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | resp := DefaultClient.Do(req) 24 | return resp, resp.Err() 25 | } 26 | 27 | // GetBatch sends multiple HTTP requests and downloads the content of the 28 | // requested URLs to the given destination directory using the given number of 29 | // concurrent worker goroutines. 30 | // 31 | // The Response for each requested URL is sent through the returned Response 32 | // channel, as soon as a worker receives a response from the remote server. The 33 | // Response can then be used to track the progress of the download while it is 34 | // in progress. 35 | // 36 | // The returned Response channel will be closed by Grab, only once all downloads 37 | // have completed or failed. 38 | // 39 | // If an error occurs during any download, it will be available via call to the 40 | // associated Response.Err. 41 | // 42 | // For control over HTTP client headers, redirect policy, and other settings, 43 | // create a Client instead. 44 | func GetBatch(workers int, dst string, urlStrs ...string) (<-chan *Response, error) { 45 | fi, err := os.Stat(dst) 46 | if err != nil { 47 | return nil, err 48 | } 49 | if !fi.IsDir() { 50 | return nil, fmt.Errorf("destination is not a directory") 51 | } 52 | 53 | reqs := make([]*Request, len(urlStrs)) 54 | for i := 0; i < len(urlStrs); i++ { 55 | req, err := NewRequest(dst, urlStrs[i]) 56 | if err != nil { 57 | return nil, err 58 | } 59 | reqs[i] = req 60 | } 61 | 62 | ch := DefaultClient.DoBatch(workers, reqs...) 63 | return ch, nil 64 | } 65 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/request.go: -------------------------------------------------------------------------------- 1 | package grab 2 | 3 | import ( 4 | "context" 5 | "hash" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | // A Hook is a user provided function that can be called by grab at various 11 | // stages of a requests lifecycle. If a hook returns an error, the associated 12 | // request is canceled and the same error is returned on the Response object. 13 | type Hook func(*Response) error 14 | 15 | // A Request represents an HTTP file transfer request to be sent by a Client. 16 | type Request struct { 17 | // Label is an arbitrary string which may used to label a Request with a 18 | // user friendly name. 19 | Label string 20 | 21 | // Tag is an arbitrary interface which may be used to relate a Request to 22 | // other data. 23 | Tag interface{} 24 | 25 | // HTTPRequest specifies the http.Request to be sent to the remote server to 26 | // initiate a file transfer. It includes request configuration such as URL, 27 | // protocol version, HTTP method, request headers and authentication. 28 | HTTPRequest *http.Request 29 | 30 | // Filename specifies the path where the file transfer will be stored in 31 | // local storage. If Filename is empty or a directory, the true Filename will 32 | // be resolved using Content-Disposition headers or the request URL. 33 | // 34 | // An empty string means the transfer will be stored in the current working 35 | // directory. 36 | Filename string 37 | 38 | // SkipExisting specifies that ErrFileExists should be returned if the 39 | // destination path already exists. The existing file will not be checked for 40 | // completeness. 41 | SkipExisting bool 42 | 43 | // NoResume specifies that a partially completed download will be restarted 44 | // without attempting to resume any existing file. If the download is already 45 | // completed in full, it will not be restarted. 46 | NoResume bool 47 | 48 | // NoCreateDirectories specifies that any missing directories in the given 49 | // Filename path should not be created automatically, if they do not already 50 | // exist. 51 | NoCreateDirectories bool 52 | 53 | // IgnoreBadStatusCodes specifies that grab should accept any status code in 54 | // the response from the remote server. Otherwise, grab expects the response 55 | // status code to be within the 2XX range (after following redirects). 56 | IgnoreBadStatusCodes bool 57 | 58 | // IgnoreRemoteTime specifies that grab should not attempt to set the 59 | // timestamp of the local file to match the remote file. 60 | IgnoreRemoteTime bool 61 | 62 | // Size specifies the expected size of the file transfer if known. If the 63 | // server response size does not match, the transfer is cancelled and 64 | // ErrBadLength returned. 65 | Size int64 66 | 67 | // BufferSize specifies the size in bytes of the buffer that is used for 68 | // transferring the requested file. Larger buffers may result in faster 69 | // throughput but will use more memory and result in less frequent updates 70 | // to the transfer progress statistics. Default: 32KB. 71 | BufferSize int 72 | 73 | // BeforeCopy is a user provided function that is called immediately before 74 | // a request starts downloading. If BeforeCopy returns an error, the request 75 | // is cancelled and the same error is returned on the Response object. 76 | BeforeCopy Hook 77 | 78 | // hash, checksum and deleteOnError - set via SetChecksum. 79 | hash hash.Hash 80 | checksum []byte 81 | deleteOnError bool 82 | 83 | // Context for cancellation and timeout - set via WithContext 84 | ctx context.Context 85 | } 86 | 87 | // NewRequest returns a new file transfer Request suitable for use with 88 | // Client.Do. 89 | func NewRequest(dst, urlStr string) (*Request, error) { 90 | if dst == "" { 91 | dst = "." 92 | } 93 | req, err := http.NewRequest("GET", urlStr, nil) 94 | if err != nil { 95 | return nil, err 96 | } 97 | return &Request{ 98 | HTTPRequest: req, 99 | Filename: dst, 100 | }, nil 101 | } 102 | 103 | // Context returns the request's context. To change the context, use 104 | // WithContext. 105 | // 106 | // The returned context is always non-nil; it defaults to the background 107 | // context. 108 | // 109 | // The context controls cancelation. 110 | func (r *Request) Context() context.Context { 111 | if r.ctx != nil { 112 | return r.ctx 113 | } 114 | 115 | return context.Background() 116 | } 117 | 118 | // WithContext returns a shallow copy of r with its context changed 119 | // to ctx. The provided ctx must be non-nil. 120 | func (r *Request) WithContext(ctx context.Context) *Request { 121 | if ctx == nil { 122 | panic("nil context") 123 | } 124 | r2 := new(Request) 125 | *r2 = *r 126 | r2.ctx = ctx 127 | r2.HTTPRequest = r2.HTTPRequest.WithContext(ctx) 128 | return r2 129 | } 130 | 131 | // URL returns the URL to be downloaded. 132 | func (r *Request) URL() *url.URL { 133 | return r.HTTPRequest.URL 134 | } 135 | 136 | // SetChecksum sets the desired hashing algorithm and checksum value to validate 137 | // a downloaded file. Once the download is complete, the given hashing algorithm 138 | // will be used to compute the actual checksum of the downloaded file. If the 139 | // checksums do not match, an error will be returned by the associated 140 | // Response.Err method. 141 | // 142 | // If deleteOnError is true, the downloaded file will be deleted automatically 143 | // if it fails checksum validation. 144 | // 145 | // To prevent corruption of the computed checksum, the given hash must not be 146 | // used by any other request or goroutines. 147 | // 148 | // To disable checksum validation, call SetChecksum with a nil hash. 149 | func (r *Request) SetChecksum(h hash.Hash, sum []byte, deleteOnError bool) { 150 | r.hash = h 151 | r.checksum = sum 152 | r.deleteOnError = deleteOnError 153 | } 154 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/states.wsd: -------------------------------------------------------------------------------- 1 | @startuml 2 | title Grab transfer state 3 | 4 | legend 5 | | # | Meaning | 6 | | D | Destination path known | 7 | | S | File size known | 8 | | O | Server options known (Accept-Ranges) | 9 | | R | Resume supported (Accept-Ranges) | 10 | | Z | Local file empty or missing | 11 | | P | Local file partially complete | 12 | endlegend 13 | 14 | [*] --> Empty 15 | [*] --> D 16 | [*] --> S 17 | [*] --> DS 18 | 19 | Empty : Filename: "" 20 | Empty : Size: 0 21 | Empty --> O : HEAD: Method not allowed 22 | Empty --> DSO : HEAD: Range not supported 23 | Empty --> DSOR : HEAD: Range supported 24 | 25 | DS : Filename: "foo.bar" 26 | DS : Size: > 0 27 | DS --> DSZ : checkExisting(): File missing 28 | DS --> DSP : checkExisting(): File partial 29 | DS --> [*] : checkExisting(): File complete 30 | DS --> ERROR 31 | 32 | S : Filename: "" 33 | S : Size: > 0 34 | S --> SO : HEAD: Method not allowed 35 | S --> DSO : HEAD: Range not supported 36 | S --> DSOR : HEAD: Range supported 37 | 38 | D : Filename: "foo.bar" 39 | D : Size: 0 40 | D --> DO : HEAD: Method not allowed 41 | D --> DSO : HEAD: Range not supported 42 | D --> DSOR : HEAD: Range supported 43 | 44 | 45 | O : Filename: "" 46 | O : Size: 0 47 | O : CanResume: false 48 | O --> DSO : GET 200 49 | O --> ERROR 50 | 51 | SO : Filename: "" 52 | SO : Size: > 0 53 | SO : CanResume: false 54 | SO --> DSO : GET: 200 55 | SO --> ERROR 56 | 57 | DO : Filename: "foo.bar" 58 | DO : Size: 0 59 | DO : CanResume: false 60 | DO --> DSO : GET 200 61 | DO --> ERROR 62 | 63 | DSZ : Filename: "foo.bar" 64 | DSZ : Size: > 0 65 | DSZ : File: empty 66 | DSZ --> DSORZ : HEAD: Range supported 67 | DSZ --> DSOZ : HEAD 405 or Range unsupported 68 | 69 | DSP : Filename: "foo.bar" 70 | DSP : Size: > 0 71 | DSP : File: partial 72 | DSP --> DSORP : HEAD: Range supported 73 | DSP --> DSOZ : HEAD: 405 or Range unsupported 74 | 75 | DSO : Filename: "foo.bar" 76 | DSO : Size: > 0 77 | DSO : CanResume: false 78 | DSO --> DSOZ : checkExisting(): File partial|missing 79 | DSO --> [*] : checkExisting(): File complete 80 | 81 | DSOR : Filename: "foo.bar" 82 | DSOR : Size: > 0 83 | DSOR : CanResume: true 84 | DSOR --> DSORP : CheckLocal: File partial 85 | DSOR --> DSORZ : CheckLocal: File missing 86 | 87 | DSORP : Filename: "foo.bar" 88 | DSORP : Size: > 0 89 | DSORP : CanResume: true 90 | DSORP : File: partial 91 | DSORP --> Transferring 92 | 93 | DSORZ : Filename: "foo.bar" 94 | DSORZ : Size: > 0 95 | DSORZ : CanResume: true 96 | DSORZ : File: empty 97 | DSORZ --> Transferring 98 | 99 | DSOZ : Filename: "foo.bar" 100 | DSOZ : Size: > 0 101 | DSOZ : CanResume: false 102 | DSOZ : File: empty 103 | DSOZ --> Transferring 104 | 105 | Transferring --> [*] 106 | Transferring --> ERROR 107 | 108 | ERROR : Something went wrong 109 | ERROR --> [*] 110 | 111 | @enduml -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/transfer.go: -------------------------------------------------------------------------------- 1 | package grab 2 | 3 | import ( 4 | "context" 5 | "io" 6 | "sync/atomic" 7 | ) 8 | 9 | type transfer struct { 10 | n int64 // must be 64bit aligned on 386 11 | ctx context.Context 12 | w io.Writer 13 | r io.Reader 14 | b []byte 15 | } 16 | 17 | func newTransfer(ctx context.Context, dst io.Writer, src io.Reader, buf []byte) *transfer { 18 | return &transfer{ 19 | ctx: ctx, 20 | w: dst, 21 | r: src, 22 | b: buf, 23 | } 24 | } 25 | 26 | // copy behaves similarly to io.CopyBuffer except that it checks for cancelation 27 | // of the given context.Context and reports progress in a thread-safe manner. 28 | func (c *transfer) copy() (written int64, err error) { 29 | if c.b == nil { 30 | c.b = make([]byte, 32*1024) 31 | } 32 | for { 33 | select { 34 | case <-c.ctx.Done(): 35 | err = c.ctx.Err() 36 | return 37 | default: 38 | // keep working 39 | } 40 | nr, er := c.r.Read(c.b) 41 | if nr > 0 { 42 | nw, ew := c.w.Write(c.b[0:nr]) 43 | if nw > 0 { 44 | written += int64(nw) 45 | atomic.StoreInt64(&c.n, written) 46 | } 47 | if ew != nil { 48 | err = ew 49 | break 50 | } 51 | if nr != nw { 52 | err = io.ErrShortWrite 53 | break 54 | } 55 | } 56 | if er != nil { 57 | if er != io.EOF { 58 | err = er 59 | } 60 | break 61 | } 62 | } 63 | return written, err 64 | } 65 | 66 | // N returns the number of bytes transferred. 67 | func (c *transfer) N() (n int64) { 68 | if c == nil { 69 | return 0 70 | } 71 | n = atomic.LoadInt64(&c.n) 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /vendor/github.com/cavaliercoder/grab/util.go: -------------------------------------------------------------------------------- 1 | package grab 2 | 3 | import ( 4 | "fmt" 5 | "mime" 6 | "net/http" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | // setLastModified sets the last modified timestamp of a local file according to 15 | // the Last-Modified header returned by a remote server. 16 | func setLastModified(resp *http.Response, filename string) error { 17 | // https://tools.ietf.org/html/rfc7232#section-2.2 18 | // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Last-Modified 19 | header := resp.Header.Get("Last-Modified") 20 | if header == "" { 21 | return nil 22 | } 23 | lastmod, err := time.Parse(http.TimeFormat, header) 24 | if err != nil { 25 | return nil 26 | } 27 | return os.Chtimes(filename, lastmod, lastmod) 28 | } 29 | 30 | // mkdirp creates all missing parent directories for the destination file path. 31 | func mkdirp(path string) error { 32 | dir := filepath.Dir(path) 33 | if fi, err := os.Stat(dir); err != nil { 34 | if !os.IsNotExist(err) { 35 | return fmt.Errorf("error checking destination directory: %v", err) 36 | } 37 | if err := os.MkdirAll(dir, 0755); err != nil { 38 | return fmt.Errorf("error creating destination directory: %v", err) 39 | } 40 | } else if !fi.IsDir() { 41 | panic("destination path is not directory") 42 | } 43 | return nil 44 | } 45 | 46 | // guessFilename returns a filename for the given http.Response. If none can be 47 | // determined ErrNoFilename is returned. 48 | func guessFilename(resp *http.Response) (string, error) { 49 | filename := resp.Request.URL.Path 50 | if cd := resp.Header.Get("Content-Disposition"); cd != "" { 51 | if _, params, err := mime.ParseMediaType(cd); err == nil { 52 | filename = params["filename"] 53 | } 54 | } 55 | 56 | // sanitize 57 | if filename == "" || strings.HasSuffix(filename, "/") || strings.Contains(filename, "\x00") { 58 | return "", ErrNoFilename 59 | } 60 | 61 | filename = filepath.Base(path.Clean("/" + filename)) 62 | if filename == "" || filename == "." || filename == "/" { 63 | return "", ErrNoFilename 64 | } 65 | 66 | return filename, nil 67 | } 68 | -------------------------------------------------------------------------------- /vendor/github.com/elazarl/go-bindata-assetfs/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Elazar Leibovich 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /vendor/github.com/elazarl/go-bindata-assetfs/README.md: -------------------------------------------------------------------------------- 1 | # go-bindata-assetfs 2 | 3 | Serve embedded files from [jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata) with `net/http`. 4 | 5 | [GoDoc](http://godoc.org/github.com/elazarl/go-bindata-assetfs) 6 | 7 | ### Installation 8 | 9 | Install with 10 | 11 | $ go get github.com/jteeuwen/go-bindata/... 12 | $ go get github.com/elazarl/go-bindata-assetfs/... 13 | 14 | ### Creating embedded data 15 | 16 | Usage is identical to [jteeuwen/go-bindata](https://github.com/jteeuwen/go-bindata) usage, 17 | instead of running `go-bindata` run `go-bindata-assetfs`. 18 | 19 | The tool will create a `bindata_assetfs.go` file, which contains the embedded data. 20 | 21 | A typical use case is 22 | 23 | $ go-bindata-assetfs data/... 24 | 25 | ### Using assetFS in your code 26 | 27 | The generated file provides an `assetFS()` function that returns a `http.Filesystem` 28 | wrapping the embedded files. What you usually want to do is: 29 | 30 | http.Handle("/", http.FileServer(assetFS())) 31 | 32 | This would run an HTTP server serving the embedded files. 33 | 34 | ## Without running binary tool 35 | 36 | You can always just run the `go-bindata` tool, and then 37 | 38 | use 39 | 40 | import "github.com/elazarl/go-bindata-assetfs" 41 | ... 42 | http.Handle("/", 43 | http.FileServer( 44 | &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, AssetInfo: AssetInfo, Prefix: "data"})) 45 | 46 | to serve files embedded from the `data` directory. 47 | -------------------------------------------------------------------------------- /vendor/github.com/elazarl/go-bindata-assetfs/assetfs.go: -------------------------------------------------------------------------------- 1 | package assetfs 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "os" 10 | "path" 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | ) 15 | 16 | var ( 17 | defaultFileTimestamp = time.Now() 18 | ) 19 | 20 | // FakeFile implements os.FileInfo interface for a given path and size 21 | type FakeFile struct { 22 | // Path is the path of this file 23 | Path string 24 | // Dir marks of the path is a directory 25 | Dir bool 26 | // Len is the length of the fake file, zero if it is a directory 27 | Len int64 28 | // Timestamp is the ModTime of this file 29 | Timestamp time.Time 30 | } 31 | 32 | func (f *FakeFile) Name() string { 33 | _, name := filepath.Split(f.Path) 34 | return name 35 | } 36 | 37 | func (f *FakeFile) Mode() os.FileMode { 38 | mode := os.FileMode(0644) 39 | if f.Dir { 40 | return mode | os.ModeDir 41 | } 42 | return mode 43 | } 44 | 45 | func (f *FakeFile) ModTime() time.Time { 46 | return f.Timestamp 47 | } 48 | 49 | func (f *FakeFile) Size() int64 { 50 | return f.Len 51 | } 52 | 53 | func (f *FakeFile) IsDir() bool { 54 | return f.Mode().IsDir() 55 | } 56 | 57 | func (f *FakeFile) Sys() interface{} { 58 | return nil 59 | } 60 | 61 | // AssetFile implements http.File interface for a no-directory file with content 62 | type AssetFile struct { 63 | *bytes.Reader 64 | io.Closer 65 | FakeFile 66 | } 67 | 68 | func NewAssetFile(name string, content []byte, timestamp time.Time) *AssetFile { 69 | if timestamp.IsZero() { 70 | timestamp = defaultFileTimestamp 71 | } 72 | return &AssetFile{ 73 | bytes.NewReader(content), 74 | ioutil.NopCloser(nil), 75 | FakeFile{name, false, int64(len(content)), timestamp}} 76 | } 77 | 78 | func (f *AssetFile) Readdir(count int) ([]os.FileInfo, error) { 79 | return nil, errors.New("not a directory") 80 | } 81 | 82 | func (f *AssetFile) Size() int64 { 83 | return f.FakeFile.Size() 84 | } 85 | 86 | func (f *AssetFile) Stat() (os.FileInfo, error) { 87 | return f, nil 88 | } 89 | 90 | // AssetDirectory implements http.File interface for a directory 91 | type AssetDirectory struct { 92 | AssetFile 93 | ChildrenRead int 94 | Children []os.FileInfo 95 | } 96 | 97 | func NewAssetDirectory(name string, children []string, fs *AssetFS) *AssetDirectory { 98 | fileinfos := make([]os.FileInfo, 0, len(children)) 99 | for _, child := range children { 100 | _, err := fs.AssetDir(filepath.Join(name, child)) 101 | fileinfos = append(fileinfos, &FakeFile{child, err == nil, 0, time.Time{}}) 102 | } 103 | return &AssetDirectory{ 104 | AssetFile{ 105 | bytes.NewReader(nil), 106 | ioutil.NopCloser(nil), 107 | FakeFile{name, true, 0, time.Time{}}, 108 | }, 109 | 0, 110 | fileinfos} 111 | } 112 | 113 | func (f *AssetDirectory) Readdir(count int) ([]os.FileInfo, error) { 114 | if count <= 0 { 115 | return f.Children, nil 116 | } 117 | if f.ChildrenRead+count > len(f.Children) { 118 | count = len(f.Children) - f.ChildrenRead 119 | } 120 | rv := f.Children[f.ChildrenRead : f.ChildrenRead+count] 121 | f.ChildrenRead += count 122 | return rv, nil 123 | } 124 | 125 | func (f *AssetDirectory) Stat() (os.FileInfo, error) { 126 | return f, nil 127 | } 128 | 129 | // AssetFS implements http.FileSystem, allowing 130 | // embedded files to be served from net/http package. 131 | type AssetFS struct { 132 | // Asset should return content of file in path if exists 133 | Asset func(path string) ([]byte, error) 134 | // AssetDir should return list of files in the path 135 | AssetDir func(path string) ([]string, error) 136 | // AssetInfo should return the info of file in path if exists 137 | AssetInfo func(path string) (os.FileInfo, error) 138 | // Prefix would be prepended to http requests 139 | Prefix string 140 | } 141 | 142 | func (fs *AssetFS) Open(name string) (http.File, error) { 143 | name = path.Join(fs.Prefix, name) 144 | if len(name) > 0 && name[0] == '/' { 145 | name = name[1:] 146 | } 147 | if b, err := fs.Asset(name); err == nil { 148 | timestamp := defaultFileTimestamp 149 | if fs.AssetInfo != nil { 150 | if info, err := fs.AssetInfo(name); err == nil { 151 | timestamp = info.ModTime() 152 | } 153 | } 154 | return NewAssetFile(name, b, timestamp), nil 155 | } 156 | if children, err := fs.AssetDir(name); err == nil { 157 | return NewAssetDirectory(name, children, fs), nil 158 | } else { 159 | // If the error is not found, return an error that will 160 | // result in a 404 error. Otherwise the server returns 161 | // a 500 error for files not found. 162 | if strings.Contains(err.Error(), "not found") { 163 | return nil, os.ErrNotExist 164 | } 165 | return nil, err 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /vendor/github.com/elazarl/go-bindata-assetfs/doc.go: -------------------------------------------------------------------------------- 1 | // assetfs allows packages to serve static content embedded 2 | // with the go-bindata tool with the standard net/http package. 3 | // 4 | // See https://github.com/jteeuwen/go-bindata for more information 5 | // about embedding binary data with go-bindata. 6 | // 7 | // Usage example, after running 8 | // $ go-bindata data/... 9 | // use: 10 | // http.Handle("/", 11 | // http.FileServer( 12 | // &assetfs.AssetFS{Asset: Asset, AssetDir: AssetDir, Prefix: "data"})) 13 | package assetfs 14 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: go 3 | go: 4 | - 1.7 5 | - 1.8 6 | - 1.9 7 | - "1.10" 8 | - tip 9 | before_install: 10 | - go get golang.org/x/tools/cmd/cover 11 | - go get github.com/mattn/goveralls 12 | - go get github.com/golang/lint/golint 13 | script: 14 | - go test -v -covermode=count -coverprofile=coverage.out 15 | - go vet ./... 16 | - test -z "$(gofmt -d -s . | tee /dev/stderr)" 17 | - test -z "$(golint ./... | tee /dev/stderr)" 18 | - $HOME/gopath/bin/goveralls -coverprofile=coverage.out -service=travis-ci 19 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Julien Schmidt. All rights reserved. 2 | 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | * Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * The names of the contributors may not be used to endorse or promote 12 | products derived from this software without specific prior written 13 | permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 16 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 17 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL JULIEN SCHMIDT BE LIABLE FOR ANY 19 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 20 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 21 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 22 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 24 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/params_go17.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package httprouter 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | ) 9 | 10 | type paramsKey struct{} 11 | 12 | // ParamsKey is the request context key under which URL params are stored. 13 | // 14 | // This is only present from go 1.7. 15 | var ParamsKey = paramsKey{} 16 | 17 | // Handler is an adapter which allows the usage of an http.Handler as a 18 | // request handle. With go 1.7+, the Params will be available in the 19 | // request context under ParamsKey. 20 | func (r *Router) Handler(method, path string, handler http.Handler) { 21 | r.Handle(method, path, 22 | func(w http.ResponseWriter, req *http.Request, p Params) { 23 | ctx := req.Context() 24 | ctx = context.WithValue(ctx, ParamsKey, p) 25 | req = req.WithContext(ctx) 26 | handler.ServeHTTP(w, req) 27 | }, 28 | ) 29 | } 30 | 31 | // ParamsFromContext pulls the URL parameters from a request context, 32 | // or returns nil if none are present. 33 | // 34 | // This is only present from go 1.7. 35 | func ParamsFromContext(ctx context.Context) Params { 36 | p, _ := ctx.Value(ParamsKey).(Params) 37 | return p 38 | } 39 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/params_legacy.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package httprouter 4 | 5 | import "net/http" 6 | 7 | // Handler is an adapter which allows the usage of an http.Handler as a 8 | // request handle. With go 1.7+, the Params will be available in the 9 | // request context under ParamsKey. 10 | func (r *Router) Handler(method, path string, handler http.Handler) { 11 | r.Handle(method, path, 12 | func(w http.ResponseWriter, req *http.Request, _ Params) { 13 | handler.ServeHTTP(w, req) 14 | }, 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /vendor/github.com/julienschmidt/httprouter/path.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 Julien Schmidt. All rights reserved. 2 | // Based on the path package, Copyright 2009 The Go Authors. 3 | // Use of this source code is governed by a BSD-style license that can be found 4 | // in the LICENSE file. 5 | 6 | package httprouter 7 | 8 | // CleanPath is the URL version of path.Clean, it returns a canonical URL path 9 | // for p, eliminating . and .. elements. 10 | // 11 | // The following rules are applied iteratively until no further processing can 12 | // be done: 13 | // 1. Replace multiple slashes with a single slash. 14 | // 2. Eliminate each . path name element (the current directory). 15 | // 3. Eliminate each inner .. path name element (the parent directory) 16 | // along with the non-.. element that precedes it. 17 | // 4. Eliminate .. elements that begin a rooted path: 18 | // that is, replace "/.." by "/" at the beginning of a path. 19 | // 20 | // If the result of this process is an empty string, "/" is returned 21 | func CleanPath(p string) string { 22 | // Turn empty string into "/" 23 | if p == "" { 24 | return "/" 25 | } 26 | 27 | n := len(p) 28 | var buf []byte 29 | 30 | // Invariants: 31 | // reading from path; r is index of next byte to process. 32 | // writing to buf; w is index of next byte to write. 33 | 34 | // path must start with '/' 35 | r := 1 36 | w := 1 37 | 38 | if p[0] != '/' { 39 | r = 0 40 | buf = make([]byte, n+1) 41 | buf[0] = '/' 42 | } 43 | 44 | trailing := n > 2 && p[n-1] == '/' 45 | 46 | // A bit more clunky without a 'lazybuf' like the path package, but the loop 47 | // gets completely inlined (bufApp). So in contrast to the path package this 48 | // loop has no expensive function calls (except 1x make) 49 | 50 | for r < n { 51 | switch { 52 | case p[r] == '/': 53 | // empty path element, trailing slash is added after the end 54 | r++ 55 | 56 | case p[r] == '.' && r+1 == n: 57 | trailing = true 58 | r++ 59 | 60 | case p[r] == '.' && p[r+1] == '/': 61 | // . element 62 | r++ 63 | 64 | case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): 65 | // .. element: remove to last / 66 | r += 2 67 | 68 | if w > 1 { 69 | // can backtrack 70 | w-- 71 | 72 | if buf == nil { 73 | for w > 1 && p[w] != '/' { 74 | w-- 75 | } 76 | } else { 77 | for w > 1 && buf[w] != '/' { 78 | w-- 79 | } 80 | } 81 | } 82 | 83 | default: 84 | // real path element. 85 | // add slash if needed 86 | if w > 1 { 87 | bufApp(&buf, p, w, '/') 88 | w++ 89 | } 90 | 91 | // copy element 92 | for r < n && p[r] != '/' { 93 | bufApp(&buf, p, w, p[r]) 94 | w++ 95 | r++ 96 | } 97 | } 98 | } 99 | 100 | // re-append trailing slash 101 | if trailing && w > 1 { 102 | bufApp(&buf, p, w, '/') 103 | w++ 104 | } 105 | 106 | if buf == nil { 107 | return p[:w] 108 | } 109 | return string(buf[:w]) 110 | } 111 | 112 | // internal helper to lazily create a buffer if necessary 113 | func bufApp(buf *[]byte, s string, w int, c byte) { 114 | if *buf == nil { 115 | if s[w] == c { 116 | return 117 | } 118 | 119 | *buf = make([]byte, len(s)) 120 | copy(*buf, s[:w]) 121 | } 122 | (*buf)[w] = c 123 | } 124 | -------------------------------------------------------------------------------- /vendor/github.com/mitchellh/mapstructure/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.9.x 5 | - tip 6 | 7 | script: 8 | - go test 9 | -------------------------------------------------------------------------------- /vendor/github.com/mitchellh/mapstructure/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Mitchell Hashimoto 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/github.com/mitchellh/mapstructure/README.md: -------------------------------------------------------------------------------- 1 | # mapstructure [![Godoc](https://godoc.org/github.com/mitchellh/mapstructure?status.svg)](https://godoc.org/github.com/mitchellh/mapstructure) 2 | 3 | mapstructure is a Go library for decoding generic map values to structures 4 | and vice versa, while providing helpful error handling. 5 | 6 | This library is most useful when decoding values from some data stream (JSON, 7 | Gob, etc.) where you don't _quite_ know the structure of the underlying data 8 | until you read a part of it. You can therefore read a `map[string]interface{}` 9 | and use this library to decode it into the proper underlying native Go 10 | structure. 11 | 12 | ## Installation 13 | 14 | Standard `go get`: 15 | 16 | ``` 17 | $ go get github.com/mitchellh/mapstructure 18 | ``` 19 | 20 | ## Usage & Example 21 | 22 | For usage and examples see the [Godoc](http://godoc.org/github.com/mitchellh/mapstructure). 23 | 24 | The `Decode` function has examples associated with it there. 25 | 26 | ## But Why?! 27 | 28 | Go offers fantastic standard libraries for decoding formats such as JSON. 29 | The standard method is to have a struct pre-created, and populate that struct 30 | from the bytes of the encoded format. This is great, but the problem is if 31 | you have configuration or an encoding that changes slightly depending on 32 | specific fields. For example, consider this JSON: 33 | 34 | ```json 35 | { 36 | "type": "person", 37 | "name": "Mitchell" 38 | } 39 | ``` 40 | 41 | Perhaps we can't populate a specific structure without first reading 42 | the "type" field from the JSON. We could always do two passes over the 43 | decoding of the JSON (reading the "type" first, and the rest later). 44 | However, it is much simpler to just decode this into a `map[string]interface{}` 45 | structure, read the "type" key, then use something like this library 46 | to decode it into the proper structure. 47 | -------------------------------------------------------------------------------- /vendor/github.com/mitchellh/mapstructure/decode_hooks.go: -------------------------------------------------------------------------------- 1 | package mapstructure 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // typedDecodeHook takes a raw DecodeHookFunc (an interface{}) and turns 12 | // it into the proper DecodeHookFunc type, such as DecodeHookFuncType. 13 | func typedDecodeHook(h DecodeHookFunc) DecodeHookFunc { 14 | // Create variables here so we can reference them with the reflect pkg 15 | var f1 DecodeHookFuncType 16 | var f2 DecodeHookFuncKind 17 | 18 | // Fill in the variables into this interface and the rest is done 19 | // automatically using the reflect package. 20 | potential := []interface{}{f1, f2} 21 | 22 | v := reflect.ValueOf(h) 23 | vt := v.Type() 24 | for _, raw := range potential { 25 | pt := reflect.ValueOf(raw).Type() 26 | if vt.ConvertibleTo(pt) { 27 | return v.Convert(pt).Interface() 28 | } 29 | } 30 | 31 | return nil 32 | } 33 | 34 | // DecodeHookExec executes the given decode hook. This should be used 35 | // since it'll naturally degrade to the older backwards compatible DecodeHookFunc 36 | // that took reflect.Kind instead of reflect.Type. 37 | func DecodeHookExec( 38 | raw DecodeHookFunc, 39 | from reflect.Type, to reflect.Type, 40 | data interface{}) (interface{}, error) { 41 | switch f := typedDecodeHook(raw).(type) { 42 | case DecodeHookFuncType: 43 | return f(from, to, data) 44 | case DecodeHookFuncKind: 45 | return f(from.Kind(), to.Kind(), data) 46 | default: 47 | return nil, errors.New("invalid decode hook signature") 48 | } 49 | } 50 | 51 | // ComposeDecodeHookFunc creates a single DecodeHookFunc that 52 | // automatically composes multiple DecodeHookFuncs. 53 | // 54 | // The composed funcs are called in order, with the result of the 55 | // previous transformation. 56 | func ComposeDecodeHookFunc(fs ...DecodeHookFunc) DecodeHookFunc { 57 | return func( 58 | f reflect.Type, 59 | t reflect.Type, 60 | data interface{}) (interface{}, error) { 61 | var err error 62 | for _, f1 := range fs { 63 | data, err = DecodeHookExec(f1, f, t, data) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | // Modify the from kind to be correct with the new data 69 | f = nil 70 | if val := reflect.ValueOf(data); val.IsValid() { 71 | f = val.Type() 72 | } 73 | } 74 | 75 | return data, nil 76 | } 77 | } 78 | 79 | // StringToSliceHookFunc returns a DecodeHookFunc that converts 80 | // string to []string by splitting on the given sep. 81 | func StringToSliceHookFunc(sep string) DecodeHookFunc { 82 | return func( 83 | f reflect.Kind, 84 | t reflect.Kind, 85 | data interface{}) (interface{}, error) { 86 | if f != reflect.String || t != reflect.Slice { 87 | return data, nil 88 | } 89 | 90 | raw := data.(string) 91 | if raw == "" { 92 | return []string{}, nil 93 | } 94 | 95 | return strings.Split(raw, sep), nil 96 | } 97 | } 98 | 99 | // StringToTimeDurationHookFunc returns a DecodeHookFunc that converts 100 | // strings to time.Duration. 101 | func StringToTimeDurationHookFunc() DecodeHookFunc { 102 | return func( 103 | f reflect.Type, 104 | t reflect.Type, 105 | data interface{}) (interface{}, error) { 106 | if f.Kind() != reflect.String { 107 | return data, nil 108 | } 109 | if t != reflect.TypeOf(time.Duration(5)) { 110 | return data, nil 111 | } 112 | 113 | // Convert it by parsing 114 | return time.ParseDuration(data.(string)) 115 | } 116 | } 117 | 118 | // StringToTimeHookFunc returns a DecodeHookFunc that converts 119 | // strings to time.Time. 120 | func StringToTimeHookFunc(layout string) DecodeHookFunc { 121 | return func( 122 | f reflect.Type, 123 | t reflect.Type, 124 | data interface{}) (interface{}, error) { 125 | if f.Kind() != reflect.String { 126 | return data, nil 127 | } 128 | if t != reflect.TypeOf(time.Time{}) { 129 | return data, nil 130 | } 131 | 132 | // Convert it by parsing 133 | return time.Parse(layout, data.(string)) 134 | } 135 | } 136 | 137 | // WeaklyTypedHook is a DecodeHookFunc which adds support for weak typing to 138 | // the decoder. 139 | // 140 | // Note that this is significantly different from the WeaklyTypedInput option 141 | // of the DecoderConfig. 142 | func WeaklyTypedHook( 143 | f reflect.Kind, 144 | t reflect.Kind, 145 | data interface{}) (interface{}, error) { 146 | dataVal := reflect.ValueOf(data) 147 | switch t { 148 | case reflect.String: 149 | switch f { 150 | case reflect.Bool: 151 | if dataVal.Bool() { 152 | return "1", nil 153 | } 154 | return "0", nil 155 | case reflect.Float32: 156 | return strconv.FormatFloat(dataVal.Float(), 'f', -1, 64), nil 157 | case reflect.Int: 158 | return strconv.FormatInt(dataVal.Int(), 10), nil 159 | case reflect.Slice: 160 | dataType := dataVal.Type() 161 | elemKind := dataType.Elem().Kind() 162 | if elemKind == reflect.Uint8 { 163 | return string(dataVal.Interface().([]uint8)), nil 164 | } 165 | case reflect.Uint: 166 | return strconv.FormatUint(dataVal.Uint(), 10), nil 167 | } 168 | } 169 | 170 | return data, nil 171 | } 172 | -------------------------------------------------------------------------------- /vendor/github.com/mitchellh/mapstructure/error.go: -------------------------------------------------------------------------------- 1 | package mapstructure 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sort" 7 | "strings" 8 | ) 9 | 10 | // Error implements the error interface and can represents multiple 11 | // errors that occur in the course of a single decode. 12 | type Error struct { 13 | Errors []string 14 | } 15 | 16 | func (e *Error) Error() string { 17 | points := make([]string, len(e.Errors)) 18 | for i, err := range e.Errors { 19 | points[i] = fmt.Sprintf("* %s", err) 20 | } 21 | 22 | sort.Strings(points) 23 | return fmt.Sprintf( 24 | "%d error(s) decoding:\n\n%s", 25 | len(e.Errors), strings.Join(points, "\n")) 26 | } 27 | 28 | // WrappedErrors implements the errwrap.Wrapper interface to make this 29 | // return value more useful with the errwrap and go-multierror libraries. 30 | func (e *Error) WrappedErrors() []error { 31 | if e == nil { 32 | return nil 33 | } 34 | 35 | result := make([]error, len(e.Errors)) 36 | for i, e := range e.Errors { 37 | result[i] = errors.New(e) 38 | } 39 | 40 | return result 41 | } 42 | 43 | func appendErrors(errors []string, err error) []string { 44 | switch e := err.(type) { 45 | case *Error: 46 | return append(errors, e.Errors...) 47 | default: 48 | return append(errors, e.Error()) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/AUTHORS: -------------------------------------------------------------------------------- 1 | # This source code refers to The Go Authors for copyright purposes. 2 | # The master list of authors is in the main Go distribution, 3 | # visible at http://tip.golang.org/AUTHORS. 4 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/CONTRIBUTORS: -------------------------------------------------------------------------------- 1 | # This source code was written by the Go contributors. 2 | # The master list of contributors is in the main Go distribution, 3 | # visible at http://tip.golang.org/CONTRIBUTORS. 4 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 The Go Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/PATENTS: -------------------------------------------------------------------------------- 1 | Additional IP Rights Grant (Patents) 2 | 3 | "This implementation" means the copyrightable works distributed by 4 | Google as part of the Go project. 5 | 6 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 7 | no-charge, royalty-free, irrevocable (except as stated in this section) 8 | patent license to make, have made, use, offer to sell, sell, import, 9 | transfer and otherwise run, modify and propagate the contents of this 10 | implementation of Go, where such license applies only to those patent 11 | claims, both currently owned or controlled by Google and acquired in 12 | the future, licensable by Google that are necessarily infringed by this 13 | implementation of Go. This grant does not include claims that would be 14 | infringed only as a consequence of further modification of this 15 | implementation. If you or your agent or exclusive licensee institute or 16 | order or agree to the institution of patent litigation against any 17 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 18 | that this implementation of Go or any code incorporated within this 19 | implementation of Go constitutes direct or contributory patent 20 | infringement, or inducement of patent infringement, then any patent 21 | rights granted to you under this License for this implementation of Go 22 | shall terminate as of the date such litigation is filed. 23 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/websocket/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package websocket 6 | 7 | import ( 8 | "bufio" 9 | "io" 10 | "net" 11 | "net/http" 12 | "net/url" 13 | ) 14 | 15 | // DialError is an error that occurs while dialling a websocket server. 16 | type DialError struct { 17 | *Config 18 | Err error 19 | } 20 | 21 | func (e *DialError) Error() string { 22 | return "websocket.Dial " + e.Config.Location.String() + ": " + e.Err.Error() 23 | } 24 | 25 | // NewConfig creates a new WebSocket config for client connection. 26 | func NewConfig(server, origin string) (config *Config, err error) { 27 | config = new(Config) 28 | config.Version = ProtocolVersionHybi13 29 | config.Location, err = url.ParseRequestURI(server) 30 | if err != nil { 31 | return 32 | } 33 | config.Origin, err = url.ParseRequestURI(origin) 34 | if err != nil { 35 | return 36 | } 37 | config.Header = http.Header(make(map[string][]string)) 38 | return 39 | } 40 | 41 | // NewClient creates a new WebSocket client connection over rwc. 42 | func NewClient(config *Config, rwc io.ReadWriteCloser) (ws *Conn, err error) { 43 | br := bufio.NewReader(rwc) 44 | bw := bufio.NewWriter(rwc) 45 | err = hybiClientHandshake(config, br, bw) 46 | if err != nil { 47 | return 48 | } 49 | buf := bufio.NewReadWriter(br, bw) 50 | ws = newHybiClientConn(config, buf, rwc) 51 | return 52 | } 53 | 54 | // Dial opens a new client connection to a WebSocket. 55 | func Dial(url_, protocol, origin string) (ws *Conn, err error) { 56 | config, err := NewConfig(url_, origin) 57 | if err != nil { 58 | return nil, err 59 | } 60 | if protocol != "" { 61 | config.Protocol = []string{protocol} 62 | } 63 | return DialConfig(config) 64 | } 65 | 66 | var portMap = map[string]string{ 67 | "ws": "80", 68 | "wss": "443", 69 | } 70 | 71 | func parseAuthority(location *url.URL) string { 72 | if _, ok := portMap[location.Scheme]; ok { 73 | if _, _, err := net.SplitHostPort(location.Host); err != nil { 74 | return net.JoinHostPort(location.Host, portMap[location.Scheme]) 75 | } 76 | } 77 | return location.Host 78 | } 79 | 80 | // DialConfig opens a new client connection to a WebSocket with a config. 81 | func DialConfig(config *Config) (ws *Conn, err error) { 82 | var client net.Conn 83 | if config.Location == nil { 84 | return nil, &DialError{config, ErrBadWebSocketLocation} 85 | } 86 | if config.Origin == nil { 87 | return nil, &DialError{config, ErrBadWebSocketOrigin} 88 | } 89 | dialer := config.Dialer 90 | if dialer == nil { 91 | dialer = &net.Dialer{} 92 | } 93 | client, err = dialWithDialer(dialer, config) 94 | if err != nil { 95 | goto Error 96 | } 97 | ws, err = NewClient(config, client) 98 | if err != nil { 99 | client.Close() 100 | goto Error 101 | } 102 | return 103 | 104 | Error: 105 | return nil, &DialError{config, err} 106 | } 107 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/websocket/dial.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package websocket 6 | 7 | import ( 8 | "crypto/tls" 9 | "net" 10 | ) 11 | 12 | func dialWithDialer(dialer *net.Dialer, config *Config) (conn net.Conn, err error) { 13 | switch config.Location.Scheme { 14 | case "ws": 15 | conn, err = dialer.Dial("tcp", parseAuthority(config.Location)) 16 | 17 | case "wss": 18 | conn, err = tls.DialWithDialer(dialer, "tcp", parseAuthority(config.Location), config.TlsConfig) 19 | 20 | default: 21 | err = ErrBadScheme 22 | } 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /vendor/golang.org/x/net/websocket/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package websocket 6 | 7 | import ( 8 | "bufio" 9 | "fmt" 10 | "io" 11 | "net/http" 12 | ) 13 | 14 | func newServerConn(rwc io.ReadWriteCloser, buf *bufio.ReadWriter, req *http.Request, config *Config, handshake func(*Config, *http.Request) error) (conn *Conn, err error) { 15 | var hs serverHandshaker = &hybiServerHandshaker{Config: config} 16 | code, err := hs.ReadHandshake(buf.Reader, req) 17 | if err == ErrBadWebSocketVersion { 18 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 19 | fmt.Fprintf(buf, "Sec-WebSocket-Version: %s\r\n", SupportedProtocolVersion) 20 | buf.WriteString("\r\n") 21 | buf.WriteString(err.Error()) 22 | buf.Flush() 23 | return 24 | } 25 | if err != nil { 26 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 27 | buf.WriteString("\r\n") 28 | buf.WriteString(err.Error()) 29 | buf.Flush() 30 | return 31 | } 32 | if handshake != nil { 33 | err = handshake(config, req) 34 | if err != nil { 35 | code = http.StatusForbidden 36 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 37 | buf.WriteString("\r\n") 38 | buf.Flush() 39 | return 40 | } 41 | } 42 | err = hs.AcceptHandshake(buf.Writer) 43 | if err != nil { 44 | code = http.StatusBadRequest 45 | fmt.Fprintf(buf, "HTTP/1.1 %03d %s\r\n", code, http.StatusText(code)) 46 | buf.WriteString("\r\n") 47 | buf.Flush() 48 | return 49 | } 50 | conn = hs.NewServerConn(buf, rwc, req) 51 | return 52 | } 53 | 54 | // Server represents a server of a WebSocket. 55 | type Server struct { 56 | // Config is a WebSocket configuration for new WebSocket connection. 57 | Config 58 | 59 | // Handshake is an optional function in WebSocket handshake. 60 | // For example, you can check, or don't check Origin header. 61 | // Another example, you can select config.Protocol. 62 | Handshake func(*Config, *http.Request) error 63 | 64 | // Handler handles a WebSocket connection. 65 | Handler 66 | } 67 | 68 | // ServeHTTP implements the http.Handler interface for a WebSocket 69 | func (s Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { 70 | s.serveWebSocket(w, req) 71 | } 72 | 73 | func (s Server) serveWebSocket(w http.ResponseWriter, req *http.Request) { 74 | rwc, buf, err := w.(http.Hijacker).Hijack() 75 | if err != nil { 76 | panic("Hijack failed: " + err.Error()) 77 | } 78 | // The server should abort the WebSocket connection if it finds 79 | // the client did not send a handshake that matches with protocol 80 | // specification. 81 | defer rwc.Close() 82 | conn, err := newServerConn(rwc, buf, req, &s.Config, s.Handshake) 83 | if err != nil { 84 | return 85 | } 86 | if conn == nil { 87 | panic("unexpected nil conn") 88 | } 89 | s.Handler(conn) 90 | } 91 | 92 | // Handler is a simple interface to a WebSocket browser client. 93 | // It checks if Origin header is valid URL by default. 94 | // You might want to verify websocket.Conn.Config().Origin in the func. 95 | // If you use Server instead of Handler, you could call websocket.Origin and 96 | // check the origin in your Handshake func. So, if you want to accept 97 | // non-browser clients, which do not send an Origin header, set a 98 | // Server.Handshake that does not check the origin. 99 | type Handler func(*Conn) 100 | 101 | func checkOrigin(config *Config, req *http.Request) (err error) { 102 | config.Origin, err = Origin(config, req) 103 | if err == nil && config.Origin == nil { 104 | return fmt.Errorf("null origin") 105 | } 106 | return err 107 | } 108 | 109 | // ServeHTTP implements the http.Handler interface for a WebSocket 110 | func (h Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { 111 | s := Server{Handler: h, Handshake: checkOrigin} 112 | s.serveWebSocket(w, req) 113 | } 114 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.4 5 | - 1.5 6 | - 1.6 7 | - tip 8 | 9 | go_import_path: gopkg.in/yaml.v2 10 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2011-2016 Canonical Ltd. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/LICENSE.libyaml: -------------------------------------------------------------------------------- 1 | The following files were ported to Go from C files of libyaml, and thus 2 | are still covered by their original copyright and license: 3 | 4 | apic.go 5 | emitterc.go 6 | parserc.go 7 | readerc.go 8 | scannerc.go 9 | writerc.go 10 | yamlh.go 11 | yamlprivateh.go 12 | 13 | Copyright (c) 2006 Kirill Simonov 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of 16 | this software and associated documentation files (the "Software"), to deal in 17 | the Software without restriction, including without limitation the rights to 18 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 19 | of the Software, and to permit persons to whom the Software is furnished to do 20 | so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/README.md: -------------------------------------------------------------------------------- 1 | # YAML support for the Go language 2 | 3 | Introduction 4 | ------------ 5 | 6 | The yaml package enables Go programs to comfortably encode and decode YAML 7 | values. It was developed within [Canonical](https://www.canonical.com) as 8 | part of the [juju](https://juju.ubuntu.com) project, and is based on a 9 | pure Go port of the well-known [libyaml](http://pyyaml.org/wiki/LibYAML) 10 | C library to parse and generate YAML data quickly and reliably. 11 | 12 | Compatibility 13 | ------------- 14 | 15 | The yaml package supports most of YAML 1.1 and 1.2, including support for 16 | anchors, tags, map merging, etc. Multi-document unmarshalling is not yet 17 | implemented, and base-60 floats from YAML 1.1 are purposefully not 18 | supported since they're a poor design and are gone in YAML 1.2. 19 | 20 | Installation and usage 21 | ---------------------- 22 | 23 | The import path for the package is *gopkg.in/yaml.v2*. 24 | 25 | To install it, run: 26 | 27 | go get gopkg.in/yaml.v2 28 | 29 | API documentation 30 | ----------------- 31 | 32 | If opened in a browser, the import path itself leads to the API documentation: 33 | 34 | * [https://gopkg.in/yaml.v2](https://gopkg.in/yaml.v2) 35 | 36 | API stability 37 | ------------- 38 | 39 | The package API for yaml v2 will remain stable as described in [gopkg.in](https://gopkg.in). 40 | 41 | 42 | License 43 | ------- 44 | 45 | The yaml package is licensed under the Apache License 2.0. Please see the LICENSE file for details. 46 | 47 | 48 | Example 49 | ------- 50 | 51 | ```Go 52 | package main 53 | 54 | import ( 55 | "fmt" 56 | "log" 57 | 58 | "gopkg.in/yaml.v2" 59 | ) 60 | 61 | var data = ` 62 | a: Easy! 63 | b: 64 | c: 2 65 | d: [3, 4] 66 | ` 67 | 68 | type T struct { 69 | A string 70 | B struct { 71 | RenamedC int `yaml:"c"` 72 | D []int `yaml:",flow"` 73 | } 74 | } 75 | 76 | func main() { 77 | t := T{} 78 | 79 | err := yaml.Unmarshal([]byte(data), &t) 80 | if err != nil { 81 | log.Fatalf("error: %v", err) 82 | } 83 | fmt.Printf("--- t:\n%v\n\n", t) 84 | 85 | d, err := yaml.Marshal(&t) 86 | if err != nil { 87 | log.Fatalf("error: %v", err) 88 | } 89 | fmt.Printf("--- t dump:\n%s\n\n", string(d)) 90 | 91 | m := make(map[interface{}]interface{}) 92 | 93 | err = yaml.Unmarshal([]byte(data), &m) 94 | if err != nil { 95 | log.Fatalf("error: %v", err) 96 | } 97 | fmt.Printf("--- m:\n%v\n\n", m) 98 | 99 | d, err = yaml.Marshal(&m) 100 | if err != nil { 101 | log.Fatalf("error: %v", err) 102 | } 103 | fmt.Printf("--- m dump:\n%s\n\n", string(d)) 104 | } 105 | ``` 106 | 107 | This example will generate the following output: 108 | 109 | ``` 110 | --- t: 111 | {Easy! {2 [3 4]}} 112 | 113 | --- t dump: 114 | a: Easy! 115 | b: 116 | c: 2 117 | d: [3, 4] 118 | 119 | 120 | --- m: 121 | map[a:Easy! b:map[c:2 d:[3 4]]] 122 | 123 | --- m dump: 124 | a: Easy! 125 | b: 126 | c: 2 127 | d: 128 | - 3 129 | - 4 130 | ``` 131 | 132 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/resolve.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "encoding/base64" 5 | "math" 6 | "strconv" 7 | "strings" 8 | "unicode/utf8" 9 | ) 10 | 11 | type resolveMapItem struct { 12 | value interface{} 13 | tag string 14 | } 15 | 16 | var resolveTable = make([]byte, 256) 17 | var resolveMap = make(map[string]resolveMapItem) 18 | 19 | func init() { 20 | t := resolveTable 21 | t[int('+')] = 'S' // Sign 22 | t[int('-')] = 'S' 23 | for _, c := range "0123456789" { 24 | t[int(c)] = 'D' // Digit 25 | } 26 | for _, c := range "yYnNtTfFoO~" { 27 | t[int(c)] = 'M' // In map 28 | } 29 | t[int('.')] = '.' // Float (potentially in map) 30 | 31 | var resolveMapList = []struct { 32 | v interface{} 33 | tag string 34 | l []string 35 | }{ 36 | {true, yaml_BOOL_TAG, []string{"y", "Y", "yes", "Yes", "YES"}}, 37 | {true, yaml_BOOL_TAG, []string{"true", "True", "TRUE"}}, 38 | {true, yaml_BOOL_TAG, []string{"on", "On", "ON"}}, 39 | {false, yaml_BOOL_TAG, []string{"n", "N", "no", "No", "NO"}}, 40 | {false, yaml_BOOL_TAG, []string{"false", "False", "FALSE"}}, 41 | {false, yaml_BOOL_TAG, []string{"off", "Off", "OFF"}}, 42 | {nil, yaml_NULL_TAG, []string{"", "~", "null", "Null", "NULL"}}, 43 | {math.NaN(), yaml_FLOAT_TAG, []string{".nan", ".NaN", ".NAN"}}, 44 | {math.Inf(+1), yaml_FLOAT_TAG, []string{".inf", ".Inf", ".INF"}}, 45 | {math.Inf(+1), yaml_FLOAT_TAG, []string{"+.inf", "+.Inf", "+.INF"}}, 46 | {math.Inf(-1), yaml_FLOAT_TAG, []string{"-.inf", "-.Inf", "-.INF"}}, 47 | {"<<", yaml_MERGE_TAG, []string{"<<"}}, 48 | } 49 | 50 | m := resolveMap 51 | for _, item := range resolveMapList { 52 | for _, s := range item.l { 53 | m[s] = resolveMapItem{item.v, item.tag} 54 | } 55 | } 56 | } 57 | 58 | const longTagPrefix = "tag:yaml.org,2002:" 59 | 60 | func shortTag(tag string) string { 61 | // TODO This can easily be made faster and produce less garbage. 62 | if strings.HasPrefix(tag, longTagPrefix) { 63 | return "!!" + tag[len(longTagPrefix):] 64 | } 65 | return tag 66 | } 67 | 68 | func longTag(tag string) string { 69 | if strings.HasPrefix(tag, "!!") { 70 | return longTagPrefix + tag[2:] 71 | } 72 | return tag 73 | } 74 | 75 | func resolvableTag(tag string) bool { 76 | switch tag { 77 | case "", yaml_STR_TAG, yaml_BOOL_TAG, yaml_INT_TAG, yaml_FLOAT_TAG, yaml_NULL_TAG: 78 | return true 79 | } 80 | return false 81 | } 82 | 83 | func resolve(tag string, in string) (rtag string, out interface{}) { 84 | if !resolvableTag(tag) { 85 | return tag, in 86 | } 87 | 88 | defer func() { 89 | switch tag { 90 | case "", rtag, yaml_STR_TAG, yaml_BINARY_TAG: 91 | return 92 | } 93 | failf("cannot decode %s `%s` as a %s", shortTag(rtag), in, shortTag(tag)) 94 | }() 95 | 96 | // Any data is accepted as a !!str or !!binary. 97 | // Otherwise, the prefix is enough of a hint about what it might be. 98 | hint := byte('N') 99 | if in != "" { 100 | hint = resolveTable[in[0]] 101 | } 102 | if hint != 0 && tag != yaml_STR_TAG && tag != yaml_BINARY_TAG { 103 | // Handle things we can lookup in a map. 104 | if item, ok := resolveMap[in]; ok { 105 | return item.tag, item.value 106 | } 107 | 108 | // Base 60 floats are a bad idea, were dropped in YAML 1.2, and 109 | // are purposefully unsupported here. They're still quoted on 110 | // the way out for compatibility with other parser, though. 111 | 112 | switch hint { 113 | case 'M': 114 | // We've already checked the map above. 115 | 116 | case '.': 117 | // Not in the map, so maybe a normal float. 118 | floatv, err := strconv.ParseFloat(in, 64) 119 | if err == nil { 120 | return yaml_FLOAT_TAG, floatv 121 | } 122 | 123 | case 'D', 'S': 124 | // Int, float, or timestamp. 125 | plain := strings.Replace(in, "_", "", -1) 126 | intv, err := strconv.ParseInt(plain, 0, 64) 127 | if err == nil { 128 | if intv == int64(int(intv)) { 129 | return yaml_INT_TAG, int(intv) 130 | } else { 131 | return yaml_INT_TAG, intv 132 | } 133 | } 134 | uintv, err := strconv.ParseUint(plain, 0, 64) 135 | if err == nil { 136 | return yaml_INT_TAG, uintv 137 | } 138 | floatv, err := strconv.ParseFloat(plain, 64) 139 | if err == nil { 140 | return yaml_FLOAT_TAG, floatv 141 | } 142 | if strings.HasPrefix(plain, "0b") { 143 | intv, err := strconv.ParseInt(plain[2:], 2, 64) 144 | if err == nil { 145 | if intv == int64(int(intv)) { 146 | return yaml_INT_TAG, int(intv) 147 | } else { 148 | return yaml_INT_TAG, intv 149 | } 150 | } 151 | uintv, err := strconv.ParseUint(plain[2:], 2, 64) 152 | if err == nil { 153 | return yaml_INT_TAG, uintv 154 | } 155 | } else if strings.HasPrefix(plain, "-0b") { 156 | intv, err := strconv.ParseInt(plain[3:], 2, 64) 157 | if err == nil { 158 | if intv == int64(int(intv)) { 159 | return yaml_INT_TAG, -int(intv) 160 | } else { 161 | return yaml_INT_TAG, -intv 162 | } 163 | } 164 | } 165 | // XXX Handle timestamps here. 166 | 167 | default: 168 | panic("resolveTable item not yet handled: " + string(rune(hint)) + " (with " + in + ")") 169 | } 170 | } 171 | if tag == yaml_BINARY_TAG { 172 | return yaml_BINARY_TAG, in 173 | } 174 | if utf8.ValidString(in) { 175 | return yaml_STR_TAG, in 176 | } 177 | return yaml_BINARY_TAG, encodeBase64(in) 178 | } 179 | 180 | // encodeBase64 encodes s as base64 that is broken up into multiple lines 181 | // as appropriate for the resulting length. 182 | func encodeBase64(s string) string { 183 | const lineLen = 70 184 | encLen := base64.StdEncoding.EncodedLen(len(s)) 185 | lines := encLen/lineLen + 1 186 | buf := make([]byte, encLen*2+lines) 187 | in := buf[0:encLen] 188 | out := buf[encLen:] 189 | base64.StdEncoding.Encode(in, []byte(s)) 190 | k := 0 191 | for i := 0; i < len(in); i += lineLen { 192 | j := i + lineLen 193 | if j > len(in) { 194 | j = len(in) 195 | } 196 | k += copy(out[k:], in[i:j]) 197 | if lines > 1 { 198 | out[k] = '\n' 199 | k++ 200 | } 201 | } 202 | return string(out[:k]) 203 | } 204 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/sorter.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | import ( 4 | "reflect" 5 | "unicode" 6 | ) 7 | 8 | type keyList []reflect.Value 9 | 10 | func (l keyList) Len() int { return len(l) } 11 | func (l keyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 12 | func (l keyList) Less(i, j int) bool { 13 | a := l[i] 14 | b := l[j] 15 | ak := a.Kind() 16 | bk := b.Kind() 17 | for (ak == reflect.Interface || ak == reflect.Ptr) && !a.IsNil() { 18 | a = a.Elem() 19 | ak = a.Kind() 20 | } 21 | for (bk == reflect.Interface || bk == reflect.Ptr) && !b.IsNil() { 22 | b = b.Elem() 23 | bk = b.Kind() 24 | } 25 | af, aok := keyFloat(a) 26 | bf, bok := keyFloat(b) 27 | if aok && bok { 28 | if af != bf { 29 | return af < bf 30 | } 31 | if ak != bk { 32 | return ak < bk 33 | } 34 | return numLess(a, b) 35 | } 36 | if ak != reflect.String || bk != reflect.String { 37 | return ak < bk 38 | } 39 | ar, br := []rune(a.String()), []rune(b.String()) 40 | for i := 0; i < len(ar) && i < len(br); i++ { 41 | if ar[i] == br[i] { 42 | continue 43 | } 44 | al := unicode.IsLetter(ar[i]) 45 | bl := unicode.IsLetter(br[i]) 46 | if al && bl { 47 | return ar[i] < br[i] 48 | } 49 | if al || bl { 50 | return bl 51 | } 52 | var ai, bi int 53 | var an, bn int64 54 | for ai = i; ai < len(ar) && unicode.IsDigit(ar[ai]); ai++ { 55 | an = an*10 + int64(ar[ai]-'0') 56 | } 57 | for bi = i; bi < len(br) && unicode.IsDigit(br[bi]); bi++ { 58 | bn = bn*10 + int64(br[bi]-'0') 59 | } 60 | if an != bn { 61 | return an < bn 62 | } 63 | if ai != bi { 64 | return ai < bi 65 | } 66 | return ar[i] < br[i] 67 | } 68 | return len(ar) < len(br) 69 | } 70 | 71 | // keyFloat returns a float value for v if it is a number/bool 72 | // and whether it is a number/bool or not. 73 | func keyFloat(v reflect.Value) (f float64, ok bool) { 74 | switch v.Kind() { 75 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 76 | return float64(v.Int()), true 77 | case reflect.Float32, reflect.Float64: 78 | return v.Float(), true 79 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 80 | return float64(v.Uint()), true 81 | case reflect.Bool: 82 | if v.Bool() { 83 | return 1, true 84 | } 85 | return 0, true 86 | } 87 | return 0, false 88 | } 89 | 90 | // numLess returns whether a < b. 91 | // a and b must necessarily have the same kind. 92 | func numLess(a, b reflect.Value) bool { 93 | switch a.Kind() { 94 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 95 | return a.Int() < b.Int() 96 | case reflect.Float32, reflect.Float64: 97 | return a.Float() < b.Float() 98 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 99 | return a.Uint() < b.Uint() 100 | case reflect.Bool: 101 | return !a.Bool() && b.Bool() 102 | } 103 | panic("not a number") 104 | } 105 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/writerc.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | // Set the writer error and return false. 4 | func yaml_emitter_set_writer_error(emitter *yaml_emitter_t, problem string) bool { 5 | emitter.error = yaml_WRITER_ERROR 6 | emitter.problem = problem 7 | return false 8 | } 9 | 10 | // Flush the output buffer. 11 | func yaml_emitter_flush(emitter *yaml_emitter_t) bool { 12 | if emitter.write_handler == nil { 13 | panic("write handler not set") 14 | } 15 | 16 | // Check if the buffer is empty. 17 | if emitter.buffer_pos == 0 { 18 | return true 19 | } 20 | 21 | // If the output encoding is UTF-8, we don't need to recode the buffer. 22 | if emitter.encoding == yaml_UTF8_ENCODING { 23 | if err := emitter.write_handler(emitter, emitter.buffer[:emitter.buffer_pos]); err != nil { 24 | return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error()) 25 | } 26 | emitter.buffer_pos = 0 27 | return true 28 | } 29 | 30 | // Recode the buffer into the raw buffer. 31 | var low, high int 32 | if emitter.encoding == yaml_UTF16LE_ENCODING { 33 | low, high = 0, 1 34 | } else { 35 | high, low = 1, 0 36 | } 37 | 38 | pos := 0 39 | for pos < emitter.buffer_pos { 40 | // See the "reader.c" code for more details on UTF-8 encoding. Note 41 | // that we assume that the buffer contains a valid UTF-8 sequence. 42 | 43 | // Read the next UTF-8 character. 44 | octet := emitter.buffer[pos] 45 | 46 | var w int 47 | var value rune 48 | switch { 49 | case octet&0x80 == 0x00: 50 | w, value = 1, rune(octet&0x7F) 51 | case octet&0xE0 == 0xC0: 52 | w, value = 2, rune(octet&0x1F) 53 | case octet&0xF0 == 0xE0: 54 | w, value = 3, rune(octet&0x0F) 55 | case octet&0xF8 == 0xF0: 56 | w, value = 4, rune(octet&0x07) 57 | } 58 | for k := 1; k < w; k++ { 59 | octet = emitter.buffer[pos+k] 60 | value = (value << 6) + (rune(octet) & 0x3F) 61 | } 62 | pos += w 63 | 64 | // Write the character. 65 | if value < 0x10000 { 66 | var b [2]byte 67 | b[high] = byte(value >> 8) 68 | b[low] = byte(value & 0xFF) 69 | emitter.raw_buffer = append(emitter.raw_buffer, b[0], b[1]) 70 | } else { 71 | // Write the character using a surrogate pair (check "reader.c"). 72 | var b [4]byte 73 | value -= 0x10000 74 | b[high] = byte(0xD8 + (value >> 18)) 75 | b[low] = byte((value >> 10) & 0xFF) 76 | b[high+2] = byte(0xDC + ((value >> 8) & 0xFF)) 77 | b[low+2] = byte(value & 0xFF) 78 | emitter.raw_buffer = append(emitter.raw_buffer, b[0], b[1], b[2], b[3]) 79 | } 80 | } 81 | 82 | // Write the raw buffer. 83 | if err := emitter.write_handler(emitter, emitter.raw_buffer); err != nil { 84 | return yaml_emitter_set_writer_error(emitter, "write error: "+err.Error()) 85 | } 86 | emitter.buffer_pos = 0 87 | emitter.raw_buffer = emitter.raw_buffer[:0] 88 | return true 89 | } 90 | -------------------------------------------------------------------------------- /vendor/gopkg.in/yaml.v2/yamlprivateh.go: -------------------------------------------------------------------------------- 1 | package yaml 2 | 3 | const ( 4 | // The size of the input raw buffer. 5 | input_raw_buffer_size = 512 6 | 7 | // The size of the input buffer. 8 | // It should be possible to decode the whole raw buffer. 9 | input_buffer_size = input_raw_buffer_size * 3 10 | 11 | // The size of the output buffer. 12 | output_buffer_size = 128 13 | 14 | // The size of the output raw buffer. 15 | // It should be possible to encode the whole output buffer. 16 | output_raw_buffer_size = (output_buffer_size*2 + 2) 17 | 18 | // The size of other stacks and queues. 19 | initial_stack_size = 16 20 | initial_queue_size = 16 21 | initial_string_size = 16 22 | ) 23 | 24 | // Check if the character at the specified position is an alphabetical 25 | // character, a digit, '_', or '-'. 26 | func is_alpha(b []byte, i int) bool { 27 | return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'Z' || b[i] >= 'a' && b[i] <= 'z' || b[i] == '_' || b[i] == '-' 28 | } 29 | 30 | // Check if the character at the specified position is a digit. 31 | func is_digit(b []byte, i int) bool { 32 | return b[i] >= '0' && b[i] <= '9' 33 | } 34 | 35 | // Get the value of a digit. 36 | func as_digit(b []byte, i int) int { 37 | return int(b[i]) - '0' 38 | } 39 | 40 | // Check if the character at the specified position is a hex-digit. 41 | func is_hex(b []byte, i int) bool { 42 | return b[i] >= '0' && b[i] <= '9' || b[i] >= 'A' && b[i] <= 'F' || b[i] >= 'a' && b[i] <= 'f' 43 | } 44 | 45 | // Get the value of a hex-digit. 46 | func as_hex(b []byte, i int) int { 47 | bi := b[i] 48 | if bi >= 'A' && bi <= 'F' { 49 | return int(bi) - 'A' + 10 50 | } 51 | if bi >= 'a' && bi <= 'f' { 52 | return int(bi) - 'a' + 10 53 | } 54 | return int(bi) - '0' 55 | } 56 | 57 | // Check if the character is ASCII. 58 | func is_ascii(b []byte, i int) bool { 59 | return b[i] <= 0x7F 60 | } 61 | 62 | // Check if the character at the start of the buffer can be printed unescaped. 63 | func is_printable(b []byte, i int) bool { 64 | return ((b[i] == 0x0A) || // . == #x0A 65 | (b[i] >= 0x20 && b[i] <= 0x7E) || // #x20 <= . <= #x7E 66 | (b[i] == 0xC2 && b[i+1] >= 0xA0) || // #0xA0 <= . <= #xD7FF 67 | (b[i] > 0xC2 && b[i] < 0xED) || 68 | (b[i] == 0xED && b[i+1] < 0xA0) || 69 | (b[i] == 0xEE) || 70 | (b[i] == 0xEF && // #xE000 <= . <= #xFFFD 71 | !(b[i+1] == 0xBB && b[i+2] == 0xBF) && // && . != #xFEFF 72 | !(b[i+1] == 0xBF && (b[i+2] == 0xBE || b[i+2] == 0xBF)))) 73 | } 74 | 75 | // Check if the character at the specified position is NUL. 76 | func is_z(b []byte, i int) bool { 77 | return b[i] == 0x00 78 | } 79 | 80 | // Check if the beginning of the buffer is a BOM. 81 | func is_bom(b []byte, i int) bool { 82 | return b[0] == 0xEF && b[1] == 0xBB && b[2] == 0xBF 83 | } 84 | 85 | // Check if the character at the specified position is space. 86 | func is_space(b []byte, i int) bool { 87 | return b[i] == ' ' 88 | } 89 | 90 | // Check if the character at the specified position is tab. 91 | func is_tab(b []byte, i int) bool { 92 | return b[i] == '\t' 93 | } 94 | 95 | // Check if the character at the specified position is blank (space or tab). 96 | func is_blank(b []byte, i int) bool { 97 | //return is_space(b, i) || is_tab(b, i) 98 | return b[i] == ' ' || b[i] == '\t' 99 | } 100 | 101 | // Check if the character at the specified position is a line break. 102 | func is_break(b []byte, i int) bool { 103 | return (b[i] == '\r' || // CR (#xD) 104 | b[i] == '\n' || // LF (#xA) 105 | b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) 106 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) 107 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9) // PS (#x2029) 108 | } 109 | 110 | func is_crlf(b []byte, i int) bool { 111 | return b[i] == '\r' && b[i+1] == '\n' 112 | } 113 | 114 | // Check if the character is a line break or NUL. 115 | func is_breakz(b []byte, i int) bool { 116 | //return is_break(b, i) || is_z(b, i) 117 | return ( // is_break: 118 | b[i] == '\r' || // CR (#xD) 119 | b[i] == '\n' || // LF (#xA) 120 | b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) 121 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) 122 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) 123 | // is_z: 124 | b[i] == 0) 125 | } 126 | 127 | // Check if the character is a line break, space, or NUL. 128 | func is_spacez(b []byte, i int) bool { 129 | //return is_space(b, i) || is_breakz(b, i) 130 | return ( // is_space: 131 | b[i] == ' ' || 132 | // is_breakz: 133 | b[i] == '\r' || // CR (#xD) 134 | b[i] == '\n' || // LF (#xA) 135 | b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) 136 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) 137 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) 138 | b[i] == 0) 139 | } 140 | 141 | // Check if the character is a line break, space, tab, or NUL. 142 | func is_blankz(b []byte, i int) bool { 143 | //return is_blank(b, i) || is_breakz(b, i) 144 | return ( // is_blank: 145 | b[i] == ' ' || b[i] == '\t' || 146 | // is_breakz: 147 | b[i] == '\r' || // CR (#xD) 148 | b[i] == '\n' || // LF (#xA) 149 | b[i] == 0xC2 && b[i+1] == 0x85 || // NEL (#x85) 150 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA8 || // LS (#x2028) 151 | b[i] == 0xE2 && b[i+1] == 0x80 && b[i+2] == 0xA9 || // PS (#x2029) 152 | b[i] == 0) 153 | } 154 | 155 | // Determine the width of the character. 156 | func width(b byte) int { 157 | // Don't replace these by a switch without first 158 | // confirming that it is being inlined. 159 | if b&0x80 == 0x00 { 160 | return 1 161 | } 162 | if b&0xE0 == 0xC0 { 163 | return 2 164 | } 165 | if b&0xF0 == 0xE0 { 166 | return 3 167 | } 168 | if b&0xF8 == 0xF0 { 169 | return 4 170 | } 171 | return 0 172 | 173 | } 174 | -------------------------------------------------------------------------------- /webapp/font/floe.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floeit/floe/817c61a9da9aaf2a2fb526d08c1daf6b11fb8d53/webapp/font/floe.woff -------------------------------------------------------------------------------- /webapp/font/floe.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floeit/floe/817c61a9da9aaf2a2fb526d08c1daf6b11fb8d53/webapp/font/floe.woff2 -------------------------------------------------------------------------------- /webapp/img/floe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/floeit/floe/817c61a9da9aaf2a2fb526d08c1daf6b11fb8d53/webapp/img/floe.png -------------------------------------------------------------------------------- /webapp/img/gear.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /webapp/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 23 | 24 |
25 |
26 |

Floe

27 |
28 |
29 |
30 |
31 | 32 |
33 |
34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /webapp/js/page/dash.js: -------------------------------------------------------------------------------- 1 | import {Panel} from '../panel/panel.js'; 2 | 3 | "use strict"; 4 | 5 | // the controller for the Dashboard 6 | export function Dash() { 7 | var panel = {}; 8 | 9 | var dataReq = { 10 | URL: '/flows', 11 | } 12 | 13 | function flowSummaryClick(ev, item) { 14 | console.log("clicked summary", ev, item, item.id); 15 | panel.evtHub.Fire({ 16 | Type: 'click', 17 | What: 'flow', 18 | ID: item.id, 19 | }) 20 | } 21 | 22 | var events = [ 23 | {El: 'box.flow', Ev: 'click', Fn: flowSummaryClick} 24 | ]; 25 | 26 | // panel is view - or part of it 27 | panel = new Panel(this, null, tplDash, '#main', events, dataReq); 28 | 29 | this.Map = function(evt) { 30 | console.log("dash got a call to Map", evt); 31 | if (evt.Type == 'rest') { 32 | var flows = evt.Value.Response.Payload.Flows; 33 | console.log(flows); 34 | return {Flows: flows}; 35 | } 36 | 37 | if (evt.Type == 'ws') { 38 | // console.log('evt.Msg', evt.Msg); 39 | } 40 | 41 | return{}; 42 | } 43 | 44 | // Keep a reference to the dash panels - TODO: needed ? 45 | var panels = {}; 46 | 47 | // AfterRender is called when the dash hs rendered containers. 48 | // we go and add the child summary panels 49 | this.AfterRender = function(data) { 50 | // ignore if initial rendering before data fetched. 51 | if (data == null ) { 52 | return; 53 | } 54 | console.log(data.Data); 55 | var flows = data.Data.Flows; 56 | for (var f in flows) { 57 | var fl = flows[f]; 58 | panels[fl.ID] = new summary(fl); 59 | panels[fl.ID].Activate(); 60 | } 61 | } 62 | return panel; 63 | } 64 | 65 | var tplDash = ` 66 | {{~it.Data.Flows :flow:index}} 67 | 68 | 69 | {{~}}` 70 | 71 | 72 | function summary(flow) { 73 | 74 | // summary panel 75 | var panel = new Panel(this, flow, tplSummary, '#'+flow.ID, []); 76 | 77 | this.Map = function(evt) { 78 | console.log("summary got a call to Map", evt); 79 | return {}; 80 | } 81 | return panel; 82 | } 83 | 84 | var tplSummary = ` 85 | ({{=it.Data.ID}}) 86 |

{{=it.Data.Name}}

87 | 88 | ` 89 | -------------------------------------------------------------------------------- /webapp/js/page/header.js: -------------------------------------------------------------------------------- 1 | import {Panel} from '../panel/panel.js'; 2 | import {RestCall} from '../panel/rest.js'; 3 | 4 | "use strict"; 5 | 6 | // the controller for the Dashboard 7 | export function Header() { 8 | var panel = {}; 9 | 10 | function evtLogout() { 11 | RestCall(panel.evtHub, "POST", "/logout"); 12 | } 13 | 14 | function evtSettings() { 15 | panel.evtHub.Fire({ 16 | Type: 'click', 17 | What: 'settings', 18 | }) 19 | } 20 | 21 | var events = [ 22 | {El: '#settings', Ev: 'click', Fn: evtSettings}, 23 | {El: '#logout', Ev: 'click', Fn: evtLogout} 24 | ]; 25 | 26 | panel = new Panel(this, {}, tpl, 'header', events); 27 | 28 | this.Map = function(evt) { 29 | var data = {}; 30 | if (evt.Type == 'unauth') { 31 | data.Authed = false; 32 | } 33 | if (evt.Type == 'auth') { 34 | data.Authed = true; 35 | } 36 | // TODO map the event data to the panel data model 37 | return data; 38 | } 39 | 40 | return panel; 41 | } 42 | 43 | var tpl = ` 44 |

45 | 53 | ` -------------------------------------------------------------------------------- /webapp/js/page/login.js: -------------------------------------------------------------------------------- 1 | import {Panel} from '../panel/panel.js'; 2 | import {RestCall} from '../panel/rest.js'; 3 | 4 | "use strict"; 5 | 6 | // the controller for the Dashboard 7 | export function Login() { 8 | var panel = {}; 9 | 10 | function login() { 11 | console.log("submitted login"); 12 | var payload = { 13 | User: "admin", 14 | Password: "password" 15 | } 16 | 17 | RestCall(panel.evtHub, "POST", "/login", payload); 18 | } 19 | 20 | var events = [ 21 | {El: 'button[name="Submit"]', Ev: 'click', Fn: login} 22 | ]; 23 | 24 | panel = new Panel(this, {}, tpl, '#main', events); 25 | 26 | this.Map = function(evt) { 27 | console.log("auth got a call to Map", evt); 28 | 29 | // TODO map the event data to the panel data model 30 | return evt.Data; 31 | } 32 | 33 | return panel; 34 | } 35 | 36 | var tpl = ` 37 |
38 |
39 | 40 |
41 | 42 | 43 | 44 | 45 | 46 |
47 |
48 | ` -------------------------------------------------------------------------------- /webapp/js/page/settings.js: -------------------------------------------------------------------------------- 1 | import {Panel} from '../panel/panel.js'; 2 | 3 | "use strict"; 4 | 5 | export function Settings() { 6 | var panel = new Panel( 7 | this, 8 | {foo: 'with poop'}, 9 | '

Here is a settings template {{=it.foo}}

', 10 | '#main', 11 | {} 12 | ); 13 | 14 | this.Map = function(evt) { 15 | console.log("settings got an Map call", evt); 16 | return {}; 17 | } 18 | 19 | return panel; 20 | } -------------------------------------------------------------------------------- /webapp/js/panel/event.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export var eventHub = { 4 | subs: {}, 5 | } 6 | 7 | eventHub.Subscribe = function(key, subscriber) { 8 | this.subs[key] = subscriber; 9 | } 10 | 11 | eventHub.Fire = function(evt) { 12 | console.log("EVENT:", evt.Type); 13 | for (const k in this.subs) { 14 | var sub = this.subs[k]; 15 | sub.Notify(evt); 16 | } 17 | } -------------------------------------------------------------------------------- /webapp/js/panel/expander.js: -------------------------------------------------------------------------------- 1 | import { store } from "./store.js"; 2 | 3 | "use strict"; 4 | 5 | var expandState = new store({}); 6 | 7 | function expandHandler(pageID, elem) { 8 | var opened = false; 9 | var id = elem.getAttribute('for') 10 | var thing = document.querySelectorAll('#expander-'+id)[0]; 11 | var origThingClass = thing.className.replace(" expand", ""); 12 | var ctrlI = elem.querySelectorAll('i')[0]; 13 | var origCtrlIClass = ctrlI.className.replace(" open", ""); 14 | 15 | return function(evt) { 16 | evt.preventDefault(); 17 | evt.stopPropagation(); 18 | 19 | var pageState = States(pageID); 20 | opened = pageState[id]; 21 | 22 | if (!opened) { 23 | opened = true; 24 | setTimeout(()=>{ 25 | thing.className = thing.className + ' expand'; 26 | ctrlI.className = origCtrlIClass + ' open'; 27 | }, 20); 28 | } else { 29 | setTimeout(()=>{ 30 | thing.className = origThingClass; 31 | ctrlI.className = origCtrlIClass; 32 | }, 20); 33 | opened = false; 34 | } 35 | pageState[id] = opened; 36 | expandState.Update(pageID, pageState); 37 | } 38 | } 39 | 40 | export function States(pageID) { 41 | var states = expandState.Get(true); 42 | var pageState = states[pageID]; 43 | if (pageState==undefined) { 44 | pageState = {} 45 | } 46 | return pageState; 47 | } 48 | 49 | export function AttacheExpander(id, root) { 50 | var els = root.querySelectorAll('.expander-ctrl'); 51 | var len = els.length; 52 | for (var i = 0; i < len; i++) { 53 | var elem = els[i]; 54 | elem.addEventListener('click', expandHandler(id, elem)); 55 | } 56 | } -------------------------------------------------------------------------------- /webapp/js/panel/form.js: -------------------------------------------------------------------------------- 1 | import {Panel} from './panel.js'; 2 | import {el} from './panel.js'; 3 | import {doT} from '../vendor/dot.js'; 4 | 5 | "use strict"; 6 | 7 | export function Form(sel, obj, onSubmit) { 8 | 9 | // grab the form values to send to the callback 10 | var submitClick = function() { 11 | var data = { 12 | ID: obj.ID, 13 | Values: {}, 14 | }; 15 | for (var f in obj.fields) { 16 | var field = obj.fields[f]; 17 | data.Values[field.id] = el('input[name="field-'+field.id+'"]').value; 18 | } 19 | // callback with the form data 20 | onSubmit(data); 21 | } 22 | 23 | var events = [ 24 | {El: '#submit-'+obj.ID, Ev: 'click', Fn: submitClick} 25 | ]; 26 | 27 | var panel = new Panel(this, obj, tplForm, sel, events); 28 | 29 | // Map not used as no events expected for forms 30 | this.Map = function(evt) { 31 | console.log("Form got an event", evt); 32 | return {}; 33 | } 34 | 35 | return panel; 36 | } 37 | 38 | var tplForm = ` 39 |
40 | 41 | {{~it.Data.fields :field:index}} 42 | {{=field.prompt}}: 43 | 44 | {{~}} 45 | 46 | 47 | 48 | 49 |
50 | ` -------------------------------------------------------------------------------- /webapp/js/panel/rest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export function RestCall(evtHub, method, url, obj) { 4 | var apiUrl = '/build/api'+url; 5 | 6 | var body = null; 7 | 8 | var xhr = new XMLHttpRequest(); 9 | xhr.open(method, apiUrl, true); 10 | 11 | xhr.setRequestHeader('Accept', 'application/json') 12 | 13 | if ((method == 'PUT' || method == 'POST') && obj != undefined) { 14 | xhr.setRequestHeader('Content-Type', 'application/json') 15 | body = JSON.stringify(obj); 16 | } 17 | 18 | xhr.ontimeout = function () { 19 | console.error("The request for " + apiUrl + " timed out."); 20 | evtHub.Fire({ 21 | Type: 'top-error', 22 | Value: "Request timed out" 23 | }); 24 | }; 25 | 26 | xhr.onload = function() { 27 | if (xhr.readyState === 4) { 28 | var resp = { 29 | Url: url, 30 | Status: xhr.status, 31 | Response: {} 32 | }; 33 | var ct = xhr.getResponseHeader("Content-Type"); 34 | if(ct && ct.includes("application/json")) { 35 | resp.Response = JSON.parse(xhr.response); 36 | } 37 | evtHub.Fire({ 38 | Type: 'rest', 39 | Value: resp, 40 | }); 41 | } 42 | }; 43 | 44 | xhr.onerror = (t, e)=>{ 45 | evtHub.Fire({ 46 | Type: 'top-error', 47 | Value: "No connection" 48 | }); 49 | }; 50 | 51 | xhr.timeout = 5000; // 5 second timeout 52 | xhr.send(body); 53 | } -------------------------------------------------------------------------------- /webapp/js/panel/store.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export function store(initial, restReq) { 4 | this.changed = true; 5 | this.data = initial; 6 | 7 | // Update updates the data at the given key and marks the Store as having a change. 8 | this.Update = function(key, val) { 9 | if (this.data == null) { 10 | this.data = {}; 11 | } 12 | this.data[key] = val; 13 | this.changed = true; 14 | } 15 | 16 | // Get returns the data at the given key. If the data is unchanged then return null, 17 | // unless force is true, then return the data in any case. 18 | this.Get = function(force) { 19 | if (!this.changed && !force) { 20 | return null; 21 | } 22 | this.changed = false; 23 | return this.data; 24 | } 25 | 26 | this.TrashAll = function(force) { 27 | this.changed = true; 28 | this.data = initial; 29 | } 30 | 31 | this.IsEmpty = function(){ 32 | return this.data == null; 33 | } 34 | 35 | this.Reset = function() { 36 | this.changed = true; 37 | this.data = initial; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /webapp/js/panel/util.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | export function PrettyDate(time) { 4 | var date = new Date(time || ""), 5 | diff = (((new Date()).getTime() - date.getTime()) / 1000), 6 | day_diff = Math.floor(diff / 86400); 7 | var year = date.getFullYear(), 8 | month = date.getMonth()+1, 9 | day = date.getDate(); 10 | 11 | if (isNaN(day_diff) || day_diff < 0 || day_diff >= 31) 12 | return ( 13 | year.toString()+'-' 14 | +((month<10) ? '0'+month.toString() : month.toString())+'-' 15 | +((day<10) ? '0'+day.toString() : day.toString()) 16 | ); 17 | 18 | var r = 19 | ( 20 | ( 21 | day_diff == 0 && 22 | ( 23 | (diff < 60 && "just now") 24 | || (diff < 120 && "1 minute ago") 25 | || (diff < 3600 && Math.floor(diff / 60) + " minutes ago") 26 | || (diff < 7200 && "1 hour ago") 27 | || (diff < 86400 && Math.floor(diff / 3600) + " hours ago") 28 | ) 29 | ) 30 | || (day_diff == 1 && "Yesterday") 31 | || (day_diff < 7 && day_diff + " days ago") 32 | || (day_diff < 31 && Math.ceil(day_diff / 7) + " weeks ago") 33 | ); 34 | return r; 35 | } 36 | 37 | export function ToHHMMSS(sec_num) { 38 | sec_num = Math.floor(sec_num) 39 | var hours = Math.floor(sec_num / 3600); 40 | var minutes = Math.floor((sec_num - (hours * 3600)) / 60); 41 | var seconds = sec_num - (hours * 3600) - (minutes * 60); 42 | 43 | 44 | if (minutes < 10) {minutes = "0"+minutes;} 45 | if (seconds < 10) {seconds = "0"+seconds;} 46 | if (hours > 0) { 47 | return hours+':'+minutes+':'+seconds; 48 | } 49 | return minutes+':'+seconds; 50 | } 51 | -------------------------------------------------------------------------------- /webapp/js/vendor/rlite.js: -------------------------------------------------------------------------------- 1 | // This library started as an experiment to see how small I could make 2 | // a functional router. It has since been optimized (and thus grown). 3 | // The redundancy and inelegance here is for the sake of either size 4 | // or speed. 5 | // 6 | // That's why router params are marked with a single char: `~` and named params are denoted `:` 7 | 8 | "use strict"; 9 | 10 | export function rlite(notFound, base, routeDefinitions) { 11 | var routes = {}; 12 | var decode = decodeURIComponent; 13 | 14 | init(); 15 | 16 | return run; 17 | 18 | function init() { 19 | for (var key in routeDefinitions) { 20 | add(base+key, routeDefinitions[key]); 21 | } 22 | }; 23 | 24 | function noop(s) { return s; } 25 | 26 | function sanitize(url) { 27 | ~url.indexOf('/?') && (url = url.replace('/?', '?')); 28 | url[0] == '/' && (url = url.slice(1)); 29 | url[url.length - 1] == '/' && (url = url.slice(0, -1)); 30 | 31 | return url; 32 | } 33 | 34 | // Recursively searches the route tree for a matching route 35 | // pieces: an array of url parts, ['users', '1', 'edit'] 36 | // esc: the function used to url escape values 37 | // i: the index of the piece being processed 38 | // rules: the route tree 39 | // params: the computed route parameters (this is mutated), and is a stack since we don't have fast immutable datatypes 40 | // 41 | // This attempts to match the most specific route, but may end int a dead-end. We then attempt a less specific 42 | // route, following named route parameters. In searching this secondary branch, we need to make sure to clear 43 | // any route params that were generated during the search of the dead-end branch. 44 | function recurseUrl(pieces, esc, i, rules, params) { 45 | if (!rules) { 46 | return; 47 | } 48 | 49 | if (i >= pieces.length) { 50 | var cb = rules['@']; 51 | return cb && { 52 | cb: cb, 53 | params: params.reduce(function(h, kv) { h[kv[0]] = kv[1]; return h; }, {}), 54 | }; 55 | } 56 | 57 | var piece = esc(pieces[i]); 58 | var paramLen = params.length; 59 | return recurseUrl(pieces, esc, i + 1, rules[piece.toLowerCase()], params) 60 | || recurseNamedUrl(pieces, esc, i + 1, rules, ':', piece, params, paramLen) 61 | || recurseNamedUrl(pieces, esc, pieces.length, rules, '*', pieces.slice(i).join('/'), params, paramLen); 62 | } 63 | 64 | // Recurses for a named route, where the name is looked up via key and associated with val 65 | function recurseNamedUrl(pieces, esc, i, rules, key, val, params, paramLen) { 66 | params.length = paramLen; // Reset any params generated in the unsuccessful search branch 67 | var subRules = rules[key]; 68 | subRules && params.push([subRules['~'], val]); 69 | return recurseUrl(pieces, esc, i, subRules, params); 70 | } 71 | 72 | function processQuery(url, ctx, esc) { 73 | if (url && ctx.cb) { 74 | var hash = url.indexOf('#'), 75 | query = (hash < 0 ? url : url.slice(0, hash)).split('&'); 76 | 77 | for (var i = 0; i < query.length; ++i) { 78 | var nameValue = query[i].split('='); 79 | 80 | ctx.params[nameValue[0]] = esc(nameValue[1]); 81 | } 82 | } 83 | 84 | return ctx; 85 | } 86 | 87 | function lookup(url) { 88 | var querySplit = sanitize(url).split('?'); 89 | var esc = ~url.indexOf('%') ? decode : noop; 90 | 91 | return processQuery(querySplit[1], recurseUrl(querySplit[0].split('/'), esc, 0, routes, []) || {}, esc); 92 | } 93 | 94 | function add(route, handler) { 95 | var pieces = route.split('/'); 96 | var rules = routes; 97 | 98 | for (var i = +(route[0] === '/'); i < pieces.length; ++i) { 99 | var piece = pieces[i]; 100 | var name = piece[0] == ':' ? ':' : piece[0] == '*' ? '*' : piece.toLowerCase(); 101 | 102 | rules = rules[name] || (rules[name] = {}); 103 | 104 | (name == ':' || name == '*') && (rules['~'] = piece.slice(1)); 105 | } 106 | 107 | rules['@'] = handler; 108 | } 109 | 110 | function run(url, arg) { 111 | var result = lookup(url); 112 | 113 | return (result.cb || notFound)(result.params, arg, url); 114 | }; 115 | }; -------------------------------------------------------------------------------- /webapp/js/ws.js: -------------------------------------------------------------------------------- 1 | import { eventHub } from './panel/event.js'; 2 | 3 | "use strict"; 4 | 5 | export function WsHub() { 6 | 7 | var l = document.location 8 | var proto = 'wss:' 9 | if (l.protocol == 'http:') { 10 | proto = 'ws:' 11 | } 12 | var wsURL = proto + "//" + l.host + "/ws" 13 | 14 | this.Notify = function(event) { 15 | // TODO - forward any WS events 16 | } 17 | 18 | this.Close = function() { 19 | ws.close(); 20 | } 21 | 22 | // subscribe this controller to the eventHub. 23 | eventHub.Subscribe("ws", this); 24 | 25 | var ws = new WebSocket(wsURL); 26 | 27 | ws.onopen = () => { 28 | ws.send("Message to send"); // TODO - do we need any kind of handshake message ? 29 | }; 30 | 31 | ws.onmessage = (evt) => { 32 | eventHub.Fire({ 33 | Type: "ws", 34 | Msg:JSON.parse(evt.data) 35 | }); 36 | }; 37 | 38 | ws.onclose = () => { 39 | console.log("Connection is closed..."); 40 | }; 41 | 42 | window.onbeforeunload = () =>{ ws.close(); }; 43 | } 44 | 45 | --------------------------------------------------------------------------------