├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── catalog ├── catalog.go └── catalog_test.go ├── classifier ├── arch.go ├── classifier.go ├── fqdn.go ├── lsb.go └── os.go ├── client ├── client.go └── etcd.go ├── contrib ├── README.md ├── autocomplete │ ├── bash_autocomplete │ └── zsh_autocomplete ├── misc │ ├── build-libgit2-static.sh │ └── build-libgit2.sh ├── supervisord │ └── gru-minion.conf └── systemd │ └── gru-minion.service ├── docs ├── CHANGELOG.md ├── README.md ├── concepts.md ├── env-vars.md ├── images │ └── memcached-dag.png ├── installation.md ├── quickstart.md └── services.md ├── graph ├── graph.go ├── graph_test.go └── node.go ├── gructl ├── command │ ├── apply.go │ ├── classifier.go │ ├── error.go │ ├── graph.go │ ├── info.go │ ├── lastseen.go │ ├── list.go │ ├── log.go │ ├── push.go │ ├── queue.go │ ├── report.go │ ├── result.go │ ├── serve.go │ └── utils.go └── gructl.go ├── integration ├── fixtures │ ├── minion-classifier.yaml │ ├── minion-lastseen.yaml │ ├── minion-list.yaml │ ├── minion-name.yaml │ └── minion-task-backlog.yaml ├── minion_classifier_test.go ├── minion_lastseen_test.go ├── minion_list_test.go ├── minion_name_test.go ├── minion_task_test.go └── utils.go ├── main.go ├── minion ├── etcd.go └── minion.go ├── resource ├── collection.go ├── file.go ├── file_test.go ├── lua.go ├── package.go ├── package_test.go ├── property.go ├── provider.go ├── resource.go ├── service_freebsd.go ├── service_linux.go ├── service_linux_test.go ├── shell.go ├── shell_test.go ├── sysrc_freebsd.go ├── sysrc_freebsd_test.go ├── testutils_test.go ├── vsphere.go ├── vsphere_cluster.go ├── vsphere_cluster_host.go ├── vsphere_datacenter.go ├── vsphere_datastore.go ├── vsphere_host.go └── vsphere_vm.go ├── site ├── README.md ├── code │ ├── memcached.lua │ ├── triggers.lua │ └── vsphere.lua └── data │ └── memcached │ └── memcached-override.conf ├── task ├── task.go └── task_test.go ├── utils ├── file.go ├── git.go ├── list.go ├── list_test.go ├── map.go ├── map_test.go ├── slice.go ├── slice_test.go └── utils.go └── version └── version.go /.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 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Ignore Emacs backup files 27 | *~ 28 | 29 | # Vim swap files 30 | *.swp 31 | 32 | # Ignore Golang vendor packages 33 | vendor/ 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | sudo: required 4 | 5 | go: 6 | - 1.8 7 | - 1.7 8 | - master 9 | 10 | install: 11 | - make get 12 | 13 | script: 14 | - make test 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer 10 | in this position and unchanged. 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX := /usr/local 2 | 3 | build: get 4 | go build -ldflags="-s -w" -o bin/gructl -v 5 | 6 | get: 7 | go get -v -t -d ./... 8 | 9 | test: 10 | go test -v ./... 11 | 12 | install: build 13 | install -m 0755 bin/gructl ${PREFIX}/bin/gructl 14 | 15 | uninstall: 16 | rm -f ${PREFIX}/bin/gructl 17 | 18 | clean: 19 | rm -f bin/gructl 20 | 21 | format: 22 | go fmt . 23 | 24 | .PHONY: build get test install uninstall clean format 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Gru - Orchestration made easy with Go and Lua 2 | 3 | [![Build Status](https://travis-ci.org/dnaeon/gru.svg)](https://travis-ci.org/dnaeon/gru) 4 | [![GoDoc](https://godoc.org/github.com/dnaeon/gru?status.svg)](https://godoc.org/github.com/dnaeon/gru) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/dnaeon/gru)](https://goreportcard.com/report/github.com/dnaeon/gru) 6 | [![Join the chat at https://gitter.im/dnaeon/gru](https://badges.gitter.im/dnaeon/gru.svg)](https://gitter.im/dnaeon/gru?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | [![Codewake](https://www.codewake.com/badges/ask_question.svg)](https://www.codewake.com/p/gru) 8 | 9 | Gru is a fast and concurrent orchestration framework powered 10 | by Go and Lua, which allows you to manage your UNIX/Linux systems 11 | with ease. 12 | 13 | ## Documentation 14 | 15 | You can find the latest documentation [here](docs/). 16 | 17 | The API documentation is available [here](https://godoc.org/github.com/dnaeon/gru). 18 | 19 | ## Features 20 | 21 | * Written in fast, compiled language - [Go](https://golang.org/) 22 | * Uses a fast, lightweight, embeddable, scripting 23 | language as the DSL - [Lua](https://www.lua.org/) 24 | * Concurrent execution of idempotent operations 25 | * Distributed - using [etcd](https://github.com/coreos/etcd) for node 26 | discovery and communication and 27 | [Git](https://git-scm.com/) for version control and data sync 28 | * Easy to deploy - comes with a single, statically linked binary 29 | * Suitable for orchestration and configuration management 30 | 31 | ## Status 32 | 33 | Gru is in constant development. Consider the API unstable as 34 | things may change without a notice. 35 | 36 | ## Contributions 37 | 38 | Gru is hosted on [Github](https://github.com/dnaeon/gru). 39 | Please contribute by reporting issues, suggesting features or by 40 | sending patches using pull requests. 41 | 42 | ## License 43 | 44 | Gru is Open Source and licensed under the 45 | [BSD License](http://opensource.org/licenses/BSD-2-Clause). 46 | 47 | ## References 48 | 49 | References to articles related to this project in one way or another. 50 | 51 | * [Managing VMware vSphere environment with Go and Lua by using Gru orchestration framework](http://dnaeon.github.io/gru-vmware-vsphere-mgmt/) 52 | * [Introducing triggers in Gru orchestration framework](http://dnaeon.github.io/introducing-triggers-in-gru/) 53 | * [Puppet vs Gru - Benchmarking Speed & Concurrency](http://dnaeon.github.io/puppet-vs-gru-benchmarking-speed-and-concurrency/) 54 | * [Extending Lua with Go types](http://dnaeon.github.io/extending-lua-with-go-types/) 55 | * [Choosing Lua as the data description and configuration language](http://dnaeon.github.io/choosing-lua-as-the-ddl-and-config-language/) 56 | * [Creating an orchestration framework in Go](http://dnaeon.github.io/gru-orchestration-framework/) 57 | * [Dependency graph resolution algorithm in Go](http://dnaeon.github.io/dependency-graph-resolution-algorithm-in-go/) 58 | * [Orchestration made easy with Gru v0.2.0](http://dnaeon.github.io/orchestration-made-easy-with-gru-v0.2.0/) 59 | * [Membership test in Go](http://dnaeon.github.io/membership-test-in-go/) 60 | * [Testing HTTP interactions in Go](http://dnaeon.github.io/testing-http-interactions-in-go/) 61 | * [Concurrent map and slice types in Go](http://dnaeon.github.io/concurrent-maps-and-slices-in-go/) 62 | * [Lua as a Configuration And Data Exchange Language](https://www.netbsd.org/~mbalmer/lua/lua_config.pdf) 63 | -------------------------------------------------------------------------------- /catalog/catalog_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package catalog 27 | 28 | import ( 29 | "log" 30 | "os" 31 | "testing" 32 | 33 | "github.com/dnaeon/gru/resource" 34 | "github.com/yuin/gopher-lua" 35 | ) 36 | 37 | func TestCatalog(t *testing.T) { 38 | L := lua.NewState() 39 | defer L.Close() 40 | resource.LuaRegisterBuiltin(L) 41 | 42 | config := &Config{ 43 | Module: "", 44 | DryRun: true, 45 | Logger: log.New(os.Stdout, "", log.LstdFlags), 46 | SiteRepo: "", 47 | L: L, 48 | } 49 | katalog := New(config) 50 | 51 | if len(katalog.Unsorted) != 0 { 52 | t.Errorf("want 0 resources, got %d\n", len(katalog.Unsorted)) 53 | } 54 | 55 | code := ` 56 | foo = resource.file.new("foo") 57 | bar = resource.file.new("bar") 58 | qux = resource.file.new("qux") 59 | catalog:add(foo, bar, qux) 60 | ` 61 | 62 | if err := L.DoString(code); err != nil { 63 | t.Error(err) 64 | } 65 | 66 | if len(katalog.Unsorted) != 3 { 67 | t.Errorf("want 3 resources, got %d\n", len(katalog.Unsorted)) 68 | } 69 | 70 | code = ` 71 | if #catalog ~= 3 then 72 | error("want 3 resources, got " .. #catalog) 73 | end 74 | ` 75 | 76 | if err := L.DoString(code); err != nil { 77 | t.Error(err) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /classifier/arch.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package classifier 27 | 28 | import "runtime" 29 | 30 | func init() { 31 | Register("arch", archProvider) 32 | } 33 | 34 | func archProvider() (string, error) { 35 | return runtime.GOARCH, nil 36 | } 37 | -------------------------------------------------------------------------------- /classifier/classifier.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package classifier 27 | 28 | import "errors" 29 | 30 | // ErrClassifierNotFound is returned if the requested classifier was 31 | // not found in the classifier registry 32 | var ErrClassifierNotFound = errors.New("Classifier key not found") 33 | 34 | // Classifier type contains a key/value pair repsenting a classifier 35 | type Classifier struct { 36 | // Classifier key 37 | Key string `json:"key"` 38 | 39 | // Classifier value 40 | Value string `json:"value"` 41 | } 42 | 43 | // Type of classifier providers 44 | // A classifier provider is what does the 45 | // actual evaluation of a classifier 46 | type provider func() (string, error) 47 | 48 | // Registry provides a global registry for all classifiers 49 | var Registry = make(map[string]provider) 50 | 51 | // Register registers a new classifier to the registry 52 | func Register(key string, p provider) error { 53 | Registry[key] = p 54 | 55 | return nil 56 | } 57 | 58 | // Get retrieves a classifier from the registry by looking up its key 59 | func Get(key string) (*Classifier, error) { 60 | c := new(Classifier) 61 | 62 | p, ok := Registry[key] 63 | if ok { 64 | // Evaluate the classifier provider 65 | value, err := p() 66 | c := &Classifier{ 67 | Key: key, 68 | Value: value, 69 | } 70 | return c, err 71 | } 72 | 73 | return c, ErrClassifierNotFound 74 | } 75 | -------------------------------------------------------------------------------- /classifier/fqdn.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package classifier 27 | 28 | import "os" 29 | 30 | func init() { 31 | Register("fqdn", fqdnProvider) 32 | } 33 | 34 | func fqdnProvider() (string, error) { 35 | hostname, err := os.Hostname() 36 | 37 | return hostname, err 38 | } 39 | -------------------------------------------------------------------------------- /classifier/lsb.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | // +build linux 27 | 28 | package classifier 29 | 30 | import ( 31 | "bytes" 32 | "os/exec" 33 | "strings" 34 | ) 35 | 36 | func init() { 37 | Register("lsbdistid", idProvider) 38 | Register("lsbdistdesc", descProvider) 39 | Register("lsbdistrelease", releaseProvider) 40 | Register("lsbdistcodename", codenameProvider) 41 | } 42 | 43 | func runLSBreleaseTool(args ...string) (string, error) { 44 | var buf bytes.Buffer 45 | 46 | cmd := exec.Command("/usr/bin/lsb_release", args...) 47 | cmd.Stdout = &buf 48 | err := cmd.Run() 49 | 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | data := strings.Split(buf.String(), ":") 55 | result := strings.TrimSpace(data[1]) 56 | 57 | return result, nil 58 | } 59 | 60 | func idProvider() (string, error) { 61 | return runLSBreleaseTool("--id") 62 | } 63 | 64 | func descProvider() (string, error) { 65 | return runLSBreleaseTool("--description") 66 | } 67 | 68 | func releaseProvider() (string, error) { 69 | return runLSBreleaseTool("--release") 70 | } 71 | 72 | func codenameProvider() (string, error) { 73 | return runLSBreleaseTool("--codename") 74 | } 75 | -------------------------------------------------------------------------------- /classifier/os.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package classifier 27 | 28 | import "runtime" 29 | 30 | func init() { 31 | Register("os", osProvider) 32 | } 33 | 34 | func osProvider() (string, error) { 35 | return runtime.GOOS, nil 36 | } 37 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package client 27 | 28 | import ( 29 | "github.com/dnaeon/gru/classifier" 30 | "github.com/dnaeon/gru/task" 31 | 32 | "github.com/pborman/uuid" 33 | ) 34 | 35 | // Client interface for interacting with minions 36 | type Client interface { 37 | // Gets all registered minions 38 | MinionList() ([]uuid.UUID, error) 39 | 40 | // Gets the name of a minion 41 | MinionName(m uuid.UUID) (string, error) 42 | 43 | // Gets the time a minion was last seen 44 | MinionLastseen(m uuid.UUID) (int64, error) 45 | 46 | // Gets a classifier of a minion 47 | MinionClassifier(m uuid.UUID, key string) (*classifier.Classifier, error) 48 | 49 | // Gets all classifier keys of a minion 50 | MinionClassifierKeys(m uuid.UUID) ([]string, error) 51 | 52 | // Gets minions which are classified with a given classifier key 53 | MinionWithClassifierKey(key string) ([]uuid.UUID, error) 54 | 55 | // Gets the result of a task for a minion 56 | MinionTaskResult(m uuid.UUID, t uuid.UUID) (*task.Task, error) 57 | 58 | // Gets the minions which have a task result with the given uuid 59 | MinionWithTaskResult(t uuid.UUID) ([]uuid.UUID, error) 60 | 61 | // Gets the tasks which are currently pending in the queue 62 | MinionTaskQueue(m uuid.UUID) ([]*task.Task, error) 63 | 64 | // Gets the uuids of tasks which have already been processed 65 | MinionTaskLog(m uuid.UUID) ([]uuid.UUID, error) 66 | 67 | // Submits a new task to a minion 68 | MinionSubmitTask(m uuid.UUID, t *task.Task) error 69 | } 70 | -------------------------------------------------------------------------------- /contrib/README.md: -------------------------------------------------------------------------------- 1 | ## Contrib 2 | 3 | This directory contains scripts and files, which may be useful, but 4 | are not part of the core Gru project. 5 | 6 | -------------------------------------------------------------------------------- /contrib/autocomplete/bash_autocomplete: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | : ${PROG:=$(basename ${BASH_SOURCE})} 4 | 5 | _cli_bash_autocomplete() { 6 | local cur opts base 7 | COMPREPLY=() 8 | cur="${COMP_WORDS[COMP_CWORD]}" 9 | opts=$( ${COMP_WORDS[@]:0:$COMP_CWORD} --generate-bash-completion ) 10 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 11 | return 0 12 | } 13 | 14 | complete -F _cli_bash_autocomplete $PROG 15 | -------------------------------------------------------------------------------- /contrib/autocomplete/zsh_autocomplete: -------------------------------------------------------------------------------- 1 | autoload -U compinit && compinit 2 | autoload -U bashcompinit && bashcompinit 3 | 4 | script_dir=$(dirname $0) 5 | source ${script_dir}/bash_autocomplete 6 | -------------------------------------------------------------------------------- /contrib/misc/build-libgit2-static.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # Script used to build a static libgit2 library 4 | # 5 | 6 | LIBGIT2_VERSION="0.24.0" 7 | wget -O libgit2-${LIBGIT2_VERSION}.tar.gz https://github.com/libgit2/libgit2/archive/v${LIBGIT2_VERSION}.tar.gz 8 | tar -xzvf libgit2-${LIBGIT2_VERSION}.tar.gz 9 | cd libgit2-${LIBGIT2_VERSION} 10 | mkdir {build,install} && cd build 11 | cmake -DTHREADSAFE=ON \ 12 | -DBUILD_CLAR=OFF \ 13 | -DBUILD_SHARED_LIBS=OFF \ 14 | -DCMAKE_C_FLAGS=-fPIC \ 15 | -DCMAKE_INSTALL_PREFIX=../install \ 16 | .. 17 | 18 | cmake --build . --target install 19 | 20 | -------------------------------------------------------------------------------- /contrib/misc/build-libgit2.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # Script used to build and install libgit2 on Travis CI 4 | # 5 | 6 | set -xe 7 | 8 | LIBGIT2_VERSION="0.24.0" 9 | wget -O libgit2-${LIBGIT2_VERSION}.tar.gz https://github.com/libgit2/libgit2/archive/v${LIBGIT2_VERSION}.tar.gz 10 | tar -xzvf libgit2-${LIBGIT2_VERSION}.tar.gz 11 | cd libgit2-${LIBGIT2_VERSION} 12 | mkdir build && cd build 13 | cmake -DBUILD_CLAR=OFF .. && make && sudo make install 14 | sudo ldconfig 15 | -------------------------------------------------------------------------------- /contrib/supervisord/gru-minion.conf: -------------------------------------------------------------------------------- 1 | [program:gru-minion] 2 | command=/usr/local/bin/gructl serve 3 | redirect_stderr=true 4 | autostart=true 5 | environment=GRUCTL_ENDPOINT="http://127.0.0.1:2379,http://127.0.0.1:4001" 6 | environment=GRU_SITEREPO="https://github.com/you/gru-site" 7 | stopsignal=INT 8 | -------------------------------------------------------------------------------- /contrib/systemd/gru-minion.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Gru orchestration framework minion 3 | Documentation=https://github.com/dnaeon/gru/docs 4 | After=network.target 5 | 6 | [Service] 7 | Type=simple 8 | Environment=GRU_ENDPOINT=http://127.0.0.1:2379,http://127.0.0.1:4001 9 | Environment=GRU_SITEREPO=https://github.com/you/gru-site 10 | ExecStart=/usr/local/bin/gructl serve 11 | WorkingDirectory=/var/lib/gru 12 | KillMode=process 13 | KillSignal=SIGINT 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | ## Documentation 2 | 3 | A good place to get started is to familiarize yourself with the 4 | [concepts](concepts.md) used in Gru. 5 | 6 | Afterwards you should check the [installation](installation.md) 7 | document which provides information on how to obtain and 8 | install Gru. 9 | 10 | Once ready with that check the [quickstart guide](quickstart.md), 11 | which will walk you through your first steps with Gru. 12 | 13 | For setting environment variables, make sure to check the 14 | [environment variables](env-vars.md) document. 15 | 16 | In order to automatically start your minions during boot-time, 17 | you should check the document for [enabling services](services.md) 18 | during boot-time. 19 | 20 | You should also check the documentation of 21 | [available resources](https://godoc.org/github.com/dnaeon/gru/resource) 22 | that you could use in your code. 23 | -------------------------------------------------------------------------------- /docs/concepts.md: -------------------------------------------------------------------------------- 1 | ## Concepts 2 | 3 | Gru is designed around the following concepts, each of which is 4 | explained below. 5 | 6 | ## Resource 7 | 8 | Resources are the core components in Gru. Each resource is 9 | responsible for handling a particular task in an idempotent manner, e.g. 10 | management of packages, management of services, executing commands, etc. 11 | 12 | ## Module 13 | 14 | A module is essentially a [Lua](https://www.lua.org/) module. 15 | 16 | Lua is used to form the foundation of the DSL language used in Gru. 17 | 18 | Within modules resources are being created and registered to the 19 | catalog. 20 | 21 | ## Catalog 22 | 23 | The catalog represents a collection of resources, which were 24 | created after evaluating a given module. 25 | 26 | Before processing the catalog all resources are first 27 | [topologically sorted](https://en.wikipedia.org/wiki/Topological_sorting), 28 | in order to determine the proper order of evaluation and processing. 29 | 30 | ## Task 31 | 32 | A task represents a message to remote minions, that a given 33 | module should be evaluated and result should be returned. 34 | 35 | The task itself also bundles additional meta data, such as the 36 | unique id of the task, the time when task has been received, 37 | processed, etc. 38 | 39 | Tasks are sent out to minions using [etcd](https://github.com/coreos/etcd). 40 | 41 | ## Client 42 | 43 | The client is used to push tasks to minions, retrieve results, 44 | generate reports about minions, etc. It is the frontend application of 45 | Gru. 46 | 47 | ## Minion 48 | 49 | The minion is a remote system which receives and processes tasks from 50 | clients. 51 | -------------------------------------------------------------------------------- /docs/env-vars.md: -------------------------------------------------------------------------------- 1 | ## Environment Variables 2 | 3 | Gru uses the following environment variables which you could set. 4 | 5 | ### GRU_ENDPOINT 6 | 7 | The [etcd](https://github.com/coreos/etcd) cluster endpoint to which 8 | client and minions connect. 9 | 10 | Default value: http://127.0.0.1:2379,http://localhost:4001 11 | 12 | ### GRU_USERNAME 13 | 14 | Username to use when authenticating against etcd. 15 | 16 | Default: none 17 | 18 | ### GRU_PASSWORD 19 | 20 | Password to use when when authenticating against etcd. 21 | 22 | Default: none 23 | 24 | ### GRU_MODULEPATH 25 | 26 | Path where modules can be discovered and loaded. 27 | 28 | ### GRU_TIMEOUT 29 | 30 | Specifies the connection timeout per request 31 | 32 | Default: 1s 33 | 34 | ### GRU_SITEREPO 35 | 36 | Specifies the path/url to the site repository 37 | 38 | Default: none 39 | 40 | ### GRU_ENVIRONMENT 41 | 42 | Specifices the environment to be used by minions when processing a task 43 | 44 | Default: production 45 | -------------------------------------------------------------------------------- /docs/images/memcached-dag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dnaeon/gru/4c9792f96b548d9bdfcb42785aa8ef320b585357/docs/images/memcached-dag.png -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | In order to build Gru you need Go version 1.7 or later. 4 | 5 | Building Gru is as easy as executing the commands below. 6 | 7 | ```bash 8 | $ git clone https://github.com/dnaeon/gru 9 | $ cd gru 10 | $ make 11 | ``` 12 | 13 | ## Optional requirements 14 | 15 | The optional requirements listed below are needed if you need to 16 | orchestrate remote systems. They are not required if you use Gru in 17 | stand-alone mode. 18 | 19 | [etcd](https://github.com/coreos/etcd) is used for discovery of 20 | minions and communication between the minions and clients, so 21 | in order to orchestrate remote minions you need to make sure that you 22 | have `etcd` up and running, so that remote minions can connect to it. 23 | 24 | For more information about installing and configuring 25 | [etcd](https://github.com/coreos/etcd), please refer to the 26 | [official etcd documentation](https://coreos.com/etcd/docs/latest/). 27 | 28 | [Git](https://git-scm.com/) is used for syncing code and data 29 | files to the remote minions, so make sure that you have Git 30 | installed as well. 31 | 32 | ## Shell Completion 33 | 34 | You can enable shell autocompletion by sourcing the 35 | correct file from the [contrib/autocomplete](../contrib/autocomplete) 36 | directory for your shell. 37 | 38 | For instance to enable bash autocompletion on an Arch Linux system, 39 | you would do. 40 | 41 | ```bash 42 | $ sudo cp contrib/autocomplete/bash_autocomplete /usr/share/bash-completion/completions/gructl 43 | ``` 44 | 45 | Note that you will need to install the `bash-completion` 46 | package, if you don't have it installed already. 47 | 48 | ## Tests 49 | 50 | You can run the tests by executing the command below. 51 | 52 | ```bash 53 | $ make test 54 | ``` 55 | -------------------------------------------------------------------------------- /docs/services.md: -------------------------------------------------------------------------------- 1 | ## Enabling services during boot-time 2 | 3 | In order to start your minions during boot-time you have two 4 | options. 5 | 6 | If your minions are running on a system that supports 7 | [systemd](https://www.freedesktop.org/wiki/Software/systemd/), 8 | you could use the provided systemd unit file for Gru. 9 | 10 | Or you could use [supervisord](http://supervisord.org/) for process 11 | control. 12 | 13 | ## systemd unit 14 | 15 | Get the systemd unit file from the [contrib/systemd](../contrib) 16 | directory and install it on your system. 17 | 18 | Check [Unit File Load Path](https://www.freedesktop.org/software/systemd/man/systemd.unit.html#Unit%20File%20Load%20Path) 19 | document for the location where you should install your unit file. 20 | 21 | Once you've got the unit in place, execute these commands which will 22 | enable and start your minion. 23 | 24 | ```bash 25 | $ sudo systemctl daemon-reload 26 | $ sudo systemctl enable gru-minion 27 | $ sudo systemctl start gru-minion 28 | ``` 29 | 30 | ## supervisord 31 | 32 | Get the supervisord config file from [contrib/supervisord](../contrib/supervisord) 33 | directory and place in under your supervisord `include` directory. 34 | 35 | Once you've got the file in place, reload the supervisord 36 | configuration, enable and start the service. 37 | 38 | ```bash 39 | $ sudo supervisorctl reread 40 | $ sudo supervisorctl reload 41 | $ sudo supervisorctl start gru-minion 42 | ``` 43 | -------------------------------------------------------------------------------- /graph/graph.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package graph 27 | 28 | import ( 29 | "errors" 30 | "fmt" 31 | "io" 32 | "strings" 33 | 34 | mapset "github.com/deckarep/golang-set" 35 | ) 36 | 37 | // ErrCircularDependency is returned when the graph cannot be 38 | // topologically sorted because of circular dependencies 39 | var ErrCircularDependency = errors.New("Circular dependency found in graph") 40 | 41 | // Graph represents a DAG graph 42 | type Graph struct { 43 | Nodes map[string]*Node 44 | } 45 | 46 | // New creates a new DAG graph 47 | func New() *Graph { 48 | g := &Graph{ 49 | Nodes: make(map[string]*Node), 50 | } 51 | 52 | return g 53 | } 54 | 55 | // AddNode adds nodes to the graph 56 | func (g *Graph) AddNode(nodes ...*Node) { 57 | for _, node := range nodes { 58 | g.Nodes[node.Name] = node 59 | } 60 | } 61 | 62 | // AddEdge connects a node with other nodes in the graph 63 | func (g *Graph) AddEdge(node *Node, edges ...*Node) { 64 | for _, edge := range edges { 65 | node.Edges = append(node.Edges, edge) 66 | } 67 | } 68 | 69 | // GetNode retrieves the node from the graph with the given name 70 | func (g *Graph) GetNode(name string) (*Node, bool) { 71 | n, ok := g.Nodes[name] 72 | 73 | return n, ok 74 | } 75 | 76 | // Sort performs a topological sort of the graph 77 | // https://en.wikipedia.org/wiki/Topological_sorting 78 | // 79 | // If the graph can be topologically sorted the result will 80 | // contain the sorted nodes. 81 | // 82 | // If the graph cannot be sorted in case of circular dependencies, 83 | // then the result will contain the remaining nodes from the graph, 84 | // which are the ones causing the circular dependency. 85 | func (g *Graph) Sort() ([]*Node, error) { 86 | var sorted []*Node 87 | 88 | // Iteratively find and remove nodes from the graph which have no edges. 89 | // If at some point there are still nodes in the graph and we cannot find 90 | // nodes without edges, that means we have a circular dependency 91 | for len(g.Nodes) > 0 { 92 | // Contains the ready nodes, which have no edges to other nodes 93 | ready := mapset.NewSet() 94 | 95 | // Find the nodes with no edges 96 | for _, node := range g.Nodes { 97 | if len(node.Edges) == 0 { 98 | ready.Add(node) 99 | } 100 | } 101 | 102 | // If there aren't any ready nodes, then we have a cicular dependency 103 | if ready.Cardinality() == 0 { 104 | // The remaining nodes in the graph are the ones causing the 105 | // circular dependency. 106 | var remaining []*Node 107 | for _, n := range g.Nodes { 108 | remaining = append(remaining, n) 109 | } 110 | return remaining, ErrCircularDependency 111 | } 112 | 113 | // Remove the ready nodes and add them to the sorted result 114 | for item := range ready.Iter() { 115 | node := item.(*Node) 116 | delete(g.Nodes, node.Name) 117 | sorted = append(sorted, node) 118 | } 119 | 120 | // Remove ready nodes from the remaining node edges as well 121 | for _, node := range g.Nodes { 122 | // Add the remaining nodes in a set 123 | currentEdgeSet := mapset.NewSet() 124 | for _, edge := range node.Edges { 125 | currentEdgeSet.Add(edge) 126 | } 127 | 128 | newEdgeSet := currentEdgeSet.Difference(ready) 129 | node.Edges = make([]*Node, 0) 130 | for edge := range newEdgeSet.Iter() { 131 | node.Edges = append(node.Edges, edge.(*Node)) 132 | } 133 | } 134 | } 135 | 136 | return sorted, nil 137 | } 138 | 139 | // AsDot generates a DOT representation for the graph 140 | // https://en.wikipedia.org/wiki/DOT_(graph_description_language) 141 | func (g *Graph) AsDot(name string, w io.Writer) { 142 | fmt.Fprintf(w, "digraph %s {\n", name) 143 | fmt.Fprintf(w, "\tlabel = %q;\n", name) 144 | fmt.Fprintf(w, "\tnodesep=1.0;\n") 145 | fmt.Fprintf(w, "\tnode [shape=box];\n") 146 | fmt.Fprintf(w, "\tedge [style=filled];\n") 147 | 148 | for _, node := range g.Nodes { 149 | var edges []string 150 | for _, edge := range node.Edges { 151 | edges = append(edges, fmt.Sprintf("%q", edge.Name)) 152 | } 153 | 154 | if len(edges) > 0 { 155 | fmt.Fprintf(w, "\t%q -> {%s};\n", node.Name, strings.Join(edges, " ")) 156 | } else { 157 | fmt.Fprintf(w, "\t%q;\n", node.Name) 158 | } 159 | } 160 | 161 | fmt.Fprintf(w, "}\n") 162 | } 163 | 164 | // Reversed creates the reversed representation of the graph 165 | func (g *Graph) Reversed() *Graph { 166 | reversed := New() 167 | 168 | // Create a map of the graph nodes 169 | nodes := make(map[string]*Node) 170 | for _, n := range g.Nodes { 171 | node := NewNode(n.Name) 172 | nodes[n.Name] = node 173 | reversed.AddNode(node) 174 | } 175 | 176 | // Connect the nodes in the graph 177 | for _, node := range g.Nodes { 178 | for _, edge := range node.Edges { 179 | reversed.AddEdge(nodes[edge.Name], nodes[node.Name]) 180 | } 181 | } 182 | 183 | return reversed 184 | } 185 | -------------------------------------------------------------------------------- /graph/graph_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package graph 27 | 28 | import ( 29 | "reflect" 30 | "testing" 31 | ) 32 | 33 | func TestWorkingGraph(t *testing.T) { 34 | g := New() 35 | 36 | // Graph node names 37 | nodeNames := []string{ 38 | "A", 39 | "B", 40 | "C", 41 | "D", 42 | "E", 43 | } 44 | 45 | // Map containing the node names and the actual node instance 46 | nodes := make(map[string]*Node) 47 | 48 | // Create nodes and add them to the graph 49 | for _, name := range nodeNames { 50 | n := NewNode(name) 51 | nodes[name] = n 52 | g.AddNode(n) 53 | } 54 | 55 | // Connect the nodes in the graph 56 | // 57 | // A 58 | // B -> C 59 | // C -> D 60 | // D -> E 61 | // E -> A 62 | // 63 | g.AddEdge(nodes["B"], nodes["C"]) 64 | g.AddEdge(nodes["C"], nodes["D"]) 65 | g.AddEdge(nodes["D"], nodes["E"]) 66 | g.AddEdge(nodes["E"], nodes["A"]) 67 | 68 | // Excepted result after topological sort 69 | wantSorted := []string{ 70 | "A", 71 | "E", 72 | "D", 73 | "C", 74 | "B", 75 | } 76 | 77 | gotNodes, err := g.Sort() 78 | if err != nil { 79 | t.Error(err) 80 | } 81 | 82 | var gotSorted []string 83 | for _, node := range gotNodes { 84 | gotSorted = append(gotSorted, node.Name) 85 | } 86 | 87 | if !reflect.DeepEqual(wantSorted, gotSorted) { 88 | t.Errorf("Want %q, got %q graph", wantSorted, gotSorted) 89 | } 90 | } 91 | 92 | func TestCircularGraph(t *testing.T) { 93 | g := New() 94 | 95 | // Node names 96 | nodeNames := []string{ 97 | "A", 98 | "B", 99 | "C", 100 | "D", 101 | "E", 102 | } 103 | 104 | // Map containing the node names and the actual node instance 105 | nodes := make(map[string]*Node) 106 | 107 | // Create nodes and add them to the graph 108 | for _, name := range nodeNames { 109 | n := NewNode(name) 110 | nodes[name] = n 111 | g.AddNode(n) 112 | } 113 | 114 | // Connect the nodes in the graph 115 | // 116 | // A 117 | // B -> C 118 | // C -> D 119 | // D -> E 120 | // E -> D <- Circular dependency here 121 | // 122 | g.AddEdge(nodes["B"], nodes["C"]) 123 | g.AddEdge(nodes["C"], nodes["D"]) 124 | g.AddEdge(nodes["D"], nodes["E"]) 125 | g.AddEdge(nodes["E"], nodes["D"]) 126 | 127 | _, err := g.Sort() 128 | if err != ErrCircularDependency { 129 | t.Errorf("want a circular dependency error, got %s", err) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /graph/node.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package graph 27 | 28 | // Node represents a single node in the graph 29 | type Node struct { 30 | // Name of the node 31 | Name string 32 | 33 | // Edges to other nodes in the graph 34 | Edges []*Node 35 | } 36 | 37 | // NewNode creates a new node with the given name 38 | func NewNode(name string) *Node { 39 | n := &Node{ 40 | Name: name, 41 | Edges: make([]*Node, 0), 42 | } 43 | 44 | return n 45 | } 46 | -------------------------------------------------------------------------------- /gructl/command/apply.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "log" 30 | "os" 31 | "runtime" 32 | 33 | "github.com/dnaeon/gru/catalog" 34 | "github.com/urfave/cli" 35 | "github.com/yuin/gopher-lua" 36 | ) 37 | 38 | // NewApplyCommand creates a new sub-command for 39 | // applying configurations on the local system 40 | func NewApplyCommand() cli.Command { 41 | cmd := cli.Command{ 42 | Name: "apply", 43 | Usage: "apply configuration on the local system", 44 | Action: execApplyCommand, 45 | Flags: []cli.Flag{ 46 | cli.StringFlag{ 47 | Name: "siterepo", 48 | Value: "", 49 | Usage: "path/url to the site repo", 50 | EnvVar: "GRU_SITEREPO", 51 | }, 52 | cli.BoolFlag{ 53 | Name: "dry-run", 54 | Usage: "just report what would be done, instead of doing it", 55 | }, 56 | cli.IntFlag{ 57 | Name: "concurrency", 58 | Usage: "number of goroutines used for concurrent processing", 59 | Value: runtime.NumCPU(), 60 | }, 61 | }, 62 | } 63 | 64 | return cmd 65 | } 66 | 67 | // Executes the "apply" command 68 | func execApplyCommand(c *cli.Context) error { 69 | if len(c.Args()) < 1 { 70 | return cli.NewExitError(errNoModuleName.Error(), 64) 71 | } 72 | 73 | concurrency := c.Int("concurrency") 74 | if concurrency < 0 { 75 | concurrency = runtime.NumCPU() 76 | } 77 | 78 | L := lua.NewState() 79 | defer L.Close() 80 | 81 | logger := log.New(os.Stdout, "", log.LstdFlags) 82 | 83 | config := &catalog.Config{ 84 | Module: c.Args()[0], 85 | DryRun: c.Bool("dry-run"), 86 | Logger: logger, 87 | SiteRepo: c.String("siterepo"), 88 | L: L, 89 | Concurrency: concurrency, 90 | } 91 | 92 | katalog := catalog.New(config) 93 | if err := katalog.Load(); err != nil { 94 | if err != nil { 95 | return cli.NewExitError(err.Error(), 1) 96 | } 97 | } 98 | 99 | status := katalog.Run() 100 | status.Summary(logger) 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /gructl/command/classifier.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | 31 | "github.com/coreos/etcd/client" 32 | "github.com/gosuri/uitable" 33 | "github.com/pborman/uuid" 34 | "github.com/urfave/cli" 35 | ) 36 | 37 | // NewClassifierCommand creates a new sub-command for retrieving 38 | // minion classifiers 39 | func NewClassifierCommand() cli.Command { 40 | cmd := cli.Command{ 41 | Name: "classifier", 42 | Usage: "list minion classifiers", 43 | Action: execClassifierCommand, 44 | } 45 | 46 | return cmd 47 | } 48 | 49 | // Executes the "classifier" command 50 | func execClassifierCommand(c *cli.Context) error { 51 | if len(c.Args()) == 0 { 52 | return cli.NewExitError(errNoMinion.Error(), 64) 53 | } 54 | 55 | arg := c.Args()[0] 56 | minion := uuid.Parse(arg) 57 | if minion == nil { 58 | return cli.NewExitError(errInvalidUUID.Error(), 64) 59 | } 60 | 61 | klient := newEtcdMinionClientFromFlags(c) 62 | 63 | // Ignore errors about missing classifier directory 64 | classifierKeys, err := klient.MinionClassifierKeys(minion) 65 | if err != nil { 66 | if eerr, ok := err.(client.Error); !ok || eerr.Code != client.ErrorCodeKeyNotFound { 67 | return cli.NewExitError(err.Error(), 1) 68 | } 69 | } 70 | 71 | if len(classifierKeys) == 0 { 72 | return nil 73 | } 74 | 75 | table := uitable.New() 76 | table.MaxColWidth = 80 77 | table.AddRow("KEY", "VALUE") 78 | for _, key := range classifierKeys { 79 | classifier, err := klient.MinionClassifier(minion, key) 80 | if err != nil { 81 | return cli.NewExitError(err.Error(), 1) 82 | } 83 | 84 | table.AddRow(classifier.Key, classifier.Value) 85 | } 86 | 87 | fmt.Println(table) 88 | 89 | return nil 90 | } 91 | -------------------------------------------------------------------------------- /gructl/command/error.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import "errors" 29 | 30 | var ( 31 | errNoMinion = errors.New("Missing minion uuid") 32 | errInvalidUUID = errors.New("Invalid uuid given") 33 | errNoMinionFound = errors.New("No minion(s) found") 34 | errNoClassifier = errors.New("Missing classifier key") 35 | errInvalidClassifier = errors.New("Invalid classifier pattern") 36 | errNoTask = errors.New("Missing task uuid") 37 | errNoModuleName = errors.New("Missing module name") 38 | errNoSiteRepo = errors.New("Missing site repo") 39 | ) 40 | -------------------------------------------------------------------------------- /gructl/command/graph.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "log" 30 | "os" 31 | 32 | "github.com/dnaeon/gru/catalog" 33 | "github.com/dnaeon/gru/graph" 34 | "github.com/dnaeon/gru/resource" 35 | "github.com/urfave/cli" 36 | "github.com/yuin/gopher-lua" 37 | ) 38 | 39 | // NewGraphCommand creates a new sub-command for 40 | // generating the resource DAG graph 41 | func NewGraphCommand() cli.Command { 42 | cmd := cli.Command{ 43 | Name: "graph", 44 | Usage: "generate graph representation of resources", 45 | Action: execGraphCommand, 46 | Flags: []cli.Flag{ 47 | cli.StringFlag{ 48 | Name: "siterepo", 49 | Value: "", 50 | Usage: "path/url to the site repo", 51 | EnvVar: "GRU_SITEREPO", 52 | }, 53 | }, 54 | } 55 | 56 | return cmd 57 | } 58 | 59 | // Executes the "graph" command 60 | func execGraphCommand(c *cli.Context) error { 61 | if len(c.Args()) < 1 { 62 | return cli.NewExitError(errNoModuleName.Error(), 64) 63 | } 64 | 65 | L := lua.NewState() 66 | defer L.Close() 67 | 68 | module := c.Args()[0] 69 | config := &catalog.Config{ 70 | Module: module, 71 | DryRun: true, 72 | Logger: log.New(os.Stdout, "", log.LstdFlags), 73 | SiteRepo: c.String("siterepo"), 74 | L: L, 75 | } 76 | 77 | katalog := catalog.New(config) 78 | resource.LuaRegisterBuiltin(L) 79 | if err := L.DoFile(module); err != nil { 80 | return cli.NewExitError(err.Error(), 1) 81 | } 82 | 83 | collection, err := resource.CreateCollection(katalog.Unsorted) 84 | if err != nil { 85 | return cli.NewExitError(err.Error(), 1) 86 | } 87 | 88 | g, err := collection.DependencyGraph() 89 | if err != nil { 90 | return cli.NewExitError(err.Error(), 1) 91 | } 92 | 93 | g.AsDot("resources", os.Stdout) 94 | g.Reversed().AsDot("reversed", os.Stdout) 95 | 96 | sorted, err := g.Sort() 97 | if err == graph.ErrCircularDependency { 98 | circular := graph.New() 99 | circular.AddNode(sorted...) 100 | circular.AsDot("circular", os.Stdout) 101 | return cli.NewExitError(graph.ErrCircularDependency.Error(), 1) 102 | } 103 | 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /gructl/command/info.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | "time" 31 | 32 | "github.com/coreos/etcd/client" 33 | "github.com/gosuri/uitable" 34 | "github.com/pborman/uuid" 35 | "github.com/urfave/cli" 36 | ) 37 | 38 | // NewInfoCommand creates a new sub-command for retrieving 39 | // minion information 40 | func NewInfoCommand() cli.Command { 41 | cmd := cli.Command{ 42 | Name: "info", 43 | Usage: "get minion info", 44 | Action: execInfoCommand, 45 | } 46 | 47 | return cmd 48 | } 49 | 50 | // Executes the "info" command 51 | func execInfoCommand(c *cli.Context) error { 52 | if len(c.Args()) == 0 { 53 | return cli.NewExitError(errNoMinion.Error(), 64) 54 | } 55 | 56 | arg := c.Args()[0] 57 | minion := uuid.Parse(arg) 58 | if minion == nil { 59 | return cli.NewExitError(errInvalidUUID.Error(), 64) 60 | } 61 | 62 | klient := newEtcdMinionClientFromFlags(c) 63 | name, err := klient.MinionName(minion) 64 | if err != nil { 65 | return cli.NewExitError(err.Error(), 1) 66 | } 67 | 68 | lastseen, err := klient.MinionLastseen(minion) 69 | if err != nil { 70 | return cli.NewExitError(err.Error(), 1) 71 | } 72 | 73 | // Ignore errors about missing queue directory 74 | taskQueue, err := klient.MinionTaskQueue(minion) 75 | if err != nil { 76 | if eerr, ok := err.(client.Error); !ok || eerr.Code != client.ErrorCodeKeyNotFound { 77 | return cli.NewExitError(err.Error(), 1) 78 | } 79 | } 80 | 81 | // Ignore errors about missing log directory 82 | taskLog, err := klient.MinionTaskLog(minion) 83 | if err != nil { 84 | if eerr, ok := err.(client.Error); !ok || eerr.Code != client.ErrorCodeKeyNotFound { 85 | return cli.NewExitError(err.Error(), 1) 86 | } 87 | } 88 | 89 | // Ignore errors about missing classifier directory 90 | classifierKeys, err := klient.MinionClassifierKeys(minion) 91 | if err != nil { 92 | if eerr, ok := err.(client.Error); !ok || eerr.Code != client.ErrorCodeKeyNotFound { 93 | return cli.NewExitError(err.Error(), 1) 94 | } 95 | } 96 | 97 | table := uitable.New() 98 | table.MaxColWidth = 80 99 | table.AddRow("Minion:", minion) 100 | table.AddRow("Name:", name) 101 | table.AddRow("Lastseen:", time.Unix(lastseen, 0)) 102 | table.AddRow("Queue:", len(taskQueue)) 103 | table.AddRow("Log:", len(taskLog)) 104 | table.AddRow("Classifiers:", len(classifierKeys)) 105 | 106 | fmt.Println(table) 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /gructl/command/lastseen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | "time" 31 | 32 | "github.com/gosuri/uitable" 33 | "github.com/urfave/cli" 34 | ) 35 | 36 | // NewLastseenCommand creates a new sub-command for 37 | // retrieving the last time minions were seen 38 | func NewLastseenCommand() cli.Command { 39 | cmd := cli.Command{ 40 | Name: "lastseen", 41 | Usage: "show when minion(s) were last seen", 42 | Action: execLastseenCommand, 43 | Flags: []cli.Flag{ 44 | cli.StringFlag{ 45 | Name: "with-classifier", 46 | Value: "", 47 | Usage: "match minions with given classifier pattern", 48 | }, 49 | }, 50 | } 51 | 52 | return cmd 53 | } 54 | 55 | // Executes the "lastseen" command 56 | func execLastseenCommand(c *cli.Context) error { 57 | client := newEtcdMinionClientFromFlags(c) 58 | 59 | cFlag := c.String("with-classifier") 60 | minions, err := parseClassifierPattern(client, cFlag) 61 | 62 | if err != nil { 63 | return cli.NewExitError(err.Error(), 1) 64 | } 65 | 66 | table := uitable.New() 67 | table.MaxColWidth = 80 68 | table.AddRow("MINION", "LASTSEEN") 69 | for _, minion := range minions { 70 | lastseen, err := client.MinionLastseen(minion) 71 | if err != nil { 72 | return cli.NewExitError(err.Error(), 1) 73 | } 74 | 75 | table.AddRow(minion, time.Unix(lastseen, 0)) 76 | } 77 | 78 | fmt.Println(table) 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /gructl/command/list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | 31 | "github.com/coreos/etcd/client" 32 | "github.com/gosuri/uitable" 33 | "github.com/urfave/cli" 34 | ) 35 | 36 | // NewListCommand creates a new sub-command for retrieving the 37 | // list of registered minions 38 | func NewListCommand() cli.Command { 39 | cmd := cli.Command{ 40 | Name: "list", 41 | Usage: "list registered minions", 42 | Action: execListCommand, 43 | Flags: []cli.Flag{ 44 | cli.StringFlag{ 45 | Name: "with-classifier", 46 | Value: "", 47 | Usage: "match minions with given classifier pattern", 48 | }, 49 | }, 50 | } 51 | 52 | return cmd 53 | } 54 | 55 | // Executes the "list" command 56 | func execListCommand(c *cli.Context) error { 57 | klient := newEtcdMinionClientFromFlags(c) 58 | 59 | cFlag := c.String("with-classifier") 60 | minions, err := parseClassifierPattern(klient, cFlag) 61 | 62 | // Ignore errors about missing minion directory 63 | if err != nil { 64 | if eerr, ok := err.(client.Error); !ok || eerr.Code != client.ErrorCodeKeyNotFound { 65 | return cli.NewExitError(err.Error(), 1) 66 | } 67 | } 68 | 69 | if len(minions) == 0 { 70 | return nil 71 | } 72 | 73 | table := uitable.New() 74 | table.MaxColWidth = 80 75 | table.AddRow("MINION", "NAME") 76 | for _, minion := range minions { 77 | name, err := klient.MinionName(minion) 78 | if err != nil { 79 | return cli.NewExitError(err.Error(), 1) 80 | } 81 | 82 | table.AddRow(minion, name) 83 | } 84 | 85 | fmt.Println(table) 86 | 87 | return nil 88 | } 89 | -------------------------------------------------------------------------------- /gructl/command/log.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | "time" 31 | 32 | "github.com/coreos/etcd/client" 33 | "github.com/gosuri/uitable" 34 | "github.com/pborman/uuid" 35 | "github.com/urfave/cli" 36 | ) 37 | 38 | // NewLogCommand creates a new sub-command for retrieving the 39 | // log of previously executed tasks by minions 40 | func NewLogCommand() cli.Command { 41 | cmd := cli.Command{ 42 | Name: "log", 43 | Usage: "list minion task log", 44 | Action: execLogCommand, 45 | } 46 | 47 | return cmd 48 | } 49 | 50 | // Executes the "log" command 51 | func execLogCommand(c *cli.Context) error { 52 | if len(c.Args()) == 0 { 53 | return cli.NewExitError(errNoMinion.Error(), 64) 54 | } 55 | 56 | minion := uuid.Parse(c.Args()[0]) 57 | if minion == nil { 58 | return cli.NewExitError(errInvalidUUID.Error(), 64) 59 | } 60 | 61 | klient := newEtcdMinionClientFromFlags(c) 62 | 63 | // Ignore errors about missing log directory 64 | log, err := klient.MinionTaskLog(minion) 65 | if err != nil { 66 | if eerr, ok := err.(client.Error); !ok || eerr.Code != client.ErrorCodeKeyNotFound { 67 | return cli.NewExitError(err.Error(), 1) 68 | } 69 | } 70 | 71 | if len(log) == 0 { 72 | return nil 73 | } 74 | 75 | table := uitable.New() 76 | table.MaxColWidth = 40 77 | table.AddRow("TASK", "STATE", "RECEIVED", "PROCESSED") 78 | for _, id := range log { 79 | t, err := klient.MinionTaskResult(minion, id) 80 | if err != nil { 81 | return cli.NewExitError(err.Error(), 1) 82 | } 83 | table.AddRow(t.ID, t.State, time.Unix(t.TimeReceived, 0), time.Unix(t.TimeProcessed, 0)) 84 | } 85 | 86 | fmt.Println(table) 87 | 88 | return nil 89 | } 90 | -------------------------------------------------------------------------------- /gructl/command/push.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | "time" 31 | 32 | "github.com/dnaeon/gru/task" 33 | 34 | "github.com/gosuri/uiprogress" 35 | "github.com/gosuri/uitable" 36 | "github.com/urfave/cli" 37 | ) 38 | 39 | // NewPushCommand creates a new sub-command for submitting 40 | // tasks to minions 41 | func NewPushCommand() cli.Command { 42 | cmd := cli.Command{ 43 | Name: "push", 44 | Usage: "send task to minion(s)", 45 | Action: execPushCommand, 46 | Flags: []cli.Flag{ 47 | cli.StringFlag{ 48 | Name: "environment", 49 | Value: "production", 50 | Usage: "specify environment to use", 51 | EnvVar: "GRU_ENVIRONMENT", 52 | }, 53 | cli.StringFlag{ 54 | Name: "with-classifier", 55 | Value: "", 56 | Usage: "match minions with given classifier pattern", 57 | }, 58 | }, 59 | } 60 | 61 | return cmd 62 | } 63 | 64 | // Executes the "push" command 65 | func execPushCommand(c *cli.Context) error { 66 | if len(c.Args()) < 1 { 67 | return cli.NewExitError(errNoModuleName.Error(), 64) 68 | } 69 | 70 | // Create the task that we send to our minions 71 | // The task's command is the module name that will be 72 | // loaded and processed by the remote minions 73 | main := c.Args()[0] 74 | t := task.New(main, c.String("environment")) 75 | 76 | client := newEtcdMinionClientFromFlags(c) 77 | 78 | cFlag := c.String("with-classifier") 79 | minions, err := parseClassifierPattern(client, cFlag) 80 | 81 | if err != nil { 82 | return cli.NewExitError(err.Error(), 1) 83 | } 84 | 85 | numMinions := len(minions) 86 | if numMinions == 0 { 87 | return cli.NewExitError(errNoMinionFound.Error(), 1) 88 | } 89 | 90 | fmt.Printf("Found %d minion(s) for task processing\n\n", numMinions) 91 | 92 | // Progress bar to display while submitting task 93 | progress := uiprogress.New() 94 | bar := progress.AddBar(numMinions) 95 | bar.AppendCompleted() 96 | bar.PrependElapsed() 97 | progress.Start() 98 | 99 | // Number of minions to which submitting the task has failed 100 | failed := 0 101 | 102 | // Submit task to minions 103 | fmt.Println("Submitting task to minion(s) ...") 104 | for _, m := range minions { 105 | err = client.MinionSubmitTask(m, t) 106 | if err != nil { 107 | fmt.Printf("Failed to submit task to %s: %s\n", m, err) 108 | failed++ 109 | } 110 | bar.Incr() 111 | } 112 | 113 | // Stop progress bar and sleep for a bit to make sure the 114 | // progress bar gets updated if we were too fast for it 115 | progress.Stop() 116 | time.Sleep(time.Millisecond * 100) 117 | 118 | // Display task report 119 | fmt.Println() 120 | table := uitable.New() 121 | table.MaxColWidth = 80 122 | table.Wrap = true 123 | table.AddRow("TASK", "SUBMITTED", "FAILED", "TOTAL") 124 | table.AddRow(t.ID, numMinions-failed, failed, numMinions) 125 | fmt.Println(table) 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /gructl/command/queue.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | "time" 31 | 32 | "github.com/coreos/etcd/client" 33 | "github.com/gosuri/uitable" 34 | "github.com/pborman/uuid" 35 | "github.com/urfave/cli" 36 | ) 37 | 38 | // NewQueueCommand creates a new sub-command for retrieving the 39 | // currently pending tasks for minions 40 | func NewQueueCommand() cli.Command { 41 | cmd := cli.Command{ 42 | Name: "queue", 43 | Usage: "list minion task queue", 44 | Action: execQueueCommand, 45 | } 46 | 47 | return cmd 48 | } 49 | 50 | // Executes the "queue" command 51 | func execQueueCommand(c *cli.Context) error { 52 | if len(c.Args()) == 0 { 53 | return cli.NewExitError(errNoMinion.Error(), 64) 54 | } 55 | 56 | minion := uuid.Parse(c.Args()[0]) 57 | if minion == nil { 58 | return cli.NewExitError(errInvalidUUID.Error(), 64) 59 | } 60 | 61 | klient := newEtcdMinionClientFromFlags(c) 62 | 63 | // Ignore errors about missing queue directory 64 | queue, err := klient.MinionTaskQueue(minion) 65 | if err != nil { 66 | if eerr, ok := err.(client.Error); !ok || eerr.Code != client.ErrorCodeKeyNotFound { 67 | return cli.NewExitError(err.Error(), 1) 68 | } 69 | } 70 | 71 | if len(queue) == 0 { 72 | return nil 73 | } 74 | 75 | table := uitable.New() 76 | table.MaxColWidth = 40 77 | table.AddRow("TASK", "STATE", "RECEIVED") 78 | for _, t := range queue { 79 | table.AddRow(t.ID, t.State, time.Unix(t.TimeReceived, 0)) 80 | } 81 | 82 | fmt.Println(table) 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /gructl/command/report.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | 31 | "github.com/gosuri/uitable" 32 | "github.com/urfave/cli" 33 | ) 34 | 35 | // NewReportCommand creates a new sub-command for 36 | // generating reports based on minion classifiers 37 | func NewReportCommand() cli.Command { 38 | cmd := cli.Command{ 39 | Name: "report", 40 | Usage: "generate classifier report", 41 | Action: execReportCommand, 42 | } 43 | 44 | return cmd 45 | } 46 | 47 | // Executes the "report" command 48 | func execReportCommand(c *cli.Context) error { 49 | if len(c.Args()) == 0 { 50 | return cli.NewExitError(errNoClassifier.Error(), 64) 51 | } 52 | 53 | classifierKey := c.Args()[0] 54 | client := newEtcdMinionClientFromFlags(c) 55 | 56 | minions, err := client.MinionWithClassifierKey(classifierKey) 57 | if err != nil { 58 | return cli.NewExitError(err.Error(), 1) 59 | } 60 | 61 | if len(minions) == 0 { 62 | return nil 63 | } 64 | 65 | report := make(map[string]int) 66 | for _, minion := range minions { 67 | classifier, err := client.MinionClassifier(minion, classifierKey) 68 | if err != nil { 69 | return cli.NewExitError(err.Error(), 1) 70 | } 71 | report[classifier.Value]++ 72 | } 73 | 74 | table := uitable.New() 75 | table.MaxColWidth = 80 76 | table.AddRow("CLASSIFIER", "VALUE", "MINION(S)") 77 | 78 | for classifierValue, minionCount := range report { 79 | table.AddRow(classifierKey, classifierValue, minionCount) 80 | } 81 | 82 | fmt.Println(table) 83 | 84 | return nil 85 | } 86 | -------------------------------------------------------------------------------- /gructl/command/result.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "fmt" 30 | "time" 31 | 32 | "github.com/gosuri/uitable" 33 | "github.com/pborman/uuid" 34 | "github.com/urfave/cli" 35 | ) 36 | 37 | // NewResultCommand creates a new sub-command for retrieving 38 | // results of previously executed tasks by minions 39 | func NewResultCommand() cli.Command { 40 | cmd := cli.Command{ 41 | Name: "result", 42 | Usage: "get task results", 43 | Action: execResultCommand, 44 | Flags: []cli.Flag{ 45 | cli.StringFlag{ 46 | Name: "minion", 47 | Usage: "get task result for given minion only", 48 | }, 49 | cli.BoolFlag{ 50 | Name: "details", 51 | Usage: "provide more details about the tasks", 52 | }, 53 | }, 54 | } 55 | 56 | return cmd 57 | } 58 | 59 | // Executes the "result" command 60 | func execResultCommand(c *cli.Context) error { 61 | if len(c.Args()) == 0 { 62 | return cli.NewExitError(errNoTask.Error(), 64) 63 | } 64 | 65 | arg := c.Args()[0] 66 | id := uuid.Parse(arg) 67 | if id == nil { 68 | return cli.NewExitError(errInvalidUUID.Error(), 64) 69 | } 70 | 71 | client := newEtcdMinionClientFromFlags(c) 72 | 73 | // If --minion flag was specified parse the 74 | // minion uuid and get the task result only 75 | // from the specified minion, otherwise find 76 | // all minions which contain the given 77 | // task and get their results 78 | var minionWithTask []uuid.UUID 79 | 80 | mFlag := c.String("minion") 81 | if mFlag == "" { 82 | // No minion was specified, get all minions 83 | // with the given task uuid 84 | m, err := client.MinionWithTaskResult(id) 85 | if err != nil { 86 | return cli.NewExitError(err.Error(), 1) 87 | } 88 | minionWithTask = m 89 | } else { 90 | // Minion was specified, get task result 91 | // from the given minion only 92 | minion := uuid.Parse(mFlag) 93 | if minion == nil { 94 | return cli.NewExitError(errInvalidUUID.Error(), 64) 95 | } 96 | minionWithTask = append(minionWithTask, minion) 97 | } 98 | 99 | if len(minionWithTask) == 0 { 100 | return nil 101 | } 102 | 103 | // Create table for the task results 104 | // If the --details flag is specified, then 105 | // create a table that holds all details about the 106 | // tasks, otherwise use a simple summary table 107 | table := uitable.New() 108 | if c.Bool("details") { 109 | table.MaxColWidth = 80 110 | table.Wrap = true 111 | } else { 112 | table.MaxColWidth = 40 113 | table.AddRow("MINION", "RESULT", "STATE") 114 | } 115 | 116 | for _, minionID := range minionWithTask { 117 | t, err := client.MinionTaskResult(minionID, id) 118 | if err != nil { 119 | return cli.NewExitError(err.Error(), 1) 120 | } 121 | 122 | if c.Bool("details") { 123 | table.AddRow("Minion:", minionID) 124 | table.AddRow("Task ID:", t.ID) 125 | table.AddRow("State:", t.State) 126 | table.AddRow("Received:", time.Unix(t.TimeReceived, 0)) 127 | table.AddRow("Processed:", time.Unix(t.TimeProcessed, 0)) 128 | table.AddRow("Result:", t.Result) 129 | } else { 130 | table.AddRow(minionID, t.Result, t.State) 131 | } 132 | } 133 | 134 | fmt.Println(table) 135 | 136 | return nil 137 | } 138 | -------------------------------------------------------------------------------- /gructl/command/serve.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "os" 30 | "os/signal" 31 | "runtime" 32 | "syscall" 33 | 34 | "github.com/dnaeon/gru/minion" 35 | "github.com/urfave/cli" 36 | ) 37 | 38 | // NewServeCommand creates a new sub-command for starting a 39 | // minion and its services 40 | func NewServeCommand() cli.Command { 41 | cmd := cli.Command{ 42 | Name: "serve", 43 | Usage: "start minion", 44 | Action: execServeCommand, 45 | Flags: []cli.Flag{ 46 | cli.IntFlag{ 47 | Name: "concurrency", 48 | Usage: "number of goroutines used for concurrent processing", 49 | Value: runtime.NumCPU(), 50 | }, 51 | cli.StringFlag{ 52 | Name: "name", 53 | Usage: "set minion name", 54 | Value: "", 55 | }, 56 | cli.StringFlag{ 57 | Name: "siterepo", 58 | Value: "", 59 | Usage: "path/url to the site repo", 60 | EnvVar: "GRU_SITEREPO", 61 | }, 62 | }, 63 | } 64 | 65 | return cmd 66 | } 67 | 68 | // Executes the "serve" command 69 | func execServeCommand(c *cli.Context) error { 70 | name, err := os.Hostname() 71 | if err != nil { 72 | return cli.NewExitError(err.Error(), 1) 73 | } 74 | 75 | concurrency := c.Int("concurrency") 76 | if concurrency < 0 { 77 | concurrency = runtime.NumCPU() 78 | } 79 | 80 | if c.String("siterepo") == "" { 81 | return cli.NewExitError(errNoSiteRepo.Error(), 64) 82 | } 83 | 84 | nameFlag := c.String("name") 85 | if nameFlag != "" { 86 | name = nameFlag 87 | } 88 | 89 | etcdCfg := etcdConfigFromFlags(c) 90 | minionCfg := &minion.EtcdMinionConfig{ 91 | Concurrency: concurrency, 92 | Name: name, 93 | SiteRepo: c.String("siterepo"), 94 | EtcdConfig: etcdCfg, 95 | } 96 | 97 | m, err := minion.NewEtcdMinion(minionCfg) 98 | if err != nil { 99 | return cli.NewExitError(err.Error(), 1) 100 | } 101 | 102 | // Channel on which the shutdown signal is sent 103 | quit := make(chan os.Signal, 1) 104 | signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) 105 | 106 | // Start minion 107 | err = m.Serve() 108 | if err != nil { 109 | return cli.NewExitError(err.Error(), 1) 110 | } 111 | 112 | // Block until a shutdown signal is received 113 | <-quit 114 | m.Stop() 115 | 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /gructl/command/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package command 27 | 28 | import ( 29 | "regexp" 30 | "strings" 31 | 32 | "github.com/dnaeon/gru/client" 33 | 34 | etcdclient "github.com/coreos/etcd/client" 35 | "github.com/pborman/uuid" 36 | "github.com/urfave/cli" 37 | ) 38 | 39 | func etcdConfigFromFlags(c *cli.Context) etcdclient.Config { 40 | eFlag := c.GlobalString("endpoint") 41 | uFlag := c.GlobalString("username") 42 | pFlag := c.GlobalString("password") 43 | tFlag := c.GlobalDuration("timeout") 44 | 45 | cfg := etcdclient.Config{ 46 | Endpoints: strings.Split(eFlag, ","), 47 | Transport: etcdclient.DefaultTransport, 48 | HeaderTimeoutPerRequest: tFlag, 49 | } 50 | 51 | if uFlag != "" && pFlag != "" { 52 | cfg.Username = uFlag 53 | cfg.Password = pFlag 54 | } 55 | 56 | return cfg 57 | } 58 | 59 | func newEtcdMinionClientFromFlags(c *cli.Context) client.Client { 60 | cfg := etcdConfigFromFlags(c) 61 | klient := client.NewEtcdMinionClient(cfg) 62 | 63 | return klient 64 | } 65 | 66 | // Parses a classifier pattern and returns 67 | // minions which match the given classifier pattern. 68 | // A classifier pattern is described as 'key=regexp', 69 | // where 'key' is a classifier key and 'regexp' is a 70 | // regular expression that is compiled and matched 71 | // against the minions' classifier values. 72 | // If 'key' is empty all registered minions are returned. 73 | // If 'regexp' is empty or missing all minions which 74 | // contain the given 'key' are returned instead. 75 | func parseClassifierPattern(klient client.Client, pattern string) ([]uuid.UUID, error) { 76 | // If no classifier pattern provided, 77 | // return all registered minions 78 | if pattern == "" { 79 | return klient.MinionList() 80 | } 81 | 82 | data := strings.SplitN(pattern, "=", 2) 83 | key := data[0] 84 | 85 | // If only a classifier key is provided, return all 86 | // minions which contain the given classifier key 87 | if len(data) == 1 { 88 | return klient.MinionWithClassifierKey(key) 89 | } 90 | 91 | toMatch := data[1] 92 | re, err := regexp.Compile(toMatch) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | minions, err := klient.MinionWithClassifierKey(key) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | var result []uuid.UUID 103 | for _, minion := range minions { 104 | klassifier, err := klient.MinionClassifier(minion, key) 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | if re.MatchString(klassifier.Value) { 110 | result = append(result, minion) 111 | } 112 | } 113 | 114 | return result, nil 115 | } 116 | -------------------------------------------------------------------------------- /gructl/gructl.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package gructl 27 | 28 | import ( 29 | "os" 30 | "time" 31 | 32 | "github.com/dnaeon/gru/gructl/command" 33 | "github.com/dnaeon/gru/version" 34 | "github.com/urfave/cli" 35 | ) 36 | 37 | // Main is the entry point of gructl 38 | func Main() { 39 | app := cli.NewApp() 40 | app.Name = "gructl" 41 | app.Version = version.Version 42 | app.EnableBashCompletion = true 43 | app.Usage = "command line tool for managing minions" 44 | app.Flags = []cli.Flag{ 45 | cli.StringFlag{ 46 | Name: "endpoint", 47 | Value: "http://127.0.0.1:2379,http://localhost:4001", 48 | Usage: "etcd cluster endpoints", 49 | EnvVar: "GRU_ENDPOINT", 50 | }, 51 | cli.StringFlag{ 52 | Name: "username", 53 | Value: "", 54 | Usage: "username to use for authentication", 55 | EnvVar: "GRU_USERNAME", 56 | }, 57 | cli.StringFlag{ 58 | Name: "password", 59 | Value: "", 60 | Usage: "password to use for authentication", 61 | EnvVar: "GRU_PASSWORD", 62 | }, 63 | cli.DurationFlag{ 64 | Name: "timeout", 65 | Value: time.Second, 66 | Usage: "connection timeout per request", 67 | EnvVar: "GRU_TIMEOUT", 68 | }, 69 | } 70 | 71 | app.Commands = []cli.Command{ 72 | command.NewApplyCommand(), 73 | command.NewListCommand(), 74 | command.NewInfoCommand(), 75 | command.NewServeCommand(), 76 | command.NewPushCommand(), 77 | command.NewClassifierCommand(), 78 | command.NewReportCommand(), 79 | command.NewQueueCommand(), 80 | command.NewLogCommand(), 81 | command.NewLastseenCommand(), 82 | command.NewResultCommand(), 83 | command.NewGraphCommand(), 84 | } 85 | 86 | app.Run(os.Args) 87 | } 88 | -------------------------------------------------------------------------------- /integration/fixtures/minion-lastseen.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | interactions: 4 | - request: 5 | body: value=1450357761 6 | form: 7 | value: 8 | - "1450357761" 9 | headers: 10 | Content-Type: 11 | - application/x-www-form-urlencoded 12 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/lastseen 13 | method: PUT 14 | response: 15 | body: | 16 | {"action":"set","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/lastseen","value":"1450357761","modifiedIndex":6,"createdIndex":6}} 17 | headers: 18 | Content-Length: 19 | - "148" 20 | Content-Type: 21 | - application/json 22 | Date: 23 | - Fri, 22 Jul 2016 13:07:03 GMT 24 | X-Etcd-Cluster-Id: 25 | - 7e27652122e8b2ae 26 | X-Etcd-Index: 27 | - "6" 28 | X-Raft-Index: 29 | - "30" 30 | X-Raft-Term: 31 | - "2" 32 | status: 201 Created 33 | code: 201 34 | - request: 35 | body: "" 36 | form: {} 37 | headers: {} 38 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/lastseen?quorum=false&recursive=false&sorted=false 39 | method: GET 40 | response: 41 | body: | 42 | {"action":"get","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/lastseen","value":"1450357761","modifiedIndex":6,"createdIndex":6}} 43 | headers: 44 | Content-Length: 45 | - "148" 46 | Content-Type: 47 | - application/json 48 | Date: 49 | - Fri, 22 Jul 2016 13:07:03 GMT 50 | X-Etcd-Cluster-Id: 51 | - 7e27652122e8b2ae 52 | X-Etcd-Index: 53 | - "6" 54 | X-Raft-Index: 55 | - "30" 56 | X-Raft-Term: 57 | - "2" 58 | status: 200 OK 59 | code: 200 60 | -------------------------------------------------------------------------------- /integration/fixtures/minion-list.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | interactions: 4 | - request: 5 | body: value=Bob 6 | form: 7 | value: 8 | - Bob 9 | headers: 10 | Content-Type: 11 | - application/x-www-form-urlencoded 12 | url: http://127.0.0.1:2379/v2/keys/gru/minion/f827bffd-bd9e-5441-be36-a92a51d0b79e/name 13 | method: PUT 14 | response: 15 | body: | 16 | {"action":"set","node":{"key":"/gru/minion/f827bffd-bd9e-5441-be36-a92a51d0b79e/name","value":"Bob","modifiedIndex":7,"createdIndex":7}} 17 | headers: 18 | Content-Length: 19 | - "137" 20 | Content-Type: 21 | - application/json 22 | Date: 23 | - Fri, 22 Jul 2016 13:07:03 GMT 24 | X-Etcd-Cluster-Id: 25 | - 7e27652122e8b2ae 26 | X-Etcd-Index: 27 | - "7" 28 | X-Raft-Index: 29 | - "31" 30 | X-Raft-Term: 31 | - "2" 32 | status: 201 Created 33 | code: 201 34 | - request: 35 | body: value=Kevin 36 | form: 37 | value: 38 | - Kevin 39 | headers: 40 | Content-Type: 41 | - application/x-www-form-urlencoded 42 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name 43 | method: PUT 44 | response: 45 | body: | 46 | {"action":"set","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name","value":"Kevin","modifiedIndex":8,"createdIndex":8}} 47 | headers: 48 | Content-Length: 49 | - "139" 50 | Content-Type: 51 | - application/json 52 | Date: 53 | - Fri, 22 Jul 2016 13:07:03 GMT 54 | X-Etcd-Cluster-Id: 55 | - 7e27652122e8b2ae 56 | X-Etcd-Index: 57 | - "8" 58 | X-Raft-Index: 59 | - "32" 60 | X-Raft-Term: 61 | - "2" 62 | status: 201 Created 63 | code: 201 64 | - request: 65 | body: value=Stuart 66 | form: 67 | value: 68 | - Stuart 69 | headers: 70 | Content-Type: 71 | - application/x-www-form-urlencoded 72 | url: http://127.0.0.1:2379/v2/keys/gru/minion/f87cf58e-1e19-57e1-bed3-9dff5064b86a/name 73 | method: PUT 74 | response: 75 | body: | 76 | {"action":"set","node":{"key":"/gru/minion/f87cf58e-1e19-57e1-bed3-9dff5064b86a/name","value":"Stuart","modifiedIndex":9,"createdIndex":9}} 77 | headers: 78 | Content-Length: 79 | - "140" 80 | Content-Type: 81 | - application/json 82 | Date: 83 | - Fri, 22 Jul 2016 13:07:03 GMT 84 | X-Etcd-Cluster-Id: 85 | - 7e27652122e8b2ae 86 | X-Etcd-Index: 87 | - "9" 88 | X-Raft-Index: 89 | - "33" 90 | X-Raft-Term: 91 | - "2" 92 | status: 201 Created 93 | code: 201 94 | - request: 95 | body: "" 96 | form: {} 97 | headers: {} 98 | url: http://127.0.0.1:2379/v2/keys/gru/minion?quorum=false&recursive=false&sorted=false 99 | method: GET 100 | response: 101 | body: | 102 | {"action":"get","node":{"key":"/gru/minion","dir":true,"nodes":[{"key":"/gru/minion/f827bffd-bd9e-5441-be36-a92a51d0b79e","dir":true,"modifiedIndex":7,"createdIndex":7},{"key":"/gru/minion/f87cf58e-1e19-57e1-bed3-9dff5064b86a","dir":true,"modifiedIndex":9,"createdIndex":9},{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170","dir":true,"modifiedIndex":4,"createdIndex":4}],"modifiedIndex":4,"createdIndex":4}} 103 | headers: 104 | Content-Length: 105 | - "417" 106 | Content-Type: 107 | - application/json 108 | Date: 109 | - Fri, 22 Jul 2016 13:07:03 GMT 110 | X-Etcd-Cluster-Id: 111 | - 7e27652122e8b2ae 112 | X-Etcd-Index: 113 | - "9" 114 | X-Raft-Index: 115 | - "33" 116 | X-Raft-Term: 117 | - "2" 118 | status: 200 OK 119 | code: 200 120 | -------------------------------------------------------------------------------- /integration/fixtures/minion-name.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | interactions: 4 | - request: 5 | body: value=Kevin 6 | form: 7 | value: 8 | - Kevin 9 | headers: 10 | Content-Type: 11 | - application/x-www-form-urlencoded 12 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name 13 | method: PUT 14 | response: 15 | body: | 16 | {"action":"set","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name","value":"Kevin","modifiedIndex":10,"createdIndex":10},"prevNode":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name","value":"Kevin","modifiedIndex":8,"createdIndex":8}} 17 | headers: 18 | Content-Length: 19 | - "267" 20 | Content-Type: 21 | - application/json 22 | Date: 23 | - Fri, 22 Jul 2016 13:07:03 GMT 24 | X-Etcd-Cluster-Id: 25 | - 7e27652122e8b2ae 26 | X-Etcd-Index: 27 | - "10" 28 | X-Raft-Index: 29 | - "34" 30 | X-Raft-Term: 31 | - "2" 32 | status: 200 OK 33 | code: 200 34 | - request: 35 | body: "" 36 | form: {} 37 | headers: {} 38 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name?quorum=false&recursive=false&sorted=false 39 | method: GET 40 | response: 41 | body: | 42 | {"action":"get","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name","value":"Kevin","modifiedIndex":10,"createdIndex":10}} 43 | headers: 44 | Content-Length: 45 | - "141" 46 | Content-Type: 47 | - application/json 48 | Date: 49 | - Fri, 22 Jul 2016 13:07:03 GMT 50 | X-Etcd-Cluster-Id: 51 | - 7e27652122e8b2ae 52 | X-Etcd-Index: 53 | - "10" 54 | X-Raft-Index: 55 | - "34" 56 | X-Raft-Term: 57 | - "2" 58 | status: 200 OK 59 | code: 200 60 | -------------------------------------------------------------------------------- /integration/fixtures/minion-task-backlog.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 1 3 | interactions: 4 | - request: 5 | body: value=Kevin 6 | form: 7 | value: 8 | - Kevin 9 | headers: 10 | Content-Type: 11 | - application/x-www-form-urlencoded 12 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name 13 | method: PUT 14 | response: 15 | body: | 16 | {"action":"set","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name","value":"Kevin","modifiedIndex":11,"createdIndex":11},"prevNode":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name","value":"Kevin","modifiedIndex":10,"createdIndex":10}} 17 | headers: 18 | Content-Length: 19 | - "269" 20 | Content-Type: 21 | - application/json 22 | Date: 23 | - Fri, 22 Jul 2016 13:07:03 GMT 24 | X-Etcd-Cluster-Id: 25 | - 7e27652122e8b2ae 26 | X-Etcd-Index: 27 | - "11" 28 | X-Raft-Index: 29 | - "35" 30 | X-Raft-Term: 31 | - "2" 32 | status: 200 OK 33 | code: 200 34 | - request: 35 | body: "" 36 | form: {} 37 | headers: {} 38 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170?quorum=false&recursive=false&sorted=false 39 | method: GET 40 | response: 41 | body: | 42 | {"action":"get","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170","dir":true,"nodes":[{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/classifier","dir":true,"modifiedIndex":4,"createdIndex":4},{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/lastseen","value":"1450357761","modifiedIndex":6,"createdIndex":6},{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/name","value":"Kevin","modifiedIndex":11,"createdIndex":11}],"modifiedIndex":4,"createdIndex":4}} 43 | headers: 44 | Content-Length: 45 | - "496" 46 | Content-Type: 47 | - application/json 48 | Date: 49 | - Fri, 22 Jul 2016 13:07:03 GMT 50 | X-Etcd-Cluster-Id: 51 | - 7e27652122e8b2ae 52 | X-Etcd-Index: 53 | - "11" 54 | X-Raft-Index: 55 | - "35" 56 | X-Raft-Term: 57 | - "2" 58 | status: 200 OK 59 | code: 200 60 | - request: 61 | body: value=%7B%22dryRun%22%3Afalse%2C%22environment%22%3A%22bar%22%2C%22command%22%3A%22foo%22%2C%22timeReceived%22%3A0%2C%22timeProcessed%22%3A0%2C%22id%22%3A%22e6d2bebd-2219-4a8c-9d30-a861097c147e%22%2C%22result%22%3A%22%22%2C%22state%22%3A%22unknown%22%7D 62 | form: 63 | value: 64 | - '{"dryRun":false,"environment":"bar","command":"foo","timeReceived":0,"timeProcessed":0,"id":"e6d2bebd-2219-4a8c-9d30-a861097c147e","result":"","state":"unknown"}' 65 | headers: 66 | Content-Type: 67 | - application/x-www-form-urlencoded 68 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/queue 69 | method: POST 70 | response: 71 | body: | 72 | {"action":"create","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/queue/00000000000000000012","value":"{\"dryRun\":false,\"environment\":\"bar\",\"command\":\"foo\",\"timeReceived\":0,\"timeProcessed\":0,\"id\":\"e6d2bebd-2219-4a8c-9d30-a861097c147e\",\"result\":\"\",\"state\":\"unknown\"}","modifiedIndex":12,"createdIndex":12}} 73 | headers: 74 | Content-Length: 75 | - "348" 76 | Content-Type: 77 | - application/json 78 | Date: 79 | - Fri, 22 Jul 2016 13:07:03 GMT 80 | X-Etcd-Cluster-Id: 81 | - 7e27652122e8b2ae 82 | X-Etcd-Index: 83 | - "12" 84 | X-Raft-Index: 85 | - "36" 86 | X-Raft-Term: 87 | - "2" 88 | status: 201 Created 89 | code: 201 90 | - request: 91 | body: "" 92 | form: {} 93 | headers: {} 94 | url: http://127.0.0.1:2379/v2/keys/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/queue?quorum=false&recursive=true&sorted=false 95 | method: GET 96 | response: 97 | body: | 98 | {"action":"get","node":{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/queue","dir":true,"nodes":[{"key":"/gru/minion/46ce0385-0e2b-5ede-8279-9cd98c268170/queue/00000000000000000012","value":"{\"dryRun\":false,\"environment\":\"bar\",\"command\":\"foo\",\"timeReceived\":0,\"timeProcessed\":0,\"id\":\"e6d2bebd-2219-4a8c-9d30-a861097c147e\",\"result\":\"\",\"state\":\"unknown\"}","modifiedIndex":12,"createdIndex":12}],"modifiedIndex":12,"createdIndex":12}} 99 | headers: 100 | Content-Length: 101 | - "468" 102 | Content-Type: 103 | - application/json 104 | Date: 105 | - Fri, 22 Jul 2016 13:07:03 GMT 106 | X-Etcd-Cluster-Id: 107 | - 7e27652122e8b2ae 108 | X-Etcd-Index: 109 | - "12" 110 | X-Raft-Index: 111 | - "36" 112 | X-Raft-Term: 113 | - "2" 114 | status: 200 OK 115 | code: 200 116 | -------------------------------------------------------------------------------- /integration/minion_classifier_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package integration 27 | 28 | import ( 29 | "reflect" 30 | "sort" 31 | "testing" 32 | 33 | "github.com/dnaeon/gru/classifier" 34 | "github.com/dnaeon/gru/minion" 35 | ) 36 | 37 | func TestMinionClassifiers(t *testing.T) { 38 | tc := mustNewTestClient("fixtures/minion-classifier") 39 | defer tc.recorder.Stop() 40 | 41 | // Classifiers to test 42 | var wantClassifierKeys []string 43 | testClassifiers := []*classifier.Classifier{ 44 | { 45 | Key: "foo", 46 | Value: "bar", 47 | }, 48 | { 49 | Key: "baz", 50 | Value: "qux", 51 | }, 52 | } 53 | 54 | cfg := &minion.EtcdMinionConfig{ 55 | Name: "Kevin", 56 | EtcdConfig: tc.config, 57 | } 58 | m, err := minion.NewEtcdMinion(cfg) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | 63 | minionID := m.ID() 64 | 65 | // Set minion classifiers 66 | for _, c := range testClassifiers { 67 | err := m.SetClassifier(c) 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | wantClassifierKeys = append(wantClassifierKeys, c.Key) 72 | } 73 | sort.Strings(wantClassifierKeys) 74 | 75 | // Get classifiers keys from etcd 76 | gotClassifierKeys, err := tc.client.MinionClassifierKeys(minionID) 77 | 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | sort.Strings(gotClassifierKeys) 83 | if !reflect.DeepEqual(wantClassifierKeys, gotClassifierKeys) { 84 | t.Errorf("want %q classifier keys, got %q classifier keys", wantClassifierKeys, gotClassifierKeys) 85 | } 86 | 87 | // Get classifier values 88 | for _, c := range testClassifiers { 89 | klassifier, err := tc.client.MinionClassifier(minionID, c.Key) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | if c.Value != klassifier.Value { 95 | t.Errorf("want %q classifier value, got %q classifier value", c.Value, klassifier.Value) 96 | } 97 | } 98 | 99 | // Get minions which contain given classifier key 100 | for _, c := range testClassifiers { 101 | minions, err := tc.client.MinionWithClassifierKey(c.Key) 102 | if err != nil { 103 | t.Fatal(err) 104 | } 105 | 106 | // We expect a single minion with the test classifier keys 107 | if len(minions) != 1 { 108 | t.Errorf("want 1 minion, got %d minion(s)", len(minions)) 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /integration/minion_lastseen_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package integration 27 | 28 | import ( 29 | "testing" 30 | 31 | "github.com/dnaeon/gru/minion" 32 | ) 33 | 34 | func TestMinionLastseen(t *testing.T) { 35 | tc := mustNewTestClient("fixtures/minion-lastseen") 36 | defer tc.recorder.Stop() 37 | 38 | cfg := &minion.EtcdMinionConfig{ 39 | Name: "Kevin", 40 | EtcdConfig: tc.config, 41 | } 42 | m, err := minion.NewEtcdMinion(cfg) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | id := m.ID() 48 | var want int64 = 1450357761 49 | 50 | err = m.SetLastseen(want) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | got, err := tc.client.MinionLastseen(id) 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | 60 | if want != got { 61 | t.Errorf("want %d lastseen, got %d lastseen", want, got) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /integration/minion_list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package integration 27 | 28 | import ( 29 | "reflect" 30 | "sort" 31 | "testing" 32 | 33 | "github.com/dnaeon/gru/minion" 34 | "github.com/pborman/uuid" 35 | ) 36 | 37 | func TestMinionList(t *testing.T) { 38 | tc := mustNewTestClient("fixtures/minion-list") 39 | defer tc.recorder.Stop() 40 | 41 | minionNames := []string{ 42 | "Bob", "Kevin", "Stuart", 43 | } 44 | 45 | wantMinions := []uuid.UUID{ 46 | uuid.Parse("f827bffd-bd9e-5441-be36-a92a51d0b79e"), // Bob 47 | uuid.Parse("46ce0385-0e2b-5ede-8279-9cd98c268170"), // Kevin 48 | uuid.Parse("f87cf58e-1e19-57e1-bed3-9dff5064b86a"), // Stuart 49 | } 50 | 51 | // Convert minion uuids as strings for 52 | // sorting and equality testing 53 | var wantMinionsAsString []string 54 | for _, m := range wantMinions { 55 | wantMinionsAsString = append(wantMinionsAsString, m.String()) 56 | } 57 | sort.Strings(wantMinionsAsString) 58 | 59 | // Register our minions in etcd 60 | for _, name := range minionNames { 61 | cfg := &minion.EtcdMinionConfig{ 62 | Name: name, 63 | EtcdConfig: tc.config, 64 | } 65 | m, err := minion.NewEtcdMinion(cfg) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | err = m.SetName(name) 71 | if err != nil { 72 | t.Error(err) 73 | } 74 | } 75 | 76 | // Get minions from etcd 77 | gotMinions, err := tc.client.MinionList() 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | // Convert retrieved minion uuids as string for 83 | // sorting and equality testing 84 | var gotMinionsAsString []string 85 | for _, m := range gotMinions { 86 | gotMinionsAsString = append(gotMinionsAsString, m.String()) 87 | } 88 | sort.Strings(gotMinionsAsString) 89 | 90 | if !reflect.DeepEqual(wantMinionsAsString, gotMinionsAsString) { 91 | t.Errorf("want %q minions, got %q minions", wantMinions, gotMinions) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /integration/minion_name_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package integration 27 | 28 | import ( 29 | "testing" 30 | 31 | "github.com/dnaeon/gru/minion" 32 | ) 33 | 34 | func TestMinionName(t *testing.T) { 35 | tc := mustNewTestClient("fixtures/minion-name") 36 | defer tc.recorder.Stop() 37 | 38 | wantName := "Kevin" 39 | cfg := &minion.EtcdMinionConfig{ 40 | Name: wantName, 41 | EtcdConfig: tc.config, 42 | } 43 | m, err := minion.NewEtcdMinion(cfg) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | minionID := m.ID() 49 | err = m.SetName(wantName) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | gotName, err := tc.client.MinionName(minionID) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | if wantName != gotName { 60 | t.Errorf("want %q, got %q", wantName, gotName) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /integration/minion_task_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package integration 27 | 28 | import ( 29 | "reflect" 30 | "testing" 31 | 32 | "github.com/dnaeon/gru/minion" 33 | "github.com/dnaeon/gru/task" 34 | 35 | "github.com/pborman/uuid" 36 | ) 37 | 38 | func TestMinionTaskBacklog(t *testing.T) { 39 | tc := mustNewTestClient("fixtures/minion-task-backlog") 40 | defer tc.recorder.Stop() 41 | 42 | // Setup our minion 43 | minionName := "Kevin" 44 | cfg := &minion.EtcdMinionConfig{ 45 | Name: minionName, 46 | EtcdConfig: tc.config, 47 | } 48 | 49 | testMinion, err := minion.NewEtcdMinion(cfg) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | 54 | minionID := testMinion.ID() 55 | 56 | err = testMinion.SetName(minionName) 57 | if err != nil { 58 | t.Fatal(err) 59 | } 60 | 61 | // Create a test task and submit it 62 | wantTask := task.New("foo", "bar") 63 | wantTask.ID = uuid.Parse("e6d2bebd-2219-4a8c-9d30-a861097c147e") 64 | 65 | err = tc.client.MinionSubmitTask(minionID, wantTask) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | // Get pending tasks and verify the task we sent is the task we get 71 | backlog, err := tc.client.MinionTaskQueue(minionID) 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | 76 | if len(backlog) != 1 { 77 | t.Errorf("want 1 backlog task, got %d backlog tasks", len(backlog)) 78 | } 79 | 80 | gotTask := backlog[0] 81 | if !reflect.DeepEqual(wantTask, gotTask) { 82 | t.Errorf("want %q task, got %q task", wantTask.ID, gotTask.ID) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /integration/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package integration 27 | 28 | import ( 29 | "github.com/dnaeon/go-vcr/recorder" 30 | "github.com/dnaeon/gru/client" 31 | 32 | etcdclient "github.com/coreos/etcd/client" 33 | ) 34 | 35 | // Minion client used during integration testing 36 | type testClient struct { 37 | client client.Client 38 | config etcdclient.Config 39 | recorder *recorder.Recorder 40 | } 41 | 42 | // Creates a new etcd client with recording enabled 43 | func mustNewTestClient(cassette string) *testClient { 44 | // Start our recorder 45 | r, err := recorder.New(cassette) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | cfg := etcdclient.Config{ 51 | Endpoints: []string{"http://127.0.0.1:2379"}, 52 | Transport: r, // Inject our transport! 53 | HeaderTimeoutPerRequest: etcdclient.DefaultRequestTimeout, 54 | } 55 | 56 | klient := client.NewEtcdMinionClient(cfg) 57 | 58 | tc := &testClient{ 59 | client: klient, 60 | config: cfg, 61 | recorder: r, 62 | } 63 | 64 | return tc 65 | } 66 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package main 27 | 28 | import "github.com/dnaeon/gru/gructl" 29 | 30 | func main() { 31 | gructl.Main() 32 | } 33 | -------------------------------------------------------------------------------- /minion/minion.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package minion 27 | 28 | import ( 29 | "github.com/dnaeon/gru/classifier" 30 | "github.com/dnaeon/gru/task" 31 | 32 | "github.com/pborman/uuid" 33 | ) 34 | 35 | // Minion interface type 36 | type Minion interface { 37 | // ID returns the unique identifier of a minion 38 | ID() uuid.UUID 39 | 40 | // SetName sets the name of the minion 41 | SetName(string) error 42 | 43 | // SetLastseen sets the time the minion was last seen 44 | SetLastseen(int64) error 45 | 46 | // SetClassifier sets a classifier for the minion 47 | SetClassifier(*classifier.Classifier) error 48 | 49 | // TaskListener listens for new tasks and processes them 50 | TaskListener(c chan<- *task.Task) error 51 | 52 | // TaskRunner runs new tasks as received by the TaskListener 53 | TaskRunner(c <-chan *task.Task) error 54 | 55 | // SaveTaskResult saves the result of a task 56 | SaveTaskResult(t *task.Task) error 57 | 58 | // Sync syncs modules and data files 59 | Sync() error 60 | 61 | // Serve start the minion 62 | Serve() error 63 | 64 | // Stop stops the minion 65 | Stop() error 66 | } 67 | -------------------------------------------------------------------------------- /resource/collection.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import ( 29 | "fmt" 30 | 31 | "github.com/dnaeon/gru/graph" 32 | ) 33 | 34 | // Collection type is a map which keys are the 35 | // resource ids and their values are the actual resources 36 | type Collection map[string]Resource 37 | 38 | // CreateCollection creates a map from 39 | func CreateCollection(resources []Resource) (Collection, error) { 40 | c := make(Collection) 41 | 42 | for _, r := range resources { 43 | id := r.ID() 44 | if _, ok := c[id]; ok { 45 | return c, fmt.Errorf("Duplicate resource declaration for %s", id) 46 | } 47 | c[id] = r 48 | } 49 | 50 | return c, nil 51 | } 52 | 53 | // DependencyGraph builds a dependency graph for the collection 54 | func (c Collection) DependencyGraph() (*graph.Graph, error) { 55 | g := graph.New() 56 | 57 | // A map containing the resource ids and their nodes in the graph 58 | nodes := make(map[string]*graph.Node) 59 | for id := range c { 60 | node := graph.NewNode(id) 61 | nodes[id] = node 62 | g.AddNode(node) 63 | } 64 | 65 | // Connect the nodes in the graph 66 | for id, r := range c { 67 | // Create edges between the nodes and the ones 68 | // required by it 69 | for _, dep := range r.Dependencies() { 70 | if _, ok := c[dep]; !ok { 71 | return g, fmt.Errorf("%s wants %s, which does not exist", id, dep) 72 | } 73 | g.AddEdge(nodes[id], nodes[dep]) 74 | } 75 | 76 | // Create edges between the nodes and the resources for 77 | // which we subscribe for changes to 78 | for dep := range r.SubscribedTo() { 79 | if _, ok := c[dep]; !ok { 80 | return g, fmt.Errorf("%s subscribes to %s, which does not exist", id, dep) 81 | } 82 | g.AddEdge(nodes[id], nodes[dep]) 83 | } 84 | } 85 | 86 | return g, nil 87 | } 88 | -------------------------------------------------------------------------------- /resource/file_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import ( 29 | "os" 30 | "testing" 31 | ) 32 | 33 | func TestFile(t *testing.T) { 34 | L := newLuaState() 35 | defer L.Close() 36 | 37 | const code = ` 38 | foo = resource.file.new("/tmp/foo") 39 | ` 40 | 41 | if err := L.DoString(code); err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | foo := luaResource(L, "foo").(*File) 46 | errorIfNotEqual(t, "file", foo.Type) 47 | errorIfNotEqual(t, "/tmp/foo", foo.Name) 48 | errorIfNotEqual(t, "present", foo.State) 49 | errorIfNotEqual(t, []string{}, foo.Require) 50 | errorIfNotEqual(t, []string{"present"}, foo.PresentStatesList) 51 | errorIfNotEqual(t, []string{"absent"}, foo.AbsentStatesList) 52 | errorIfNotEqual(t, true, foo.Concurrent) 53 | errorIfNotEqual(t, "/tmp/foo", foo.Path) 54 | errorIfNotEqual(t, os.FileMode(0644), foo.Mode) 55 | errorIfNotEqual(t, "", foo.Source) 56 | } 57 | 58 | func TestDirectory(t *testing.T) { 59 | L := newLuaState() 60 | defer L.Close() 61 | 62 | const code = ` 63 | bar = resource.directory.new("/tmp/bar") 64 | ` 65 | 66 | if err := L.DoString(code); err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | bar := luaResource(L, "bar").(*Directory) 71 | errorIfNotEqual(t, "directory", bar.Type) 72 | errorIfNotEqual(t, "/tmp/bar", bar.Name) 73 | errorIfNotEqual(t, "present", bar.State) 74 | errorIfNotEqual(t, []string{}, bar.Require) 75 | errorIfNotEqual(t, []string{"present"}, bar.PresentStatesList) 76 | errorIfNotEqual(t, []string{"absent"}, bar.AbsentStatesList) 77 | errorIfNotEqual(t, true, bar.Concurrent) 78 | errorIfNotEqual(t, "/tmp/bar", bar.Path) 79 | errorIfNotEqual(t, os.FileMode(0755), bar.Mode) 80 | errorIfNotEqual(t, false, bar.Parents) 81 | } 82 | 83 | func TestLink(t *testing.T) { 84 | L := newLuaState() 85 | defer L.Close() 86 | 87 | const code = ` 88 | qux = resource.link.new("/tmp/qux") 89 | qux.source = "/tmp/foo" 90 | ` 91 | 92 | if err := L.DoString(code); err != nil { 93 | t.Fatal(err) 94 | } 95 | 96 | qux := luaResource(L, "qux").(*Link) 97 | errorIfNotEqual(t, "link", qux.Type) 98 | errorIfNotEqual(t, "/tmp/qux", qux.Name) 99 | errorIfNotEqual(t, "present", qux.State) 100 | errorIfNotEqual(t, []string{}, qux.Require) 101 | errorIfNotEqual(t, []string{"present"}, qux.PresentStatesList) 102 | errorIfNotEqual(t, []string{"absent"}, qux.AbsentStatesList) 103 | errorIfNotEqual(t, true, qux.Concurrent) 104 | errorIfNotEqual(t, "/tmp/foo", qux.Source) 105 | errorIfNotEqual(t, false, qux.Hard) 106 | } 107 | -------------------------------------------------------------------------------- /resource/lua.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import ( 29 | "github.com/yuin/gopher-lua" 30 | "layeh.com/gopher-luar" 31 | ) 32 | 33 | // functionRegistry contains the functions to be registered in Lua. 34 | var functionRegistry = make([]FunctionItem, 0) 35 | 36 | // DefaultResourceNamespace is the Lua table where resources are being 37 | // registered to, when using the default namespace. 38 | const DefaultResourceNamespace = "resource" 39 | 40 | // DefaultFunctionNamespace is the Lua table where functions are being 41 | // registered to, when using the default namespace. 42 | const DefaultFunctionNamespace = "stdlib" 43 | 44 | // FunctionItem type represents a single item from the function registry. 45 | type FunctionItem struct { 46 | // Name of the function to register in Lua 47 | Name string 48 | 49 | // Namespace is the Lua table where the function will be registered to 50 | Namespace string 51 | 52 | // Function to execute when called from Lua 53 | Function interface{} 54 | } 55 | 56 | // RegisterFunction registers a function to the registry. 57 | func RegisterFunction(items ...FunctionItem) { 58 | functionRegistry = append(functionRegistry, items...) 59 | } 60 | 61 | // LuaRegisterBuiltin registers resource providers and functions in Lua. 62 | func LuaRegisterBuiltin(L *lua.LState) { 63 | // Register functions in Lua 64 | for _, item := range functionRegistry { 65 | namespace := L.GetGlobal(item.Namespace) 66 | if lua.LVIsFalse(namespace) { 67 | namespace = L.NewTable() 68 | L.SetGlobal(item.Namespace, namespace) 69 | } 70 | L.SetField(namespace, item.Name, luar.New(L, item.Function)) 71 | } 72 | 73 | // Register resource providers in Lua 74 | for _, item := range providerRegistry { 75 | // Wrap resource providers, so that we can properly handle any 76 | // errors returned by providers during resource instantiation. 77 | // Since we don't want to return the error to Lua, this is the 78 | // place where we handle any errors returned by providers. 79 | wrapper := func(p Provider) lua.LGFunction { 80 | return func(L *lua.LState) int { 81 | // Create the resource by calling it's provider 82 | r, err := p(L.CheckString(1)) 83 | if err != nil { 84 | L.RaiseError(err.Error()) 85 | } 86 | 87 | L.Push(luar.New(L, r)) 88 | return 1 // Number of arguments returned to Lua 89 | } 90 | } 91 | 92 | // Create the resource namespace 93 | namespace := L.GetGlobal(item.Namespace) 94 | if lua.LVIsFalse(namespace) { 95 | namespace = L.NewTable() 96 | L.SetGlobal(item.Namespace, namespace) 97 | } 98 | 99 | tbl := L.NewTable() 100 | tbl.RawSetH(lua.LString("new"), L.NewFunction(wrapper(item.Provider))) 101 | 102 | L.SetField(namespace, item.Type, tbl) 103 | } 104 | } 105 | 106 | func init() { 107 | logf := FunctionItem{ 108 | Name: "logf", 109 | Namespace: "stdlib", 110 | Function: Logf, 111 | } 112 | 113 | RegisterFunction(logf) 114 | } 115 | -------------------------------------------------------------------------------- /resource/package_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import "testing" 29 | 30 | func TestPacman(t *testing.T) { 31 | L := newLuaState() 32 | defer L.Close() 33 | 34 | const code = ` 35 | tmux = resource.pacman.new("tmux") 36 | ` 37 | 38 | if err := L.DoString(code); err != nil { 39 | t.Fatal(err) 40 | } 41 | 42 | pkg := luaResource(L, "tmux").(*Pacman) 43 | errorIfNotEqual(t, "package", pkg.Type) 44 | errorIfNotEqual(t, "tmux", pkg.Name) 45 | errorIfNotEqual(t, "installed", pkg.State) 46 | errorIfNotEqual(t, []string{}, pkg.Require) 47 | errorIfNotEqual(t, []string{"present", "installed"}, pkg.PresentStatesList) 48 | errorIfNotEqual(t, []string{"absent", "deinstalled"}, pkg.AbsentStatesList) 49 | errorIfNotEqual(t, false, pkg.Concurrent) 50 | errorIfNotEqual(t, "tmux", pkg.Package) 51 | errorIfNotEqual(t, "", pkg.Version) 52 | } 53 | 54 | func TestYum(t *testing.T) { 55 | L := newLuaState() 56 | defer L.Close() 57 | 58 | const code = ` 59 | tmux = resource.yum.new("tmux") 60 | ` 61 | 62 | if err := L.DoString(code); err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | pkg := luaResource(L, "tmux").(*Yum) 67 | errorIfNotEqual(t, "package", pkg.Type) 68 | errorIfNotEqual(t, "tmux", pkg.Name) 69 | errorIfNotEqual(t, "installed", pkg.State) 70 | errorIfNotEqual(t, []string{}, pkg.Require) 71 | errorIfNotEqual(t, []string{"present", "installed"}, pkg.PresentStatesList) 72 | errorIfNotEqual(t, []string{"absent", "deinstalled"}, pkg.AbsentStatesList) 73 | errorIfNotEqual(t, false, pkg.Concurrent) 74 | errorIfNotEqual(t, "tmux", pkg.Package) 75 | errorIfNotEqual(t, "", pkg.Version) 76 | } 77 | -------------------------------------------------------------------------------- /resource/property.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | // Property type represents a resource property, which can be 29 | // evaluated and set if needed. 30 | type Property interface { 31 | // Name returns the property name 32 | Name() string 33 | 34 | // Set sets the property to it's desired state. 35 | Set() error 36 | 37 | // IsSynced returns a boolean indicating whether the 38 | // resource property is in sync or not. 39 | IsSynced() (bool, error) 40 | } 41 | 42 | // ResourceProperty type implements the Property interface. 43 | type ResourceProperty struct { 44 | // PropertySetFunc is the type of the function that is called when 45 | // setting a resource property to it's desired state. 46 | PropertySetFunc func() error 47 | 48 | // PropertyIsSyncedFunc is the type of the function that is called when 49 | // determining whether a resource property is in the desired state. 50 | PropertyIsSyncedFunc func() (bool, error) 51 | 52 | // PropertyName is the name of the property. 53 | PropertyName string 54 | } 55 | 56 | // Set sets the property to it's desired state. 57 | func (rp *ResourceProperty) Set() error { 58 | return rp.PropertySetFunc() 59 | } 60 | 61 | // IsSynced returns a boolean indicating whether the 62 | // resource property is in the desired state. 63 | func (rp *ResourceProperty) IsSynced() (bool, error) { 64 | return rp.PropertyIsSyncedFunc() 65 | } 66 | 67 | // Name returns the property name. 68 | func (rp *ResourceProperty) Name() string { 69 | return rp.PropertyName 70 | } 71 | -------------------------------------------------------------------------------- /resource/provider.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | // providerRegistry contains the registered providers 29 | var providerRegistry = make([]ProviderItem, 0) 30 | 31 | // Provider type is the type which creates new resources 32 | type Provider func(name string) (Resource, error) 33 | 34 | // ProviderItem type represents a single item from the 35 | // provider registry. 36 | type ProviderItem struct { 37 | // Type name of the provider 38 | Type string 39 | 40 | // Provider is the actual resource provider 41 | Provider Provider 42 | 43 | // Namespace represents the Lua table that the 44 | // provider will be registered in 45 | Namespace string 46 | } 47 | 48 | // RegisterProvider registers a provider to the registry. 49 | func RegisterProvider(items ...ProviderItem) { 50 | providerRegistry = append(providerRegistry, items...) 51 | } 52 | -------------------------------------------------------------------------------- /resource/service_freebsd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | // +build freebsd 27 | 28 | package resource 29 | 30 | import ( 31 | "fmt" 32 | "os/exec" 33 | ) 34 | 35 | // Service type is a resource which manages services on a 36 | // FreeBSD system. 37 | // 38 | // Example: 39 | // svc = resource.service.new("nginx") 40 | // svc.state = "running" 41 | // svc.enable = true 42 | // svc.rcvar = "nginx_enable" 43 | type Service struct { 44 | Base 45 | 46 | // If true then enable the service during boot-time 47 | Enable bool `luar:"enable"` 48 | 49 | // RCVar (see rc.subr(8)), set to {svcname}_enable by default. 50 | // If service doesn't define rcvar, you should set svc.rcvar = "". 51 | RCVar string `luar:"rcvar"` 52 | } 53 | 54 | // NewService creates a new resource for managing services 55 | // on a FreeBSD system. 56 | func NewService(name string) (Resource, error) { 57 | s := &Service{ 58 | Base: Base{ 59 | Name: name, 60 | Type: "service", 61 | State: "running", 62 | Require: make([]string, 0), 63 | PresentStatesList: []string{"present", "running"}, 64 | AbsentStatesList: []string{"absent", "stopped"}, 65 | Concurrent: false, 66 | Subscribe: make(TriggerMap), 67 | }, 68 | Enable: true, 69 | RCVar: fmt.Sprintf("%v_enable", name), 70 | } 71 | 72 | // Set resource properties 73 | s.PropertyList = []Property{ 74 | &ResourceProperty{ 75 | PropertyName: "enable", 76 | PropertySetFunc: s.setEnable, 77 | PropertyIsSyncedFunc: s.isEnableSynced, 78 | }, 79 | } 80 | 81 | return s, nil 82 | } 83 | 84 | // Evaluate evaluates the state of the resource. 85 | func (s *Service) Evaluate() (State, error) { 86 | state := State{ 87 | Current: "unknown", 88 | Want: s.State, 89 | } 90 | 91 | // TODO: handle non existent service 92 | err := exec.Command("service", s.Name, "onestatus").Run() 93 | if err != nil { 94 | state.Current = "stopped" 95 | } else { 96 | state.Current = "running" 97 | } 98 | 99 | return state, nil 100 | } 101 | 102 | // Create starts the service. 103 | func (s *Service) Create() error { 104 | Logf("%s starting service\n", s.ID()) 105 | 106 | return exec.Command("service", s.Name, "onestart").Run() 107 | } 108 | 109 | // Delete stops the service. 110 | func (s *Service) Delete() error { 111 | Logf("%s stopping service\n", s.ID()) 112 | 113 | return exec.Command("service", s.Name, "onestop").Run() 114 | } 115 | 116 | // isEnableSynced checks whether the service is in the desired state. 117 | func (s *Service) isEnableSynced() (bool, error) { 118 | var enabled bool 119 | 120 | err := exec.Command("service", s.Name, "enabled").Run() 121 | switch err { 122 | case nil: 123 | enabled = true 124 | default: 125 | enabled = false 126 | } 127 | 128 | return enabled == s.Enable, nil 129 | } 130 | 131 | // setEnable enables or disables the service during boot-time. 132 | func (s *Service) setEnable() error { 133 | if s.RCVar == "" { 134 | return nil 135 | } 136 | 137 | var rcValue string 138 | switch s.Enable { 139 | case true: 140 | rcValue = "YES" 141 | case false: 142 | rcValue = "NO" 143 | } 144 | 145 | // TODO: rcvar should probably be deleted from rc.conf, when disabling service. 146 | // Compare default value (sysrc -D) with requested (rcValue) and if they match, delete rcvar. 147 | // Currently we just set it to NO. 148 | err := exec.Command("sysrc", fmt.Sprintf(`%s=%s`, s.RCVar, rcValue)).Run() 149 | 150 | return err 151 | } 152 | 153 | func init() { 154 | service := ProviderItem{ 155 | Type: "service", 156 | Provider: NewService, 157 | Namespace: DefaultResourceNamespace, 158 | } 159 | 160 | RegisterProvider(service) 161 | } 162 | -------------------------------------------------------------------------------- /resource/service_linux_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | // +build linux 27 | 28 | package resource 29 | 30 | import ( 31 | "testing" 32 | 33 | "github.com/coreos/go-systemd/util" 34 | ) 35 | 36 | func TestService(t *testing.T) { 37 | if !util.IsRunningSystemd() { 38 | return 39 | } 40 | 41 | L := newLuaState() 42 | defer L.Close() 43 | 44 | const code = ` 45 | svc = resource.service.new("nginx") 46 | ` 47 | 48 | if err := L.DoString(code); err != nil { 49 | t.Fatal(err) 50 | } 51 | 52 | svc := luaResource(L, "svc").(*Service) 53 | errorIfNotEqual(t, "service", svc.Type) 54 | errorIfNotEqual(t, "nginx", svc.Name) 55 | errorIfNotEqual(t, "running", svc.State) 56 | errorIfNotEqual(t, []string{}, svc.Require) 57 | errorIfNotEqual(t, []string{"present", "running"}, svc.PresentStatesList) 58 | errorIfNotEqual(t, []string{"absent", "stopped"}, svc.AbsentStatesList) 59 | errorIfNotEqual(t, true, svc.Concurrent) 60 | errorIfNotEqual(t, true, svc.Enable) 61 | } 62 | -------------------------------------------------------------------------------- /resource/shell.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import ( 29 | "os" 30 | "os/exec" 31 | "strings" 32 | ) 33 | 34 | // Shell type is a resource which executes shell commands. 35 | // 36 | // The command that is to be executed should be idempotent. 37 | // If the command that is to be executed is not idempotent on it's own, 38 | // in order to achieve idempotency of the resource you should set the 39 | // "creates" field to a filename that can be checked for existence. 40 | // 41 | // Example: 42 | // sh = resource.shell.new("touch /tmp/foo") 43 | // sh.creates = "/tmp/foo" 44 | // 45 | // Same example as the above one, but written in a different way. 46 | // 47 | // Example: 48 | // sh = resource.shell.new("creates the /tmp/foo file") 49 | // sh.command = "/usr/bin/touch /tmp/foo" 50 | // sh.creates = "/tmp/foo" 51 | type Shell struct { 52 | Base 53 | 54 | // Command to be executed. Defaults to the resource name. 55 | Command string `luar:"command"` 56 | 57 | // File to be checked for existence before executing the command. 58 | Creates string `luar:"creates"` 59 | 60 | // Mute flag indicates whether output from the command should be 61 | // dislayed or suppressed 62 | Mute bool `luar:"mute"` 63 | } 64 | 65 | // NewShell creates a new resource for executing shell commands 66 | func NewShell(name string) (Resource, error) { 67 | s := &Shell{ 68 | Base: Base{ 69 | Name: name, 70 | Type: "shell", 71 | State: "present", 72 | Require: make([]string, 0), 73 | PresentStatesList: []string{"present"}, 74 | AbsentStatesList: []string{"absent"}, 75 | Concurrent: true, 76 | Subscribe: make(TriggerMap), 77 | }, 78 | Command: name, 79 | Creates: "", 80 | Mute: false, 81 | } 82 | 83 | return s, nil 84 | } 85 | 86 | // Evaluate evaluates the state of the resource 87 | func (s *Shell) Evaluate() (State, error) { 88 | // Assumes that the command to be executed is idempotent 89 | // 90 | // Sets the current state to absent and wanted to be present, 91 | // which will cause the command to be executed. 92 | // 93 | // If the command to be executed is not idempotent on it's own, 94 | // in order to ensure idempotency we should specify a file, 95 | // that can be checked for existence. 96 | state := State{ 97 | Current: "absent", 98 | Want: s.State, 99 | } 100 | 101 | if s.Creates != "" { 102 | _, err := os.Stat(s.Creates) 103 | if os.IsNotExist(err) { 104 | state.Current = "absent" 105 | } else { 106 | state.Current = "present" 107 | } 108 | } 109 | 110 | return state, nil 111 | } 112 | 113 | // Create executes the shell command 114 | func (s *Shell) Create() error { 115 | Logf("%s executing command\n", s.ID()) 116 | 117 | args := strings.Fields(s.Command) 118 | cmd := exec.Command(args[0], args[1:]...) 119 | out, err := cmd.CombinedOutput() 120 | 121 | if !s.Mute { 122 | for _, line := range strings.Split(string(out), "\n") { 123 | Logf("%s %s\n", s.ID(), line) 124 | } 125 | } 126 | 127 | return err 128 | } 129 | 130 | // Delete is a no-op 131 | func (s *Shell) Delete() error { 132 | return nil 133 | } 134 | 135 | // Update is a no-op 136 | func (s *Shell) Update() error { 137 | return nil 138 | } 139 | 140 | func init() { 141 | item := ProviderItem{ 142 | Type: "shell", 143 | Provider: NewShell, 144 | Namespace: DefaultResourceNamespace, 145 | } 146 | 147 | RegisterProvider(item) 148 | } 149 | -------------------------------------------------------------------------------- /resource/shell_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import "testing" 29 | 30 | func TestShell(t *testing.T) { 31 | L := newLuaState() 32 | defer L.Close() 33 | 34 | const code = ` 35 | sh = resource.shell.new("create /tmp/foo file") 36 | sh.command = "touch /tmp/foo" 37 | sh.creates = "/tmp/foo" 38 | ` 39 | 40 | if err := L.DoString(code); err != nil { 41 | t.Fatal(err) 42 | } 43 | 44 | sh := luaResource(L, "sh").(*Shell) 45 | errorIfNotEqual(t, "shell", sh.Type) 46 | errorIfNotEqual(t, "create /tmp/foo file", sh.Name) 47 | errorIfNotEqual(t, "present", sh.State) 48 | errorIfNotEqual(t, []string{}, sh.Require) 49 | errorIfNotEqual(t, []string{"present"}, sh.PresentStatesList) 50 | errorIfNotEqual(t, []string{"absent"}, sh.AbsentStatesList) 51 | errorIfNotEqual(t, true, sh.Concurrent) 52 | errorIfNotEqual(t, "touch /tmp/foo", sh.Command) 53 | errorIfNotEqual(t, "/tmp/foo", sh.Creates) 54 | } 55 | -------------------------------------------------------------------------------- /resource/sysrc_freebsd.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | // +build freebsd 27 | 28 | package resource 29 | 30 | import ( 31 | "fmt" 32 | "os/exec" 33 | "regexp" 34 | ) 35 | 36 | // SysRC is a resource which manages rc.conf variables. 37 | // 38 | // Example: 39 | // rcvar = resource.sysrc.new("keyrate") 40 | // rcvar.state = "present" 41 | // rcvar.value = "fast" 42 | type SysRC struct { 43 | Base 44 | Value string `luar:"value"` 45 | } 46 | 47 | // NewSysRC creates a new resource for managing rc.conf variables 48 | // on a FreeBSD system. 49 | func NewSysRC(name string) (Resource, error) { 50 | s := &SysRC{ 51 | Base: Base{ 52 | Name: name, 53 | Type: "sysrc", 54 | State: "present", 55 | Require: make([]string, 0), 56 | PresentStatesList: []string{"present"}, 57 | AbsentStatesList: []string{"absent"}, 58 | Concurrent: false, 59 | Subscribe: make(TriggerMap), 60 | }, 61 | } 62 | 63 | return s, nil 64 | } 65 | 66 | // Evaluate evaluates the state of the resource. 67 | func (s *SysRC) Evaluate() (State, error) { 68 | state := State{ 69 | Current: "unknown", 70 | Want: s.State, 71 | } 72 | 73 | out, err := exec.Command("sysrc", s.Name).CombinedOutput() 74 | if err != nil { 75 | state.Current = "absent" 76 | return state, nil 77 | } 78 | state.Current = "present" 79 | 80 | k, v, err := parseSysRCOutput(string(out)) 81 | if err != nil { 82 | return state, err 83 | } 84 | 85 | if s.Name != k { 86 | return state, fmt.Errorf("bug: expected rcvar %v, got %v", s.Name, k) 87 | } 88 | 89 | if s.Value != v { 90 | state.Current = "absent" 91 | } 92 | 93 | return state, nil 94 | } 95 | 96 | // Create adds variable to rc.conf. 97 | func (s *SysRC) Create() error { 98 | Logf("%s adding rcvar\n", s.ID()) 99 | 100 | return exec.Command("sysrc", fmt.Sprintf("%s=%s", s.Name, s.Value)).Run() 101 | } 102 | 103 | // Delete removes variable from rc.conf. 104 | func (s *SysRC) Delete() error { 105 | Logf("%s removing rcvar\n", s.ID()) 106 | 107 | return exec.Command("sysrc", "-x", s.Name).Run() 108 | } 109 | 110 | // Update sets variable in rc.conf to s.Value. 111 | func (s *SysRC) Update() error { 112 | Logf("%s setting rcvar to %s\n", s.ID(), s.Value) 113 | 114 | return exec.Command("sysrc", fmt.Sprintf("%s=%s", s.Name, s.Value)).Run() 115 | } 116 | 117 | var sysRCre = regexp.MustCompile("(.*): (.*)") 118 | 119 | // ParseSysRCOutput parses output from sysrc command. 120 | func parseSysRCOutput(out string) (k, v string, err error) { 121 | m := sysRCre.FindStringSubmatch(out) 122 | if m == nil { 123 | return "", "", fmt.Errorf("bug: sysrc output %q didn't match regexp", out) 124 | } 125 | return m[1], m[2], nil 126 | } 127 | 128 | func init() { 129 | sysrc := ProviderItem{ 130 | Type: "sysrc", 131 | Provider: NewSysRC, 132 | Namespace: DefaultResourceNamespace, 133 | } 134 | 135 | RegisterProvider(sysrc) 136 | } 137 | -------------------------------------------------------------------------------- /resource/sysrc_freebsd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import "testing" 29 | 30 | func TestParseSysRCOutput(t *testing.T) { 31 | table := []struct { 32 | Input string 33 | K string 34 | V string 35 | }{ 36 | { 37 | Input: "keyrate: fast\n", 38 | K: "keyrate", 39 | V: "fast", 40 | }, 41 | { 42 | Input: "dumpdev: \n", 43 | K: "dumpdev", 44 | V: "", 45 | }, 46 | } 47 | 48 | for _, item := range table { 49 | t.Run(item.Input, func(t *testing.T) { 50 | k, v, err := parseSysRCOutput(item.Input) 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | if k != item.K || v != item.V { 55 | t.Errorf("expected: k=%q, v=%q, got: k=%q, v=%q", item.K, item.V, k, v) 56 | } 57 | }) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /resource/testutils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import ( 29 | "fmt" 30 | "path/filepath" 31 | "reflect" 32 | "runtime" 33 | "testing" 34 | 35 | "github.com/yuin/gopher-lua" 36 | ) 37 | 38 | // newLuaState creates a new Lua state and registers the 39 | // resource providers in Lua. It is up to the caller to 40 | // close the Lua state once done with it. 41 | func newLuaState() *lua.LState { 42 | L := lua.NewState() 43 | LuaRegisterBuiltin(L) 44 | 45 | return L 46 | } 47 | 48 | // luaResource retrieves a resource by it's name 49 | func luaResource(L *lua.LState, name string) interface{} { 50 | return L.GetGlobal(name).(*lua.LUserData).Value 51 | } 52 | 53 | func positionString(level int) string { 54 | _, file, line, _ := runtime.Caller(level + 1) 55 | 56 | return fmt.Sprintf("%v:%v:", filepath.Base(file), line) 57 | } 58 | 59 | func errorIfNotEqual(t *testing.T, v1, v2 interface{}) { 60 | if !reflect.DeepEqual(v1, v2) { 61 | t.Errorf("%v want '%v', got '%v'", positionString(1), v1, v2) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /resource/vsphere.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import ( 29 | "context" 30 | "errors" 31 | "fmt" 32 | "net/url" 33 | 34 | "github.com/vmware/govmomi" 35 | "github.com/vmware/govmomi/find" 36 | "github.com/vmware/govmomi/object" 37 | ) 38 | 39 | // VSphereNamespace is the table name in Lua where vSphere resources are 40 | // being registered to. 41 | const VSphereNamespace = "vsphere" 42 | 43 | // ErrNoUsername error is returned when no username is provided for 44 | // establishing a connection to the remote VMware vSphere API endpoint. 45 | var ErrNoUsername = errors.New("No username provided") 46 | 47 | // ErrNoPassword error is returned when no password is provided for 48 | // establishing a connection to the remote VMware vSphere API endpoint. 49 | var ErrNoPassword = errors.New("No password provided") 50 | 51 | // ErrNoEndpoint error is returned when no VMware vSphere API endpoint is 52 | // provided. 53 | var ErrNoEndpoint = errors.New("No endpoint provided") 54 | 55 | // ErrNotVC error is returned when the remote endpoint is not a vCenter system. 56 | var ErrNotVC = errors.New("Not a VMware vCenter endpoint") 57 | 58 | // BaseVSphere type is the base type for all vSphere related resources. 59 | type BaseVSphere struct { 60 | Base 61 | 62 | // Username to use when connecting to the vSphere endpoint. 63 | // Defaults to an empty string. 64 | Username string `luar:"username"` 65 | 66 | // Password to use when connecting to the vSphere endpoint. 67 | // Defaults to an empty string. 68 | Password string `luar:"password"` 69 | 70 | // Endpoint to the VMware vSphere API. Defaults to an empty string. 71 | Endpoint string `luar:"endpoint"` 72 | 73 | // Path to use when creating the object managed by the resource. 74 | // Defaults to "/". 75 | Path string `luar:"path"` 76 | 77 | // If set to true then allow connecting to vSphere API endpoints with 78 | // self-signed certificates. Defaults to false. 79 | Insecure bool `luar:"insecure"` 80 | 81 | url *url.URL `luar:"-"` 82 | ctx context.Context `luar:"-"` 83 | cancel context.CancelFunc `luar:"-"` 84 | client *govmomi.Client `luar:"-"` 85 | finder *find.Finder `luar:"-"` 86 | } 87 | 88 | // ID returns the unique resource id for the resource 89 | func (bv *BaseVSphere) ID() string { 90 | return fmt.Sprintf("%s[%s@%s]", bv.Type, bv.Name, bv.Endpoint) 91 | } 92 | 93 | // Validate validates the resource. 94 | func (bv *BaseVSphere) Validate() error { 95 | if err := bv.Base.Validate(); err != nil { 96 | return err 97 | } 98 | 99 | if bv.Username == "" { 100 | return ErrNoUsername 101 | } 102 | 103 | if bv.Password == "" { 104 | return ErrNoPassword 105 | } 106 | 107 | if bv.Endpoint == "" { 108 | return ErrNoEndpoint 109 | } 110 | 111 | // Validate the URL to the API endpoint and set the username and password info 112 | endpoint, err := url.Parse(bv.Endpoint) 113 | if err != nil { 114 | return err 115 | } 116 | endpoint.User = url.UserPassword(bv.Username, bv.Password) 117 | bv.url = endpoint 118 | 119 | return nil 120 | } 121 | 122 | // Initialize establishes a connection to the remote vSphere API endpoint. 123 | func (bv *BaseVSphere) Initialize() error { 124 | bv.ctx, bv.cancel = context.WithCancel(context.Background()) 125 | 126 | // Connect and login to the VMWare vSphere API endpoint 127 | c, err := govmomi.NewClient(bv.ctx, bv.url, bv.Insecure) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | bv.client = c 133 | bv.finder = find.NewFinder(bv.client.Client, true) 134 | 135 | return nil 136 | } 137 | 138 | // Close closes the connection to the remote vSphere API endpoint. 139 | func (bv *BaseVSphere) Close() error { 140 | defer bv.cancel() 141 | 142 | return bv.client.Logout(bv.ctx) 143 | } 144 | 145 | // vSphereRemoveHost disconnects an ESXi host from the 146 | // vCenter server and then removes it. 147 | func vSphereRemoveHost(ctx context.Context, obj *object.HostSystem) error { 148 | disconnectTask, err := obj.Disconnect(ctx) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | if err := disconnectTask.Wait(ctx); err != nil { 154 | return err 155 | } 156 | 157 | destroyTask, err := obj.Destroy(ctx) 158 | if err != nil { 159 | return err 160 | } 161 | 162 | return destroyTask.Wait(ctx) 163 | } 164 | 165 | func init() { 166 | datacenter := ProviderItem{ 167 | Type: "datacenter", 168 | Provider: NewDatacenter, 169 | Namespace: VSphereNamespace, 170 | } 171 | 172 | cluster := ProviderItem{ 173 | Type: "cluster", 174 | Provider: NewCluster, 175 | Namespace: VSphereNamespace, 176 | } 177 | 178 | clusterHost := ProviderItem{ 179 | Type: "cluster_host", 180 | Provider: NewClusterHost, 181 | Namespace: VSphereNamespace, 182 | } 183 | 184 | host := ProviderItem{ 185 | Type: "host", 186 | Provider: NewHost, 187 | Namespace: VSphereNamespace, 188 | } 189 | 190 | vm := ProviderItem{ 191 | Type: "vm", 192 | Provider: NewVirtualMachine, 193 | Namespace: VSphereNamespace, 194 | } 195 | 196 | datastoreNfs := ProviderItem{ 197 | Type: "datastore_nfs", 198 | Provider: NewDatastoreNfs, 199 | Namespace: VSphereNamespace, 200 | } 201 | 202 | RegisterProvider(datacenter, cluster, clusterHost, host, vm, datastoreNfs) 203 | } 204 | -------------------------------------------------------------------------------- /resource/vsphere_cluster_host.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import ( 29 | "path" 30 | 31 | "github.com/vmware/govmomi/find" 32 | "github.com/vmware/govmomi/vim25/types" 33 | ) 34 | 35 | // ClusterHost type is a resource which manages hosts in a 36 | // VMware vSphere cluster. 37 | // 38 | // Example: 39 | // host = vsphere.cluster_host.new("esxi01.example.org") 40 | // host.endpoint = "https://vc01.example.org/sdk" 41 | // host.username = "root" 42 | // host.password = "myp4ssw0rd" 43 | // host.state = "present" 44 | // host.path = "/MyDatacenter/host/MyCluster" 45 | // host.esxi_username = "root" 46 | // host.esxi_password = "esxip4ssw0rd" 47 | type ClusterHost struct { 48 | BaseVSphere 49 | 50 | // EsxiUsername is the username used to connect to the 51 | // remote ESXi host. Defaults to an empty string. 52 | EsxiUsername string `luar:"esxi_username"` 53 | 54 | // EsxiPassword is the password used to connect to the 55 | // remote ESXi host. Defaults to an empty string. 56 | EsxiPassword string `luar:"esxi_password"` 57 | 58 | // SSL thumbprint of the host. Defaults to an empty string. 59 | SslThumbprint string `luar:"ssl_thumbprint"` 60 | 61 | // Force flag specifies whether or not to forcefully add the 62 | // host to the cluster, possibly disconnecting it from any other 63 | // connected vCenter servers. Defaults to false. 64 | Force bool `luar:"force"` 65 | 66 | // Port to connect to on the remote ESXi host. Defaults to 443. 67 | Port int32 `luar:"port"` 68 | 69 | // License to attach to the host. Defaults to an empty string. 70 | License string `luar:"license"` 71 | } 72 | 73 | // NewClusterHost creates a new resource for managing hosts in a 74 | // VMware vSphere cluster. 75 | func NewClusterHost(name string) (Resource, error) { 76 | ch := &ClusterHost{ 77 | BaseVSphere: BaseVSphere{ 78 | Base: Base{ 79 | Name: name, 80 | Type: "cluster_host", 81 | State: "present", 82 | Require: make([]string, 0), 83 | PresentStatesList: []string{"present"}, 84 | AbsentStatesList: []string{"absent"}, 85 | Concurrent: true, 86 | Subscribe: make(TriggerMap), 87 | }, 88 | Username: "", 89 | Password: "", 90 | Endpoint: "", 91 | Insecure: false, 92 | Path: "/", 93 | }, 94 | EsxiUsername: "", 95 | EsxiPassword: "", 96 | SslThumbprint: "", 97 | Force: false, 98 | Port: 443, 99 | License: "", 100 | } 101 | 102 | return ch, nil 103 | } 104 | 105 | // Evaluate evaluates the state of the host in the cluster. 106 | func (ch *ClusterHost) Evaluate() (State, error) { 107 | state := State{ 108 | Current: "unknown", 109 | Want: ch.State, 110 | } 111 | 112 | _, err := ch.finder.HostSystem(ch.ctx, path.Join(ch.Path, ch.Name)) 113 | if err != nil { 114 | // Host is absent 115 | if _, ok := err.(*find.NotFoundError); ok { 116 | state.Current = "absent" 117 | return state, nil 118 | } 119 | 120 | // Something else happened 121 | return state, err 122 | } 123 | 124 | state.Current = "present" 125 | 126 | return state, nil 127 | } 128 | 129 | // Create adds the host to the cluster. 130 | func (ch *ClusterHost) Create() error { 131 | Logf("%s adding host to %s\n", ch.ID(), ch.Path) 132 | 133 | obj, err := ch.finder.ClusterComputeResource(ch.ctx, ch.Path) 134 | if err != nil { 135 | return err 136 | } 137 | 138 | spec := types.HostConnectSpec{ 139 | HostName: ch.Name, 140 | Port: ch.Port, 141 | SslThumbprint: ch.SslThumbprint, 142 | UserName: ch.EsxiUsername, 143 | Password: ch.EsxiPassword, 144 | Force: ch.Force, 145 | LockdownMode: "", 146 | } 147 | 148 | task, err := obj.AddHost(ch.ctx, spec, true, &ch.License, nil) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | return task.Wait(ch.ctx) 154 | } 155 | 156 | // Delete disconnects the host and then removes it. 157 | func (ch *ClusterHost) Delete() error { 158 | Logf("%s removing host from %s\n", ch.ID(), ch.Path) 159 | 160 | obj, err := ch.finder.HostSystem(ch.ctx, path.Join(ch.Path, ch.Name)) 161 | if err != nil { 162 | return err 163 | } 164 | 165 | return vSphereRemoveHost(ch.ctx, obj) 166 | } 167 | -------------------------------------------------------------------------------- /resource/vsphere_datacenter.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package resource 27 | 28 | import "github.com/vmware/govmomi/find" 29 | 30 | // Datacenter type is a resource which manages datacenters in a 31 | // VMware vSphere environment. 32 | // 33 | // Example: 34 | // dc = vsphere.datacenter.new("my-datacenter") 35 | // dc.endpoint = "https://vc01.example.org/sdk" 36 | // dc.username = "root" 37 | // dc.password = "myp4ssw0rd" 38 | // dc.insecure = true 39 | // dc.state = "present" 40 | // dc.path = "/SomePath" 41 | type Datacenter struct { 42 | BaseVSphere 43 | } 44 | 45 | // NewDatacenter creates a new resource for managing datacenters in a 46 | // VMware vSphere environment. 47 | func NewDatacenter(name string) (Resource, error) { 48 | d := &Datacenter{ 49 | BaseVSphere: BaseVSphere{ 50 | Base: Base{ 51 | Name: name, 52 | Type: "datacenter", 53 | State: "present", 54 | Require: make([]string, 0), 55 | PresentStatesList: []string{"present"}, 56 | AbsentStatesList: []string{"absent"}, 57 | Concurrent: true, 58 | Subscribe: make(TriggerMap), 59 | }, 60 | Username: "", 61 | Password: "", 62 | Endpoint: "", 63 | Insecure: false, 64 | Path: "/", 65 | }, 66 | } 67 | 68 | return d, nil 69 | } 70 | 71 | // Evaluate evaluates the state of the datacenter. 72 | func (d *Datacenter) Evaluate() (State, error) { 73 | state := State{ 74 | Current: "unknown", 75 | Want: d.State, 76 | } 77 | 78 | _, err := d.finder.Datacenter(d.ctx, d.Name) 79 | if err != nil { 80 | // Datacenter is absent 81 | if _, ok := err.(*find.NotFoundError); ok { 82 | state.Current = "absent" 83 | return state, nil 84 | } 85 | 86 | // Something else happened 87 | return state, err 88 | } 89 | 90 | state.Current = "present" 91 | 92 | return state, nil 93 | } 94 | 95 | // Create creates a new datacenter. 96 | func (d *Datacenter) Create() error { 97 | Logf("%s creating datacenter in %s\n", d.ID(), d.Path) 98 | 99 | folder, err := d.finder.FolderOrDefault(d.ctx, d.Path) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | _, err = folder.CreateDatacenter(d.ctx, d.Name) 105 | 106 | return err 107 | } 108 | 109 | // Delete removes the datacenter. 110 | func (d *Datacenter) Delete() error { 111 | Logf("%s removing datacenter from %s\n", d.ID(), d.Path) 112 | 113 | dc, err := d.finder.Datacenter(d.ctx, d.Name) 114 | if err != nil { 115 | return err 116 | } 117 | 118 | task, err := dc.Destroy(d.ctx) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | return task.Wait(d.ctx) 124 | } 125 | -------------------------------------------------------------------------------- /site/README.md: -------------------------------------------------------------------------------- 1 | ## Site Repo 2 | 3 | This directory contains an example of a site repo for Gru. 4 | 5 | A site repo in Gru is essentially a Git repository with branches, 6 | where each branch maps to an environment, which can be used by 7 | remote minions. 8 | 9 | In order to use this site repo, simply copy the contents of this 10 | directory and add them to a Git repository, which you can use by 11 | your minions. 12 | 13 | ```bash 14 | $ cp -a site ~/gru-site 15 | $ cd ~/gru-site 16 | $ git init 17 | $ git add 18 | $ git commit -m 'Initial commit of site repo' 19 | $ git checkout -b production 20 | ``` 21 | 22 | Once you've got the site repo in Git you can start you minions by 23 | pointing them to your site repo, e.g. 24 | 25 | ```bash 26 | $ gructl serve --siterepo https://github.com/you/gru-site 27 | ``` 28 | -------------------------------------------------------------------------------- /site/code/memcached.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Gru module for installing and configuring memcached 3 | -- 4 | 5 | -- Manage the memcached package 6 | pkg = resource.package.new("memcached") 7 | pkg.state = "present" 8 | 9 | -- Path to the systemd drop-in unit directory 10 | systemd_dir = "/etc/systemd/system/memcached.service.d/" 11 | 12 | -- Manage the systemd drop-in unit directory 13 | unit_dir = resource.directory.new(systemd_dir) 14 | unit_dir.state = "present" 15 | unit_dir.require = { 16 | pkg:ID(), 17 | } 18 | 19 | -- Manage the systemd drop-in unit 20 | unit_file = resource.file.new(systemd_dir .. "override.conf") 21 | unit_file.state = "present" 22 | unit_file.mode = tonumber("0644", 8) 23 | unit_file.source = "data/memcached/memcached-override.conf" 24 | unit_file.require = { 25 | unit_dir:ID(), 26 | } 27 | 28 | -- Instruct systemd(1) to reload it's configuration 29 | systemd_reload = resource.shell.new("systemctl daemon-reload") 30 | systemd_reload.require = { 31 | unit_file:ID(), 32 | } 33 | 34 | -- Manage the memcached service 35 | svc = resource.service.new("memcached") 36 | svc.state = "running" 37 | svc.enable = true 38 | svc.require = { 39 | pkg:ID(), 40 | unit_file:ID(), 41 | } 42 | 43 | -- Finally, register the resources to the catalog 44 | catalog:add(pkg, unit_dir, unit_file, systemd_reload, svc) 45 | -------------------------------------------------------------------------------- /site/code/triggers.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Example code for using triggers in resources 3 | -- 4 | 5 | -- Manage the SNMP package 6 | pkg = resource.package.new("net-snmp") 7 | pkg.state = "present" 8 | 9 | -- Manage the config file for SNMP daemon 10 | config = resource.file.new("/etc/snmp/snmpd.conf") 11 | config.state = "present" 12 | config.content = "rocommunity public" 13 | config.require = { pkg:ID() } 14 | 15 | -- Manage the SNMP service 16 | svc = resource.service.new("snmpd") 17 | svc.state = "running" 18 | svc.enable = true 19 | svc.require = { pkg:ID(), config:ID() } 20 | 21 | -- Subscribe for changes in the config file resource. 22 | -- Reload the SNMP daemon service if the config file has changed. 23 | svc.subscribe[config:ID()] = function() 24 | os.execute("systemctl reload snmpd") 25 | end 26 | 27 | -- Subscribe for changes in the package resource. 28 | -- Restart the SNMP daemon service if the package has changed. 29 | svc.subscribe[pkg:ID()] = function() 30 | os.execute("systemctl restart snmpd") 31 | end 32 | 33 | -- Add resources to the catalog 34 | catalog:add(pkg, config, svc) 35 | -------------------------------------------------------------------------------- /site/code/vsphere.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Example code for managing VMware vSphere environment 3 | -- 4 | 5 | -- 6 | -- The credentials to the remote VMware vSphere API endpoint 7 | -- 8 | credentials = { 9 | username = "root", 10 | password = "rootp4ssw0rd", 11 | endpoint = "https://vc01.example.org/sdk", 12 | insecure = true, --> Needed if the vCenter is using a self-signed certificate 13 | } 14 | 15 | -- 16 | -- Manage the VMware vSphere Datacenter 17 | -- 18 | dc = vsphere.datacenter.new("MyDatacenter") 19 | dc.username = credentials.username 20 | dc.password = credentials.password 21 | dc.endpoint = credentials.endpoint 22 | dc.insecure = credentials.insecure 23 | dc.state = "present" 24 | 25 | catalog:add(dc) 26 | 27 | -- 28 | -- Manage the VMware vSphere Cluster 29 | -- 30 | cluster = vsphere.cluster.new("MyCluster") 31 | cluster.endpoint = credentials.endpoint 32 | cluster.username = credentials.username 33 | cluster.password = credentials.password 34 | cluster.insecure = credentials.insecure 35 | 36 | cluster.state = "present" 37 | cluster.path = "/MyDatacenter/host" 38 | cluster.config = { 39 | enable_drs = true, 40 | drs_behavior = "fullyAutomated", 41 | } 42 | cluster.require = { dc:ID() } --> The cluster depends on the datacenter 43 | 44 | catalog:add(cluster) 45 | 46 | -- 47 | -- Add an ESXi host to the VMware vSphere Cluster 48 | -- 49 | host = vsphere.cluster_host.new("esxi01.example.org") 50 | host.endpoint = credentials.endpoint 51 | host.username = credentials.username 52 | host.password = credentials.password 53 | host.insecure = credentials.insecure 54 | 55 | host.state = "present" 56 | host.path = "/MyDatacenter/host/MyCluster" 57 | host.esxi_username = "root" 58 | host.esxi_password = "esxi_p4ssw0rd" 59 | host.ssl_thumbprint = "ssl-thumbprint-of-host" 60 | host.require = { cluster:ID() } --> The ESXi host depends on the cluster 61 | 62 | catalog:add(host) 63 | 64 | -- 65 | -- Mount an NFS datastore on our ESXi host 66 | -- 67 | datastore = vsphere.datastore_nfs.new("vm-storage01") 68 | datastore.endpoint = credentials.endpoint 69 | datastore.username = credentials.username 70 | datastore.password = credentials.password 71 | datastore.insecure = credentials.insecure 72 | 73 | datastore.state = "present" 74 | datastore.path = "/MyDatacenter/datastore" 75 | datastore.hosts = { 76 | "/MyDatacenter/host/MyCluster/esxi01.example.org", 77 | } 78 | datastore.nfs_server = "nfs01.example.org" 79 | datastore.nfs_path = "/storage/vm-storage01" 80 | datastore.mode = "readWrite" 81 | datastore.require = { host:ID() } --> The datastore depends on the ESXi host 82 | 83 | catalog:add(datastore) 84 | 85 | -- 86 | -- Manage VMware vSphere Virtual Machines 87 | -- 88 | names = { "kevin", "bob", "stuart" } --> You know these guys, right? 89 | 90 | for _, name in ipairs(names) do 91 | vm = vsphere.vm.new(name) 92 | vm.endpoint = credentials.endpoint 93 | vm.username = credentials.username 94 | vm.password = credentials.password 95 | vm.insecure = credentials.insecure 96 | 97 | vm.state = "present" 98 | vm.path = "/MyDatacenter/vm" 99 | vm.pool = "/MyDatacenter/host/MyCluster" 100 | vm.datastore = "/MyDatacenter/datastore/vm-storage01" 101 | vm.wait_for_ip = true 102 | vm.power_state = "poweredOn" 103 | vm.template_config = { 104 | use = "/MyDatacenter/vm/Templates/centos-7-x86-64-template", 105 | } 106 | vm.require = { host:ID(), datastore:ID() } --> The VM depends on the ESXi host and datastore 107 | 108 | catalog:add(vm) 109 | end 110 | -------------------------------------------------------------------------------- /site/data/memcached/memcached-override.conf: -------------------------------------------------------------------------------- 1 | [Service] 2 | ExecStart= 3 | ExecStart=/usr/bin/memcached 4 | -------------------------------------------------------------------------------- /task/task.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package task 27 | 28 | import "github.com/pborman/uuid" 29 | 30 | // Task states 31 | const ( 32 | // Unknown state of the task 33 | // This is the default state of a task 34 | // when new task is initially created 35 | TaskStateUnknown = "unknown" 36 | 37 | // Task has been received by the 38 | // minion and is queued for execution 39 | TaskStateQueued = "queued" 40 | 41 | // Task is being processed 42 | TaskStateProcessing = "processing" 43 | 44 | // Task has been processed by the 45 | // minion and was flagged as successful 46 | TaskStateSuccess = "success" 47 | 48 | // Task has been processed by the 49 | // minion and was flagged as failed 50 | TaskStateFailed = "failed" 51 | 52 | // Task has been skipped 53 | TaskStateSkipped = "skipped" 54 | ) 55 | 56 | // Task type represents a task that is processed by minions 57 | type Task struct { 58 | // Do not take any actions, just report what would be done 59 | DryRun bool `json:"dryRun"` 60 | 61 | // Environment to use for this task 62 | Environment string `json:"environment"` 63 | 64 | // Command to be processed 65 | Command string `json:"command"` 66 | 67 | // Time when the command was sent for processing 68 | TimeReceived int64 `json:"timeReceived"` 69 | 70 | // Time when the command was processed 71 | TimeProcessed int64 `json:"timeProcessed"` 72 | 73 | // Task unique id 74 | ID uuid.UUID `json:"id"` 75 | 76 | // Result of task after processing 77 | Result string `json:"result"` 78 | 79 | // Task state 80 | State string `json:"state"` 81 | } 82 | 83 | // New creates a new task 84 | func New(command, environment string) *Task { 85 | t := &Task{ 86 | Command: command, 87 | Environment: environment, 88 | ID: uuid.NewRandom(), 89 | State: TaskStateUnknown, 90 | } 91 | 92 | return t 93 | } 94 | -------------------------------------------------------------------------------- /task/task_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package task 27 | 28 | import "testing" 29 | 30 | func TestTaskState(t *testing.T) { 31 | dummyTask := New("foo", "bar") 32 | got := dummyTask.State 33 | want := TaskStateUnknown 34 | if want != got { 35 | t.Errorf("Incorrect task state: want %q, got %q", want, got) 36 | } 37 | } 38 | 39 | func TestTaskTimeReceivedProcessed(t *testing.T) { 40 | dummyTask := New("foo", "bar") 41 | 42 | // Task time received and processed should be 0 when initially created 43 | var want int64 44 | 45 | got := dummyTask.TimeReceived 46 | if want != got { 47 | t.Errorf("Incorrect task time received: want %q, got %q", want, got) 48 | } 49 | 50 | got = dummyTask.TimeProcessed 51 | if want != got { 52 | t.Errorf("Incorrect task time processed: want %q, got %q", want, got) 53 | } 54 | } 55 | 56 | func TestTaskResult(t *testing.T) { 57 | dummyTask := New("foo", "bar") 58 | got := dummyTask.Result 59 | want := "" 60 | 61 | if want != got { 62 | t.Errorf("Incorrect task result: want %q, got %q", want, got) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /utils/git.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package utils 27 | 28 | import ( 29 | "os/exec" 30 | "strings" 31 | ) 32 | 33 | // GitRepo type manages a VCS repository with Git 34 | type GitRepo struct { 35 | // Local path to the repository 36 | Path string 37 | 38 | // Upstream URL of the Git repository 39 | Upstream string 40 | 41 | // Path to the Git tool 42 | git string 43 | } 44 | 45 | // NewGitRepo creates a new Git repository 46 | func NewGitRepo(path, upstream string) (*GitRepo, error) { 47 | cmd, err := exec.LookPath("git") 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | repo := &GitRepo{ 53 | Path: path, 54 | Upstream: upstream, 55 | git: cmd, 56 | } 57 | 58 | return repo, nil 59 | } 60 | 61 | // Fetch fetches from the given remote 62 | func (gr *GitRepo) Fetch(remote string) ([]byte, error) { 63 | return exec.Command(gr.git, "-C", gr.Path, "fetch", remote).CombinedOutput() 64 | } 65 | 66 | // Pull pulls from the given remote and merges changes into the 67 | // local branch 68 | func (gr *GitRepo) Pull(remote, branch string) ([]byte, error) { 69 | out, err := gr.Checkout(branch) 70 | if err != nil { 71 | return out, err 72 | } 73 | 74 | return exec.Command(gr.git, "-C", gr.Path, "pull", remote).CombinedOutput() 75 | } 76 | 77 | // Checkout checks out a given local branch 78 | func (gr *GitRepo) Checkout(branch string) ([]byte, error) { 79 | return exec.Command(gr.git, "-C", gr.Path, "checkout", branch).CombinedOutput() 80 | } 81 | 82 | // CheckoutDetached checks out a given local branch in detached mode 83 | func (gr *GitRepo) CheckoutDetached(branch string) ([]byte, error) { 84 | return exec.Command(gr.git, "-C", gr.Path, "checkout", "--detach", branch).CombinedOutput() 85 | } 86 | 87 | // Clone clones the upstream repository 88 | func (gr *GitRepo) Clone() ([]byte, error) { 89 | return exec.Command(gr.git, "clone", gr.Upstream, gr.Path).CombinedOutput() 90 | } 91 | 92 | // Head returns the SHA1 commit id at HEAD 93 | func (gr *GitRepo) Head() (string, error) { 94 | head, err := exec.Command(gr.git, "-C", gr.Path, "rev-parse", "--short", "HEAD").CombinedOutput() 95 | if err != nil { 96 | return "", err 97 | } 98 | 99 | return strings.Trim(string(head), "\n"), nil 100 | } 101 | 102 | // IsGitRepo checks if the repository is a valid Git repository 103 | func (gr *GitRepo) IsGitRepo() bool { 104 | err := exec.Command(gr.git, "-C", gr.Path, "rev-parse").Run() 105 | if err != nil { 106 | return false 107 | } 108 | 109 | return true 110 | } 111 | -------------------------------------------------------------------------------- /utils/list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package utils 27 | 28 | // List type represents a slice of strings 29 | type List []string 30 | 31 | // NewList creates a new list with the given items 32 | func NewList(s ...string) List { 33 | l := make(List, 0, len(s)) 34 | for _, v := range s { 35 | l = append(l, v) 36 | } 37 | 38 | return l 39 | } 40 | 41 | // Contains returns a boolean indicating whether the list 42 | // contains the given string. 43 | func (l List) Contains(x string) bool { 44 | for _, v := range l { 45 | if v == x { 46 | return true 47 | } 48 | } 49 | 50 | return false 51 | } 52 | 53 | // Len is the number of items in the list. 54 | func (l List) Len() int { 55 | return len(l) 56 | } 57 | 58 | // String type represents a string 59 | type String struct { 60 | str string 61 | } 62 | 63 | // NewString creates a new string 64 | func NewString(s string) String { 65 | return String{ 66 | str: s, 67 | } 68 | } 69 | 70 | // String implements the fmt.Stringer interface 71 | func (s String) String() string { 72 | return s.str 73 | } 74 | 75 | // IsInList returns a boolean indicating whether the string is 76 | // contained within a given list 77 | func (s String) IsInList(l List) bool { 78 | return l.Contains(s.str) 79 | } 80 | -------------------------------------------------------------------------------- /utils/list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package utils 27 | 28 | import "testing" 29 | 30 | func TestListContains(t *testing.T) { 31 | l := NewList("foo", "bar", "qux") 32 | want := "foo" 33 | 34 | if !l.Contains(want) { 35 | t.Errorf("list does not contain %q", want) 36 | } 37 | } 38 | 39 | func TestStringInList(t *testing.T) { 40 | l := NewList("foo", "bar", "qux") 41 | s := NewString("foo") 42 | 43 | if !s.IsInList(l) { 44 | t.Errorf("string %q is not in list", s) 45 | } 46 | } 47 | 48 | func TestList_Len(t *testing.T) { 49 | l := NewList("foo", "bar", "qux") 50 | 51 | if n := l.Len(); n != 3 { 52 | t.Errorf("the number of items in the list is not 3 but %v", n) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /utils/map.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package utils 27 | 28 | import "sync" 29 | 30 | // ConcurrentMap is a map type that can be safely shared between 31 | // goroutines that require read/write access to a map 32 | type ConcurrentMap struct { 33 | sync.RWMutex 34 | items map[string]interface{} 35 | } 36 | 37 | // ConcurrentMapItem contains a key/value pair item of a concurrent map 38 | type ConcurrentMapItem struct { 39 | Key string 40 | Value interface{} 41 | } 42 | 43 | // NewConcurrentMap creates a new concurrent map 44 | func NewConcurrentMap() *ConcurrentMap { 45 | cm := &ConcurrentMap{ 46 | items: make(map[string]interface{}), 47 | } 48 | 49 | return cm 50 | } 51 | 52 | // Set adds an item to a concurrent map 53 | func (cm *ConcurrentMap) Set(key string, value interface{}) { 54 | cm.Lock() 55 | defer cm.Unlock() 56 | 57 | cm.items[key] = value 58 | } 59 | 60 | // Get retrieves the value for a concurrent map item 61 | func (cm *ConcurrentMap) Get(key string) (interface{}, bool) { 62 | cm.Lock() 63 | defer cm.Unlock() 64 | 65 | value, ok := cm.items[key] 66 | 67 | return value, ok 68 | } 69 | 70 | // Iter iterates over the items in a concurrent map 71 | // Each item is sent over a channel, so that 72 | // we can iterate over the map using the builtin range keyword 73 | func (cm *ConcurrentMap) Iter() <-chan ConcurrentMapItem { 74 | c := make(chan ConcurrentMapItem) 75 | 76 | f := func() { 77 | cm.Lock() 78 | defer cm.Unlock() 79 | 80 | for k, v := range cm.items { 81 | c <- ConcurrentMapItem{k, v} 82 | } 83 | close(c) 84 | } 85 | go f() 86 | 87 | return c 88 | } 89 | 90 | // Len is the number of items in the concurrent map. 91 | func (cm *ConcurrentMap) Len() int { 92 | cm.RLock() 93 | defer cm.RUnlock() 94 | 95 | return len(cm.items) 96 | } 97 | -------------------------------------------------------------------------------- /utils/map_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package utils 27 | 28 | import "testing" 29 | 30 | func TestConcurrentMap_Len(t *testing.T) { 31 | cm := NewConcurrentMap() 32 | cm.Set("foo", "FOO") 33 | cm.Set("bar", "BAR") 34 | cm.Set("qux", "QUX") 35 | 36 | if n := cm.Len(); n != 3 { 37 | t.Errorf("the number of items in the concurrent map is not 3 but %v", n) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /utils/slice.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package utils 27 | 28 | import "sync" 29 | 30 | // ConcurrentSlice type that can be safely shared between goroutines 31 | type ConcurrentSlice struct { 32 | sync.RWMutex 33 | items []interface{} 34 | } 35 | 36 | // ConcurrentSliceItem contains the index/value pair of an item in a 37 | // concurrent slice 38 | type ConcurrentSliceItem struct { 39 | Index int 40 | Value interface{} 41 | } 42 | 43 | // NewConcurrentSlice creates a new concurrent slice 44 | func NewConcurrentSlice() *ConcurrentSlice { 45 | cs := &ConcurrentSlice{ 46 | items: make([]interface{}, 0), 47 | } 48 | 49 | return cs 50 | } 51 | 52 | // Append adds an item to the concurrent slice 53 | func (cs *ConcurrentSlice) Append(item interface{}) { 54 | cs.Lock() 55 | defer cs.Unlock() 56 | 57 | cs.items = append(cs.items, item) 58 | } 59 | 60 | // Iter iterates over the items in the concurrent slice 61 | // Each item is sent over a channel, so that 62 | // we can iterate over the slice using the builin range keyword 63 | func (cs *ConcurrentSlice) Iter() <-chan ConcurrentSliceItem { 64 | c := make(chan ConcurrentSliceItem) 65 | 66 | f := func() { 67 | cs.Lock() 68 | defer cs.Unlock() 69 | for index, value := range cs.items { 70 | c <- ConcurrentSliceItem{index, value} 71 | } 72 | close(c) 73 | } 74 | go f() 75 | 76 | return c 77 | } 78 | 79 | // Len is the number of items in the concurrent slice. 80 | func (cs *ConcurrentSlice) Len() int { 81 | cs.RLock() 82 | defer cs.RUnlock() 83 | 84 | return len(cs.items) 85 | } 86 | -------------------------------------------------------------------------------- /utils/slice_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package utils 27 | 28 | import "testing" 29 | 30 | func TestConcurrentSlice_Len(t *testing.T) { 31 | cs := NewConcurrentSlice() 32 | cs.Append("foo") 33 | cs.Append("bar") 34 | cs.Append("qux") 35 | 36 | if n := cs.Len(); n != 3 { 37 | t.Errorf("the number of items in the concurrent slice is not 3 but %v", n) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /utils/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package utils 27 | 28 | import "github.com/pborman/uuid" 29 | 30 | // GenerateUUID generates a new uuid for a minion 31 | func GenerateUUID(name string) uuid.UUID { 32 | u := uuid.NewSHA1(uuid.NameSpace_DNS, []byte(name)) 33 | 34 | return u 35 | } 36 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015-2017 Marin Atanasov Nikolov 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 6 | // are met: 7 | // 8 | // 1. Redistributions of source code must retain the above copyright 9 | // notice, this list of conditions and the following disclaimer 10 | // in this position and unchanged. 11 | // 2. Redistributions in binary form must reproduce the above copyright 12 | // notice, this list of conditions and the following disclaimer in the 13 | // documentation and/or other materials provided with the distribution. 14 | // 15 | // THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR 16 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 17 | // OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 18 | // IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, 19 | // INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 20 | // NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24 | // THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | 26 | package version 27 | 28 | // Version is the version of Gru 29 | const Version = "0.5.0" 30 | --------------------------------------------------------------------------------