├── .gitignore ├── .travis.yml ├── BUILTINS.md ├── LICENSE ├── README.md ├── Rakefile ├── Vagrantfile ├── async.go ├── base ├── modules │ └── gem.yml ├── roles │ └── build_essential │ │ └── tasks │ │ └── main.yml └── site.yml ├── builtin.go ├── builtin_test.go ├── cmd └── tachyon.go ├── command.go ├── config.go ├── download.go ├── environment.go ├── expand.go ├── future.go ├── lisp ├── builtin.go ├── builtin_test.go ├── cons.go ├── cons_test.go ├── evaler.go ├── evaler_test.go ├── proc.go ├── scope.go ├── scope_test.go ├── tokens.go ├── tokens_test.go ├── value.go └── vector.go ├── main.go ├── net ├── net.go └── s3 │ └── s3.go ├── package ├── apt │ ├── apt.go │ └── apt_test.go └── package.go ├── path.go ├── playbook.go ├── playbook_test.go ├── procmgmt ├── procmgmt.go └── upstart │ ├── poststart.sample │ ├── poststop.sample │ ├── prestart.sample │ ├── prestop.sample │ ├── test-daemon.conf.sample │ ├── upstart.go │ └── upstart_test.go ├── release └── upload.yml ├── reporter.go ├── runner.go ├── scope.go ├── scripts ├── detect.sh └── install.sh ├── ssh.go ├── tachyon.go ├── task.go ├── test.go ├── test ├── common_vars.yml ├── default_os.yml ├── download.yml ├── future.yml ├── future2.yml ├── inc_child.yml ├── inc_child2.yml ├── inc_parent.yml ├── inc_parent2.yml ├── incplaybook.yml ├── items.yml ├── on_vagrant.yml ├── on_vagrant2.yml ├── on_vagrant3.yml ├── playbook1.yml ├── register.yml ├── roles │ ├── role1 │ │ ├── handlers │ │ │ └── main.yml │ │ └── tasks │ │ │ └── main.yml │ ├── role2 │ │ └── vars │ │ │ └── main.yml │ ├── role3 │ │ └── tasks │ │ │ ├── get.yml │ │ │ └── main.yml │ ├── role4 │ │ └── tasks │ │ │ ├── main.yml │ │ │ └── special.yml │ ├── role6 │ │ ├── files │ │ │ └── my_script.sh │ │ └── tasks │ │ │ └── main.yml │ ├── role7 │ │ └── meta │ │ │ └── main.yml │ └── role8 │ │ └── modules │ │ └── test.yml ├── sample ├── site1.yml ├── site10.yml ├── site2.yml ├── site3.yml ├── site4.yml ├── site5.yml ├── site6.yml ├── site7.yml ├── site8.yml ├── site9.yml ├── test_script.sh ├── vagrant.yml ├── vagrant2.yml └── vagrant3.yml ├── upstart ├── config.go ├── config_test.go ├── upstart.go └── upstart_test.go ├── util.go ├── vagrant-tachyon ├── roles │ ├── build_essential │ │ └── tasks │ │ │ └── main.yml │ └── golang │ │ ├── files │ │ └── go.sh │ │ └── tasks │ │ └── main.yml └── site.yml └── vars.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | .vagrant/ 3 | tachyon 4 | tachyon-linux-amd64 5 | scratch/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: go 3 | install: 4 | - sudo apt-get update 5 | - sudo apt-get install at # for testing upstart 6 | - mkdir ~/go 7 | - go get -d -v ./... && go build -v ./... 8 | go: 9 | - 1.4 10 | script: sudo GOPATH=~/gopath PATH=$PATH `which go` test -v ./... 11 | -------------------------------------------------------------------------------- /BUILTINS.md: -------------------------------------------------------------------------------- 1 | ### Modules that should be builtin 2 | 3 | * download 4 | * untar 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Evan Phoenix 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## tachyon 2 | 3 | [![Build Status](https://travis-ci.org/vektra/tachyon.svg?branch=master)](https://travis-ci.org/vektra/tachyon) 4 | 5 | Tachyon is an experimental configuration management tool inspired by ansible implemented in golang. 6 | 7 | #### Ok.. why? 8 | 9 | I find the best way to learn something is to try to implement it. 10 | I'm curious about ansible's model for configuration management and 11 | as a fun weekend project began I this project. 12 | 13 | #### Is this usable? 14 | 15 | If you need to run some yaml that executes commands via shell/command, sure! 16 | Otherwise no. I'll probably continue to play with it, adding more functionality 17 | and fleshing out some ideas I've got. 18 | 19 | #### Oohh what ideas? 20 | 21 | * Exploit golang's single binary module to bootstrap machines and run plays remotely. 22 | * Use golang's concurrency to make management of large scale changes easy. 23 | * Use github.com/evanphx/ssh to do integrated ssh 24 | * Allow creation of modules via templated tasks 25 | 26 | #### Is that a lisp directory I see? 27 | 28 | It is! ansible uses python as it's implementation lang and thus also uses it as 29 | it's runtime eval language. Obviously I can't do that and I don't wish to runtime 30 | eval any golang code. Thus I have opted to embed a simple lisp intepreter 31 | (taken and modified from [https://github.com/janne/go-lisp](https://github.com/janne/go-lisp)) 32 | to run code. For instance: 33 | 34 | ```yaml 35 | name: Tell everyone things are great 36 | action: shell echo wooooo! 37 | when: $(== everything "awesome") 38 | ``` 39 | 40 | #### What should I do with this? 41 | 42 | Whatever you want. Play around, tell me what you think about it. Send PRs for crazy ass 43 | features! 44 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | 2 | flags = "" 3 | 4 | namespace :build do 5 | task :deps do 6 | sh "go get ./..." 7 | end 8 | 9 | task :host do 10 | sh "go build #{flags} cmd/tachyon.go" 11 | end 12 | 13 | task :linux do 14 | sh "sh -c 'GOOS=linux GOARCH=amd64 go build #{flags} -o tachyon-linux-amd64 cmd/tachyon.go'" 15 | end 16 | 17 | task :nightly do 18 | flags = %Q!-ldflags "-X main.Release nightly"! 19 | end 20 | 21 | task :all => [:host, :linux] 22 | end 23 | 24 | namespace :test do 25 | task :normal do 26 | sh "go test -v" 27 | end 28 | 29 | task :package do 30 | sh "sudo GOPATH=#{ENV['GOPATH']} /usr/bin/env go test ./package/apt -v" 31 | end 32 | end 33 | 34 | task :test => ["build:deps", "test:normal", "test:package"] 35 | 36 | task :default => :test 37 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # Vagrantfile API/syntax version. Don't touch unless you know what you're doing! 5 | VAGRANTFILE_API_VERSION = "2" 6 | 7 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 8 | # All Vagrant configuration is done here. The most common configuration 9 | # options are documented and commented below. For a complete reference, 10 | # please see the online documentation at vagrantup.com. 11 | 12 | # Every Vagrant virtual environment requires a box to build off of. 13 | config.vm.box = "modern-precise" 14 | 15 | # The url from where the 'config.vm.box' box will be fetched if it 16 | # doesn't already exist on the user's system. 17 | config.vm.box_url = "https://www.dropbox.com/s/qp0qkqsddlzql0t/modern-precise.box" 18 | 19 | # Create a forwarded port mapping which allows access to a specific port 20 | # within the machine from a port on the host machine. In the example below, 21 | # accessing "localhost:8080" will access port 80 on the guest machine. 22 | # config.vm.network :forwarded_port, guest: 80, host: 8080 23 | 24 | # Create a private network, which allows host-only access to the machine 25 | # using a specific IP. 26 | # config.vm.network :private_network, ip: "192.168.33.10" 27 | 28 | # Create a public network, which generally matched to bridged network. 29 | # Bridged networks make the machine appear as another physical device on 30 | # your network. 31 | # config.vm.network :public_network 32 | 33 | # If true, then any SSH connections made will enable agent forwarding. 34 | # Default value: false 35 | # config.ssh.forward_agent = true 36 | 37 | # Share an additional folder to the guest VM. The first argument is 38 | # the path on the host to the actual folder. The second argument is 39 | # the path on the guest to mount the folder. And the optional third 40 | # argument is a set of non-required options. 41 | # config.vm.synced_folder "../data", "/vagrant_data" 42 | 43 | # Provider-specific configuration so you can fine-tune various 44 | # backing providers for Vagrant. These expose provider-specific options. 45 | # Example for VirtualBox: 46 | # 47 | # config.vm.provider :virtualbox do |vb| 48 | # # Don't boot with headless mode 49 | # vb.gui = true 50 | # 51 | # # Use VBoxManage to customize the VM. For example to change memory: 52 | # vb.customize ["modifyvm", :id, "--memory", "1024"] 53 | # end 54 | # 55 | # View the documentation for the provider you're using for more 56 | # information on available options. 57 | 58 | config.vm.provision :tachyon do |t| 59 | t.tachyon_path = "vagrant-tachyon" 60 | end 61 | 62 | # Enable provisioning with Puppet stand alone. Puppet manifests 63 | # are contained in a directory path relative to this Vagrantfile. 64 | # You will need to create the manifests directory and a manifest in 65 | # the file base.pp in the manifests_path directory. 66 | # 67 | # An example Puppet manifest to provision the message of the day: 68 | # 69 | # # group { "puppet": 70 | # # ensure => "present", 71 | # # } 72 | # # 73 | # # File { owner => 0, group => 0, mode => 0644 } 74 | # # 75 | # # file { '/etc/motd': 76 | # # content => "Welcome to your Vagrant-built virtual machine! 77 | # # Managed by Puppet.\n" 78 | # # } 79 | # 80 | # config.vm.provision :puppet do |puppet| 81 | # puppet.manifests_path = "manifests" 82 | # puppet.manifest_file = "site.pp" 83 | # end 84 | 85 | # Enable provisioning with chef solo, specifying a cookbooks path, roles 86 | # path, and data_bags path (all relative to this Vagrantfile), and adding 87 | # some recipes and/or roles. 88 | # 89 | # config.vm.provision :chef_solo do |chef| 90 | # chef.cookbooks_path = "../my-recipes/cookbooks" 91 | # chef.roles_path = "../my-recipes/roles" 92 | # chef.data_bags_path = "../my-recipes/data_bags" 93 | # chef.add_recipe "mysql" 94 | # chef.add_role "web" 95 | # 96 | # # You may also specify custom JSON attributes: 97 | # chef.json = { :mysql_password => "foo" } 98 | # end 99 | 100 | # Enable provisioning with chef server, specifying the chef server URL, 101 | # and the path to the validation key (relative to this Vagrantfile). 102 | # 103 | # The Opscode Platform uses HTTPS. Substitute your organization for 104 | # ORGNAME in the URL and validation key. 105 | # 106 | # If you have your own Chef Server, use the appropriate URL, which may be 107 | # HTTP instead of HTTPS depending on your configuration. Also change the 108 | # validation key to validation.pem. 109 | # 110 | # config.vm.provision :chef_client do |chef| 111 | # chef.chef_server_url = "https://api.opscode.com/organizations/ORGNAME" 112 | # chef.validation_key_path = "ORGNAME-validator.pem" 113 | # end 114 | # 115 | # If you're using the Opscode platform, your validator client is 116 | # ORGNAME-validator, replacing ORGNAME with your organization name. 117 | # 118 | # If you have your own Chef Server, the default validation client name is 119 | # chef-validator, unless you changed the configuration. 120 | # 121 | # chef.validation_client_name = "ORGNAME-validator" 122 | end 123 | -------------------------------------------------------------------------------- /async.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | type AsyncAction struct { 4 | Task *Task 5 | Error error 6 | Result *Result 7 | status chan *AsyncAction 8 | } 9 | 10 | func (a *AsyncAction) Init(r *Runner) { 11 | r.wait.Add(1) 12 | a.status = r.AsyncChannel() 13 | } 14 | 15 | func (a *AsyncAction) Finish(res *Result, err error) { 16 | a.Error = err 17 | a.Result = res 18 | a.status <- a 19 | } 20 | 21 | func (r *Runner) handleAsync() { 22 | for { 23 | act := <-r.async 24 | 25 | r.env.report.FinishAsyncTask(act) 26 | 27 | if act.Error == nil { 28 | for _, x := range act.Task.Notify() { 29 | r.AddNotify(x) 30 | } 31 | } 32 | 33 | r.wait.Done() 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /base/modules/gem.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: gem 3 | tasks: 4 | - name: Check for installation 5 | shell: 6 | command: gem list $name | grep $name 7 | ignore_failure: true 8 | register: check 9 | 10 | - name: Install gem 11 | shell: gem install --no-ri --no-rdoc $name 12 | when: $(== check.rc 1) 13 | -------------------------------------------------------------------------------- /base/roles/build_essential/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update apt-get cache 3 | apt: update_cache=yes cache_time=60m 4 | 5 | - name: Install build packages 6 | apt: pkg=$item state=present 7 | with_items: 8 | - autoconf 9 | - binutils-doc 10 | - bison 11 | - build-essential 12 | - flex 13 | -------------------------------------------------------------------------------- /base/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - build_essential 5 | -------------------------------------------------------------------------------- /builtin.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "crypto/md5" 7 | "encoding/hex" 8 | "fmt" 9 | "github.com/flynn/go-shlex" 10 | "io" 11 | "os" 12 | "os/exec" 13 | "path/filepath" 14 | "strings" 15 | "sync" 16 | "syscall" 17 | ) 18 | 19 | func captureCmd(c *exec.Cmd, show bool) ([]byte, []byte, error) { 20 | stdout, err := c.StdoutPipe() 21 | 22 | if err != nil { 23 | return nil, nil, err 24 | } 25 | 26 | defer stdout.Close() 27 | 28 | var wg sync.WaitGroup 29 | 30 | var bout bytes.Buffer 31 | var berr bytes.Buffer 32 | 33 | prefix := []byte(`| `) 34 | 35 | wg.Add(1) 36 | go func() { 37 | defer wg.Done() 38 | buf := bufio.NewReader(stdout) 39 | 40 | for { 41 | line, err := buf.ReadSlice('\n') 42 | 43 | if err != nil { 44 | break 45 | } 46 | 47 | bout.Write(line) 48 | 49 | if show { 50 | os.Stdout.Write(prefix) 51 | os.Stdout.Write(line) 52 | } 53 | } 54 | }() 55 | 56 | stderr, err := c.StderrPipe() 57 | 58 | if err != nil { 59 | stdout.Close() 60 | return nil, nil, err 61 | } 62 | 63 | defer stderr.Close() 64 | 65 | wg.Add(1) 66 | go func() { 67 | defer wg.Done() 68 | buf := bufio.NewReader(stderr) 69 | 70 | for { 71 | line, err := buf.ReadSlice('\n') 72 | 73 | if err != nil { 74 | break 75 | } 76 | 77 | berr.Write(line) 78 | 79 | if show { 80 | os.Stdout.Write(prefix) 81 | os.Stdout.Write(line) 82 | } 83 | } 84 | }() 85 | 86 | c.Start() 87 | 88 | wg.Wait() 89 | 90 | err = c.Wait() 91 | 92 | return bout.Bytes(), berr.Bytes(), err 93 | } 94 | 95 | type CommandResult struct { 96 | ReturnCode int 97 | Stdout []byte 98 | Stderr []byte 99 | } 100 | 101 | func RunCommand(env *CommandEnv, parts ...string) (*CommandResult, error) { 102 | c := exec.Command(parts[0], parts[1:]...) 103 | 104 | if env.Env.config.ShowCommandOutput { 105 | fmt.Printf("RUN: %s\n", strings.Join(parts, " ")) 106 | } 107 | 108 | rc := 0 109 | 110 | stdout, stderr, err := captureCmd(c, env.Env.config.ShowCommandOutput) 111 | if err != nil { 112 | if _, ok := err.(*exec.ExitError); ok { 113 | rc = 1 114 | } else { 115 | return nil, err 116 | } 117 | } 118 | 119 | return &CommandResult{rc, stdout, stderr}, nil 120 | } 121 | 122 | func RunCommandInEnv(env *CommandEnv, unixEnv []string, parts ...string) (*CommandResult, error) { 123 | c := exec.Command(parts[0], parts[1:]...) 124 | c.Env = unixEnv 125 | 126 | if env.Env.config.ShowCommandOutput { 127 | fmt.Printf("RUN: %s\n", strings.Join(parts, " ")) 128 | } 129 | 130 | rc := 0 131 | 132 | stdout, stderr, err := captureCmd(c, env.Env.config.ShowCommandOutput) 133 | if err != nil { 134 | if _, ok := err.(*exec.ExitError); ok { 135 | rc = 1 136 | } else { 137 | return nil, err 138 | } 139 | } 140 | 141 | return &CommandResult{rc, stdout, stderr}, nil 142 | } 143 | 144 | func runCmd(env *CommandEnv, ignore bool, parts ...string) (*Result, error) { 145 | cmd, err := RunCommand(env, parts...) 146 | if !ignore && err != nil { 147 | return nil, err 148 | } 149 | 150 | r := NewResult(true) 151 | 152 | r.Add("rc", cmd.ReturnCode) 153 | r.Add("stdout", strings.TrimSpace(string(cmd.Stdout))) 154 | r.Add("stderr", strings.TrimSpace(string(cmd.Stderr))) 155 | 156 | if str, ok := renderShellResult(r); ok { 157 | r.Add("_result", str) 158 | } 159 | 160 | return r, nil 161 | } 162 | 163 | type CommandCmd struct { 164 | Command string `tachyon:"command,required"` 165 | Creates string `tachyon:"creates"` 166 | IgnoreFail bool `tachyon:"ignore_failure"` 167 | } 168 | 169 | func (cmd *CommandCmd) Run(env *CommandEnv) (*Result, error) { 170 | if cmd.Creates != "" { 171 | if _, err := os.Stat(cmd.Creates); err == nil { 172 | r := NewResult(false) 173 | r.Add("rc", 0) 174 | r.Add("exists", cmd.Creates) 175 | 176 | return r, nil 177 | } 178 | } 179 | 180 | parts, err := shlex.Split(cmd.Command) 181 | 182 | if err != nil { 183 | return nil, err 184 | } 185 | 186 | return runCmd(env, cmd.IgnoreFail, parts...) 187 | } 188 | 189 | func (cmd *CommandCmd) ParseArgs(s Scope, args string) (Vars, error) { 190 | if args == "" { 191 | return Vars{}, nil 192 | } 193 | 194 | return Vars{"command": Any(args)}, nil 195 | } 196 | 197 | type ShellCmd struct { 198 | Command string `tachyon:"command,required"` 199 | Creates string `tachyon:"creates"` 200 | IgnoreFail bool `tachyon:"ignore_failure"` 201 | } 202 | 203 | func (cmd *ShellCmd) Run(env *CommandEnv) (*Result, error) { 204 | if cmd.Creates != "" { 205 | if _, err := os.Stat(cmd.Creates); err == nil { 206 | r := NewResult(false) 207 | r.Add("rc", 0) 208 | r.Add("exists", cmd.Creates) 209 | 210 | return r, nil 211 | } 212 | } 213 | 214 | return runCmd(env, cmd.IgnoreFail, "sh", "-c", cmd.Command) 215 | } 216 | 217 | func (cmd *ShellCmd) ParseArgs(s Scope, args string) (Vars, error) { 218 | if args == "" { 219 | return Vars{}, nil 220 | } 221 | 222 | return Vars{"command": Any(args)}, nil 223 | } 224 | 225 | func renderShellResult(res *Result) (string, bool) { 226 | rcv, ok := res.Get("rc") 227 | if !ok { 228 | return "", false 229 | } 230 | 231 | stdoutv, ok := res.Get("stdout") 232 | if !ok { 233 | return "", false 234 | } 235 | 236 | stderrv, ok := res.Get("stderr") 237 | if !ok { 238 | return "", false 239 | } 240 | 241 | rc := rcv.Read().(int) 242 | stdout := stdoutv.Read().(string) 243 | stderr := stderrv.Read().(string) 244 | 245 | if rc == 0 && len(stdout) == 0 && len(stderr) == 0 { 246 | return "", true 247 | } else if len(stderr) == 0 && len(stdout) < 60 { 248 | stdout = strings.Replace(stdout, "\n", " ", -1) 249 | return fmt.Sprintf(`rc: %d, stdout: "%s"`, rc, stdout), true 250 | } 251 | 252 | return "", false 253 | } 254 | 255 | type CopyCmd struct { 256 | Src string `tachyon:"src,required"` 257 | Dest string `tachyon:"dest,required"` 258 | } 259 | 260 | func md5file(path string) ([]byte, error) { 261 | h := md5.New() 262 | 263 | i, err := os.Open(path) 264 | if err != nil { 265 | return nil, err 266 | } 267 | 268 | if _, err := io.Copy(h, i); err != nil { 269 | return nil, err 270 | } 271 | 272 | return h.Sum(nil), nil 273 | } 274 | 275 | func (cmd *CopyCmd) Run(env *CommandEnv) (*Result, error) { 276 | var src string 277 | 278 | if cmd.Src[0] == '/' { 279 | src = cmd.Src 280 | } else { 281 | src = env.Paths.File(cmd.Src) 282 | } 283 | 284 | input, err := os.Open(src) 285 | 286 | if err != nil { 287 | return nil, err 288 | } 289 | 290 | srcStat, err := os.Stat(src) 291 | if err != nil { 292 | return nil, err 293 | } 294 | 295 | srcDigest, err := md5file(src) 296 | if err != nil { 297 | return nil, err 298 | } 299 | 300 | var dstDigest []byte 301 | 302 | defer input.Close() 303 | 304 | dest := cmd.Dest 305 | 306 | link := false 307 | 308 | destStat, err := os.Lstat(dest) 309 | if err == nil { 310 | if destStat.IsDir() { 311 | dest = filepath.Join(dest, filepath.Base(src)) 312 | } else { 313 | dstDigest, _ = md5file(dest) 314 | } 315 | 316 | link = destStat.Mode()&os.ModeSymlink != 0 317 | } 318 | 319 | rd := ResultData{ 320 | "md5sum": Any(hex.EncodeToString(srcDigest)), 321 | "src": Any(src), 322 | "dest": Any(dest), 323 | } 324 | 325 | if dstDigest != nil && bytes.Equal(srcDigest, dstDigest) { 326 | changed := false 327 | 328 | if destStat.Mode() != srcStat.Mode() { 329 | changed = true 330 | if err := os.Chmod(dest, srcStat.Mode()); err != nil { 331 | return nil, err 332 | } 333 | } 334 | 335 | if ostat, ok := srcStat.Sys().(*syscall.Stat_t); ok { 336 | if estat, ok := destStat.Sys().(*syscall.Stat_t); ok { 337 | if ostat.Uid != estat.Uid || ostat.Gid != estat.Gid { 338 | changed = true 339 | os.Chown(dest, int(ostat.Uid), int(ostat.Gid)) 340 | } 341 | } 342 | } 343 | 344 | return WrapResult(changed, rd), nil 345 | } 346 | 347 | tmp := fmt.Sprintf("%s.tmp.%d", cmd.Dest, os.Getpid()) 348 | 349 | output, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY, 0644) 350 | 351 | if err != nil { 352 | return nil, err 353 | } 354 | 355 | defer output.Close() 356 | 357 | if _, err = io.Copy(output, input); err != nil { 358 | os.Remove(tmp) 359 | return nil, err 360 | } 361 | 362 | if link { 363 | os.Remove(dest) 364 | } 365 | 366 | if err := os.Chmod(tmp, srcStat.Mode()); err != nil { 367 | os.Remove(tmp) 368 | return nil, err 369 | } 370 | 371 | if ostat, ok := srcStat.Sys().(*syscall.Stat_t); ok { 372 | os.Chown(tmp, int(ostat.Uid), int(ostat.Gid)) 373 | } 374 | 375 | err = os.Rename(tmp, dest) 376 | if err != nil { 377 | os.Remove(tmp) 378 | return nil, err 379 | } 380 | 381 | return WrapResult(true, rd), nil 382 | } 383 | 384 | type ScriptCmd struct { 385 | Script string `tachyon:"command,required"` 386 | Creates string `tachyon:"creates"` 387 | IgnoreFail bool `tachyon:"ignore_failure"` 388 | } 389 | 390 | func (cmd *ScriptCmd) ParseArgs(s Scope, args string) (Vars, error) { 391 | if args == "" { 392 | return Vars{}, nil 393 | } 394 | 395 | return Vars{"command": Any(args)}, nil 396 | } 397 | 398 | func (cmd *ScriptCmd) Run(env *CommandEnv) (*Result, error) { 399 | if cmd.Creates != "" { 400 | if _, err := os.Stat(cmd.Creates); err == nil { 401 | r := NewResult(false) 402 | r.Add("rc", 0) 403 | r.Add("exists", cmd.Creates) 404 | 405 | return r, nil 406 | } 407 | } 408 | 409 | script := cmd.Script 410 | 411 | parts, err := shlex.Split(cmd.Script) 412 | if err == nil { 413 | script = parts[0] 414 | } 415 | 416 | path := env.Paths.File(script) 417 | 418 | _, err = os.Stat(path) 419 | if err != nil { 420 | return nil, err 421 | } 422 | 423 | runArgs := append([]string{"sh", path}, parts[1:]...) 424 | 425 | return runCmd(env, cmd.IgnoreFail, runArgs...) 426 | } 427 | 428 | func init() { 429 | RegisterCommand("command", &CommandCmd{}) 430 | RegisterCommand("shell", &ShellCmd{}) 431 | RegisterCommand("copy", &CopyCmd{}) 432 | RegisterCommand("script", &ScriptCmd{}) 433 | } 434 | -------------------------------------------------------------------------------- /builtin_test.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | ) 11 | 12 | func inTmp(blk func()) { 13 | dir, err := os.Getwd() 14 | if err != nil { 15 | panic(err) 16 | } 17 | 18 | tmpDir := filepath.Join("test", fmt.Sprintf("builtin-test-%d", os.Getpid())) 19 | os.Mkdir(tmpDir, 0755) 20 | os.Chdir(tmpDir) 21 | 22 | defer os.RemoveAll(tmpDir) 23 | defer os.Chdir(dir) 24 | 25 | blk() 26 | } 27 | 28 | var testData = []byte("test") 29 | var testData2 = []byte("foobar") 30 | 31 | func TestCopySimple(t *testing.T) { 32 | inTmp(func() { 33 | ioutil.WriteFile("a.txt", testData, 0644) 34 | 35 | res, err := RunAdhocTask("copy", "src=a.txt dest=b.txt") 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | if !res.Changed { 41 | t.Errorf("The copy didn't change anything") 42 | } 43 | 44 | data, err := ioutil.ReadFile("b.txt") 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | if !bytes.Equal(testData, data) { 50 | t.Errorf("The copy didn't move the righte bytes") 51 | } 52 | }) 53 | } 54 | 55 | func TestCopyFailsOnMissingSrc(t *testing.T) { 56 | inTmp(func() { 57 | _, err := RunAdhocTask("copy", "src=a.txt dest=b.txt") 58 | if err == nil { 59 | t.Errorf("Copy did not fail") 60 | } 61 | }) 62 | } 63 | 64 | func TestCopyShowsNoChangeWhenFilesTheSame(t *testing.T) { 65 | inTmp(func() { 66 | ioutil.WriteFile("a.txt", testData, 0644) 67 | ioutil.WriteFile("b.txt", testData, 0644) 68 | 69 | res, err := RunAdhocTask("copy", "src=a.txt dest=b.txt") 70 | if err != nil { 71 | panic(err) 72 | } 73 | 74 | if res.Changed { 75 | t.Errorf("The copy changed something incorrectly") 76 | } 77 | 78 | if res.Data["md5sum"].Read().(string) == "" { 79 | t.Errorf("md5sum not returned") 80 | } 81 | }) 82 | } 83 | 84 | func TestCopyMakesFileInDir(t *testing.T) { 85 | inTmp(func() { 86 | ioutil.WriteFile("a.txt", testData, 0644) 87 | os.Mkdir("b", 0755) 88 | 89 | res, err := RunAdhocTask("copy", "src=a.txt dest=b") 90 | if err != nil { 91 | panic(err) 92 | } 93 | 94 | if !res.Changed { 95 | t.Errorf("The copy didn't change anything") 96 | } 97 | 98 | data, err := ioutil.ReadFile("b/a.txt") 99 | if err != nil { 100 | panic(err) 101 | } 102 | 103 | if !bytes.Equal(testData, data) { 104 | t.Errorf("The copy didn't move the righte bytes") 105 | } 106 | }) 107 | } 108 | 109 | func TestCopyRemovesALink(t *testing.T) { 110 | inTmp(func() { 111 | ioutil.WriteFile("a.txt", testData, 0644) 112 | ioutil.WriteFile("c.txt", testData2, 0644) 113 | 114 | os.Symlink("c.txt", "b.txt") 115 | 116 | res, err := RunAdhocTask("copy", "src=a.txt dest=b.txt") 117 | if err != nil { 118 | panic(err) 119 | } 120 | 121 | if !res.Changed { 122 | t.Errorf("The copy didn't change anything") 123 | } 124 | 125 | stat, err := os.Stat("b.txt") 126 | 127 | if !stat.Mode().IsRegular() { 128 | t.Errorf("copy didn't remove the link") 129 | } 130 | 131 | data, err := ioutil.ReadFile("b.txt") 132 | if err != nil { 133 | panic(err) 134 | } 135 | 136 | if !bytes.Equal(testData, data) { 137 | t.Errorf("The copy didn't move the righte bytes") 138 | } 139 | 140 | data, err = ioutil.ReadFile("c.txt") 141 | if err != nil { 142 | panic(err) 143 | } 144 | 145 | if !bytes.Equal(testData2, data) { 146 | t.Errorf("c.txt was overriden improperly") 147 | } 148 | }) 149 | } 150 | 151 | func TestCopyPreservesMode(t *testing.T) { 152 | inTmp(func() { 153 | ioutil.WriteFile("a.txt", testData, 0755) 154 | 155 | _, err := RunAdhocTask("copy", "src=a.txt dest=b.txt") 156 | if err != nil { 157 | panic(err) 158 | } 159 | 160 | stat, err := os.Stat("b.txt") 161 | if err != nil { 162 | panic(err) 163 | } 164 | 165 | if stat.Mode().Perm() != 0755 { 166 | t.Errorf("Copy didn't preserve the perms") 167 | } 168 | }) 169 | } 170 | 171 | func TestCommand(t *testing.T) { 172 | res, err := RunAdhocTask("command", "date") 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | if !res.Changed { 178 | t.Errorf("changed not properly set") 179 | } 180 | 181 | if res.Data["rc"].Read().(int) != 0 { 182 | t.Errorf("return code not captured") 183 | } 184 | 185 | if res.Data["stdout"].Read().(string) == "" { 186 | t.Errorf("stdout was not captured: '%s'", res.Data["stdout"].Read()) 187 | } 188 | } 189 | 190 | func TestShell(t *testing.T) { 191 | res, err := RunAdhocTask("shell", "echo \"hello dear\"") 192 | if err != nil { 193 | panic(err) 194 | } 195 | 196 | if res.Data["rc"].Read().(int) != 0 { 197 | t.Errorf("return code not captured") 198 | } 199 | 200 | if res.Data["stdout"].Read().(string) != "hello dear" { 201 | t.Errorf("stdout was not captured: '%s'", res.Data["stdout"].Read()) 202 | } 203 | } 204 | 205 | func TestShellSeesNonZeroRC(t *testing.T) { 206 | res, err := RunAdhocTask("shell", "exit 1") 207 | if err != nil { 208 | panic(err) 209 | } 210 | 211 | if res.Data["rc"].Read().(int) != 1 { 212 | t.Errorf("return code not captured") 213 | } 214 | 215 | if res.Data["stdout"].Read().(string) != "" { 216 | t.Errorf("stdout was not captured") 217 | } 218 | } 219 | 220 | func TestScriptExecutesRelative(t *testing.T) { 221 | res, err := RunAdhocTask("script", "test/test_script.sh") 222 | if err != nil { 223 | panic(err) 224 | } 225 | 226 | if res.Data["rc"].Read().(int) != 0 { 227 | t.Errorf("return code not captured") 228 | } 229 | 230 | if res.Data["stdout"].Read().(string) != "hello script" { 231 | t.Errorf("stdout was not captured") 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /cmd/tachyon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/vektra/tachyon" 5 | _ "github.com/vektra/tachyon/net" 6 | _ "github.com/vektra/tachyon/package" 7 | _ "github.com/vektra/tachyon/procmgmt" 8 | "os" 9 | ) 10 | 11 | var Release string 12 | 13 | func main() { 14 | if Release != "" { 15 | tachyon.Release = Release 16 | } 17 | 18 | os.Exit(tachyon.Main(os.Args)) 19 | } 20 | -------------------------------------------------------------------------------- /command.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "reflect" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | type ResultData map[string]Value 12 | 13 | func (rd ResultData) Set(key string, v interface{}) { 14 | rd[key] = Any(v) 15 | } 16 | 17 | func (rd ResultData) Get(key string) interface{} { 18 | if a, ok := rd[key]; !ok { 19 | return nil 20 | } else { 21 | return a.Read() 22 | } 23 | } 24 | 25 | type Result struct { 26 | Changed bool 27 | Failed bool 28 | Data ResultData 29 | } 30 | 31 | func (r *Result) MarshalJSON() ([]byte, error) { 32 | o := make(map[string]interface{}) 33 | m := make(map[string]interface{}) 34 | 35 | o["changed"] = r.Changed 36 | o["failed"] = r.Failed 37 | o["data"] = m 38 | 39 | for k, v := range r.Data { 40 | m[k] = v.Read() 41 | } 42 | 43 | return json.Marshal(o) 44 | } 45 | 46 | func (r *Result) Get(key string) (Value, bool) { 47 | v, ok := r.Data[key] 48 | 49 | return v, ok 50 | } 51 | 52 | func (r *Result) Add(key string, v interface{}) { 53 | r.Data[key] = Any(v) 54 | } 55 | 56 | func WrapResult(changed bool, data ResultData) *Result { 57 | return &Result{changed, false, data} 58 | } 59 | 60 | func NewResult(changed bool) *Result { 61 | return &Result{changed, false, make(ResultData)} 62 | } 63 | 64 | func FailureResult(err error) *Result { 65 | res := &Result{false, true, make(ResultData)} 66 | res.Add("error", err.Error()) 67 | 68 | return res 69 | } 70 | 71 | type CommandEnv struct { 72 | Env *Environment 73 | Paths Paths 74 | progress ProgressReporter 75 | } 76 | 77 | func NewCommandEnv(env *Environment, task *Task) *CommandEnv { 78 | return &CommandEnv{ 79 | Env: env, 80 | Paths: task.Paths, 81 | progress: env.report, 82 | } 83 | } 84 | 85 | func (e *CommandEnv) Progress(str string) { 86 | if e.progress == nil { 87 | fmt.Printf("=== %s\n", str) 88 | } else { 89 | e.progress.Progress(str) 90 | } 91 | } 92 | 93 | type Command interface { 94 | Run(env *CommandEnv) (*Result, error) 95 | } 96 | 97 | type ArgParser interface { 98 | ParseArgs(s Scope, args string) (Vars, error) 99 | } 100 | 101 | type Commands map[string]reflect.Type 102 | 103 | var AvailableCommands Commands 104 | 105 | var initAvailable sync.Once 106 | 107 | func RegisterCommand(name string, cmd Command) { 108 | initAvailable.Do(func() { 109 | AvailableCommands = make(Commands) 110 | }) 111 | 112 | ref := reflect.ValueOf(cmd) 113 | e := ref.Elem() 114 | 115 | AvailableCommands[name] = e.Type() 116 | } 117 | 118 | func MakeCommand(s Scope, task *Task, args string) (Command, Vars, error) { 119 | name := task.Command() 120 | 121 | t, ok := AvailableCommands[name] 122 | 123 | if !ok { 124 | return nil, nil, fmt.Errorf("Unknown command: %s", name) 125 | } 126 | 127 | obj := reflect.New(t) 128 | 129 | var sm Vars 130 | var err error 131 | 132 | if ap, ok := obj.Interface().(ArgParser); ok { 133 | sm, err = ap.ParseArgs(s, args) 134 | } else { 135 | sm, err = ParseSimpleMap(s, args) 136 | } 137 | 138 | if err != nil { 139 | return nil, nil, err 140 | } 141 | 142 | for ik, iv := range task.Vars { 143 | if str, ok := iv.Read().(string); ok { 144 | exp, err := ExpandVars(s, str) 145 | if err != nil { 146 | return nil, nil, err 147 | } 148 | 149 | sm[ik] = Any(exp) 150 | } else { 151 | sm[ik] = iv 152 | } 153 | } 154 | 155 | e := obj.Elem() 156 | 157 | for i := 0; i < t.NumField(); i++ { 158 | f := t.Field(i) 159 | 160 | name := strings.ToLower(f.Name) 161 | required := false 162 | 163 | parts := strings.Split(f.Tag.Get("tachyon"), ",") 164 | 165 | switch len(parts) { 166 | case 0: 167 | // nothing 168 | case 1: 169 | name = parts[0] 170 | case 2: 171 | name = parts[0] 172 | switch parts[1] { 173 | case "required": 174 | required = true 175 | default: 176 | return nil, nil, fmt.Errorf("Unsupported tag flag: %s", parts[1]) 177 | } 178 | } 179 | 180 | if val, ok := sm[name]; ok { 181 | ef := e.Field(i) 182 | 183 | switch ef.Interface().(type) { 184 | case bool: 185 | ef.Set(reflect.ValueOf(val.Read())) 186 | case map[string]string: 187 | iv := val.Read() 188 | m := make(map[string]string) 189 | 190 | switch iv := iv.(type) { 191 | case map[interface{}]interface{}: 192 | for k, v := range iv { 193 | m[fmt.Sprintf("%v", k)] = fmt.Sprintf("%v", v) 194 | } 195 | case map[string]interface{}: 196 | for k, v := range iv { 197 | m[k] = fmt.Sprintf("%v", v) 198 | } 199 | case map[string]string: 200 | m = iv 201 | } 202 | 203 | ef.Set(reflect.ValueOf(m)) 204 | default: 205 | val := fmt.Sprintf("%v", val.Read()) 206 | enum := f.Tag.Get("enum") 207 | if enum != "" { 208 | found := false 209 | 210 | for _, p := range strings.Split(enum, ",") { 211 | if p == val { 212 | found = true 213 | break 214 | } 215 | } 216 | 217 | if !found { 218 | return nil, nil, fmt.Errorf("Invalid value '%s' for variable '%s'. Possibles: %s", val, name, enum) 219 | } 220 | } 221 | 222 | ef.Set(reflect.ValueOf(val)) 223 | } 224 | } else if required { 225 | return nil, nil, fmt.Errorf("Missing value for %s", f.Name) 226 | } 227 | } 228 | 229 | return obj.Interface().(Command), sm, nil 230 | } 231 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | type Config struct { 4 | ShowCommandOutput bool 5 | } 6 | 7 | var DefaultConfig = &Config{false} 8 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "os" 10 | ) 11 | 12 | type DownloadCmd struct { 13 | Url string `tachyon:"url,required"` 14 | Dest string `tachyon:"dest"` 15 | Sha256sum string `tachyon:"sha256sum"` 16 | Once bool `tachyon:"once"` 17 | } 18 | 19 | func (d *DownloadCmd) Run(env *CommandEnv) (*Result, error) { 20 | destPath := d.Dest 21 | 22 | var out *os.File 23 | var err error 24 | 25 | if destPath == "" { 26 | out, err = env.Env.TempFile("download") 27 | destPath = out.Name() 28 | 29 | if err != nil { 30 | return nil, err 31 | } 32 | } else { 33 | if d.Once { 34 | fi, err := os.Stat(destPath) 35 | if err == nil { 36 | r := NewResult(false) 37 | r.Data.Set("size", fi.Size()) 38 | r.Data.Set("path", destPath) 39 | 40 | return r, nil 41 | } 42 | } 43 | 44 | out, err = os.OpenFile(destPath, os.O_CREATE|os.O_WRONLY, 0644) 45 | if err != nil { 46 | return nil, err 47 | } 48 | } 49 | 50 | defer out.Close() 51 | 52 | resp, err := http.Get(d.Url) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | if resp.StatusCode/100 != 2 { 58 | return nil, fmt.Errorf("Unable to download '%s', code: %d", d.Url, resp.StatusCode) 59 | } 60 | 61 | s := sha256.New() 62 | 63 | tee := io.MultiWriter(out, s) 64 | 65 | n, err := io.Copy(tee, resp.Body) 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | r := NewResult(true) 71 | r.Data.Set("size", n) 72 | r.Data.Set("path", destPath) 73 | r.Data.Set("sha256sum", hex.EncodeToString(s.Sum(nil))) 74 | 75 | return r, nil 76 | } 77 | 78 | func init() { 79 | RegisterCommand("download", &DownloadCmd{}) 80 | } 81 | -------------------------------------------------------------------------------- /environment.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "errors" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | type Environment struct { 10 | Vars Scope 11 | report Reporter 12 | config *Config 13 | tmpDir string 14 | 15 | Paths Paths 16 | } 17 | 18 | func NewEnv(s Scope, cfg *Config) *Environment { 19 | e := new(Environment) 20 | e.report = sCLIReporter 21 | e.Vars = s 22 | e.config = cfg 23 | 24 | d, err := ioutil.TempDir("", "tachyon") 25 | if err == nil { 26 | e.tmpDir = d 27 | } 28 | 29 | e.Paths = SimplePath{"."} 30 | 31 | return e 32 | } 33 | 34 | func (e *Environment) ReportJSON() { 35 | e.report = sJsonChunkReporter 36 | } 37 | 38 | var eNoTmpDir = errors.New("No tempdir available") 39 | 40 | func (e *Environment) TempFile(prefix string) (*os.File, error) { 41 | if e.tmpDir == "" { 42 | return nil, eNoTmpDir 43 | } 44 | 45 | dest, err := ioutil.TempFile(e.tmpDir, prefix) 46 | return dest, err 47 | } 48 | 49 | func (e *Environment) Cleanup() { 50 | os.RemoveAll(e.tmpDir) 51 | } 52 | 53 | func (e *Environment) SetPaths(n Paths) Paths { 54 | cur := e.Paths 55 | e.Paths = n 56 | return cur 57 | } 58 | -------------------------------------------------------------------------------- /expand.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "github.com/vektra/tachyon/lisp" 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | var cTemplateStart = []byte(`{{`) 13 | var cTemplateEnd = []byte(`}}`) 14 | var cExprStart = []byte(`$(`) 15 | var cExprEnd = []byte(`)`) 16 | 17 | var eUnclosedTemplate = errors.New("Unclosed template") 18 | var eUnclosedExpr = errors.New("Unclosed lisp expression") 19 | 20 | func expandTemplates(s Scope, args string) (string, error) { 21 | a := []byte(args) 22 | 23 | var buf bytes.Buffer 24 | 25 | for { 26 | idx := bytes.Index(a, cTemplateStart) 27 | 28 | if idx == -1 { 29 | buf.Write(a) 30 | break 31 | } 32 | 33 | buf.Write(a[:idx]) 34 | 35 | in := a[idx+2:] 36 | 37 | fin := bytes.Index(in, cTemplateEnd) 38 | 39 | if fin == -1 { 40 | return "", eUnclosedTemplate 41 | } 42 | 43 | name := bytes.TrimSpace(in[:fin]) 44 | 45 | parts := strings.Split(string(name), ".") 46 | 47 | var ( 48 | val Value 49 | ok bool 50 | ) 51 | 52 | if len(parts) == 1 { 53 | val, ok = s.Get(string(name)) 54 | } else { 55 | cur := parts[0] 56 | 57 | val, ok = s.Get(cur) 58 | 59 | for _, sub := range parts[1:] { 60 | m, ok := val.(Map) 61 | if !ok { 62 | m, ok = val.Read().(Map) 63 | if !ok { 64 | return "", fmt.Errorf("Variable '%s' is not a Map (%T)", cur, val.Read()) 65 | } 66 | } 67 | 68 | val, ok = m.Get(sub) 69 | if !ok { 70 | return "", fmt.Errorf("Variable '%s' has no key '%s'", cur, sub) 71 | } 72 | cur = sub 73 | } 74 | } 75 | 76 | if ok { 77 | switch val := val.Read().(type) { 78 | case int64, int: 79 | buf.WriteString(fmt.Sprintf("%d", val)) 80 | default: 81 | buf.WriteString(fmt.Sprintf("%s", val)) 82 | } 83 | 84 | a = in[fin+2:] 85 | } else { 86 | return "", fmt.Errorf("Undefined variable: %s", string(name)) 87 | } 88 | } 89 | 90 | return buf.String(), nil 91 | } 92 | 93 | func findExprClose(buf []byte) int { 94 | opens := 0 95 | 96 | for idx, r := range buf { 97 | switch r { 98 | case ')': 99 | opens-- 100 | 101 | if opens == 0 { 102 | return idx 103 | } 104 | 105 | case '(': 106 | opens++ 107 | } 108 | } 109 | 110 | return -1 111 | } 112 | 113 | func varChar(r rune) bool { 114 | if unicode.IsLetter(r) { 115 | return true 116 | } 117 | if unicode.IsDigit(r) { 118 | return true 119 | } 120 | if r == '_' { 121 | return true 122 | } 123 | return false 124 | } 125 | 126 | func inferValue(val Value) lisp.Value { 127 | switch lv := val.Read().(type) { 128 | case int: 129 | return lisp.NumberValue(int64(lv)) 130 | case int32: 131 | return lisp.NumberValue(int64(lv)) 132 | case int64: 133 | return lisp.NumberValue(lv) 134 | case string: 135 | return lisp.StringValue(lv) 136 | case *Result: 137 | return lisp.MapValue(&lispResult{lv}) 138 | default: 139 | } 140 | 141 | return lisp.StringValue(fmt.Sprintf("%s", val.Read())) 142 | } 143 | 144 | type lispResult struct { 145 | res *Result 146 | } 147 | 148 | func (lr *lispResult) Get(key string) (lisp.Value, bool) { 149 | v, ok := lr.res.Get(key) 150 | 151 | if !ok { 152 | return lisp.Nil, false 153 | } 154 | 155 | return inferValue(v), true 156 | } 157 | 158 | type lispInferredScope struct { 159 | Scope Scope 160 | } 161 | 162 | func (s lispInferredScope) Get(key string) (lisp.Value, bool) { 163 | val, ok := s.Scope.Get(key) 164 | 165 | if !ok { 166 | return lisp.Nil, false 167 | } 168 | 169 | return inferValue(val), true 170 | } 171 | 172 | func (s lispInferredScope) Set(key string, v lisp.Value) lisp.Value { 173 | s.Scope.Set(key, v.Interface()) 174 | return v 175 | } 176 | 177 | func (s lispInferredScope) Create(key string, v lisp.Value) lisp.Value { 178 | s.Scope.Set(key, v.Interface()) 179 | return v 180 | } 181 | 182 | var cDollar = []byte(`$`) 183 | 184 | func ExpandVars(s Scope, args string) (string, error) { 185 | args, err := expandTemplates(s, args) 186 | 187 | if err != nil { 188 | return "", err 189 | } 190 | 191 | a := []byte(args) 192 | 193 | var buf bytes.Buffer 194 | 195 | for { 196 | idx := bytes.Index(a, cDollar) 197 | 198 | if idx == -1 { 199 | buf.Write(a) 200 | break 201 | } else if a[idx+1] == '(' { 202 | buf.Write(a[:idx]) 203 | 204 | in := a[idx+1:] 205 | 206 | fin := findExprClose(in) 207 | 208 | if fin == -1 { 209 | return "", eUnclosedExpr 210 | } 211 | 212 | sexp := in[:fin+1] 213 | 214 | ls := lispInferredScope{s} 215 | 216 | val, err := lisp.EvalString(string(sexp), ls) 217 | 218 | if err != nil { 219 | return "", err 220 | } 221 | 222 | buf.WriteString(val.String()) 223 | a = in[fin+1:] 224 | } else { 225 | buf.Write(a[:idx]) 226 | 227 | in := a[idx+1:] 228 | 229 | fin := 0 230 | 231 | for fin < len(in) { 232 | if !varChar(rune(in[fin])) { 233 | break 234 | } 235 | fin++ 236 | } 237 | 238 | if val, ok := s.Get(string(in[:fin])); ok { 239 | switch val := val.Read().(type) { 240 | case int64, int: 241 | buf.WriteString(fmt.Sprintf("%d", val)) 242 | default: 243 | buf.WriteString(fmt.Sprintf("%s", val)) 244 | } 245 | 246 | a = in[fin:] 247 | } else { 248 | return "", fmt.Errorf("Undefined variable: %s", string(in[:fin])) 249 | } 250 | } 251 | } 252 | 253 | return buf.String(), nil 254 | } 255 | -------------------------------------------------------------------------------- /future.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type Future struct { 9 | Task *Task 10 | Start time.Time 11 | Runtime time.Duration 12 | 13 | result *Result 14 | err error 15 | wg sync.WaitGroup 16 | } 17 | 18 | func NewFuture(start time.Time, task *Task, f func() (*Result, error)) *Future { 19 | fut := &Future{Start: start, Task: task} 20 | 21 | fut.wg.Add(1) 22 | 23 | go func() { 24 | r, e := f() 25 | fut.result = r 26 | fut.err = e 27 | fut.Runtime = time.Since(fut.Start) 28 | fut.wg.Done() 29 | }() 30 | 31 | return fut 32 | } 33 | 34 | func (f *Future) Wait() { 35 | f.wg.Wait() 36 | } 37 | 38 | func (f *Future) Value() (*Result, error) { 39 | f.Wait() 40 | return f.result, f.err 41 | } 42 | 43 | func (f *Future) Read() interface{} { 44 | f.Wait() 45 | return f.result 46 | } 47 | 48 | type Futures map[string]*Future 49 | 50 | type FutureScope struct { 51 | Scope 52 | futures Futures 53 | } 54 | 55 | func NewFutureScope(parent Scope) *FutureScope { 56 | return &FutureScope{ 57 | Scope: parent, 58 | futures: Futures{}, 59 | } 60 | } 61 | 62 | func (fs *FutureScope) Get(key string) (Value, bool) { 63 | if v, ok := fs.futures[key]; ok { 64 | return v, ok 65 | } 66 | 67 | return fs.Scope.Get(key) 68 | } 69 | 70 | func (fs *FutureScope) AddFuture(key string, f *Future) { 71 | fs.futures[key] = f 72 | } 73 | 74 | func (fs *FutureScope) Wait() { 75 | for _, f := range fs.futures { 76 | f.Wait() 77 | } 78 | } 79 | 80 | func (fs *FutureScope) Results() []RunResult { 81 | var results []RunResult 82 | 83 | for _, f := range fs.futures { 84 | f.Wait() 85 | 86 | results = append(results, RunResult{f.Task, f.result, f.Runtime}) 87 | } 88 | 89 | return results 90 | } 91 | -------------------------------------------------------------------------------- /lisp/builtin.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import "fmt" 4 | 5 | type Builtin struct{} 6 | 7 | var builtin = Builtin{} 8 | 9 | var builtin_commands = map[string]string{ 10 | "+": "Add", 11 | "-": "Sub", 12 | "*": "Mul", 13 | "==": "Eq", 14 | ">": "Gt", 15 | "<": "Lt", 16 | ">=": "Gte", 17 | "<=": "Lte", 18 | "display": "Display", 19 | "cons": "Cons", 20 | "car": "Car", 21 | "cdr": "Cdr", 22 | } 23 | 24 | func (Builtin) Display(vars ...Value) (Value, error) { 25 | if len(vars) == 1 { 26 | fmt.Println(vars[0]) 27 | } else { 28 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 29 | } 30 | return vars[0], nil 31 | } 32 | 33 | func (Builtin) Cons(vars ...Value) (Value, error) { 34 | if len(vars) == 2 { 35 | cons := Cons{&vars[0], &vars[1]} 36 | return Value{consValue, &cons}, nil 37 | } else { 38 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 39 | } 40 | } 41 | 42 | func (Builtin) Car(vars ...Value) (Value, error) { 43 | if len(vars) == 1 && vars[0].typ == consValue { 44 | cons := vars[0].Cons() 45 | return *cons.car, nil 46 | } else { 47 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 48 | } 49 | } 50 | 51 | func (Builtin) Cdr(vars ...Value) (Value, error) { 52 | if len(vars) == 1 && vars[0].typ == consValue { 53 | cons := vars[0].Cons() 54 | return *cons.cdr, nil 55 | } else { 56 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 57 | } 58 | } 59 | 60 | func (Builtin) Add(vars ...Value) (Value, error) { 61 | var sum int64 62 | for _, v := range vars { 63 | if v.typ == numberValue { 64 | sum += v.Number() 65 | } else { 66 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 67 | } 68 | } 69 | return Value{numberValue, sum}, nil 70 | } 71 | 72 | func (Builtin) Sub(vars ...Value) (Value, error) { 73 | if vars[0].typ != numberValue { 74 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 75 | } 76 | sum := vars[0].Number() 77 | for _, v := range vars[1:] { 78 | if v.typ == numberValue { 79 | sum -= v.Number() 80 | } else { 81 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 82 | } 83 | } 84 | return Value{numberValue, sum}, nil 85 | } 86 | 87 | func (Builtin) Mul(vars ...Value) (Value, error) { 88 | if vars[0].typ != numberValue { 89 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 90 | } 91 | sum := vars[0].Number() 92 | for _, v := range vars[1:] { 93 | if v.typ == numberValue { 94 | sum *= v.Number() 95 | } else { 96 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 97 | } 98 | } 99 | return Value{numberValue, sum}, nil 100 | } 101 | 102 | func (Builtin) Eq(vars ...Value) (Value, error) { 103 | for i := 1; i < len(vars); i++ { 104 | v1 := vars[i-1] 105 | v2 := vars[i] 106 | 107 | if v1.typ != v2.typ { 108 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 109 | } else if v1.typ == numberValue { 110 | if v1.Number() != v2.Number() { 111 | return False, nil 112 | } 113 | } else if v1.typ == stringValue { 114 | if v1.String() != v2.String() { 115 | return False, nil 116 | } 117 | } else { 118 | return Nil, fmt.Errorf("Unsupported argument type: %v", vars) 119 | } 120 | } 121 | return True, nil 122 | } 123 | 124 | func (Builtin) Gt(vars ...Value) (Value, error) { 125 | for i := 1; i < len(vars); i++ { 126 | v1 := vars[i-1] 127 | v2 := vars[i] 128 | if v1.typ != numberValue || v2.typ != numberValue { 129 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 130 | } else if !(v1.Number() > v2.Number()) { 131 | return False, nil 132 | } 133 | } 134 | return True, nil 135 | } 136 | 137 | func (Builtin) Lt(vars ...Value) (Value, error) { 138 | for i := 1; i < len(vars); i++ { 139 | v1 := vars[i-1] 140 | v2 := vars[i] 141 | if v1.typ != numberValue || v2.typ != numberValue { 142 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 143 | } else if !(v1.Number() < v2.Number()) { 144 | return False, nil 145 | } 146 | } 147 | return True, nil 148 | } 149 | 150 | func (Builtin) Gte(vars ...Value) (Value, error) { 151 | for i := 1; i < len(vars); i++ { 152 | v1 := vars[i-1] 153 | v2 := vars[i] 154 | if v1.typ != numberValue || v2.typ != numberValue { 155 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 156 | } else if !(v1.Number() >= v2.Number()) { 157 | return False, nil 158 | } 159 | } 160 | return True, nil 161 | } 162 | 163 | func (Builtin) Lte(vars ...Value) (Value, error) { 164 | for i := 1; i < len(vars); i++ { 165 | v1 := vars[i-1] 166 | v2 := vars[i] 167 | if v1.typ != numberValue || v2.typ != numberValue { 168 | return Nil, fmt.Errorf("Badly formatted arguments: %v", vars) 169 | } else if !(v1.Number() <= v2.Number()) { 170 | return False, nil 171 | } 172 | } 173 | return True, nil 174 | } 175 | -------------------------------------------------------------------------------- /lisp/builtin_test.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import "testing" 4 | 5 | func num(i int64) Value { 6 | return Value{numberValue, i} 7 | } 8 | 9 | func TestCar(t *testing.T) { 10 | a, b := Value{stringValue, "a"}, Value{stringValue, "b"} 11 | cons := Value{consValue, &Cons{&a, &b}} 12 | if response, err := builtin.Car(cons); response != a || err != nil { 13 | t.Errorf("Car %v should be %v, was %v", cons, a, response) 14 | } 15 | } 16 | 17 | func TestCdr(t *testing.T) { 18 | a, b := Value{stringValue, "a"}, Value{stringValue, "b"} 19 | cons := Value{consValue, &Cons{&a, &b}} 20 | if response, err := builtin.Cdr(cons); response != b || err != nil { 21 | t.Errorf("Cdr %v should be %v, was %v", cons, b, response) 22 | } 23 | } 24 | 25 | func TestAdd(t *testing.T) { 26 | cons := Cons{&Value{symbolValue, "+"}, nil} 27 | if !cons.isBuiltin() { 28 | t.Errorf("+ is not correcly setup") 29 | } 30 | 31 | if sum, err := builtin.Add(num(1), num(2), num(3)); sum != num(6) || err != nil { 32 | t.Errorf("1 + 2 + 3 should == 6, is %v, error: %v", sum, err) 33 | } 34 | } 35 | 36 | func TestSub(t *testing.T) { 37 | if sum, err := builtin.Sub(num(5), num(2), num(1)); sum != num(2) || err != nil { 38 | t.Errorf("5 - 2 - 1 should == 2, is %v, error: %v", sum, err) 39 | } 40 | } 41 | 42 | func TestMul(t *testing.T) { 43 | if sum, err := builtin.Mul(num(2), num(3), num(4)); sum != num(24) || err != nil { 44 | t.Errorf("2 * 3 * 4 should == 24, is %v, error: %v", sum, err) 45 | } 46 | } 47 | 48 | func TestGt(t *testing.T) { 49 | if result, err := builtin.Gt(num(4), num(3), num(2)); result == False || err != nil { 50 | t.Errorf("4 > 3 > 2 should == true, is %v, error: %v", result, err) 51 | } 52 | 53 | if result, err := builtin.Gt(num(4), num(4), num(2)); result == True || err != nil { 54 | t.Errorf("4 > 4 > 2 should == true, is %v, error: %v", result, err) 55 | } 56 | } 57 | 58 | func TestLt(t *testing.T) { 59 | if result, err := builtin.Lt(num(2), num(3), num(4)); result == False || err != nil { 60 | t.Errorf("2 < 3 < 4 should == true, is %v, error: %v", result, err) 61 | } 62 | } 63 | 64 | func TestGte(t *testing.T) { 65 | if result, err := builtin.Gte(num(4), num(4), num(2)); result == False || err != nil { 66 | t.Errorf("4 >= 4 >= 2 should == true, is %v, error: %v", result, err) 67 | } 68 | } 69 | 70 | func TestLte(t *testing.T) { 71 | if result, err := builtin.Lte(num(2), num(2), num(4)); result == False || err != nil { 72 | t.Errorf("2 <= 2 <= 4 should == true, is %v, error: %v", result, err) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /lisp/cons.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "strings" 7 | ) 8 | 9 | type Cons struct { 10 | car *Value 11 | cdr *Value 12 | } 13 | 14 | func (c Cons) Eval(scope ScopedVars) (val Value, err error) { 15 | if c.List() { 16 | if v, err := c.car.Eval(scope); err != nil { 17 | return Nil, err 18 | } else if *c.cdr == Nil { 19 | return v, nil 20 | } else { 21 | return c.cdr.Cons().Eval(scope) 22 | } 23 | } else { 24 | return Value{consValue, c}, nil 25 | } 26 | } 27 | 28 | func (cons Cons) Execute(scope ScopedVars) (Value, error) { 29 | if !cons.List() { 30 | return Nil, fmt.Errorf("Combination must be a proper list: %v", cons) 31 | } 32 | switch cons.car.String() { 33 | case "quote": 34 | return cons.quoteForm(scope) 35 | case "read": 36 | return cons.readForm(scope) 37 | case "if": 38 | return cons.ifForm(scope) 39 | case "or": 40 | return cons.orForm(scope) 41 | case "set!": 42 | return cons.setForm(scope) 43 | case "define": 44 | return cons.defineForm(scope) 45 | case "lambda": 46 | return cons.lambdaForm(scope) 47 | case "begin": 48 | return cons.beginForm(scope) 49 | default: 50 | if cons.isBuiltin() { 51 | return cons.runBuiltin(scope) 52 | } else { 53 | return cons.procForm(scope) 54 | } 55 | } 56 | } 57 | 58 | func (c Cons) List() bool { 59 | return c.cdr.typ == consValue || c.cdr.typ == nilValue 60 | } 61 | 62 | func (c Cons) Map(f func(v Value) (Value, error)) ([]Value, error) { 63 | result := make([]Value, 0) 64 | if value, err := f(*c.car); err != nil { 65 | return nil, err 66 | } else { 67 | result = append(result, value) 68 | } 69 | if *c.cdr != Nil { 70 | if values, err := c.cdr.Cons().Map(f); err != nil { 71 | return nil, err 72 | } else { 73 | result = append(result, values...) 74 | } 75 | } 76 | return result, nil 77 | } 78 | 79 | func (c Cons) Len() int { 80 | l := 0 81 | if *c.car != Nil { 82 | l++ 83 | if *c.cdr != Nil { 84 | l += c.cdr.Cons().Len() 85 | } 86 | } 87 | return l 88 | } 89 | 90 | func (c Cons) Vector() Vector { 91 | v, _ := c.Map(func(v Value) (Value, error) { 92 | return v, nil 93 | }) 94 | return v 95 | } 96 | 97 | func (c Cons) String() string { 98 | s := strings.Join(c.Stringify(), " ") 99 | return fmt.Sprintf(`(%v)`, s) 100 | } 101 | 102 | func (c Cons) Stringify() []string { 103 | result := make([]string, 0) 104 | result = append(result, c.car.String()) 105 | switch c.cdr.typ { 106 | case nilValue: 107 | case consValue: 108 | result = append(result, c.cdr.Cons().Stringify()...) 109 | default: 110 | result = append(result, ".", c.cdr.String()) 111 | } 112 | return result 113 | } 114 | 115 | func (cons Cons) procForm(scope ScopedVars) (val Value, err error) { 116 | if val, err = cons.car.Eval(scope); err == nil { 117 | if val.typ == procValue { 118 | var args Vector 119 | if args, err = cons.cdr.Cons().Map(func(v Value) (Value, error) { 120 | return v.Eval(scope) 121 | }); err != nil { 122 | return 123 | } else { 124 | val, err = val.val.(Proc).Call(scope, args) 125 | } 126 | } else { 127 | err = fmt.Errorf("The object %v is not applicable", val) 128 | } 129 | } 130 | return 131 | } 132 | 133 | func (cons Cons) beginForm(scope ScopedVars) (val Value, err error) { 134 | return cons.cdr.Cons().Eval(scope) 135 | } 136 | 137 | func (cons Cons) setForm(scope ScopedVars) (val Value, err error) { 138 | expr := cons.Vector() 139 | if len(expr) == 3 { 140 | key := expr[1].String() 141 | if _, ok := scope.Get(key); ok { 142 | val, err = expr[2].Eval(scope) 143 | if err == nil { 144 | scope.Set(key, val) 145 | } 146 | } else { 147 | err = fmt.Errorf("Unbound variable: %v", key) 148 | } 149 | } else { 150 | err = fmt.Errorf("Ill-formed special form: %v", cons) 151 | } 152 | return 153 | } 154 | 155 | func (cons Cons) ifForm(scope ScopedVars) (val Value, err error) { 156 | expr := cons.Vector() 157 | val = Nil 158 | if len(expr) < 3 || len(expr) > 4 { 159 | err = fmt.Errorf("Ill-formed special form: %v", expr) 160 | } else { 161 | r, err := expr[1].Eval(scope) 162 | if err == nil { 163 | if !(r.typ == symbolValue && r.String() == "false") && r != Nil && len(expr) > 2 { 164 | val, err = expr[2].Eval(scope) 165 | } else if len(expr) == 4 { 166 | val, err = expr[3].Eval(scope) 167 | } 168 | } 169 | } 170 | return 171 | } 172 | 173 | func (cons Cons) orForm(scope ScopedVars) (val Value, err error) { 174 | expr := cons.Vector() 175 | val = Nil 176 | if len(expr) < 1 { 177 | err = fmt.Errorf("Ill-formed special form: %v", expr) 178 | } else { 179 | var r Value 180 | 181 | for i := 1; i < len(expr); i++ { 182 | r, err = expr[i].Eval(scope) 183 | if err != nil { 184 | return 185 | } 186 | 187 | if r.typ == symbolValue { 188 | var ok bool 189 | 190 | val, ok = scope.Get(r.String()) 191 | 192 | if ok { 193 | return 194 | } 195 | } else { 196 | val = r 197 | return 198 | } 199 | } 200 | } 201 | 202 | return 203 | } 204 | 205 | func (cons Cons) lambdaForm(scope ScopedVars) (val Value, err error) { 206 | if cons.cdr.typ == consValue { 207 | lambda := cons.cdr.Cons() 208 | if (lambda.car.typ == consValue || lambda.car.typ == nilValue) && lambda.cdr.typ == consValue { 209 | params := lambda.car.Cons().Vector() 210 | val = Value{procValue, Proc{params, lambda.cdr.Cons(), scope}} 211 | } else { 212 | err = fmt.Errorf("Ill-formed special form: %v", cons) 213 | } 214 | } else { 215 | err = fmt.Errorf("Ill-formed special form: %v", cons) 216 | } 217 | return 218 | } 219 | 220 | func (cons Cons) quoteForm(scope ScopedVars) (val Value, err error) { 221 | if cons.cdr != nil { 222 | if *cons.cdr.Cons().cdr == Nil { 223 | val = *cons.cdr.Cons().car 224 | } else { 225 | val = Value{consValue, cons} 226 | } 227 | } else { 228 | err = fmt.Errorf("Ill-formed special form: %v", cons) 229 | } 230 | return 231 | } 232 | 233 | func (cons Cons) readForm(scope ScopedVars) (val Value, err error) { 234 | val, err = cons.cdr.Cons().car.Eval(scope) 235 | return val, err 236 | } 237 | 238 | func (cons Cons) defineForm(scope ScopedVars) (val Value, err error) { 239 | expr := cons.Vector() 240 | if len(expr) >= 2 && len(expr) <= 3 { 241 | if expr[1].typ == symbolValue { 242 | key := expr[1].String() 243 | if len(expr) == 3 { 244 | var i Value 245 | if i, err = expr[2].Eval(scope); err == nil { 246 | scope.Create(key, i) 247 | } 248 | } else { 249 | scope.Create(key, Nil) 250 | } 251 | return expr[1], err 252 | } 253 | } 254 | return Nil, fmt.Errorf("Ill-formed special form: %v", expr) 255 | } 256 | 257 | func (cons Cons) isBuiltin() bool { 258 | s := cons.car.String() 259 | if _, ok := builtin_commands[s]; ok { 260 | return true 261 | } 262 | return false 263 | } 264 | 265 | func (cons Cons) runBuiltin(scope ScopedVars) (val Value, err error) { 266 | cmd := builtin_commands[cons.car.String()] 267 | vars, err := cons.cdr.Cons().Map(func(v Value) (Value, error) { 268 | return v.Eval(scope) 269 | }) 270 | 271 | if err != nil { 272 | return Nil, err 273 | } 274 | 275 | values := []reflect.Value{} 276 | for _, v := range vars { 277 | values = append(values, reflect.ValueOf(v)) 278 | } 279 | result := reflect.ValueOf(&builtin).MethodByName(cmd).Call(values) 280 | val = result[0].Interface().(Value) 281 | err, _ = result[1].Interface().(error) 282 | return 283 | } 284 | -------------------------------------------------------------------------------- /lisp/cons_test.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func cons() Cons { 8 | v1 := &Value{numberValue, int64(1)} 9 | v2 := &Value{numberValue, int64(2)} 10 | v3 := &Value{numberValue, int64(3)} 11 | c2 := &Value{consValue, &Cons{v3, &Value{nilValue, nil}}} 12 | c1 := &Value{consValue, &Cons{v2, c2}} 13 | return Cons{v1, c1} 14 | } 15 | 16 | func TestConsMap(t *testing.T) { 17 | s, _ := cons().Map(func(v Value) (Value, error) { 18 | return Value{numberValue, v.val.(int64) + 1}, nil 19 | }) 20 | if len(s) != 3 || s[0].val != int64(2) || s[1].val != int64(3) || s[2].val != int64(4) { 21 | t.Errorf("Expected (1 2 3), got %v", s) 22 | } 23 | } 24 | 25 | func TestConsLen(t *testing.T) { 26 | got := cons().Len() 27 | if got != 3 { 28 | t.Errorf("Expected 3, got %v\n", got) 29 | } 30 | } 31 | 32 | func TestConsVector(t *testing.T) { 33 | s := cons().Vector() 34 | if len(s) != 3 || s[0].val != int64(1) || s[1].val != int64(2) || s[2].val != int64(3) { 35 | t.Errorf("Expected (1 2 3), got %v", s) 36 | } 37 | } 38 | 39 | func TestConsString(t *testing.T) { 40 | expected := "(1 2 3)" 41 | s := cons().String() 42 | if s != expected { 43 | t.Errorf("Cons.String() failed. Expected %v, got %v", expected, s) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /lisp/evaler.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | func EvalString(line string, scope ScopedVars) (Value, error) { 4 | expanded, err := NewTokens(line).Expand() 5 | if err != nil { 6 | return Nil, err 7 | } 8 | parsed, err := expanded.Parse() 9 | if err != nil { 10 | return Nil, err 11 | } 12 | evaled, err := parsed.Eval(scope) 13 | if err != nil { 14 | return Nil, err 15 | } 16 | return evaled, nil 17 | } 18 | -------------------------------------------------------------------------------- /lisp/evaler_test.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import "testing" 4 | import "fmt" 5 | 6 | func TestEval(t *testing.T) { 7 | var tests = []struct { 8 | in string 9 | out string 10 | }{ 11 | {"()", "()"}, 12 | {"42", "42"}, 13 | {"1 2 3", "3"}, 14 | {"(+ 42 13)", "55"}, 15 | {"(+ (+ 1 2 3) 4)", "10"}, 16 | {"(quote (1 2 3))", "(1 2 3)"}, 17 | {"(quote (1 (+ 1 2) 3))", "(1 (+ 1 2) 3)"}, 18 | {"(quote hej)", "hej"}, 19 | {"(cons 1 2)", "(1 . 2)"}, 20 | {"(car (cons 1 2))", "1"}, 21 | {"(cdr (cons 1 2))", "2"}, 22 | {"(cons 1 ())", "(1)"}, 23 | {"(cons 1 :(2))", "(1 2)"}, 24 | {":hej", "hej"}, 25 | {"::hej", "(quote hej)"}, 26 | {":(hej hopp)", "(hej hopp)"}, 27 | {"(quote (hej))", "(hej)"}, 28 | {"(if true (+ 1 1) 3)", "2"}, 29 | {"(if false 42 1)", "1"}, 30 | {"(if false 42)", "()"}, 31 | {"(begin (define x) (if x 1 2))", "2"}, 32 | {"(define r 3)", "r"}, 33 | {"(begin 5 (+ 3 4))", "7"}, 34 | {"(begin (define p 3) (+ 39 p))", "42"}, 35 | {"(begin (define p 3) (set! p 4) (+ 1 p))", "5"}, 36 | {"(begin (define p 3) (set! p (+ 1 1)) p)", "2"}, 37 | {"(begin (define pi (+ 3 14)) pi)", "17"}, 38 | {"((lambda (a) (+ a 1)) 42)", "43"}, 39 | {"(begin (define p 10) p)", "10"}, 40 | {"(begin (define inc (lambda (a) (+ a 1))) (inc 42))", "43"}, 41 | // {"(define a 10) ((lambda () (define a 20))) a", "10"}, 42 | {"(define a 0) ((lambda () (set! a 10))) a", "10"}, 43 | {"((lambda (i) i) (+ 5 5))", "10"}, 44 | {"(define inc ((lambda () (begin (define a 0) (lambda () (set! a (+ a 1))))))) (inc) (inc)", "2"}, 45 | {"(define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1)))))) (fact 20)", "2432902008176640000"}, 46 | } 47 | 48 | for _, test := range tests { 49 | if actual, err := EvalString(test.in, scope); err != nil { 50 | t.Error(err) 51 | } else if fmt.Sprintf("%v", actual) != test.out { 52 | t.Errorf("Eval \"%v\" gives \"%v\", want \"%v\"", test.in, actual, test.out) 53 | } 54 | } 55 | } 56 | 57 | func TestEvalFailures(t *testing.T) { 58 | var tests = []struct { 59 | in string 60 | out string 61 | }{ 62 | {"hello", "Unbound variable: hello"}, 63 | {"(set! undefined 42)", "Unbound variable: undefined"}, 64 | {"(lambda (a))", "Ill-formed special form: (lambda (a))"}, 65 | {"(1 2 3)", "The object 1 is not applicable"}, 66 | {"(1", "List was opened but not closed"}, 67 | {"(set! a)", "Ill-formed special form: (set! a)"}, 68 | } 69 | 70 | for _, test := range tests { 71 | if _, err := EvalString(test.in, scope); err == nil || err.Error() != test.out { 72 | t.Errorf("Parse('%v'), want error '%v', got '%v'", test.in, test.out, err) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /lisp/proc.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import "fmt" 4 | 5 | type Proc struct { 6 | params Vector 7 | body Cons 8 | scope ScopedVars 9 | } 10 | 11 | func (p Proc) String() string { 12 | return "" 13 | } 14 | 15 | func (p Proc) Call(scope ScopedVars, params Vector) (val Value, err error) { 16 | if len(p.params) == len(params) { 17 | scope = p.scope 18 | for i, name := range p.params { 19 | scope.Create(name.String(), params[i]) 20 | } 21 | val, err = p.body.Eval(scope) 22 | } else { 23 | err = fmt.Errorf("%v has been called with %v arguments; it requires exactly %v arguments", p, len(params), len(p.params)) 24 | } 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /lisp/scope.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | var scope *Scope 4 | 5 | func init() { 6 | scope = NewScope() 7 | scope.AddEnv() 8 | } 9 | 10 | type ScopedVars interface { 11 | Get(key string) (Value, bool) 12 | Set(key string, val Value) Value 13 | Create(key string, val Value) Value 14 | } 15 | 16 | type Env map[string]Value 17 | 18 | type Scope struct { 19 | parent *Scope 20 | envs []*Env 21 | } 22 | 23 | func NewScope() *Scope { 24 | scope := &Scope{} 25 | scope.envs = make([]*Env, 0) 26 | return scope 27 | } 28 | 29 | func NewNestedScope(parent *Scope) *Scope { 30 | scope := &Scope{} 31 | scope.parent = parent 32 | scope.envs = make([]*Env, 0) 33 | return scope 34 | } 35 | 36 | func (s *Scope) Dup() *Scope { 37 | scope := &Scope{} 38 | scope.envs = make([]*Env, len(s.envs)) 39 | copy(scope.envs, s.envs) 40 | return scope 41 | } 42 | 43 | func (s *Scope) Env() *Env { 44 | if len(s.envs) > 0 { 45 | return s.envs[len(s.envs)-1] 46 | } 47 | return nil 48 | } 49 | 50 | func (s *Scope) AddEnv() *Env { 51 | env := make(Env) 52 | s.envs = append(s.envs, &env) 53 | return &env 54 | } 55 | 56 | func (s *Scope) DropEnv() *Env { 57 | s.envs[len(s.envs)-1] = nil 58 | s.envs = s.envs[:len(s.envs)-1] 59 | return s.Env() 60 | } 61 | 62 | func (s *Scope) Create(key string, value Value) Value { 63 | env := *s.Env() 64 | env[key] = value 65 | return value 66 | } 67 | 68 | func (s *Scope) Set(key string, value Value) Value { 69 | t := s 70 | 71 | for t != nil { 72 | for i := len(s.envs) - 1; i >= 0; i-- { 73 | env := *s.envs[i] 74 | if _, ok := env[key]; ok { 75 | env[key] = value 76 | return value 77 | } 78 | } 79 | 80 | t = t.parent 81 | } 82 | 83 | return s.Create(key, value) 84 | } 85 | 86 | func (s *Scope) Get(key string) (val Value, ok bool) { 87 | t := s 88 | 89 | for t != nil { 90 | for i := len(s.envs) - 1; i >= 0; i-- { 91 | env := *s.envs[i] 92 | if val, ok = env[key]; ok { 93 | return 94 | } 95 | } 96 | 97 | t = t.parent 98 | } 99 | 100 | return 101 | } 102 | -------------------------------------------------------------------------------- /lisp/scope_test.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import "testing" 4 | 5 | func TestScope(t *testing.T) { 6 | scope := NewScope() 7 | if scope.Env() != nil { 8 | t.Errorf("Env should be nil initially") 9 | } 10 | 11 | env := scope.AddEnv() 12 | if env != scope.Env() { 13 | t.Errorf("AddEnv() returns %v, should be same as scope.Env(): %v", env, scope.Env()) 14 | } 15 | 16 | env2 := scope.AddEnv() 17 | if env2 != scope.Env() { 18 | t.Errorf("AddEnv() returns %v, should be same as scope.Env(): %v", env2, scope.Env()) 19 | } 20 | 21 | env3 := scope.DropEnv() 22 | if env3 != scope.Env() { 23 | t.Errorf("DropEnv() returns %v, should be same as scope.Env(): %v", env3, scope.Env()) 24 | } 25 | 26 | if env != env3 { 27 | t.Errorf("Original env: %v should be same as dropped env from DropEnv(): %v", env, env3) 28 | } 29 | 30 | env4 := scope.DropEnv() 31 | if env4 != nil { 32 | t.Errorf("DropEnv should be back to nil but is %v", env4) 33 | } 34 | } 35 | 36 | func TestEnv(t *testing.T) { 37 | scope := NewScope() 38 | scope.AddEnv() 39 | if v1 := scope.Create("foo", Value{symbolValue, "bar"}); v1 != (Value{symbolValue, "bar"}) { 40 | t.Errorf("Env.Create should return bar but returned %v", v1) 41 | } 42 | 43 | if v2, ok := scope.Get("foo"); v2 != (Value{symbolValue, "bar"}) && ok { 44 | t.Errorf("Failed to Create and Get foo, got %v, %v", v2, ok) 45 | } 46 | 47 | if _, ok := scope.Get("undefined"); ok { 48 | t.Errorf("Get of undefined should give false but is %v", ok) 49 | } 50 | 51 | scope.AddEnv() 52 | 53 | if v3, ok := scope.Get("foo"); v3 != (Value{symbolValue, "bar"}) { 54 | t.Errorf("Failed to Get foo in sub env, got %v, %v", v3, ok) 55 | } 56 | 57 | scope.Create("bar", (Value{symbolValue, "baz"})) 58 | 59 | scope.DropEnv() 60 | if _, ok := scope.Get("bar"); ok { 61 | t.Errorf("We should not be able to get local var bar") 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /lisp/tokens.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | ) 8 | 9 | type Tokens []*Token 10 | 11 | type tokenType uint8 12 | 13 | type Token struct { 14 | typ tokenType 15 | val string 16 | } 17 | 18 | type Pattern struct { 19 | typ tokenType 20 | regexp *regexp.Regexp 21 | } 22 | 23 | // func (t Token) String() string { 24 | // return fmt.Sprintf("%v", t.val) 25 | // } 26 | 27 | func (t *Token) String() string { 28 | return fmt.Sprintf("%v", t.val) 29 | } 30 | 31 | const ( 32 | whitespaceToken tokenType = iota 33 | commentToken 34 | stringToken 35 | numberToken 36 | openToken 37 | closeToken 38 | symbolToken 39 | ) 40 | 41 | func (t *Token) Type() string { 42 | switch t.typ { 43 | case commentToken: 44 | return "comment" 45 | case stringToken: 46 | return "string" 47 | case numberToken: 48 | return "number" 49 | case openToken: 50 | return "open" 51 | case closeToken: 52 | return "close" 53 | case symbolToken: 54 | return "symbol" 55 | default: 56 | return "unknown" 57 | } 58 | } 59 | 60 | func patterns() []Pattern { 61 | return []Pattern{ 62 | {whitespaceToken, regexp.MustCompile(`^\s+`)}, 63 | {commentToken, regexp.MustCompile(`^;.*`)}, 64 | {stringToken, regexp.MustCompile(`^("(\\.|[^"])*")`)}, 65 | {numberToken, regexp.MustCompile(`^((([0-9]+)?\.)?[0-9]+)`)}, 66 | {openToken, regexp.MustCompile(`^(\()`)}, 67 | {closeToken, regexp.MustCompile(`^(\))`)}, 68 | {symbolToken, regexp.MustCompile(`^(:|[^\s();]+)`)}, 69 | } 70 | } 71 | 72 | func NewTokens(program string) (tokens Tokens) { 73 | for pos := 0; pos < len(program); { 74 | for _, pattern := range patterns() { 75 | if matches := pattern.regexp.FindStringSubmatch(program[pos:]); matches != nil { 76 | if len(matches) > 1 { 77 | tokens = append(tokens, &Token{pattern.typ, matches[1]}) 78 | } 79 | pos = pos + len(matches[0]) 80 | break 81 | } 82 | } 83 | } 84 | return 85 | } 86 | 87 | // Expand until there are no more expansions to do 88 | func (tokens Tokens) Expand() (result Tokens, err error) { 89 | var updated bool 90 | for i := 0; i < len(tokens); i++ { 91 | var start int 92 | quote := Token{symbolToken, ":"} 93 | if *tokens[i] != quote { 94 | result = append(result, tokens[i]) 95 | } else { 96 | updated = true 97 | for start = i + 1; *tokens[start] == quote; start++ { 98 | result = append(result, tokens[start]) 99 | } 100 | if tokens[i+1].typ == openToken { 101 | if i, err = tokens.findClose(start + 1); err != nil { 102 | return nil, err 103 | } 104 | } else { 105 | i = start 106 | } 107 | result = append(result, &Token{openToken, "("}, &Token{symbolToken, "quote"}) 108 | result = append(result, tokens[start:i+1]...) 109 | result = append(result, &Token{closeToken, ")"}) 110 | } 111 | } 112 | if updated { 113 | result, err = result.Expand() 114 | } 115 | return 116 | } 117 | 118 | func (tokens Tokens) Parse() (cons Cons, err error) { 119 | var pos int 120 | var current *Cons 121 | for pos < len(tokens) { 122 | if current == nil { 123 | cons = Cons{&Nil, &Nil} 124 | current = &cons 125 | } else { 126 | previous_current := current 127 | current = &Cons{&Nil, &Nil} 128 | previous_current.cdr = &Value{consValue, current} 129 | } 130 | t := tokens[pos] 131 | switch t.typ { 132 | case numberToken: 133 | if i, err := strconv.ParseInt(t.val, 10, 0); err != nil { 134 | err = fmt.Errorf("Failed to convert number: %v", t.val) 135 | } else { 136 | current.car = &Value{numberValue, i} 137 | pos++ 138 | } 139 | case stringToken: 140 | current.car = &Value{stringValue, t.val[1 : len(t.val)-1]} 141 | pos++ 142 | case symbolToken: 143 | current.car = &Value{symbolValue, t.val} 144 | pos++ 145 | case openToken: 146 | var nested Cons 147 | start := pos + 1 148 | var end int 149 | if end, err = tokens.findClose(start); err != nil { 150 | return 151 | } 152 | if start == end { 153 | current.car = &Nil 154 | } else { 155 | if nested, err = tokens[start:end].Parse(); err != nil { 156 | return 157 | } 158 | current.car = &Value{consValue, &nested} 159 | } 160 | pos = end + 1 161 | case closeToken: 162 | err = fmt.Errorf("List was closed but not opened") 163 | } 164 | } 165 | return 166 | } 167 | 168 | func (t Tokens) findClose(start int) (int, error) { 169 | depth := 1 170 | for i := start; i < len(t); i++ { 171 | t := t[i] 172 | switch t.typ { 173 | case openToken: 174 | depth++ 175 | case closeToken: 176 | depth-- 177 | } 178 | if depth == 0 { 179 | return i, nil 180 | } 181 | } 182 | return 0, fmt.Errorf("List was opened but not closed") 183 | } 184 | -------------------------------------------------------------------------------- /lisp/tokens_test.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func equalSlices(a, b Tokens) bool { 9 | if len(a) != len(b) { 10 | return false 11 | } 12 | for i, v := range a { 13 | if v.val != b[i].val || v.typ != b[i].typ { 14 | return false 15 | } 16 | } 17 | return true 18 | } 19 | 20 | func TestNewTokens(t *testing.T) { 21 | var tests = []struct { 22 | in string 23 | out Tokens 24 | }{ 25 | {"(define a 42)", Tokens{{openToken, "("}, {symbolToken, "define"}, {symbolToken, "a"}, {numberToken, "42"}, {closeToken, ")"}}}, 26 | {"\t(quote\n\t\t(a b c)) ", Tokens{{openToken, "("}, {symbolToken, "quote"}, {openToken, "("}, {symbolToken, "a"}, {symbolToken, "b"}, {symbolToken, "c"}, {closeToken, ")"}, {closeToken, ")"}}}, 27 | {"hello ; dude\n\tworld", Tokens{{symbolToken, "hello"}, {symbolToken, "world"}}}, 28 | {"test \"a string\"", Tokens{{symbolToken, "test"}, {stringToken, "\"a string\""}}}, 29 | {"\"only string\"", Tokens{{stringToken, "\"only string\""}}}, 30 | {"\"string\\nwith\\\"escape\\tcharacters\"", Tokens{{stringToken, "\"string\\nwith\\\"escape\\tcharacters\""}}}, 31 | {"\"hej\\\"hello\"", Tokens{{stringToken, "\"hej\\\"hello\""}}}, 32 | } 33 | 34 | for _, test := range tests { 35 | x := NewTokens(test.in) 36 | if !equalSlices(x, test.out) { 37 | t.Errorf("NewTokens \"%v\" gives \"%v\", expected \"%v\"", test.in, x, test.out) 38 | } 39 | } 40 | } 41 | 42 | func TestParse(t *testing.T) { 43 | var tests = []struct { 44 | in string 45 | out string 46 | }{ 47 | {"42", "(42)"}, 48 | {"(+ (+ 1 2) 3)", "((+ (+ 1 2) 3))"}, 49 | } 50 | for _, test := range tests { 51 | if parsed, err := NewTokens(test.in).Parse(); err != nil { 52 | t.Errorf("%v\n", err) 53 | } else { 54 | result := fmt.Sprintf("%v", parsed.String()) 55 | if result != test.out { 56 | t.Errorf("Parse \"%v\" gives \"%v\", expected \"%v\"", test.in, result, test.out) 57 | } 58 | } 59 | } 60 | } 61 | 62 | func TestParseFailures(t *testing.T) { 63 | var tests = []string{ 64 | "(42", 65 | } 66 | for _, in := range tests { 67 | if x, err := NewTokens(in).Parse(); err == nil { 68 | t.Errorf("Parse('%v') = '%v', want error", in, x) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lisp/value.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Value struct { 9 | typ valueType 10 | val interface{} 11 | } 12 | 13 | type Map interface { 14 | Get(key string) (Value, bool) 15 | } 16 | 17 | func (v Value) Interface() interface{} { 18 | return v.val 19 | } 20 | 21 | var Nil = Value{nilValue, nil} 22 | var False = Value{symbolValue, "false"} 23 | var True = Value{symbolValue, "true"} 24 | 25 | type valueType uint8 26 | 27 | const ( 28 | nilValue valueType = iota 29 | symbolValue 30 | numberValue 31 | stringValue 32 | vectorValue 33 | procValue 34 | consValue 35 | mapValue 36 | ) 37 | 38 | func NumberValue(n int64) Value { 39 | return Value{typ: numberValue, val: n} 40 | } 41 | 42 | func StringValue(s string) Value { 43 | return Value{typ: stringValue, val: s} 44 | } 45 | 46 | func MapValue(m Map) Value { 47 | return Value{typ: mapValue, val: m} 48 | } 49 | 50 | func (v Value) Eval(scope ScopedVars) (Value, error) { 51 | switch v.typ { 52 | case consValue: 53 | return v.Cons().Execute(scope) 54 | case symbolValue: 55 | sym := v.String() 56 | 57 | parts := strings.Split(sym, ".") 58 | 59 | var ( 60 | v Value 61 | ok bool 62 | ) 63 | 64 | if len(parts) == 1 { 65 | v, ok = scope.Get(sym) 66 | } else { 67 | v, ok = scope.Get(parts[0]) 68 | 69 | for _, sub := range parts[1:] { 70 | if v.typ != mapValue { 71 | return Nil, fmt.Errorf("Variable '%s' is not a map (%v)", parts[0], v) 72 | } 73 | 74 | v, ok = v.Interface().(Map).Get(sub) 75 | } 76 | } 77 | 78 | if ok { 79 | return v, nil 80 | } else if sym == "true" || sym == "false" { 81 | return Value{symbolValue, sym}, nil 82 | } else { 83 | return Nil, fmt.Errorf("Unbound variable: %v", sym) 84 | } 85 | default: 86 | return v, nil 87 | } 88 | } 89 | 90 | func (v Value) String() string { 91 | switch v.typ { 92 | case numberValue: 93 | return fmt.Sprintf("%d", v.val.(int64)) 94 | case nilValue: 95 | return "()" 96 | default: 97 | return fmt.Sprintf("%v", v.val) 98 | } 99 | } 100 | 101 | func (v Value) Inspect() string { 102 | switch v.typ { 103 | case stringValue: 104 | return fmt.Sprintf(`"%v"`, v.val) 105 | case vectorValue: 106 | return v.val.(Vector).Inspect() 107 | default: 108 | return v.String() 109 | } 110 | } 111 | 112 | func (v Value) Cons() Cons { 113 | if v.typ == consValue { 114 | return *v.val.(*Cons) 115 | } else { 116 | return Cons{&v, &Nil} 117 | } 118 | } 119 | 120 | func (v Value) Number() int64 { 121 | return v.val.(int64) 122 | } 123 | -------------------------------------------------------------------------------- /lisp/vector.go: -------------------------------------------------------------------------------- 1 | package lisp 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Vector []Value 9 | 10 | func (s Vector) String() string { 11 | var arr []string 12 | for _, v := range s { 13 | arr = append(arr, v.String()) 14 | } 15 | return fmt.Sprintf(`[%v]`, strings.Join(arr, " ")) 16 | } 17 | 18 | func (s Vector) Inspect() string { 19 | var arr []string 20 | for _, v := range s { 21 | arr = append(arr, v.Inspect()) 22 | } 23 | return fmt.Sprintf(`[%v]`, strings.Join(arr, " ")) 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jessevdk/go-flags" 6 | "os" 7 | "path/filepath" 8 | ) 9 | 10 | type Options struct { 11 | Vars map[string]string `short:"s" long:"set" description:"Set a variable"` 12 | ShowOutput bool `short:"o" long:"output" description:"Show command output"` 13 | Host string `short:"t" long:"host" description:"Run the playbook on another host"` 14 | Development bool `long:"dev" description:"Use a dev version of tachyon"` 15 | CleanHost bool `long:"clean-host" description:"Clean the host cache before using"` 16 | Debug bool `short:"d" long:"debug" description:"Show all information about commands"` 17 | Release string `long:"release" description:"The release to use when remotely invoking tachyon"` 18 | JSON bool `long:"json" description:"Output the run details in chunked json"` 19 | Install bool `long:"install" description:"Install tachyon a remote machine"` 20 | } 21 | 22 | var Release string = "dev" 23 | var Arg0 string 24 | 25 | func Main(args []string) int { 26 | var opts Options 27 | 28 | abs, err := filepath.Abs(args[0]) 29 | if err != nil { 30 | panic(err) 31 | } 32 | 33 | Arg0 = abs 34 | 35 | parser := flags.NewParser(&opts, flags.Default) 36 | 37 | for _, o := range parser.Command.Group.Groups()[0].Options() { 38 | if o.LongName == "release" { 39 | o.Default = []string{Release} 40 | } 41 | } 42 | args, err = parser.ParseArgs(args) 43 | 44 | if err != nil { 45 | if serr, ok := err.(*flags.Error); ok { 46 | if serr.Type == flags.ErrHelp { 47 | return 2 48 | } 49 | } 50 | 51 | fmt.Printf("Error parsing options: %s", err) 52 | return 1 53 | } 54 | 55 | if !opts.Install && len(args) != 2 { 56 | fmt.Printf("Usage: tachyon [options] \n") 57 | return 1 58 | } 59 | 60 | if opts.Host != "" { 61 | return runOnHost(&opts, args) 62 | } 63 | 64 | cfg := &Config{ShowCommandOutput: opts.ShowOutput} 65 | 66 | ns := NewNestedScope(nil) 67 | 68 | for k, v := range opts.Vars { 69 | ns.Set(k, v) 70 | } 71 | 72 | env := NewEnv(ns, cfg) 73 | defer env.Cleanup() 74 | 75 | if opts.JSON { 76 | env.ReportJSON() 77 | } 78 | 79 | playbook, err := NewPlaybook(env, args[1]) 80 | if err != nil { 81 | fmt.Printf("Error loading plays: %s\n", err) 82 | return 1 83 | } 84 | 85 | cur, err := os.Getwd() 86 | if err != nil { 87 | fmt.Printf("Unable to figure out the current directory: %s\n", err) 88 | return 1 89 | } 90 | 91 | defer os.Chdir(cur) 92 | os.Chdir(playbook.baseDir) 93 | 94 | runner := NewRunner(env, playbook.Plays) 95 | err = runner.Run(env) 96 | 97 | if err != nil { 98 | fmt.Fprintf(os.Stderr, "Error running playbook: %s\n", err) 99 | return 1 100 | } 101 | 102 | return 0 103 | } 104 | 105 | func runOnHost(opts *Options, args []string) int { 106 | if opts.Install { 107 | fmt.Printf("=== Installing tachyon on %s\n", opts.Host) 108 | } else { 109 | fmt.Printf("=== Executing playbook on %s\n", opts.Host) 110 | } 111 | 112 | var playbook string 113 | 114 | if !opts.Install { 115 | playbook = args[1] 116 | } 117 | 118 | t := &Tachyon{ 119 | Target: opts.Host, 120 | Debug: opts.Debug, 121 | Clean: opts.CleanHost, 122 | Dev: opts.Development, 123 | Playbook: playbook, 124 | Release: opts.Release, 125 | InstallOnly: opts.Install, 126 | } 127 | 128 | _, err := RunAdhocCommand(t, "") 129 | if err != nil { 130 | fmt.Printf("Error: %s\n", err) 131 | return 1 132 | } 133 | 134 | return 0 135 | } 136 | -------------------------------------------------------------------------------- /net/net.go: -------------------------------------------------------------------------------- 1 | package net 2 | 3 | import ( 4 | _ "github.com/vektra/tachyon/net/s3" 5 | ) 6 | -------------------------------------------------------------------------------- /net/s3/s3.go: -------------------------------------------------------------------------------- 1 | package s3 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "crypto/md5" 7 | "encoding/hex" 8 | "fmt" 9 | "github.com/crowdmob/goamz/aws" 10 | "github.com/crowdmob/goamz/s3" 11 | "github.com/vektra/tachyon" 12 | "io" 13 | "os" 14 | "time" 15 | ) 16 | 17 | type S3 struct { 18 | Bucket string `tachyon:"bucket,required"` 19 | PutFile string `tachyon:"put_file"` 20 | GetFile string `tachyon:"get_file"` 21 | At string `tachyon:"at"` 22 | Public bool `tachyon:"public"` 23 | ContentType string `tachyon:"content_type"` 24 | Writable bool `tachyon:"writable"` 25 | GZip bool `tachyon:"gzip"` 26 | } 27 | 28 | func (s *S3) Run(env *tachyon.CommandEnv) (*tachyon.Result, error) { 29 | auth, err := aws.GetAuth("", "", "", time.Time{}) 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | c := s3.New(auth, aws.USWest2) 35 | b := c.Bucket(s.Bucket) 36 | 37 | res := tachyon.NewResult(true) 38 | 39 | res.Add("bucket", s.Bucket) 40 | res.Add("remote", s.At) 41 | 42 | if s.PutFile != "" { 43 | path := env.Paths.File(s.PutFile) 44 | 45 | f, err := os.Open(path) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | if f == nil { 51 | return nil, fmt.Errorf("Unknown local file %s", s.PutFile) 52 | } 53 | 54 | defer f.Close() 55 | 56 | var perm s3.ACL 57 | 58 | if s.Public { 59 | if s.Writable { 60 | perm = s3.PublicReadWrite 61 | } else { 62 | perm = s3.PublicRead 63 | } 64 | } else { 65 | perm = s3.Private 66 | } 67 | 68 | ct := s.ContentType 69 | if ct == "" { 70 | ct = "application/octet-stream" 71 | } 72 | 73 | fi, err := f.Stat() 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | var ( 79 | input io.Reader 80 | opts s3.Options 81 | size int64 82 | ) 83 | 84 | h := md5.New() 85 | 86 | if s.GZip { 87 | var buf bytes.Buffer 88 | 89 | z := gzip.NewWriter(io.MultiWriter(h, &buf)) 90 | 91 | _, err = io.Copy(z, f) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | z.Close() 97 | 98 | opts.ContentEncoding = "gzip" 99 | 100 | input = &buf 101 | size = int64(buf.Len()) 102 | } else { 103 | input = io.TeeReader(f, h) 104 | size = fi.Size() 105 | } 106 | 107 | err = b.PutReader(s.At, input, size, ct, perm, opts) 108 | 109 | rep, err := b.Head(s.At, nil) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | localMD5 := hex.EncodeToString(h.Sum(nil)) 115 | 116 | res.Add("wrote", size) 117 | res.Add("local", s.PutFile) 118 | res.Add("md5", localMD5) 119 | 120 | etag := rep.Header.Get("ETag") 121 | if etag != "" { 122 | etag = etag[1 : len(etag)-1] 123 | 124 | if localMD5 != etag { 125 | return nil, fmt.Errorf("corruption uploading file detected") 126 | } 127 | } 128 | 129 | } else if s.GetFile != "" { 130 | f, err := os.OpenFile(s.GetFile, os.O_CREATE|os.O_WRONLY, 0644) 131 | if err != nil { 132 | return nil, err 133 | } 134 | 135 | defer f.Close() 136 | 137 | i, err := b.GetReader(s.At) 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | defer i.Close() 143 | 144 | n, err := io.Copy(f, i) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | res.Add("read", n) 150 | res.Add("local", s.GetFile) 151 | } else { 152 | return nil, fmt.Errorf("Specify put_file or get_file") 153 | } 154 | 155 | return res, nil 156 | } 157 | 158 | func init() { 159 | tachyon.RegisterCommand("s3", &S3{}) 160 | } 161 | -------------------------------------------------------------------------------- /package/apt/apt.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vektra/tachyon" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "regexp" 10 | "time" 11 | ) 12 | 13 | type Apt struct { 14 | Pkg string `tachyon:"pkg"` 15 | State string `tachyon:"state" enum:"present,install,absent,remove"` 16 | Cache bool `tachyon:"update_cache"` 17 | CacheTime string `tachyon:"cache_time"` 18 | Dry bool `tachyon:"dryrun"` 19 | } 20 | 21 | var installed = regexp.MustCompile(`Installed: ([^\n]+)`) 22 | var candidate = regexp.MustCompile(`Candidate: ([^\n]+)`) 23 | 24 | func (a *Apt) Run(env *tachyon.CommandEnv) (*tachyon.Result, error) { 25 | state := a.State 26 | if state == "" { 27 | state = "present" 28 | } 29 | 30 | if a.Cache { 31 | home, err := tachyon.HomeDir() 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | checkFile := filepath.Join(home, ".tachyon", "apt-cache-timestamp") 37 | 38 | runUpdate := true 39 | 40 | if a.CacheTime != "" { 41 | fi, err := os.Stat(checkFile) 42 | if err == nil { 43 | dur, err := time.ParseDuration(a.CacheTime) 44 | if err != nil { 45 | return nil, fmt.Errorf("cache_time was not in the proper format") 46 | } 47 | 48 | runUpdate = time.Now().After(fi.ModTime().Add(dur)) 49 | } 50 | } 51 | 52 | if runUpdate { 53 | _, err := tachyon.RunCommand(env, "apt-get", "update") 54 | if err != nil { 55 | return nil, err 56 | } 57 | ioutil.WriteFile(checkFile, []byte(``), 0666) 58 | } 59 | } 60 | 61 | if a.Pkg == "" { 62 | simp := tachyon.NewResult(true) 63 | simp.Add("cache", "updated") 64 | 65 | return simp, nil 66 | } 67 | 68 | out, err := tachyon.RunCommand(env, "apt-cache", "policy", a.Pkg) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | res := installed.FindSubmatch(out.Stdout) 74 | if res == nil { 75 | return nil, fmt.Errorf("No package '%s' available", a.Pkg) 76 | } 77 | 78 | curVer := string(res[1]) 79 | if curVer == "(none)" { 80 | curVer = "" 81 | } 82 | 83 | res = candidate.FindSubmatch(out.Stdout) 84 | if res == nil { 85 | return nil, fmt.Errorf("Error parsing apt-cache output") 86 | } 87 | 88 | canVer := string(res[1]) 89 | 90 | if state == "absent" { 91 | rd := tachyon.ResultData{} 92 | 93 | if curVer == "" { 94 | return tachyon.WrapResult(false, rd), nil 95 | } 96 | 97 | rd.Set("removed", curVer) 98 | 99 | _, err = tachyon.RunCommand(env, "apt-get", "remove", "-y", a.Pkg) 100 | 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return tachyon.WrapResult(true, rd), nil 106 | } 107 | 108 | rd := tachyon.ResultData{} 109 | rd.Set("installed", curVer) 110 | rd.Set("candidate", canVer) 111 | 112 | if state == "present" && curVer == canVer { 113 | return tachyon.WrapResult(false, rd), nil 114 | } 115 | 116 | if a.Dry { 117 | rd.Set("dryrun", true) 118 | return tachyon.WrapResult(true, rd), nil 119 | } 120 | 121 | e := append(os.Environ(), "DEBIAN_FRONTEND=noninteractive", "DEBIAN_PRIORITY=critical") 122 | 123 | _, err = tachyon.RunCommandInEnv(env, e, "apt-get", "install", "-y", a.Pkg) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | rd.Set("installed", canVer) 129 | 130 | return tachyon.WrapResult(true, rd), nil 131 | } 132 | 133 | func init() { 134 | tachyon.RegisterCommand("apt", &Apt{}) 135 | } 136 | -------------------------------------------------------------------------------- /package/apt/apt_test.go: -------------------------------------------------------------------------------- 1 | package apt 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vektra/tachyon" 6 | "os/exec" 7 | "testing" 8 | ) 9 | 10 | var runAptTests = false 11 | 12 | func init() { 13 | c := exec.Command("which", "apt-cache") 14 | c.Run() 15 | runAptTests = c.ProcessState.Success() 16 | } 17 | 18 | func TestAptDryRun(t *testing.T) { 19 | if !runAptTests { 20 | return 21 | } 22 | 23 | res, err := tachyon.RunAdhocTask("apt", "pkg=acct dryrun=true") 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | if !res.Changed { 29 | t.Error("No change detected") 30 | } 31 | 32 | if res.Data.Get("installed") != "" { 33 | t.Error("incorrectly found an installed version") 34 | } 35 | 36 | if res.Data.Get("candidate") == "" { 37 | t.Error("no candidate found") 38 | } 39 | 40 | if res.Data.Get("dryrun") != true { 41 | t.Error("dryrun not true") 42 | } 43 | } 44 | 45 | func removeAcct() { 46 | exec.Command("apt-get", "remove", "-y", "--force-yes", "acct").CombinedOutput() 47 | } 48 | 49 | func TestAptInstallAndRemoves(t *testing.T) { 50 | if !runAptTests { 51 | return 52 | } 53 | 54 | defer removeAcct() 55 | 56 | res, err := tachyon.RunAdhocTask("apt", "pkg=acct") 57 | if err != nil { 58 | panic(err) 59 | } 60 | 61 | if !res.Changed { 62 | t.Fatal("No change detected") 63 | } 64 | 65 | grep := fmt.Sprintf(`apt-cache policy acct | grep "Installed: %s"`, 66 | res.Data.Get("installed")) 67 | 68 | _, err = exec.Command("sh", "-c", grep).CombinedOutput() 69 | 70 | if err != nil { 71 | t.Errorf("package did not install") 72 | } 73 | 74 | // Test that it skips too 75 | // Do this here instead of another test because installing is slow 76 | 77 | res2, err := tachyon.RunAdhocTask("apt", "pkg=acct") 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | if res2.Changed { 83 | t.Fatal("acct was reinstalled incorrectly") 84 | } 85 | 86 | res3, err := tachyon.RunAdhocTask("apt", "pkg=acct state=absent") 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | if !res3.Changed { 92 | t.Fatal("acct was not removed") 93 | } 94 | 95 | if res3.Data.Get("removed") != res.Data.Get("installed") { 96 | t.Fatalf("removed isn't set to the version removed: '%s '%s'", 97 | res3.Data.Get("removed"), res.Data.Get("installed")) 98 | } 99 | 100 | res4, err := tachyon.RunAdhocTask("apt", "pkg=acct state=absent") 101 | if err != nil { 102 | panic(err) 103 | } 104 | 105 | if res4.Changed { 106 | t.Fatal("acct was removed again") 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /package/package.go: -------------------------------------------------------------------------------- 1 | package packages 2 | 3 | import ( 4 | _ "github.com/vektra/tachyon/package/apt" 5 | ) 6 | -------------------------------------------------------------------------------- /path.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "path/filepath" 5 | ) 6 | 7 | type Paths interface { 8 | Base() string 9 | Role(name string) string 10 | Vars(name string) string 11 | Task(name string) string 12 | Handler(name string) string 13 | File(name string) string 14 | Meta(name string) string 15 | } 16 | 17 | type SimplePath struct { 18 | Root string 19 | } 20 | 21 | func (s SimplePath) Base() string { 22 | return s.Root 23 | } 24 | 25 | func (s SimplePath) Role(name string) string { 26 | return filepath.Join(s.Root, "roles", name) 27 | } 28 | 29 | func (s SimplePath) Vars(name string) string { 30 | return filepath.Join(s.Root, name) 31 | } 32 | 33 | func (s SimplePath) Task(name string) string { 34 | return filepath.Join(s.Root, name) 35 | } 36 | 37 | func (s SimplePath) Handler(name string) string { 38 | return filepath.Join(s.Root, name) 39 | } 40 | 41 | func (s SimplePath) File(name string) string { 42 | return filepath.Join(s.Root, name) 43 | } 44 | 45 | func (s SimplePath) Meta(name string) string { 46 | return filepath.Join(s.Root, name) 47 | } 48 | 49 | type SeparatePaths struct { 50 | Top string 51 | Root string 52 | } 53 | 54 | func (s SeparatePaths) Base() string { 55 | return s.Root 56 | } 57 | 58 | func (s SeparatePaths) Role(name string) string { 59 | return filepath.Join(s.Top, "roles", name) 60 | } 61 | 62 | func (s SeparatePaths) Vars(name string) string { 63 | return filepath.Join(s.Root, "vars", name) 64 | } 65 | 66 | func (s SeparatePaths) Task(name string) string { 67 | return filepath.Join(s.Root, "tasks", name) 68 | } 69 | 70 | func (s SeparatePaths) Handler(name string) string { 71 | return filepath.Join(s.Root, "handlers", name) 72 | } 73 | 74 | func (s SeparatePaths) File(name string) string { 75 | return filepath.Join(s.Root, "files", name) 76 | } 77 | 78 | func (s SeparatePaths) Meta(name string) string { 79 | return filepath.Join(s.Root, "meta", name) 80 | } 81 | -------------------------------------------------------------------------------- /playbook.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/flynn/go-shlex" 7 | "io/ioutil" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | type VarsFiles []interface{} 15 | 16 | type Notifications []string 17 | 18 | type Play struct { 19 | Hosts string 20 | Connection string 21 | Vars Scope 22 | VarsFiles VarsFiles 23 | Tasks Tasks 24 | Handlers Tasks 25 | Roles []string 26 | Modules map[string]*Module 27 | 28 | baseDir string 29 | } 30 | 31 | type Playbook struct { 32 | Path string 33 | baseDir string 34 | Plays []*Play 35 | Env *Environment 36 | Vars *NestedScope 37 | } 38 | 39 | func NewPlaybook(env *Environment, p string) (*Playbook, error) { 40 | baseDir, err := filepath.Abs(filepath.Dir(p)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | pb := &Playbook{ 46 | Path: p, 47 | baseDir: baseDir, 48 | Env: env, 49 | Vars: NewNestedScope(env.Vars), 50 | } 51 | 52 | pb.Vars.Set("playbook_dir", baseDir) 53 | 54 | cur, err := os.Getwd() 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | defer os.Chdir(cur) 60 | os.Chdir(baseDir) 61 | 62 | defer env.SetPaths(env.SetPaths(SimplePath{"."})) 63 | 64 | plays, err := pb.LoadPlays(filepath.Base(p), pb.Vars) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | pb.Plays = plays 70 | 71 | return pb, nil 72 | } 73 | 74 | type playData struct { 75 | Include string 76 | Vars map[string]interface{} 77 | Hosts string 78 | Vars_files []interface{} 79 | Tasks []TaskData 80 | Handlers []TaskData 81 | Roles []interface{} 82 | } 83 | 84 | var eInvalidPlaybook = errors.New("Invalid playbook yaml") 85 | 86 | func (pb *Playbook) LoadPlays(fpath string, s Scope) ([]*Play, error) { 87 | var seq []playData 88 | 89 | var plays []*Play 90 | 91 | err := yamlFile(fpath, &seq) 92 | 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | for _, item := range seq { 98 | if item.Include != "" { 99 | spath := item.Include 100 | 101 | // Make a new scope and put the vars into it. The subplays 102 | // will use this scope as their parent. 103 | ns := NewNestedScope(s) 104 | 105 | if item.Vars != nil { 106 | if err := ns.addVars(item.Vars); err != nil { 107 | return nil, err 108 | } 109 | } 110 | 111 | parts, err := shlex.Split(spath) 112 | if err == nil { 113 | spath = parts[0] 114 | for _, tok := range parts[1:] { 115 | if k, v, ok := split2(tok, "="); ok { 116 | ns.Set(k, inferString(v)) 117 | } 118 | } 119 | } 120 | 121 | sub, err := pb.LoadPlays(path.Join(pb.baseDir, spath), ns.Flatten()) 122 | 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | if !ns.Empty() { 128 | for _, play := range sub { 129 | play.Vars = SpliceOverrides(play.Vars, ns) 130 | } 131 | } 132 | 133 | plays = append(plays, sub...) 134 | } else { 135 | play, err := parsePlay(pb.Env, s, fpath, pb.baseDir, &item) 136 | 137 | if err != nil { 138 | return nil, err 139 | } 140 | 141 | plays = append(plays, play) 142 | } 143 | } 144 | 145 | return plays, nil 146 | } 147 | 148 | func formatError(where string) error { 149 | return fmt.Errorf("Invalid playbook yaml: %s", where) 150 | } 151 | 152 | func (p *Play) importTasks(env *Environment, tasks *Tasks, file string, s Scope, tds []TaskData) error { 153 | for _, x := range tds { 154 | if _, ok := x["include"]; ok { 155 | err := p.decodeTasksFile(env, tasks, s, x) 156 | if err != nil { 157 | return err 158 | } 159 | } else { 160 | task := &Task{data: x, Play: p, File: file} 161 | err := task.Init(env) 162 | if err != nil { 163 | return err 164 | } 165 | 166 | *tasks = append(*tasks, task) 167 | } 168 | } 169 | 170 | return nil 171 | } 172 | 173 | func (p *Play) decodeTasksFile(env *Environment, tasks *Tasks, s Scope, td TaskData) error { 174 | path := td["include"].(string) 175 | 176 | parts := strings.SplitN(path, " ", 2) 177 | 178 | path, err := ExpandVars(s, parts[0]) 179 | if err != nil { 180 | return err 181 | } 182 | 183 | args := "" 184 | 185 | if len(parts) == 2 { 186 | args = parts[1] 187 | } 188 | 189 | filePath := env.Paths.Task(path) 190 | 191 | return p.importTasksFile(env, tasks, filePath, args, s, td) 192 | } 193 | 194 | func (p *Play) importTasksFile(env *Environment, tasks *Tasks, filePath string, args string, s Scope, td TaskData) error { 195 | 196 | var tds []TaskData 197 | 198 | err := yamlFile(filePath, &tds) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | iv := make(Vars) 204 | 205 | if args != "" { 206 | sm, err := ParseSimpleMap(s, args) 207 | if err != nil { 208 | return err 209 | } 210 | 211 | for k, v := range sm { 212 | iv[k] = v 213 | } 214 | } 215 | 216 | // Inject yaml structured vars 217 | if xvars, ok := td["vars"]; ok { 218 | if cast, ok := xvars.(map[interface{}]interface{}); ok { 219 | for gk, gv := range cast { 220 | iv[gk.(string)] = Any(gv) 221 | } 222 | } 223 | } 224 | 225 | // Inject all additional keys 226 | for k, v := range td { 227 | switch k { 228 | case "include", "vars": 229 | continue 230 | default: 231 | iv[k] = Any(v) 232 | } 233 | } 234 | 235 | for _, x := range tds { 236 | if _, ok := x["include"]; ok { 237 | err := p.decodeTasksFile(env, tasks, s, x) 238 | if err != nil { 239 | return err 240 | } 241 | } else { 242 | task := &Task{data: x, Play: p, File: filePath} 243 | err := task.Init(env) 244 | if err != nil { 245 | return err 246 | } 247 | task.IncludeVars = iv 248 | *tasks = append(*tasks, task) 249 | } 250 | } 251 | 252 | return nil 253 | } 254 | 255 | func (p *Play) importMeta(env *Environment, path string, s Scope) error { 256 | var m map[string]interface{} 257 | 258 | err := yamlFile(path, &m) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | if deps, ok := m["dependencies"]; ok { 264 | if seq, ok := deps.([]interface{}); ok { 265 | for _, m := range seq { 266 | name, err := p.importRole(env, m, s) 267 | if err != nil { 268 | return err 269 | } 270 | 271 | p.Roles = append(p.Roles, name) 272 | } 273 | } 274 | } 275 | 276 | return nil 277 | } 278 | 279 | type Module struct { 280 | Name string 281 | TaskDatas []TaskData `yaml:"tasks"` 282 | RawVars map[string]interface{} `yaml:"vars"` 283 | 284 | ModVars Vars 285 | ModTasks []*Task 286 | } 287 | 288 | func (p *Play) importModule(env *Environment, path string, s Scope) error { 289 | var mod Module 290 | 291 | err := yamlFile(path, &mod) 292 | if err != nil { 293 | return err 294 | } 295 | 296 | mod.ModVars = make(Vars) 297 | 298 | // Inject yaml structured vars 299 | if mod.RawVars != nil { 300 | for k, v := range mod.RawVars { 301 | mod.ModVars[k] = Any(v) 302 | } 303 | } 304 | 305 | for _, x := range mod.TaskDatas { 306 | task := &Task{data: x, Play: p, File: path} 307 | err := task.Init(env) 308 | if err != nil { 309 | return err 310 | } 311 | mod.ModTasks = append(mod.ModTasks, task) 312 | } 313 | 314 | p.Modules[mod.Name] = &mod 315 | 316 | return nil 317 | } 318 | 319 | func (p *Play) importRole(env *Environment, o interface{}, s Scope) (string, error) { 320 | var role string 321 | 322 | ts := NewNestedScope(s) 323 | td := TaskData{} 324 | 325 | switch so := o.(type) { 326 | case string: 327 | role = so 328 | case map[interface{}]interface{}: 329 | for k, v := range so { 330 | sk := k.(string) 331 | 332 | if sk == "role" { 333 | role = v.(string) 334 | } else { 335 | ts.Set(sk, v) 336 | td[sk] = v 337 | } 338 | } 339 | default: 340 | return "", formatError("role not a map") 341 | } 342 | 343 | parts := strings.SplitN(role, " ", 2) 344 | 345 | if len(parts) == 2 { 346 | role = parts[0] 347 | 348 | sm, err := ParseSimpleMap(ts, parts[1]) 349 | if err != nil { 350 | return "", err 351 | } 352 | 353 | for k, v := range sm { 354 | td[k] = v 355 | } 356 | } 357 | 358 | parent, child, specific := split2(role, "::") 359 | if specific { 360 | role = parent 361 | } 362 | 363 | dir := env.Paths.Role(role) 364 | 365 | if _, err := os.Stat(dir); err != nil { 366 | return "", fmt.Errorf("No role named %s available", role) 367 | } 368 | 369 | base := p.baseDir 370 | 371 | cur := env.Paths 372 | 373 | sep := SeparatePaths{Top: base, Root: cur.Role(role)} 374 | 375 | defer env.SetPaths(env.SetPaths(sep)) 376 | 377 | if specific { 378 | taskPath := env.Paths.Task(child + ".yml") 379 | 380 | if fileExist(taskPath) { 381 | err := p.importTasksFile(env, &p.Tasks, taskPath, "", ts, td) 382 | if err != nil { 383 | return "", err 384 | } 385 | } else { 386 | return "", fmt.Errorf("Missing specific tasks %s::%s", role, child) 387 | } 388 | 389 | return parent + "::" + child, nil 390 | } 391 | 392 | metaPath := env.Paths.Meta("main.yml") 393 | 394 | if fileExist(metaPath) { 395 | err := p.importMeta(env, metaPath, ts) 396 | if err != nil { 397 | return "", err 398 | } 399 | } 400 | 401 | taskPath := env.Paths.Task("main.yml") 402 | 403 | if fileExist(taskPath) { 404 | err := p.importTasksFile(env, &p.Tasks, taskPath, "", ts, td) 405 | if err != nil { 406 | return "", err 407 | } 408 | } 409 | 410 | handlers := env.Paths.Handler("main.yml") 411 | 412 | if fileExist(handlers) { 413 | err := p.importTasksFile(env, &p.Handlers, handlers, "", ts, td) 414 | if err != nil { 415 | return "", err 416 | } 417 | } 418 | 419 | vars := filepath.Join(base, "roles", role, "vars", "main.yml") 420 | 421 | if fileExist(vars) { 422 | err := ImportVarsFile(p.Vars, vars) 423 | if err != nil { 424 | return "", err 425 | } 426 | } 427 | 428 | modules := filepath.Join(env.Paths.Base(), "modules") 429 | 430 | if files, err := ioutil.ReadDir(modules); err == nil { 431 | for _, file := range files { 432 | if !file.IsDir() { 433 | err := p.importModule(env, filepath.Join(modules, file.Name()), ts) 434 | if err != nil { 435 | return "", err 436 | } 437 | } 438 | } 439 | } 440 | 441 | return role, nil 442 | } 443 | 444 | func (play *Play) importVarsFiles(env *Environment) error { 445 | for _, file := range play.VarsFiles { 446 | switch file := file.(type) { 447 | case string: 448 | ImportVarsFile(play.Vars, env.Paths.Vars(file)) 449 | break 450 | case []interface{}: 451 | for _, ent := range file { 452 | epath := env.Paths.Vars(ent.(string)) 453 | 454 | if _, err := os.Stat(epath); err == nil { 455 | err = ImportVarsFile(play.Vars, epath) 456 | 457 | if err != nil { 458 | return err 459 | } 460 | 461 | break 462 | } 463 | } 464 | } 465 | } 466 | 467 | return nil 468 | } 469 | 470 | func parsePlay(env *Environment, s Scope, file, dir string, m *playData) (*Play, error) { 471 | var play Play 472 | 473 | if m.Hosts == "" { 474 | m.Hosts = "all" 475 | } 476 | 477 | play.Hosts = m.Hosts 478 | play.Vars = NewNestedScope(s) 479 | play.Modules = make(map[string]*Module) 480 | 481 | var err error 482 | 483 | for sk, iv := range m.Vars { 484 | if sv, ok := iv.(string); ok { 485 | iv, err = ExpandVars(s, sv) 486 | if err != nil { 487 | return nil, err 488 | } 489 | } 490 | 491 | play.Vars.Set(sk, iv) 492 | } 493 | 494 | play.VarsFiles = m.Vars_files 495 | play.baseDir = dir 496 | 497 | err = play.importVarsFiles(env) 498 | if err != nil { 499 | return nil, err 500 | } 501 | 502 | if len(m.Tasks) > 0 { 503 | err := play.importTasks(env, &play.Tasks, file, s, m.Tasks) 504 | if err != nil { 505 | return nil, err 506 | } 507 | } 508 | 509 | if len(m.Handlers) > 0 { 510 | err := play.importTasks(env, &play.Handlers, file, s, m.Tasks) 511 | if err != nil { 512 | return nil, err 513 | } 514 | } 515 | 516 | for _, role := range m.Roles { 517 | name, err := play.importRole(env, role, s) 518 | if err != nil { 519 | return nil, err 520 | } 521 | 522 | play.Roles = append(play.Roles, name) 523 | } 524 | 525 | return &play, nil 526 | } 527 | -------------------------------------------------------------------------------- /playbook_test.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "path/filepath" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestSimplePlaybook(t *testing.T) { 10 | env := NewEnv(NewNestedScope(nil), DefaultConfig) 11 | p, err := NewPlaybook(env, "test/playbook1.yml") 12 | 13 | if err != nil { 14 | panic(err) 15 | } 16 | 17 | if len(p.Plays) != 2 { 18 | t.Fatalf("Didn't load 2 playbooks, loaded: %d", len(p.Plays)) 19 | } 20 | 21 | x := p.Plays[1] 22 | 23 | if x.Hosts != "all" { 24 | t.Errorf("Hosts not all: was %s", x.Hosts) 25 | } 26 | 27 | vars := x.Vars 28 | 29 | a, ok := vars.Get("answer") 30 | 31 | if !ok { 32 | t.Fatalf("No var 'answer'") 33 | } 34 | 35 | if a.Read() != "Wuh, I think so" { 36 | t.Errorf("Unable to decode string var: %#v", a) 37 | } 38 | 39 | a, ok = vars.Get("port") 40 | 41 | if !ok { 42 | t.Fatalf("No var 'port'") 43 | } 44 | 45 | if a.Read() != 5150 { 46 | t.Errorf("Unable to decode numeric var: %#v", a.Read()) 47 | } 48 | 49 | if len(x.VarsFiles) != 2 { 50 | t.Fatalf("Unable to decode varsfiles, got %d", len(x.VarsFiles)) 51 | } 52 | 53 | f := x.VarsFiles[0] 54 | 55 | if f != "common_vars.yml" { 56 | t.Errorf("Unable to decode literal vars_files") 57 | } 58 | 59 | f2 := x.VarsFiles[1].([]interface{}) 60 | 61 | if f2[1].(string) != "default_os.yml" { 62 | t.Errorf("Unable to decode list vars_files") 63 | } 64 | 65 | tasks := x.Tasks 66 | 67 | if len(tasks) < 5 { 68 | t.Errorf("Failed to decode the proper number of tasks: %d", len(tasks)) 69 | } 70 | 71 | if tasks[3].Args() != "echo {{port}}" { 72 | t.Errorf("Failed to decode templating in action: %#v", tasks[3].Args()) 73 | } 74 | } 75 | 76 | func totalRuntime(results []RunResult) time.Duration { 77 | cur := time.Duration(0) 78 | 79 | for _, res := range results { 80 | cur += res.Runtime 81 | } 82 | 83 | return cur 84 | } 85 | 86 | func TestPlaybookFuturesRunInParallel(t *testing.T) { 87 | run, _, err := RunCapture("test/future.yml") 88 | if err != nil { 89 | t.Fatalf("Unable to load test/future.yml") 90 | } 91 | 92 | total := run.Runtime.Seconds() 93 | 94 | if total > 1.1 || total < 0.9 { 95 | t.Errorf("Futures did not run in parallel: %f", total) 96 | } 97 | } 98 | 99 | func TestPlaybookFuturesCanBeWaitedOn(t *testing.T) { 100 | run, _, err := RunCapture("test/future.yml") 101 | if err != nil { 102 | t.Fatalf("Unable to load test/future.yml") 103 | } 104 | 105 | total := run.Runtime.Seconds() 106 | 107 | if total > 1.1 || total < 0.9 { 108 | t.Errorf("Futures did not run in parallel: %f", total) 109 | } 110 | } 111 | 112 | func TestPlaybookTaskIncludes(t *testing.T) { 113 | res, _, err := RunCapture("test/inc_parent.yml") 114 | if err != nil { 115 | t.Fatalf("Unable to run test/inc_parent.yml") 116 | } 117 | 118 | if filepath.Base(res.Results[0].Task.File) != "inc_child.yml" { 119 | t.Fatalf("Did not include tasks from child") 120 | } 121 | } 122 | 123 | func TestPlaybookTaskIncludesCanHaveVars(t *testing.T) { 124 | res, _, err := RunCapture("test/inc_parent2.yml") 125 | if err != nil { 126 | t.Fatalf("Unable to run test/inc_parent2.yml: %s", err) 127 | } 128 | 129 | d := res.Results[0].Result 130 | 131 | if v, ok := d.Get("stdout"); !ok || v.Read() != "oscar" { 132 | t.Fatalf("A variable was not passed into the included file") 133 | } 134 | 135 | d = res.Results[1].Result 136 | 137 | if v, ok := d.Get("stdout"); !ok || v.Read() != "ellen" { 138 | t.Fatalf("A variable was not passed into the included file") 139 | } 140 | 141 | d = res.Results[2].Result 142 | 143 | if v, ok := d.Get("stdout"); !ok || v.Read() != "Los Angeles" { 144 | t.Fatalf("A variable was not passed into the included file") 145 | } 146 | } 147 | 148 | func TestPlaybookRoleTasksInclude(t *testing.T) { 149 | res, _, err := RunCapture("test/site1.yml") 150 | if err != nil { 151 | t.Fatalf("Unable to run test/site1.yml: %s", err) 152 | } 153 | 154 | if len(res.Results) == 0 { 155 | t.Fatalf("tasks were not included from the role") 156 | } 157 | 158 | d := res.Results[0].Result 159 | 160 | if v, ok := d.Get("stdout"); !ok || v.Read() != "in role" { 161 | t.Fatalf("Task did not run from role") 162 | } 163 | } 164 | 165 | func TestPlaybookRoleHandlersInclude(t *testing.T) { 166 | res, _, err := RunCapture("test/site1.yml") 167 | if err != nil { 168 | t.Fatalf("Unable to run test/site1.yml: %s", err) 169 | } 170 | 171 | if len(res.Results) == 0 { 172 | t.Fatalf("tasks were not included from the role") 173 | } 174 | 175 | d := res.Results[1].Result 176 | 177 | if v, ok := d.Get("stdout"); !ok || v.Read() != "in role handler" { 178 | t.Fatalf("Task did not run from role") 179 | } 180 | } 181 | 182 | func TestPlaybookRoleVarsInclude(t *testing.T) { 183 | res, _, err := RunCapture("test/site2.yml") 184 | if err != nil { 185 | t.Fatalf("Unable to run test/site2.yml: %s", err) 186 | } 187 | 188 | if len(res.Results) == 0 { 189 | t.Fatalf("tasks were not included from the role") 190 | } 191 | 192 | d := res.Results[0].Result 193 | 194 | if v, ok := d.Get("stdout"); !ok || v.Read() != "from role var" { 195 | t.Fatalf("Task did not run from role") 196 | } 197 | } 198 | 199 | func TestPlaybookRoleAcceptsVars(t *testing.T) { 200 | res, _, err := RunCapture("test/site3.yml") 201 | if err != nil { 202 | t.Fatalf("Unable to run test/site3.yml: %s", err) 203 | } 204 | 205 | if len(res.Results) == 0 { 206 | t.Fatalf("tasks were not included from the role") 207 | } 208 | 209 | d := res.Results[0].Result 210 | 211 | if v, ok := d.Get("stdout"); !ok || v.Read() != "from site3" { 212 | t.Fatalf("Task did not run from role") 213 | } 214 | } 215 | 216 | func TestPlaybookRoleAcceptsInlineVars(t *testing.T) { 217 | res, _, err := RunCapture("test/site4.yml") 218 | if err != nil { 219 | t.Fatalf("Unable to run test/site4.yml: %s", err) 220 | } 221 | 222 | if len(res.Results) == 0 { 223 | t.Fatalf("tasks were not included from the role") 224 | } 225 | 226 | d := res.Results[0].Result 227 | 228 | if v, ok := d.Get("stdout"); !ok || v.Read() != "from site4" { 229 | t.Fatalf("Task did not run from role: %#v", d) 230 | } 231 | } 232 | 233 | func TestPlaybookRoleIncludesSeeRoleFiles(t *testing.T) { 234 | res, _, err := RunCapture("test/site5.yml") 235 | if err != nil { 236 | t.Fatalf("Unable to run test/site5.yml: %s", err) 237 | } 238 | 239 | if len(res.Results) == 0 { 240 | t.Fatalf("tasks were not included from the role") 241 | } 242 | 243 | d := res.Results[0].Result 244 | 245 | if v, ok := d.Get("stdout"); !ok || v.Read() != "in special" { 246 | t.Fatalf("Task did not run from role: %#v", d) 247 | } 248 | } 249 | 250 | func TestPlaybookRoleFilesAreSeen(t *testing.T) { 251 | res, _, err := RunCapture("test/site6.yml") 252 | if err != nil { 253 | t.Fatalf("Unable to run test/site6.yml: %s", err) 254 | } 255 | 256 | if len(res.Results) == 0 { 257 | t.Fatalf("tasks were not included from the role") 258 | } 259 | 260 | d := res.Results[0].Result 261 | 262 | if v, ok := d.Get("stdout"); !ok || v.Read() != "in my script" { 263 | t.Fatalf("Task did not run from role: %#v", d) 264 | } 265 | } 266 | 267 | func TestPlaybookRoleDependenciesAreInvoked(t *testing.T) { 268 | res, _, err := RunCapture("test/site7.yml") 269 | if err != nil { 270 | t.Fatalf("Unable to run test/site7.yml: %s", err) 271 | } 272 | 273 | if len(res.Results) == 0 { 274 | t.Fatalf("tasks were not included from the role") 275 | } 276 | 277 | d := res.Results[0].Result 278 | 279 | if v, ok := d.Get("stdout"); !ok || v.Read() != "role7" { 280 | t.Fatalf("Task did not run from role: %#v", d) 281 | } 282 | } 283 | 284 | func TestPlaybookWithItems(t *testing.T) { 285 | res, _, err := RunCapture("test/items.yml") 286 | if err != nil { 287 | t.Fatalf("Unable to run test/items.yml: %s", err) 288 | } 289 | 290 | if len(res.Results) != 3 { 291 | t.Fatalf("tasks were not included from the role") 292 | } 293 | 294 | if v, ok := res.Results[0].Result.Get("stdout"); !ok || v.Read() != "a" { 295 | t.Fatal("first isnt 'a'") 296 | } 297 | 298 | if v, ok := res.Results[1].Result.Get("stdout"); !ok || v.Read() != "b" { 299 | t.Fatal("second isnt 'b'") 300 | } 301 | 302 | if v, ok := res.Results[2].Result.Get("stdout"); !ok || v.Read() != "c" { 303 | t.Fatal("third isnt 'c'") 304 | } 305 | 306 | } 307 | 308 | func TestPlaybookRoleModulesAreAvailable(t *testing.T) { 309 | res, _, err := RunCapture("test/site8.yml") 310 | if err != nil { 311 | t.Fatalf("Unable to run test/site8.yml: %s", err) 312 | } 313 | 314 | if len(res.Results) == 0 { 315 | t.Fatalf("tasks were not included from the role") 316 | } 317 | 318 | d := res.Results[0].Result 319 | 320 | if v, ok := d.Get("stdout"); !ok || v.Read() != "from module" { 321 | t.Fatalf("Task did not run from role: %#v", d) 322 | } 323 | } 324 | 325 | func TestPlaybookRoleModulesCanUseYAMLArgs(t *testing.T) { 326 | res, _, err := RunCapture("test/site9.yml") 327 | if err != nil { 328 | t.Fatalf("Unable to run test/site9.yml: %s", err) 329 | } 330 | 331 | if len(res.Results) == 0 { 332 | t.Fatalf("tasks were not included from the role") 333 | } 334 | 335 | d := res.Results[0].Result 336 | 337 | if v, ok := d.Get("stdout"); !ok || v.Read() != "from module" { 338 | t.Fatalf("Task did not run from role: %#v", d) 339 | } 340 | } 341 | 342 | func TestPlaybookRoleSubTasks(t *testing.T) { 343 | res, _, err := RunCapture("test/site10.yml") 344 | if err != nil { 345 | t.Fatalf("Unable to run test/site10.yml: %s", err) 346 | } 347 | 348 | if len(res.Results) == 0 { 349 | t.Fatalf("tasks were not included from the role") 350 | } 351 | 352 | d := res.Results[0].Result 353 | 354 | if v, ok := d.Get("stdout"); !ok || v.Read() != "in get" { 355 | t.Fatalf("Task did not run from role: %#v", d) 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /procmgmt/procmgmt.go: -------------------------------------------------------------------------------- 1 | package procmgmt 2 | 3 | import ( 4 | _ "github.com/vektra/tachyon/procmgmt/upstart" 5 | ) 6 | -------------------------------------------------------------------------------- /procmgmt/upstart/poststart.sample: -------------------------------------------------------------------------------- 1 | echo "this is a poststart sample script" 2 | -------------------------------------------------------------------------------- /procmgmt/upstart/poststop.sample: -------------------------------------------------------------------------------- 1 | echo "this is a poststop sample script" 2 | -------------------------------------------------------------------------------- /procmgmt/upstart/prestart.sample: -------------------------------------------------------------------------------- 1 | echo "this is a prestart sample script" 2 | -------------------------------------------------------------------------------- /procmgmt/upstart/prestop.sample: -------------------------------------------------------------------------------- 1 | echo "this is a prestop sample script" 2 | -------------------------------------------------------------------------------- /procmgmt/upstart/test-daemon.conf.sample: -------------------------------------------------------------------------------- 1 | description "a test daemon" 2 | 3 | start on filesystem or runlevel [2345] 4 | stop on runlevel [!2345] 5 | 6 | umask 022 7 | 8 | # 'sshd -D' leaks stderr and confuses things in conjunction with 'console log' 9 | console none 10 | 11 | exec /bin/date 12 | -------------------------------------------------------------------------------- /procmgmt/upstart/upstart.go: -------------------------------------------------------------------------------- 1 | package upstart 2 | 3 | import ( 4 | "fmt" 5 | "github.com/vektra/tachyon" 6 | us "github.com/vektra/tachyon/upstart" 7 | "io/ioutil" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | type Install struct { 13 | Name string `tachyon:"name"` 14 | File string `tachyon:"file"` 15 | } 16 | 17 | func (d *Install) Run(env *tachyon.CommandEnv) (*tachyon.Result, error) { 18 | dest := filepath.Join("/etc/init", d.Name+".conf") 19 | 20 | cpy := &tachyon.CopyCmd{ 21 | Src: d.File, 22 | Dest: dest, 23 | } 24 | 25 | res, err := cpy.Run(env) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | res.Add("name", d.Name) 31 | 32 | return res, nil 33 | } 34 | 35 | type Daemon struct { 36 | Name string `tachyon:"name"` 37 | Command string `tachyon:"command"` 38 | Foreground bool `tachyon:"foreground"` 39 | OneFork bool `tachyon:"one_fork"` 40 | Instance string `tachyon:"instance"` 41 | PreStart string `tachyon:"pre_start"` 42 | PostStart string `tachyon:"post_start"` 43 | PreStop string `tachyon:"pre_stop"` 44 | PostStop string `tachyon:"post_stop"` 45 | Env map[string]string `tachyon:"env"` 46 | } 47 | 48 | func setScript(env *tachyon.CommandEnv, code *us.Code, val string) error { 49 | if val == "" { 50 | return nil 51 | } 52 | 53 | if val[0] == '@' { 54 | body, err := ioutil.ReadFile(env.Paths.File(val[1:])) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | code.Script = us.Script(body) 60 | } else { 61 | code.Script = us.Script(val) 62 | } 63 | 64 | return nil 65 | } 66 | 67 | func (d *Daemon) Run(env *tachyon.CommandEnv) (*tachyon.Result, error) { 68 | cfg := us.DaemonConfig(d.Name, d.Command) 69 | cfg.Env = d.Env 70 | 71 | if d.Foreground { 72 | cfg.Foreground() 73 | } 74 | 75 | if d.OneFork { 76 | cfg.Expect = "fork" 77 | } 78 | 79 | err := setScript(env, &cfg.PreStart, d.PreStart) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | err = setScript(env, &cfg.PostStart, d.PostStart) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | err = setScript(env, &cfg.PreStop, d.PreStop) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | err = setScript(env, &cfg.PostStop, d.PostStop) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | cfg.Instance = d.Instance 100 | 101 | err = cfg.Install() 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | res := tachyon.NewResult(true) 107 | res.Add("name", d.Name) 108 | 109 | return res, nil 110 | } 111 | 112 | type Task struct { 113 | Name string `tachyon:"name"` 114 | Command string `tachyon:"command"` 115 | Instance string `tachyon:"instance"` 116 | PreStart string `tachyon:"pre_start"` 117 | PostStart string `tachyon:"post_start"` 118 | PreStop string `tachyon:"pre_stop"` 119 | PostStop string `tachyon:"post_stop"` 120 | Env map[string]string `tachyon:"env"` 121 | } 122 | 123 | func (t *Task) Run(env *tachyon.CommandEnv) (*tachyon.Result, error) { 124 | cfg := us.TaskConfig(t.Name, t.Command) 125 | cfg.Env = t.Env 126 | 127 | cfg.Instance = t.Instance 128 | 129 | err := setScript(env, &cfg.PreStart, t.PreStart) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | err = setScript(env, &cfg.PostStart, t.PostStart) 135 | if err != nil { 136 | return nil, err 137 | } 138 | 139 | err = setScript(env, &cfg.PreStop, t.PreStop) 140 | if err != nil { 141 | return nil, err 142 | } 143 | 144 | err = setScript(env, &cfg.PostStop, t.PostStop) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | err = cfg.Install() 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | res := tachyon.NewResult(true) 155 | res.Add("name", t.Name) 156 | 157 | return res, nil 158 | } 159 | 160 | type Restart struct { 161 | Name string `tachyon:"name"` 162 | } 163 | 164 | func (r *Restart) Run(env *tachyon.CommandEnv) (*tachyon.Result, error) { 165 | conn, err := us.Dial() 166 | if err != nil { 167 | return nil, err 168 | } 169 | 170 | job, err := conn.Job(r.Name) 171 | if err != nil { 172 | return nil, err 173 | } 174 | 175 | inst, err := job.Restart() 176 | if err != nil { 177 | return nil, err 178 | } 179 | 180 | pid, err := inst.Pid() 181 | if err != nil { 182 | return nil, err 183 | } 184 | 185 | res := tachyon.NewResult(true) 186 | res.Add("name", r.Name) 187 | res.Add("pid", pid) 188 | 189 | return res, nil 190 | } 191 | 192 | type Stop struct { 193 | Name string `tachyon:"name"` 194 | } 195 | 196 | func (r *Stop) Run(env *tachyon.CommandEnv) (*tachyon.Result, error) { 197 | conn, err := us.Dial() 198 | if err != nil { 199 | return nil, err 200 | } 201 | 202 | job, err := conn.Job(r.Name) 203 | if err != nil { 204 | return nil, err 205 | } 206 | 207 | err = job.Stop() 208 | if err != nil { 209 | if strings.Index(err.Error(), "Unknown instance") == 0 { 210 | res := tachyon.NewResult(false) 211 | res.Add("name", r.Name) 212 | 213 | return res, nil 214 | } 215 | } 216 | 217 | res := tachyon.NewResult(true) 218 | res.Add("name", r.Name) 219 | 220 | return res, nil 221 | } 222 | 223 | type Start struct { 224 | Name string `tachyon:"name"` 225 | Env map[string]string `tachyon:"env"` 226 | } 227 | 228 | func (r *Start) Run(env *tachyon.CommandEnv) (*tachyon.Result, error) { 229 | conn, err := us.Dial() 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | var ienv []string 235 | 236 | for k, v := range r.Env { 237 | ienv = append(ienv, fmt.Sprintf("%s=%s", k, v)) 238 | } 239 | 240 | job, err := conn.Job(r.Name) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | inst, err := job.StartWithOptions(ienv, true) 246 | if err != nil { 247 | if strings.Index(err.Error(), "Job is already running") == 0 { 248 | res := tachyon.NewResult(false) 249 | res.Add("name", r.Name) 250 | 251 | return res, nil 252 | } 253 | return nil, err 254 | } 255 | 256 | pid, err := inst.Pid() 257 | if err != nil { 258 | return nil, err 259 | } 260 | 261 | res := tachyon.NewResult(true) 262 | res.Add("name", r.Name) 263 | res.Add("pid", pid) 264 | 265 | return res, nil 266 | } 267 | 268 | func init() { 269 | tachyon.RegisterCommand("upstart/install", &Install{}) 270 | tachyon.RegisterCommand("upstart/daemon", &Daemon{}) 271 | tachyon.RegisterCommand("upstart/task", &Task{}) 272 | tachyon.RegisterCommand("upstart/restart", &Restart{}) 273 | tachyon.RegisterCommand("upstart/stop", &Stop{}) 274 | tachyon.RegisterCommand("upstart/start", &Start{}) 275 | } 276 | -------------------------------------------------------------------------------- /procmgmt/upstart/upstart_test.go: -------------------------------------------------------------------------------- 1 | package upstart 2 | 3 | import ( 4 | "bytes" 5 | "github.com/vektra/tachyon" 6 | us "github.com/vektra/tachyon/upstart" 7 | "io/ioutil" 8 | "os" 9 | "os/exec" 10 | "testing" 11 | ) 12 | 13 | var jobName string = "atd" 14 | var runUpstartTests = false 15 | 16 | func init() { 17 | if s := os.Getenv("TEST_JOB"); s != "" { 18 | jobName = s 19 | } 20 | 21 | c := exec.Command("which", "initctl") 22 | c.Run() 23 | runUpstartTests = c.ProcessState.Success() 24 | } 25 | 26 | func TestInstall(t *testing.T) { 27 | if !runUpstartTests { 28 | t.SkipNow() 29 | } 30 | 31 | dest := "/etc/init/upstart-test-daemon.conf" 32 | 33 | defer os.Remove(dest) 34 | 35 | opts := `name=upstart-test-daemon file=test-daemon.conf.sample` 36 | 37 | res, err := tachyon.RunAdhocTask("upstart/install", opts) 38 | if err != nil { 39 | panic(err) 40 | } 41 | 42 | if !res.Changed { 43 | t.Fatal("no change detected") 44 | } 45 | 46 | _, err = os.Stat(dest) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | } 51 | 52 | func TestDaemon(t *testing.T) { 53 | if !runUpstartTests { 54 | t.SkipNow() 55 | } 56 | 57 | defer os.Remove("/etc/init/upstart-test-daemon.conf") 58 | 59 | opts := `name=upstart-test-daemon command="date"` 60 | 61 | res, err := tachyon.RunAdhocTask("upstart/daemon", opts) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | if !res.Changed { 67 | t.Fatal("no change detected") 68 | } 69 | } 70 | 71 | func TestDaemonScripts(t *testing.T) { 72 | if !runUpstartTests { 73 | t.SkipNow() 74 | } 75 | 76 | dest := "/etc/init/upstart-test-daemon.conf" 77 | 78 | defer os.Remove(dest) 79 | 80 | opts := `name=upstart-test-daemon command="date" pre_start=@prestart.sample post_start=@poststart.sample pre_stop=@prestop.sample post_stop=@poststop.sample` 81 | 82 | res, err := tachyon.RunAdhocTask("upstart/daemon", opts) 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | if !res.Changed { 88 | t.Fatal("no change detected") 89 | } 90 | 91 | body, err := ioutil.ReadFile(dest) 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | idx := bytes.Index(body, []byte("this is a prestart sample script")) 97 | if idx == -1 { 98 | t.Error("config didn't contain our script") 99 | } 100 | 101 | idx = bytes.Index(body, []byte("this is a poststart sample script")) 102 | if idx == -1 { 103 | t.Error("config didn't contain our script") 104 | } 105 | 106 | idx = bytes.Index(body, []byte("this is a prestop sample script")) 107 | if idx == -1 { 108 | t.Error("config didn't contain our script") 109 | } 110 | 111 | idx = bytes.Index(body, []byte("this is a poststop sample script")) 112 | if idx == -1 { 113 | t.Error("config didn't contain our script") 114 | } 115 | } 116 | 117 | func TestTaskScripts(t *testing.T) { 118 | if !runUpstartTests { 119 | t.SkipNow() 120 | } 121 | 122 | dest := "/etc/init/upstart-test-daemon.conf" 123 | 124 | defer os.Remove(dest) 125 | 126 | opts := `name=upstart-test-daemon command="date" pre_start=@prestart.sample post_start=@poststart.sample pre_stop=@prestop.sample post_stop=@poststop.sample` 127 | 128 | res, err := tachyon.RunAdhocTask("upstart/task", opts) 129 | if err != nil { 130 | panic(err) 131 | } 132 | 133 | if !res.Changed { 134 | t.Fatal("no change detected") 135 | } 136 | 137 | body, err := ioutil.ReadFile(dest) 138 | if err != nil { 139 | panic(err) 140 | } 141 | 142 | idx := bytes.Index(body, []byte("this is a prestart sample script")) 143 | if idx == -1 { 144 | t.Error("config didn't contain our script") 145 | } 146 | 147 | idx = bytes.Index(body, []byte("this is a poststart sample script")) 148 | if idx == -1 { 149 | t.Error("config didn't contain our script") 150 | } 151 | 152 | idx = bytes.Index(body, []byte("this is a prestop sample script")) 153 | if idx == -1 { 154 | t.Error("config didn't contain our script") 155 | } 156 | 157 | idx = bytes.Index(body, []byte("this is a poststop sample script")) 158 | if idx == -1 { 159 | t.Error("config didn't contain our script") 160 | } 161 | } 162 | 163 | func TestTask(t *testing.T) { 164 | if !runUpstartTests { 165 | t.SkipNow() 166 | } 167 | 168 | defer os.Remove("/etc/init/upstart-test-task.conf") 169 | 170 | opts := `name=upstart-test-task command="date"` 171 | 172 | res, err := tachyon.RunAdhocTask("upstart/task", opts) 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | if !res.Changed { 178 | t.Fatal("no change detected") 179 | } 180 | } 181 | 182 | func TestRestart(t *testing.T) { 183 | if !runUpstartTests { 184 | t.SkipNow() 185 | } 186 | 187 | opts := "name=" + jobName 188 | 189 | u, err := us.Dial() 190 | if err != nil { 191 | panic(err) 192 | } 193 | 194 | j, err := u.Job(jobName) 195 | if err != nil { 196 | panic(err) 197 | } 198 | 199 | prev, err := j.Pid() 200 | if err != nil { 201 | panic(err) 202 | } 203 | 204 | res, err := tachyon.RunAdhocTask("upstart/restart", opts) 205 | if err != nil { 206 | panic(err) 207 | } 208 | 209 | if !res.Changed { 210 | t.Fatal("no change detected") 211 | } 212 | 213 | cur, err := j.Pid() 214 | if err != nil { 215 | panic(err) 216 | } 217 | 218 | if res.Data.Get("pid") != cur { 219 | t.Log(res.Data) 220 | t.Fatal("pid not set properly") 221 | } 222 | 223 | if prev == cur { 224 | t.Fatal("restart did not happen") 225 | } 226 | } 227 | 228 | func TestStop(t *testing.T) { 229 | if !runUpstartTests { 230 | t.SkipNow() 231 | } 232 | 233 | opts := "name=" + jobName 234 | 235 | u, err := us.Dial() 236 | if err != nil { 237 | panic(err) 238 | } 239 | 240 | j, err := u.Job(jobName) 241 | if err != nil { 242 | panic(err) 243 | } 244 | 245 | defer j.Start() 246 | 247 | res, err := tachyon.RunAdhocTask("upstart/stop", opts) 248 | if err != nil { 249 | panic(err) 250 | } 251 | 252 | if !res.Changed { 253 | t.Fatal("no change detected") 254 | } 255 | 256 | res, err = tachyon.RunAdhocTask("upstart/stop", opts) 257 | if err != nil { 258 | panic(err) 259 | } 260 | 261 | if res.Changed { 262 | t.Fatal("change detected improperly") 263 | } 264 | } 265 | 266 | func TestStart(t *testing.T) { 267 | if !runUpstartTests { 268 | t.SkipNow() 269 | } 270 | 271 | opts := "name=" + jobName 272 | 273 | u, err := us.Dial() 274 | if err != nil { 275 | panic(err) 276 | } 277 | 278 | j, err := u.Job(jobName) 279 | if err != nil { 280 | panic(err) 281 | } 282 | 283 | defer j.Start() 284 | 285 | err = j.Stop() 286 | if err != nil { 287 | panic(err) 288 | } 289 | 290 | res, err := tachyon.RunAdhocTask("upstart/start", opts) 291 | if err != nil { 292 | panic(err) 293 | } 294 | 295 | if !res.Changed { 296 | t.Fatal("no change detected") 297 | } 298 | 299 | act, ok := res.Get("pid") 300 | if !ok { 301 | t.Fatal("pid not set") 302 | } 303 | 304 | pid, err := j.Pid() 305 | if err != nil { 306 | panic(err) 307 | } 308 | 309 | if pid != act.Read() { 310 | t.Fatal("job did not start?") 311 | } 312 | 313 | res, err = tachyon.RunAdhocTask("upstart/start", opts) 314 | if err != nil { 315 | panic(err) 316 | } 317 | 318 | if res.Changed { 319 | t.Fatal("change detected improperly") 320 | } 321 | 322 | } 323 | 324 | func TestStartWithEnv(t *testing.T) { 325 | if !runUpstartTests { 326 | t.SkipNow() 327 | } 328 | 329 | opts := "name=" + jobName 330 | 331 | u, err := us.Dial() 332 | if err != nil { 333 | panic(err) 334 | } 335 | 336 | j, err := u.Job(jobName) 337 | if err != nil { 338 | panic(err) 339 | } 340 | 341 | defer j.Start() 342 | 343 | err = j.Stop() 344 | if err != nil { 345 | panic(err) 346 | } 347 | 348 | td := tachyon.TaskData{ 349 | "upstart/start": map[interface{}]interface{}{ 350 | "name": jobName, 351 | "env": map[interface{}]interface{}{ 352 | "BAR": "foo", 353 | }, 354 | }, 355 | } 356 | 357 | res, err := tachyon.RunAdhocTaskVars(td) 358 | if err != nil { 359 | panic(err) 360 | } 361 | 362 | if !res.Changed { 363 | t.Fatal("no change detected") 364 | } 365 | 366 | act, ok := res.Get("pid") 367 | if !ok { 368 | t.Fatal("pid not set") 369 | } 370 | 371 | pid, err := j.Pid() 372 | if err != nil { 373 | panic(err) 374 | } 375 | 376 | if pid != act.Read() { 377 | t.Fatal("job did not start?") 378 | } 379 | 380 | res, err = tachyon.RunAdhocTask("upstart/start", opts) 381 | if err != nil { 382 | panic(err) 383 | } 384 | 385 | if res.Changed { 386 | t.Fatal("change detected improperly") 387 | } 388 | 389 | } 390 | -------------------------------------------------------------------------------- /release/upload.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - vars: 3 | main: ../cmd/tachyon.go 4 | tmp: tmp/$release 5 | opts: -ldflags "-X main.Release $release" 6 | 7 | tasks: 8 | - name: Make tempdir 9 | shell: mkdir -p $tmp 10 | 11 | - name: Build tachyon-{{item.os}}-{{item.arch}} 12 | shell: GOOS={{item.os}} GOARCH={{item.arch}} go build $opts -o $tmp/tachyon-{{item.os}}-{{item.arch}} $main 13 | with_items: 14 | - { os: linux, arch: amd64 } 15 | - { os: linux, arch: 386 } 16 | - { os: darwin, arch: amd64 } 17 | - { os: darwin, arch: 386 } 18 | 19 | - name: Make sums 20 | shell: cd $tmp && shasum tachyon* > sums 21 | 22 | - name: GPG sign sums 23 | shell: cd $tmp && gpg --yes -b -u A408199F -a sums 24 | 25 | - name: Upload tachyons 26 | s3: 27 | bucket: tachyon.vektra.io 28 | put_file: $tmp/$item 29 | at: $release/$item 30 | public: yes 31 | gzip: yes 32 | with_items: 33 | - tachyon-linux-amd64 34 | - tachyon-linux-386 35 | - tachyon-darwin-amd64 36 | - tachyon-darwin-386 37 | 38 | - name: Upload sums 39 | s3: 40 | bucket: tachyon.vektra.io 41 | put_file: $tmp/$item 42 | at: $release/$item 43 | public: yes 44 | with_items: 45 | - sums 46 | - sums.asc 47 | -------------------------------------------------------------------------------- /reporter.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | type Reporter interface { 13 | StartTasks(r *Runner) 14 | FinishTasks(r *Runner) 15 | StartHandlers(r *Runner) 16 | FinishHandlers(r *Runner) 17 | 18 | StartTask(task *Task, name, args string, vars Vars) 19 | FinishTask(task *Task, res *Result) 20 | 21 | FinishAsyncTask(act *AsyncAction) 22 | Progress(str string) 23 | JSONProgress(data []byte) error 24 | } 25 | 26 | type ProgressReporter interface { 27 | Progress(string) 28 | JSONProgress(data []byte) error 29 | } 30 | 31 | type CLIReporter struct { 32 | out io.Writer 33 | Start time.Time 34 | } 35 | 36 | var sCLIReporter *CLIReporter = &CLIReporter{out: os.Stdout} 37 | 38 | func (c *CLIReporter) StartTasks(r *Runner) { 39 | c.Start = time.Now() 40 | fmt.Fprintf(c.out, "== tasks @ %v\n", r.Start) 41 | } 42 | 43 | func (c *CLIReporter) FinishTasks(r *Runner) { 44 | dur := time.Since(c.Start) 45 | 46 | fmt.Fprintf(c.out, "%7.3f ! Waiting on all tasks to finish...\n", dur.Seconds()) 47 | } 48 | 49 | func (c *CLIReporter) StartHandlers(r *Runner) { 50 | dur := time.Since(c.Start) 51 | 52 | fmt.Fprintf(c.out, "%7.3f ! Running any handlers\n", dur.Seconds()) 53 | } 54 | 55 | func (c *CLIReporter) FinishHandlers(r *Runner) {} 56 | 57 | func (c *CLIReporter) StartTask(task *Task, name, args string, vars Vars) { 58 | dur := time.Since(c.Start) 59 | 60 | if task.Async() { 61 | fmt.Fprintf(c.out, "%7.3f - %s &\n", dur.Seconds(), name) 62 | } else { 63 | fmt.Fprintf(c.out, "%7.3f - %s\n", dur.Seconds(), name) 64 | } 65 | 66 | fmt.Fprintf(c.out, "%7.3f %s: %s\n", dur.Seconds(), task.Command(), inlineVars(vars)) 67 | } 68 | 69 | func (c *CLIReporter) Progress(str string) { 70 | dur := time.Since(c.Start) 71 | 72 | lines := strings.Split(str, "\n") 73 | out := strings.Join(lines, fmt.Sprintf("\n%7.3f + ", dur.Seconds())) 74 | 75 | fmt.Fprintf(c.out, "%7.3f + %s\n", dur.Seconds(), out) 76 | } 77 | 78 | func (c *CLIReporter) JSONProgress(data []byte) error { 79 | cr := JsonChunkReconstitute{c} 80 | return cr.Input(data) 81 | } 82 | 83 | func (c *CLIReporter) FinishTask(task *Task, res *Result) { 84 | if res == nil { 85 | return 86 | } 87 | 88 | dur := time.Since(c.Start) 89 | 90 | indent := fmt.Sprintf("%7.3f ", dur.Seconds()) 91 | 92 | label := "result" 93 | 94 | if res.Changed == false { 95 | label = "check" 96 | } else if res.Failed == true { 97 | label = "failed" 98 | } 99 | 100 | if render, ok := res.Get("_result"); ok { 101 | out, ok := render.Read().(string) 102 | if ok { 103 | out = strings.TrimSpace(out) 104 | 105 | if out != "" { 106 | lines := strings.Split(out, "\n") 107 | indented := strings.Join(lines, indent+"\n") 108 | 109 | fmt.Fprintf(c.out, "%7.3f * %s:\n", dur.Seconds(), label) 110 | fmt.Fprintf(c.out, "%7.3f %s\n", dur.Seconds(), indented) 111 | } 112 | 113 | return 114 | } 115 | } 116 | 117 | if len(res.Data) > 0 { 118 | fmt.Fprintf(c.out, "%7.3f * %s:\n", dur.Seconds(), label) 119 | fmt.Fprintf(c.out, "%s\n", indentedVars(Vars(res.Data), indent)) 120 | } 121 | } 122 | 123 | func (c *CLIReporter) FinishAsyncTask(act *AsyncAction) { 124 | dur := time.Since(c.Start) 125 | 126 | if act.Error == nil { 127 | fmt.Fprintf(c.out, "%7.3f * %s (async success)\n", dur.Seconds(), act.Task.Name()) 128 | } else { 129 | fmt.Fprintf(c.out, "%7.3f * %s (async error:%s)\n", dur.Seconds(), act.Task.Name(), act.Error) 130 | } 131 | } 132 | 133 | type AdhocProgress struct { 134 | out io.Writer 135 | Start time.Time 136 | } 137 | 138 | func (a *AdhocProgress) Progress(str string) { 139 | dur := time.Since(a.Start) 140 | 141 | lines := strings.Split(str, "\n") 142 | out := strings.Join(lines, fmt.Sprintf("\n%7.3f ", dur.Seconds())) 143 | 144 | fmt.Fprintf(a.out, "%7.3f %s\n", dur.Seconds(), out) 145 | } 146 | 147 | func (a *AdhocProgress) JSONProgress(data []byte) error { 148 | cr := JsonChunkReconstitute{a} 149 | return cr.Input(data) 150 | } 151 | 152 | type JsonChunkReporter struct { 153 | out io.Writer 154 | Start time.Time 155 | } 156 | 157 | func (c *JsonChunkReporter) send(args ...interface{}) { 158 | b := ijson(args...) 159 | fmt.Fprintf(c.out, "%d\n%s\n", len(b), string(b)) 160 | } 161 | 162 | var sJsonChunkReporter *JsonChunkReporter = &JsonChunkReporter{out: os.Stdout} 163 | 164 | func (c *JsonChunkReporter) StartTasks(r *Runner) { 165 | c.Start = r.Start 166 | c.send("phase", "start", "time", r.Start.String()) 167 | } 168 | 169 | func (c *JsonChunkReporter) FinishTasks(r *Runner) { 170 | c.send("phase", "finish") 171 | } 172 | 173 | func (c *JsonChunkReporter) StartHandlers(r *Runner) { 174 | c.send("phase", "start_handlers") 175 | } 176 | 177 | func (c *JsonChunkReporter) FinishHandlers(r *Runner) { 178 | c.send("phase", "finish_handlers") 179 | } 180 | 181 | func (c *JsonChunkReporter) StartTask(task *Task, name, args string, vars Vars) { 182 | dur := time.Since(c.Start).Seconds() 183 | 184 | typ := "sync" 185 | 186 | if task.Async() { 187 | typ = "async" 188 | } 189 | 190 | c.send( 191 | "phase", "start_task", 192 | "type", typ, 193 | "name", name, 194 | "command", task.Command(), 195 | "args", args, 196 | "vars", vars, 197 | "delta", dur) 198 | } 199 | 200 | func (c *JsonChunkReporter) Progress(str string) { 201 | dur := time.Since(c.Start).Seconds() 202 | 203 | c.send( 204 | "phase", "progress", 205 | "delta", dur, 206 | "progress", str) 207 | } 208 | 209 | func (c *JsonChunkReporter) JSONProgress(data []byte) error { 210 | dur := time.Since(c.Start).Seconds() 211 | 212 | raw := json.RawMessage(data) 213 | 214 | c.send( 215 | "phase", "json_progress", 216 | "delta", dur, 217 | "progress", &raw) 218 | 219 | return nil 220 | } 221 | 222 | func (c *JsonChunkReporter) FinishTask(task *Task, res *Result) { 223 | if res == nil { 224 | return 225 | } 226 | 227 | dur := time.Since(c.Start).Seconds() 228 | 229 | c.send( 230 | "phase", "finish_task", 231 | "delta", dur, 232 | "result", res) 233 | } 234 | 235 | func (c *JsonChunkReporter) FinishAsyncTask(act *AsyncAction) { 236 | dur := time.Since(c.Start).Seconds() 237 | 238 | if act.Error == nil { 239 | c.send( 240 | "phase", "finish_task", 241 | "delta", dur, 242 | "result", act.Result) 243 | } else { 244 | c.send( 245 | "phase", "finish_task", 246 | "delta", dur, 247 | "error", act.Error) 248 | } 249 | } 250 | 251 | type JsonChunkReconstitute struct { 252 | report ProgressReporter 253 | } 254 | 255 | func (j *JsonChunkReconstitute) Input(data []byte) error { 256 | m := make(map[string]interface{}) 257 | 258 | err := json.Unmarshal(data, &m) 259 | if err != nil { 260 | return err 261 | } 262 | 263 | return j.InputMap(m, 0) 264 | } 265 | 266 | func (j *JsonChunkReconstitute) InputMap(m map[string]interface{}, depth int) error { 267 | phase, ok := m["phase"] 268 | if !ok { 269 | return fmt.Errorf("No phase specified") 270 | } 271 | 272 | var prefix string 273 | 274 | if depth > 0 { 275 | prefix = fmt.Sprintf("[%d] ", depth) 276 | } 277 | 278 | switch phase { 279 | case "start": 280 | time, ok := m["time"] 281 | if !ok { 282 | time = "(unknown)" 283 | } 284 | 285 | j.report.Progress(fmt.Sprintf("%sremote tasks @ %s", prefix, time)) 286 | case "start_task": 287 | j.report.Progress(fmt.Sprintf("%s- %s", prefix, m["name"])) 288 | mv := m["vars"].(map[string]interface{}) 289 | j.report.Progress(fmt.Sprintf("%s %s: %s", prefix, m["command"], inlineMap(mv))) 290 | case "finish_task": 291 | res := m["result"].(map[string]interface{}) 292 | data := res["data"].(map[string]interface{}) 293 | 294 | label := "result" 295 | 296 | if res["changed"].(bool) == false { 297 | label = "check" 298 | } else if res["failed"].(bool) == true { 299 | label = "failed" 300 | } 301 | 302 | reported := false 303 | 304 | if v, ok := data["_result"]; ok { 305 | if str, ok := v.(string); ok { 306 | if str != "" { 307 | j.report.Progress(fmt.Sprintf("%s* %s:", prefix, label)) 308 | j.report.Progress(prefix + " " + str) 309 | } 310 | reported = true 311 | } 312 | } 313 | 314 | if !reported { 315 | if len(data) > 0 { 316 | j.report.Progress(fmt.Sprintf("%s* %s:", prefix, label)) 317 | j.report.Progress(indentedMap(data, prefix+" ")) 318 | } 319 | } 320 | case "json_progress": 321 | ds := m["progress"].(map[string]interface{}) 322 | 323 | j.InputMap(ds, depth+1) 324 | } 325 | 326 | return nil 327 | } 328 | -------------------------------------------------------------------------------- /runner.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "os" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type RunResult struct { 10 | Task *Task 11 | Result *Result 12 | Runtime time.Duration 13 | } 14 | 15 | type Runner struct { 16 | env *Environment 17 | plays []*Play 18 | wait sync.WaitGroup 19 | to_notify map[string]struct{} 20 | async chan *AsyncAction 21 | report Reporter 22 | 23 | Results []RunResult 24 | Start time.Time 25 | Runtime time.Duration 26 | } 27 | 28 | func NewRunner(env *Environment, plays []*Play) *Runner { 29 | r := &Runner{ 30 | env: env, 31 | plays: plays, 32 | to_notify: make(map[string]struct{}), 33 | async: make(chan *AsyncAction), 34 | report: env.report, 35 | } 36 | 37 | go r.handleAsync() 38 | 39 | return r 40 | } 41 | 42 | func (r *Runner) SetReport(rep Reporter) { 43 | r.report = rep 44 | } 45 | 46 | func (r *Runner) AddNotify(n string) { 47 | r.to_notify[n] = struct{}{} 48 | } 49 | 50 | func (r *Runner) ShouldRunHandler(name string) bool { 51 | _, ok := r.to_notify[name] 52 | 53 | return ok 54 | } 55 | 56 | func (r *Runner) AsyncChannel() chan *AsyncAction { 57 | return r.async 58 | } 59 | 60 | func (r *Runner) Run(env *Environment) error { 61 | start := time.Now() 62 | r.Start = start 63 | 64 | defer func() { 65 | r.Runtime = time.Since(start) 66 | }() 67 | 68 | r.report.StartTasks(r) 69 | 70 | for _, play := range r.plays { 71 | fs := NewFutureScope(play.Vars) 72 | 73 | for _, task := range play.Tasks { 74 | err := r.runTask(env, play, task, fs, fs) 75 | if err != nil { 76 | return err 77 | } 78 | } 79 | 80 | r.Results = append(r.Results, fs.Results()...) 81 | } 82 | 83 | r.report.FinishTasks(r) 84 | 85 | r.wait.Wait() 86 | 87 | r.report.StartHandlers(r) 88 | 89 | for _, play := range r.plays { 90 | fs := NewFutureScope(play.Vars) 91 | 92 | for _, task := range play.Handlers { 93 | if r.ShouldRunHandler(task.Name()) { 94 | err := r.runTask(env, play, task, fs, fs) 95 | 96 | if err != nil { 97 | return err 98 | } 99 | } 100 | } 101 | 102 | fs.Wait() 103 | } 104 | 105 | r.report.FinishHandlers(r) 106 | 107 | return nil 108 | } 109 | 110 | func RunAdhocTask(cmd, args string) (*Result, error) { 111 | env := NewEnv(NewNestedScope(nil), &Config{}) 112 | defer env.Cleanup() 113 | 114 | task := AdhocTask(cmd, args) 115 | 116 | str, err := ExpandVars(env.Vars, task.Args()) 117 | if err != nil { 118 | return nil, err 119 | } 120 | 121 | obj, _, err := MakeCommand(env.Vars, task, str) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | ar := &AdhocProgress{out: os.Stdout, Start: time.Now()} 127 | 128 | ce := &CommandEnv{Env: env, Paths: env.Paths, progress: ar} 129 | 130 | return obj.Run(ce) 131 | } 132 | 133 | func RunAdhocTaskVars(td TaskData) (*Result, error) { 134 | env := NewEnv(NewNestedScope(nil), &Config{}) 135 | defer env.Cleanup() 136 | 137 | task := &Task{data: td} 138 | task.Init(env) 139 | 140 | obj, _, err := MakeCommand(env.Vars, task, "") 141 | if err != nil { 142 | return nil, err 143 | } 144 | 145 | ar := &AdhocProgress{out: os.Stdout, Start: time.Now()} 146 | 147 | ce := &CommandEnv{Env: env, Paths: env.Paths, progress: ar} 148 | 149 | return obj.Run(ce) 150 | } 151 | 152 | func RunAdhocCommand(cmd Command, args string) (*Result, error) { 153 | env := NewEnv(NewNestedScope(nil), &Config{}) 154 | defer env.Cleanup() 155 | 156 | ar := &AdhocProgress{out: os.Stdout, Start: time.Now()} 157 | 158 | ce := &CommandEnv{Env: env, Paths: env.Paths, progress: ar} 159 | 160 | return cmd.Run(ce) 161 | } 162 | 163 | type PriorityScope struct { 164 | task Vars 165 | rest Scope 166 | } 167 | 168 | func (p *PriorityScope) Get(key string) (Value, bool) { 169 | if p.task != nil { 170 | if v, ok := p.task[key]; ok { 171 | return Any(v), true 172 | } 173 | } 174 | 175 | return p.rest.Get(key) 176 | } 177 | 178 | func (p *PriorityScope) Set(key string, val interface{}) { 179 | p.rest.Set(key, val) 180 | } 181 | 182 | func boolify(str string) bool { 183 | switch str { 184 | case "", "false", "no": 185 | return false 186 | default: 187 | return true 188 | } 189 | } 190 | 191 | type ModuleRun struct { 192 | Play *Play 193 | Task *Task 194 | Module *Module 195 | Runner *Runner 196 | Scope Scope 197 | FutureScope *FutureScope 198 | Vars Vars 199 | } 200 | 201 | func (m *ModuleRun) Run(env *CommandEnv) (*Result, error) { 202 | for _, task := range m.Module.ModTasks { 203 | ns := NewNestedScope(m.Scope) 204 | 205 | for k, v := range m.Vars { 206 | ns.Set(k, v) 207 | } 208 | 209 | err := m.Runner.runTask(env.Env, m.Play, task, ns, m.FutureScope) 210 | if err != nil { 211 | return nil, err 212 | } 213 | } 214 | 215 | return NewResult(true), nil 216 | } 217 | 218 | func (r *Runner) runTaskItems(env *Environment, play *Play, task *Task, s Scope, fs *FutureScope, start time.Time) error { 219 | 220 | for _, item := range task.Items() { 221 | ns := NewNestedScope(s) 222 | ns.Set("item", item) 223 | 224 | name, err := ExpandVars(ns, task.Name()) 225 | if err != nil { 226 | return err 227 | } 228 | 229 | str, err := ExpandVars(ns, task.Args()) 230 | if err != nil { 231 | return err 232 | } 233 | 234 | cmd, sm, err := MakeCommand(ns, task, str) 235 | if err != nil { 236 | return err 237 | } 238 | 239 | r.report.StartTask(task, name, str, sm) 240 | 241 | ce := NewCommandEnv(env, task) 242 | 243 | res, err := cmd.Run(ce) 244 | 245 | if name := task.Register(); name != "" { 246 | fs.Set(name, res) 247 | } 248 | 249 | runtime := time.Since(start) 250 | 251 | if err != nil { 252 | res = FailureResult(err) 253 | } 254 | 255 | r.Results = append(r.Results, RunResult{task, res, runtime}) 256 | 257 | r.report.FinishTask(task, res) 258 | 259 | if err == nil { 260 | for _, x := range task.Notify() { 261 | r.AddNotify(x) 262 | } 263 | } else { 264 | return err 265 | } 266 | } 267 | 268 | return nil 269 | } 270 | 271 | func (r *Runner) runTask(env *Environment, play *Play, task *Task, s Scope, fs *FutureScope) error { 272 | ps := &PriorityScope{task.IncludeVars, s} 273 | 274 | start := time.Now() 275 | 276 | if when := task.When(); when != "" { 277 | when, err := ExpandVars(ps, when) 278 | 279 | if err != nil { 280 | return err 281 | } 282 | 283 | if !boolify(when) { 284 | return nil 285 | } 286 | } 287 | 288 | if items := task.Items(); items != nil { 289 | return r.runTaskItems(env, play, task, s, fs, start) 290 | } 291 | 292 | name, err := ExpandVars(ps, task.Name()) 293 | if err != nil { 294 | return err 295 | } 296 | 297 | str, err := ExpandVars(ps, task.Args()) 298 | if err != nil { 299 | return err 300 | } 301 | 302 | var cmd Command 303 | 304 | var argVars Vars 305 | 306 | if mod, ok := play.Modules[task.Command()]; ok { 307 | sm, err := ParseSimpleMap(s, str) 308 | if err != nil { 309 | return err 310 | } 311 | 312 | for ik, iv := range task.Vars { 313 | if str, ok := iv.Read().(string); ok { 314 | exp, err := ExpandVars(s, str) 315 | if err != nil { 316 | return err 317 | } 318 | 319 | sm[ik] = Any(exp) 320 | } else { 321 | sm[ik] = iv 322 | } 323 | } 324 | 325 | cmd = &ModuleRun{ 326 | Play: play, 327 | Task: task, 328 | Module: mod, 329 | Runner: r, 330 | Scope: s, 331 | FutureScope: NewFutureScope(s), 332 | Vars: sm, 333 | } 334 | 335 | argVars = sm 336 | } else { 337 | cmd, argVars, err = MakeCommand(ps, task, str) 338 | 339 | if err != nil { 340 | return err 341 | } 342 | } 343 | 344 | r.report.StartTask(task, name, str, argVars) 345 | 346 | ce := NewCommandEnv(env, task) 347 | 348 | if name := task.Future(); name != "" { 349 | future := NewFuture(start, task, func() (*Result, error) { 350 | return cmd.Run(ce) 351 | }) 352 | 353 | fs.AddFuture(name, future) 354 | 355 | return nil 356 | } 357 | 358 | if task.Async() { 359 | asyncAction := &AsyncAction{Task: task} 360 | asyncAction.Init(r) 361 | 362 | go func() { 363 | asyncAction.Finish(cmd.Run(ce)) 364 | }() 365 | } else { 366 | res, err := cmd.Run(ce) 367 | 368 | if name := task.Register(); name != "" { 369 | fs.Set(name, res) 370 | } 371 | 372 | runtime := time.Since(start) 373 | 374 | if err != nil { 375 | res = FailureResult(err) 376 | } 377 | 378 | r.Results = append(r.Results, RunResult{task, res, runtime}) 379 | 380 | r.report.FinishTask(task, res) 381 | 382 | if err == nil { 383 | for _, x := range task.Notify() { 384 | r.AddNotify(x) 385 | } 386 | } else { 387 | return err 388 | } 389 | } 390 | 391 | return err 392 | } 393 | -------------------------------------------------------------------------------- /scope.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | ) 7 | 8 | type Value interface { 9 | Read() interface{} 10 | } 11 | 12 | type AnyValue struct { 13 | v interface{} 14 | } 15 | 16 | func (a AnyValue) Read() interface{} { 17 | return a.v 18 | } 19 | 20 | type AnyMap struct { 21 | m map[interface{}]interface{} 22 | } 23 | 24 | func (a AnyMap) Read() interface{} { 25 | return a.m 26 | } 27 | 28 | func (a AnyMap) Get(key string) (Value, bool) { 29 | if v, ok := a.m[key]; ok { 30 | return Any(v), true 31 | } 32 | 33 | return nil, false 34 | } 35 | 36 | type StrMap struct { 37 | m map[string]interface{} 38 | } 39 | 40 | func (a StrMap) Get(key string) (Value, bool) { 41 | if v, ok := a.m[key]; ok { 42 | return Any(v), true 43 | } 44 | 45 | return nil, false 46 | } 47 | 48 | func (a StrMap) Read() interface{} { 49 | return a.m 50 | } 51 | 52 | func (a AnyValue) MarshalJSON() ([]byte, error) { 53 | return json.Marshal(a.v) 54 | } 55 | 56 | func (a AnyMap) MarshalJSON() ([]byte, error) { 57 | return json.Marshal(a.m) 58 | } 59 | 60 | func (a StrMap) MarshalJSON() ([]byte, error) { 61 | return json.Marshal(a.m) 62 | } 63 | 64 | func Any(v interface{}) Value { 65 | switch sv := v.(type) { 66 | case AnyValue: 67 | return sv 68 | case map[interface{}]interface{}: 69 | return AnyMap{sv} 70 | case map[string]interface{}: 71 | return StrMap{sv} 72 | default: 73 | return AnyValue{v} 74 | } 75 | } 76 | 77 | func (a AnyValue) GetYAML() (string, interface{}) { 78 | return "", a.v 79 | } 80 | 81 | func (a AnyValue) SetYAML(tag string, v interface{}) bool { 82 | a.v = v 83 | return true 84 | } 85 | 86 | type Map interface { 87 | Get(key string) (Value, bool) 88 | } 89 | 90 | type Scope interface { 91 | Get(key string) (Value, bool) 92 | Set(key string, val interface{}) 93 | } 94 | 95 | type ScopeGetter interface { 96 | Get(key string) (Value, bool) 97 | } 98 | 99 | func SV(v interface{}, ok bool) interface{} { 100 | if !ok { 101 | return nil 102 | } 103 | 104 | return v 105 | } 106 | 107 | type NestedScope struct { 108 | Scope Scope 109 | Vars Vars 110 | } 111 | 112 | func NewNestedScope(parent Scope) *NestedScope { 113 | return &NestedScope{parent, make(Vars)} 114 | } 115 | 116 | func SpliceOverrides(cur Scope, override *NestedScope) *NestedScope { 117 | ns := NewNestedScope(cur) 118 | 119 | for k, v := range override.Vars { 120 | ns.Set(k, v) 121 | } 122 | 123 | return ns 124 | } 125 | 126 | func (n *NestedScope) Get(key string) (v Value, ok bool) { 127 | v, ok = n.Vars[key] 128 | if !ok && n.Scope != nil { 129 | v, ok = n.Scope.Get(key) 130 | } 131 | 132 | return 133 | } 134 | 135 | func (n *NestedScope) Set(key string, v interface{}) { 136 | n.Vars[key] = Any(v) 137 | } 138 | 139 | func (n *NestedScope) Empty() bool { 140 | return len(n.Vars) == 0 141 | } 142 | 143 | func (n *NestedScope) Flatten() Scope { 144 | if len(n.Vars) == 0 && n.Scope != nil { 145 | return n.Scope 146 | } 147 | 148 | return n 149 | } 150 | 151 | func (n *NestedScope) addMapVars(mv map[interface{}]interface{}) error { 152 | for k, v := range mv { 153 | if sk, ok := k.(string); ok { 154 | if sv, ok := v.(string); ok { 155 | var err error 156 | 157 | v, err = ExpandVars(n, sv) 158 | if err != nil { 159 | return err 160 | } 161 | } 162 | 163 | n.Set(sk, v) 164 | } 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func (n *NestedScope) addVars(vars interface{}) (err error) { 171 | switch mv := vars.(type) { 172 | case map[interface{}]interface{}: 173 | err = n.addMapVars(mv) 174 | case []interface{}: 175 | for _, i := range mv { 176 | err = n.addVars(i) 177 | if err != nil { 178 | return 179 | } 180 | } 181 | } 182 | 183 | return 184 | } 185 | 186 | func ImportVarsFile(s Scope, path string) error { 187 | var fv map[string]string 188 | 189 | err := yamlFile(path, &fv) 190 | 191 | if err != nil { 192 | return err 193 | } 194 | 195 | for k, v := range fv { 196 | s.Set(k, inferString(v)) 197 | } 198 | 199 | return nil 200 | } 201 | 202 | func DisplayScope(s Scope) { 203 | if ns, ok := s.(*NestedScope); ok { 204 | DisplayScope(ns.Scope) 205 | 206 | for k, v := range ns.Vars { 207 | fmt.Printf("%s: %v\n", k, v) 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /scripts/detect.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | arch=`uname -m` 4 | 5 | case $arch in 6 | x86_64 ) 7 | arch="amd64" ;; 8 | 486 | 586 | 686 ) 9 | arch="386" ;; 10 | * ) 11 | echo "Unsupported arch: $arch" 12 | exit 1 13 | ;; 14 | esac 15 | 16 | os=`uname` 17 | 18 | case $os in 19 | Darwin ) 20 | os="darwin" 21 | ;; 22 | Linux ) 23 | os="linux" 24 | ;; 25 | * ) 26 | echo "Unsupported os: $os" 27 | exit 1 28 | esac 29 | 30 | echo "tachyon-$os-$arch" 31 | -------------------------------------------------------------------------------- /scripts/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | arch=`uname -m` 4 | 5 | case $arch in 6 | x86_64 ) 7 | arch="amd64" ;; 8 | 486 | 586 | 686 ) 9 | arch="386" ;; 10 | * ) 11 | echo "Unsupported arch: $arch" 12 | exit 1 13 | ;; 14 | esac 15 | 16 | os=`uname` 17 | 18 | case $os in 19 | Darwin ) 20 | os="darwin" 21 | ;; 22 | Linux ) 23 | os="linux" 24 | ;; 25 | * ) 26 | echo "Unsupported os: $os" 27 | exit 1 28 | esac 29 | 30 | bin="tachyon-$os-$arch" 31 | 32 | echo "Determined your tachyon binary to be: $bin" 33 | 34 | cur=$(curl -s https://s3-us-west-2.amazonaws.com/tachyon.vektra.io/release) 35 | 36 | if test "$?" != "0"; then 37 | echo "Error computing current release" 38 | exit 1 39 | fi 40 | 41 | echo "Current release is: $cur" 42 | 43 | url="https://s3-us-west-2.amazonaws.com/tachyon.vektra.io/$cur/$bin" 44 | 45 | echo "Downloading $url..." 46 | 47 | curl --compressed -o tachyon $url 48 | 49 | chmod a+x tachyon 50 | 51 | echo "" 52 | echo "Tachyon downloaded to current directory" 53 | echo "We suggest you move it to somewhere in your PATH, like ~/bin" 54 | echo "" 55 | echo "Enjoy!" 56 | -------------------------------------------------------------------------------- /ssh.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | type SSH struct { 12 | Host string 13 | Config string 14 | Debug bool 15 | 16 | removeConfig bool 17 | sshCCOptions []string 18 | sshCSOptions []string 19 | controlPath string 20 | 21 | persistent *exec.Cmd 22 | } 23 | 24 | func (s *SSH) CommandWithOptions(cmd string, args ...string) []string { 25 | sshArgs := []string{cmd} 26 | sshArgs = append(sshArgs, s.sshCCOptions...) 27 | 28 | if s.Config != "" { 29 | sshArgs = append(sshArgs, "-F", s.Config) 30 | } 31 | 32 | return append(sshArgs, args...) 33 | } 34 | 35 | func (s *SSH) RsyncCommand() string { 36 | sshArgs := []string{"ssh"} 37 | sshArgs = append(sshArgs, s.sshCCOptions...) 38 | 39 | if s.Config != "" { 40 | sshArgs = append(sshArgs, "-F", s.Config) 41 | } 42 | 43 | return strings.Join(sshArgs, " ") 44 | } 45 | 46 | func (s *SSH) SSHCommand(cmd string, args ...string) []string { 47 | sshArgs := []string{cmd} 48 | sshArgs = append(sshArgs, s.sshCCOptions...) 49 | 50 | if s.Config != "" { 51 | sshArgs = append(sshArgs, "-F", s.Config) 52 | } 53 | 54 | sshArgs = append(sshArgs, s.Host) 55 | 56 | return append(sshArgs, args...) 57 | } 58 | 59 | func NewSSH(host string) *SSH { 60 | s := &SSH{ 61 | Host: host, 62 | } 63 | 64 | if strings.Index(host, ":vagrant") == 0 { 65 | var target string 66 | 67 | if host == ":vagrant" { 68 | target = "default" 69 | } else { 70 | parts := strings.Split(host, ":") 71 | target = parts[2] 72 | } 73 | 74 | s.ImportVagrant(target) 75 | } 76 | 77 | home, err := HomeDir() 78 | if err != nil { 79 | panic(err) 80 | } 81 | 82 | tachDir := home + "/.tachyon" 83 | 84 | if _, err := os.Stat(tachDir); err != nil { 85 | err = os.Mkdir(tachDir, 0755) 86 | if err != nil { 87 | panic(err) 88 | } 89 | } 90 | 91 | s.controlPath = fmt.Sprintf("%s/tachyon-cp-ssh-%d", tachDir, os.Getpid()) 92 | 93 | s.sshCCOptions = []string{"-o", "StrictHostKeyChecking=no"} 94 | 95 | s.sshCSOptions = []string{ 96 | "-o", "ControlMaster=yes", 97 | "-o", "ControlPersist=no", 98 | "-o", "ControlPath=" + s.controlPath, 99 | } 100 | 101 | return s 102 | } 103 | 104 | func (s *SSH) Cleanup() { 105 | if s.persistent != nil { 106 | s.persistent.Process.Kill() 107 | s.persistent.Wait() 108 | } 109 | 110 | if s.removeConfig { 111 | os.Remove(s.Config) 112 | } 113 | } 114 | 115 | func (s *SSH) ImportVagrant(target string) bool { 116 | s.Host = target 117 | s.removeConfig = true 118 | 119 | out, err := exec.Command("vagrant", "ssh-config", target).CombinedOutput() 120 | if err != nil { 121 | fmt.Printf("Unable to execute 'vagrant ssh-config': %s\n", err) 122 | return false 123 | } 124 | 125 | f, err := ioutil.TempFile("", "tachyon") 126 | if err != nil { 127 | fmt.Printf("Unable to make tempfile: %s\n", err) 128 | return false 129 | } 130 | 131 | _, err = f.Write(out) 132 | if err != nil { 133 | fmt.Printf("Unable to write to tempfile: %s\n", err) 134 | return false 135 | } 136 | 137 | f.Close() 138 | 139 | s.Config = f.Name() 140 | 141 | return true 142 | } 143 | 144 | func (s *SSH) Start() error { 145 | s.sshCCOptions = append(s.sshCCOptions, "-S", s.controlPath) 146 | 147 | sshArgs := s.sshCSOptions 148 | 149 | if s.Config != "" { 150 | sshArgs = append(sshArgs, "-F", s.Config) 151 | } 152 | 153 | sshArgs = append(sshArgs, "-N", s.Host) 154 | 155 | c := exec.Command("ssh", sshArgs...) 156 | 157 | err := c.Start() 158 | if err == nil { 159 | s.persistent = c 160 | } 161 | 162 | return err 163 | } 164 | 165 | func (s *SSH) Command(args ...string) *exec.Cmd { 166 | args = s.SSHCommand("ssh", args...) 167 | return exec.Command(args[0], args[1:]...) 168 | } 169 | 170 | func (s *SSH) Run(args ...string) error { 171 | c := s.Command(args...) 172 | 173 | if s.Debug { 174 | fmt.Fprintf(os.Stderr, "Run: %#v\n", c.Args) 175 | c.Stdout = os.Stdout 176 | c.Stderr = os.Stderr 177 | } 178 | 179 | return c.Run() 180 | } 181 | 182 | func (s *SSH) RunAndCapture(args ...string) ([]byte, error) { 183 | c := s.Command(args...) 184 | 185 | if s.Debug { 186 | fmt.Fprintf(os.Stderr, "Run: %#v\n", c.Args) 187 | } 188 | 189 | out, err := c.CombinedOutput() 190 | 191 | if s.Debug { 192 | fmt.Fprintf(os.Stderr, "Output:\n%s\n", string(out)) 193 | } 194 | 195 | return out, err 196 | } 197 | 198 | func (s *SSH) RunAndShow(args ...string) error { 199 | c := s.Command(args...) 200 | 201 | if s.Debug { 202 | fmt.Fprintf(os.Stderr, "Run: %#v\n", c.Args) 203 | } 204 | 205 | c.Stdout = os.Stdout 206 | c.Stderr = os.Stderr 207 | 208 | return c.Run() 209 | } 210 | 211 | func (s *SSH) CopyToHost(src, dest string) error { 212 | args := s.CommandWithOptions("scp", src, s.Host+":"+dest) 213 | c := exec.Command(args[0], args[1:]...) 214 | 215 | if s.Debug { 216 | fmt.Fprintf(os.Stderr, "Run: %#v\n", c.Args) 217 | c.Stdout = os.Stdout 218 | c.Stderr = os.Stderr 219 | } 220 | 221 | return c.Run() 222 | } 223 | -------------------------------------------------------------------------------- /tachyon.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path/filepath" 11 | "strconv" 12 | "strings" 13 | ) 14 | 15 | var cUpdateScript = []byte(`#!/bin/bash 16 | 17 | cd .tachyon 18 | 19 | REL=$TACHYON_RELEASE 20 | BIN=tachyon-$TACHYON_OS-$TACHYON_ARCH 21 | 22 | if test -f tachyon; then 23 | CUR=$(< release) 24 | if test "$REL" != "$CUR"; then 25 | echo "Detected tachyon of old release ($CUR), removing." 26 | rm tachyon 27 | fi 28 | fi 29 | 30 | if which curl > /dev/null; then 31 | DL="curl -O" 32 | elif which wget > /dev/null; then 33 | DL="wget" 34 | else 35 | echo "No curl or wget, unable to pull a release" 36 | exit 1 37 | fi 38 | 39 | if ! test -f tachyon; then 40 | echo "Downloading $REL/$BIN..." 41 | 42 | $DL https://s3-us-west-2.amazonaws.com/tachyon.vektra.io/$REL/sums 43 | if which gpg > /dev/null; then 44 | gpg --keyserver keys.gnupg.net --recv-key A408199F & 45 | $DL https://s3-us-west-2.amazonaws.com/tachyon.vektra.io/$REL/sums.asc & 46 | fi 47 | 48 | $DL https://s3-us-west-2.amazonaws.com/tachyon.vektra.io/$REL/$BIN 49 | 50 | wait 51 | 52 | if which gpg > /dev/null; then 53 | if ! gpg --verify sums.asc; then 54 | echo "Signature verification failed! Aborting!" 55 | exit 1 56 | fi 57 | fi 58 | 59 | mv $BIN $BIN.gz 60 | 61 | # If gunzip fails, it's because the file isn't gzip'd, so we 62 | # assume it's already in the correct format. 63 | if ! gunzip $BIN.gz; then 64 | mv $BIN.gz $BIN 65 | fi 66 | 67 | if which shasum > /dev/null; then 68 | if ! (grep $BIN sums | shasum -c); then 69 | echo "Sum verification failed!" 70 | exit 1 71 | fi 72 | else 73 | echo "No shasum available to verify files" 74 | fi 75 | 76 | echo $REL > release 77 | 78 | chmod a+x $BIN 79 | ln -s $BIN tachyon 80 | fi 81 | `) 82 | 83 | func normalizeArch(arch string) string { 84 | switch arch { 85 | case "x86_64": 86 | return "amd64" 87 | default: 88 | return arch 89 | } 90 | } 91 | 92 | type Tachyon struct { 93 | Target string `tachyon:"target"` 94 | Debug bool `tachyon:"debug"` 95 | Clean bool `tachyon:"clean"` 96 | Dev bool `tachyon:"dev"` 97 | Playbook string `tachyon:"playbook"` 98 | Release string `tachyon:"release"` 99 | NoJSON bool `tachyon:"no_json"` 100 | InstallOnly bool `tachyon:"install_only"` 101 | } 102 | 103 | func (t *Tachyon) Run(env *CommandEnv) (*Result, error) { 104 | if t.Release == "" { 105 | t.Release = Release 106 | } 107 | 108 | ssh := NewSSH(t.Target) 109 | ssh.Debug = t.Debug 110 | 111 | defer ssh.Cleanup() 112 | 113 | // err := ssh.Start() 114 | // if err != nil { 115 | // return nil, fmt.Errorf("Error starting persistent SSH connection: %s\n", err) 116 | // } 117 | 118 | var bootstrap string 119 | 120 | if t.Clean { 121 | bootstrap = "rm -rf .tachyon && mkdir -p .tachyon" 122 | } else { 123 | bootstrap = "mkdir -p .tachyon" 124 | } 125 | 126 | out, err := ssh.RunAndCapture(bootstrap + " && uname && uname -m") 127 | if err != nil { 128 | return nil, fmt.Errorf("Error creating remote .tachyon dir: %s (%s)", err, string(out)) 129 | } 130 | 131 | tos, arch, ok := split2(string(out), "\n") 132 | if !ok { 133 | return nil, fmt.Errorf("Unable to figure out os and arch of remote machine\n") 134 | } 135 | 136 | tos = strings.ToLower(tos) 137 | arch = normalizeArch(strings.TrimSpace(arch)) 138 | 139 | binary := fmt.Sprintf("tachyon-%s-%s", tos, arch) 140 | 141 | if t.Dev { 142 | env.Progress("Copying development tachyon...") 143 | 144 | path := filepath.Dir(Arg0) 145 | 146 | err = ssh.CopyToHost(filepath.Join(path, binary), ".tachyon/"+binary+".new") 147 | if err != nil { 148 | return nil, fmt.Errorf("Error copying tachyon to vagrant: %s\n", err) 149 | } 150 | 151 | ssh.Run(fmt.Sprintf("cd .tachyon && mv %[1]s.new %[1]s && ln -fs %[1]s tachyon", binary)) 152 | } else { 153 | env.Progress("Updating tachyon release...") 154 | 155 | c := ssh.Command("cat > .tachyon/update && chmod a+x .tachyon/update") 156 | 157 | c.Stdout = os.Stdout 158 | c.Stdin = bytes.NewReader(cUpdateScript) 159 | err = c.Run() 160 | if err != nil { 161 | return nil, fmt.Errorf("Error updating, well, the updater: %s\n", err) 162 | } 163 | 164 | cmd := fmt.Sprintf("TACHYON_RELEASE=%s TACHYON_OS=%s TACHYON_ARCH=%s ./.tachyon/update", t.Release, tos, arch) 165 | err = ssh.Run(cmd) 166 | if err != nil { 167 | return nil, fmt.Errorf("Error running updater: %s", err) 168 | } 169 | } 170 | 171 | if t.InstallOnly { 172 | res := NewResult(true) 173 | res.Add("target", t.Target) 174 | res.Add("install_only", true) 175 | 176 | return res, nil 177 | } 178 | 179 | var src string 180 | 181 | var main string 182 | 183 | fi, err := os.Stat(t.Playbook) 184 | if err != nil { 185 | return nil, err 186 | } 187 | 188 | if fi.IsDir() { 189 | src, err = filepath.Abs(t.Playbook) 190 | if err != nil { 191 | return nil, fmt.Errorf("Unable to resolve %s: %s", t.Playbook, err) 192 | } 193 | main = "site.yml" 194 | } else { 195 | abs, err := filepath.Abs(t.Playbook) 196 | if err != nil { 197 | return nil, fmt.Errorf("Unable to resolve %s: %s", t.Playbook, err) 198 | } 199 | 200 | main = filepath.Base(abs) 201 | src = filepath.Dir(abs) 202 | } 203 | 204 | src += "/" 205 | 206 | env.Progress("Syncing playbook...") 207 | 208 | c := exec.Command("rsync", "-av", "-e", ssh.RsyncCommand(), src, ssh.Host+":.tachyon/playbook") 209 | 210 | if t.Debug { 211 | c.Stdout = os.Stdout 212 | } 213 | 214 | err = c.Run() 215 | 216 | if err != nil { 217 | return nil, fmt.Errorf("Error copying playbook to vagrant: %s\n", err) 218 | } 219 | 220 | env.Progress("Running playbook...") 221 | 222 | var format string 223 | 224 | if !t.NoJSON { 225 | format = "--json" 226 | } 227 | 228 | startCmd := fmt.Sprintf("cd .tachyon && sudo ./tachyon %s playbook/%s", format, main) 229 | 230 | c = ssh.Command(startCmd) 231 | 232 | if t.Debug { 233 | fmt.Fprintf(os.Stderr, "Run: %#v\n", c.Args) 234 | } 235 | 236 | stream, err := c.StdoutPipe() 237 | if err != nil { 238 | return nil, err 239 | } 240 | 241 | c.Stderr = os.Stderr 242 | 243 | c.Start() 244 | 245 | input := bufio.NewReader(stream) 246 | 247 | for { 248 | str, err := input.ReadString('\n') 249 | if err != nil { 250 | break 251 | } 252 | 253 | sz, err := strconv.Atoi(strings.TrimSpace(str)) 254 | if err != nil { 255 | break 256 | } 257 | 258 | data := make([]byte, sz) 259 | 260 | _, err = input.Read(data) 261 | if err != nil { 262 | break 263 | } 264 | 265 | _, err = input.ReadByte() 266 | if err != nil { 267 | break 268 | } 269 | 270 | env.progress.JSONProgress(data) 271 | } 272 | 273 | if err != nil { 274 | if err != io.EOF { 275 | fmt.Printf("error: %s\n", err) 276 | } 277 | } 278 | 279 | err = c.Wait() 280 | if err != nil { 281 | return nil, fmt.Errorf("Error running playbook remotely: %s", err) 282 | } 283 | 284 | res := NewResult(true) 285 | res.Add("target", t.Target) 286 | res.Add("playbook", t.Playbook) 287 | 288 | return res, nil 289 | } 290 | 291 | func init() { 292 | RegisterCommand("tachyon", &Tachyon{}) 293 | } 294 | -------------------------------------------------------------------------------- /task.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type Task struct { 9 | Play *Play 10 | File string 11 | 12 | data TaskData 13 | cmd string 14 | args string 15 | Vars Vars 16 | 17 | IncludeVars Vars 18 | Paths Paths 19 | } 20 | 21 | type TaskData map[string]interface{} 22 | 23 | type Tasks []*Task 24 | 25 | func AdhocTask(cmd, args string) *Task { 26 | return &Task{ 27 | cmd: cmd, 28 | args: args, 29 | data: TaskData{"name": "adhoc"}, 30 | Vars: make(Vars), 31 | } 32 | } 33 | 34 | var cOptions = []string{ 35 | "name", "action", "notify", "async", "poll", 36 | "when", "future", "register", "with_items", 37 | } 38 | 39 | func (t *Task) Init(env *Environment) error { 40 | t.Vars = make(Vars) 41 | 42 | for k, v := range t.data { 43 | found := false 44 | 45 | for _, i := range cOptions { 46 | if k == i { 47 | found = true 48 | break 49 | } 50 | } 51 | 52 | if !found { 53 | if t.cmd != "" { 54 | return fmt.Errorf("Duplicate command '%s', already: %s", k, t.cmd) 55 | } 56 | 57 | t.cmd = k 58 | if m, ok := v.(map[interface{}]interface{}); ok { 59 | for ik, iv := range m { 60 | t.Vars[fmt.Sprintf("%v", ik)] = Any(iv) 61 | } 62 | } else { 63 | t.args = fmt.Sprintf("%v", v) 64 | } 65 | } 66 | } 67 | 68 | if t.cmd == "" { 69 | act, ok := t.data["action"] 70 | if !ok { 71 | return fmt.Errorf("No action specified") 72 | } 73 | 74 | parts := strings.SplitN(fmt.Sprintf("%v", act), " ", 2) 75 | 76 | t.cmd = parts[0] 77 | 78 | if len(parts) == 2 { 79 | t.args = parts[1] 80 | } 81 | } 82 | 83 | t.Paths = env.Paths 84 | 85 | return nil 86 | } 87 | 88 | func (t *Task) Command() string { 89 | return t.cmd 90 | } 91 | 92 | func (t *Task) Args() string { 93 | return t.args 94 | } 95 | 96 | func (t *Task) Name() string { 97 | return t.data["name"].(string) 98 | } 99 | 100 | func (t *Task) Register() string { 101 | if v, ok := t.data["register"]; ok { 102 | return v.(string) 103 | } 104 | 105 | return "" 106 | } 107 | 108 | func (t *Task) Future() string { 109 | if v, ok := t.data["future"]; ok { 110 | return v.(string) 111 | } 112 | 113 | return "" 114 | } 115 | 116 | func (t *Task) When() string { 117 | if v, ok := t.data["when"]; ok { 118 | return v.(string) 119 | } 120 | 121 | return "" 122 | } 123 | 124 | func (t *Task) Notify() []string { 125 | var v interface{} 126 | var ok bool 127 | 128 | if v, ok = t.data["notify"]; !ok { 129 | return nil 130 | } 131 | 132 | var list []interface{} 133 | 134 | if list, ok = v.([]interface{}); !ok { 135 | return nil 136 | } 137 | 138 | out := make([]string, len(list)) 139 | 140 | for i, x := range list { 141 | out[i] = x.(string) 142 | } 143 | 144 | return out 145 | } 146 | 147 | func (t *Task) Async() bool { 148 | _, ok := t.data["async"] 149 | 150 | return ok 151 | } 152 | 153 | func (t *Task) Items() []interface{} { 154 | if v, ok := t.data["with_items"]; ok { 155 | if a, ok := v.([]interface{}); ok { 156 | return a 157 | } 158 | } 159 | 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /test.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func RunCapture(path string) (*Runner, string, error) { 10 | cfg := &Config{ShowCommandOutput: false} 11 | 12 | ns := NewNestedScope(nil) 13 | 14 | env := NewEnv(ns, cfg) 15 | defer env.Cleanup() 16 | 17 | playbook, err := NewPlaybook(env, path) 18 | if err != nil { 19 | fmt.Printf("Error loading plays: %s\n", err) 20 | return nil, "", err 21 | } 22 | 23 | var buf bytes.Buffer 24 | 25 | reporter := CLIReporter{out: &buf} 26 | 27 | runner := NewRunner(env, playbook.Plays) 28 | runner.SetReport(&reporter) 29 | 30 | cur, err := os.Getwd() 31 | if err != nil { 32 | return nil, "", err 33 | } 34 | 35 | defer os.Chdir(cur) 36 | os.Chdir(playbook.baseDir) 37 | 38 | err = runner.Run(env) 39 | 40 | if err != nil { 41 | return nil, "", err 42 | } 43 | 44 | return runner, buf.String(), nil 45 | } 46 | -------------------------------------------------------------------------------- /test/common_vars.yml: -------------------------------------------------------------------------------- 1 | --- 2 | duck: quack 3 | cow: moo 4 | extguard: " '$favcolor' == 'blue' " 5 | -------------------------------------------------------------------------------- /test/default_os.yml: -------------------------------------------------------------------------------- 1 | --- 2 | testing: default 3 | -------------------------------------------------------------------------------- /test/download.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - name: download a file 5 | download: url=http://www.google.com 6 | register: goog 7 | - name: show 8 | shell: echo "{{ goog.path }}" 9 | -------------------------------------------------------------------------------- /test/future.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - name: sleep 1 5 | shell: sleep 1 6 | future: one 7 | - name: sleep 2 8 | shell: sleep 1 9 | future: two 10 | -------------------------------------------------------------------------------- /test/future2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - name: sleep 1 5 | shell: sleep 1 && echo "hello" 6 | future: greeting 7 | - name: depend 8 | shell: echo $(read greeting.stdout) 9 | - name: depend2 10 | shell: echo {{greeting.stdout}} 11 | -------------------------------------------------------------------------------- /test/inc_child.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: in child 3 | shell: echo "weee" 4 | -------------------------------------------------------------------------------- /test/inc_child2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: show name 3 | shell: echo $name 4 | - name: show host 5 | shell: echo $host 6 | - name: show city 7 | shell: echo $city 8 | -------------------------------------------------------------------------------- /test/inc_parent.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - include: inc_child.yml 5 | -------------------------------------------------------------------------------- /test/inc_parent2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - include: inc_child2.yml name=oscar 5 | city: Los Angeles 6 | vars: 7 | host: ellen 8 | -------------------------------------------------------------------------------- /test/incplaybook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | vars: 4 | sub2: "defined in incplaybook" 5 | 6 | tasks: 7 | - name: test exposed include vars 8 | shell: echo inc $sub1 9 | - name: test include vars override 10 | shell: echo sub2 is $sub2 11 | 12 | -------------------------------------------------------------------------------- /test/items.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | tasks: 4 | - name: just say it 5 | shell: echo $item 6 | with_items: 7 | - a 8 | - b 9 | - c 10 | -------------------------------------------------------------------------------- /test/on_vagrant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - tasks: 4 | - name: Get memory 5 | shell: free 6 | -------------------------------------------------------------------------------- /test/on_vagrant2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - tasks: 4 | - name: Run on self via ssh 5 | tachyon: 6 | target: vagrant@localhost 7 | playbook: on_vagrant.yml 8 | dev: true 9 | -------------------------------------------------------------------------------- /test/on_vagrant3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - tasks: 4 | - name: Run on self via ssh 5 | tachyon: 6 | target: vagrant@localhost 7 | playbook: on_vagrant2.yml 8 | dev: true 9 | -------------------------------------------------------------------------------- /test/playbook1.yml: -------------------------------------------------------------------------------- 1 | # extremely simple test of the most basic of playbook engine/functions 2 | --- 3 | - include: incplaybook.yml sub1=from_include 4 | vars: 5 | sub2: "from include" 6 | 7 | - hosts: all 8 | connection: local 9 | 10 | # the 'weasels' string should show up in the output 11 | 12 | vars: 13 | answer: "Wuh, I think so" 14 | port: 5150 15 | 16 | # we should have import events for common_vars and CentOS.yml (if run on CentOS) 17 | # sorry, tests are a bit platform specific just for now 18 | 19 | vars_files: 20 | - common_vars.yml 21 | - [ '{{facter_operatingsystem.yml}}', 'default_os.yml' ] 22 | 23 | tasks: 24 | 25 | - name: test basic success command 26 | action: command true 27 | 28 | - name: test basic success command 2 29 | action: command true 30 | 31 | - name: test keyed command syntax 32 | command: true 33 | 34 | - name: test basic shell, plus two ways to dereference a variable 35 | action: shell echo {{port}} 36 | 37 | - name: test vars_files imports 38 | action: shell echo {{duck}} {{cow}} {{testing}} 39 | notify: 40 | - on change 1 41 | 42 | # in the command below, the test file should contain a valid template 43 | # and trigger the change handler 44 | 45 | - name: test copy 46 | action: copy src=test/sample dest=/tmp/ansible_test_data_copy.out 47 | notify: 48 | - on change 1 49 | 50 | - name: test copy 2 51 | copy: 52 | src: test/sample 53 | dest: /tmp/tachyon_test_data_copy2.out 54 | 55 | - name: test dollar expansion 56 | action: shell echo $duck 57 | 58 | - name: test outer vars 59 | action: shell echo $(or :user "unknown") 60 | # there should be various poll events within the range 61 | 62 | - name: async poll test 63 | action: shell sleep 5 64 | async: 10 65 | poll: 3 66 | 67 | # the following command should be skipped 68 | 69 | - name: this should be skipped 70 | action: shell echo 'if you see this, this is wrong' 71 | when: $(== 2 3) 72 | 73 | # this should run 74 | - name: this should be run 75 | action: shell echo 'if you see this, everything is good' 76 | when: $(== 3 3) 77 | notify: 78 | - on change 2 79 | 80 | # this should not run 81 | - name: this should be not run variables 82 | action: shell echo 'if you see this, you can matching variables is busted' 83 | when: $(== duck "xxx") 84 | 85 | # this should run 86 | - name: this should be run variables 87 | action: shell echo 'if you see this, you can match variables' 88 | when: $(== duck "quack") 89 | notify: 90 | - on change 2 91 | 92 | handlers: 93 | 94 | # in the above test example, this should fire ONCE (at the end) 95 | - name: on change 1 96 | action: shell echo 'this should fire once' 97 | 98 | # in the above test example, this should fire ONCE (at the end) 99 | 100 | - name: on change 2 101 | action: shell echo 'this should fire once also' 102 | 103 | # in the above test example, this should NOT FIRE 104 | 105 | - name: on change 3 106 | action: shell echo 'if you see this, this is wrong' 107 | -------------------------------------------------------------------------------- /test/register.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: all 4 | tasks: 5 | - name: get date 6 | shell: date 7 | register: date 8 | - name: show 9 | shell: echo "{{ date.stdout }}" 10 | 11 | -------------------------------------------------------------------------------- /test/roles/role1/handlers/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: role handler 3 | shell: echo in role handler 4 | -------------------------------------------------------------------------------- /test/roles/role1/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: echo role 3 | shell: echo in role 4 | notify: 5 | - role handler 6 | -------------------------------------------------------------------------------- /test/roles/role2/vars/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | rolevar: from role var 3 | -------------------------------------------------------------------------------- /test/roles/role3/tasks/get.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Show in get 4 | shell: echo in get 5 | -------------------------------------------------------------------------------- /test/roles/role3/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: show name 3 | shell: echo $name 4 | -------------------------------------------------------------------------------- /test/roles/role4/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - include: special.yml 3 | -------------------------------------------------------------------------------- /test/roles/role4/tasks/special.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: announce 3 | shell: echo "in special" 4 | -------------------------------------------------------------------------------- /test/roles/role6/files/my_script.sh: -------------------------------------------------------------------------------- 1 | echo "in my script" 2 | -------------------------------------------------------------------------------- /test/roles/role6/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: run my script 3 | script: my_script.sh 4 | -------------------------------------------------------------------------------- /test/roles/role7/meta/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | dependencies: 3 | - role: role3 4 | name: role7 5 | -------------------------------------------------------------------------------- /test/roles/role8/modules/test.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test 3 | tasks: 4 | - name: Print out the name 5 | shell: echo $name 6 | -------------------------------------------------------------------------------- /test/sample: -------------------------------------------------------------------------------- 1 | blah 2 | -------------------------------------------------------------------------------- /test/site1.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - role1 5 | -------------------------------------------------------------------------------- /test/site10.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - hosts: all 4 | roles: 5 | - role3::get 6 | -------------------------------------------------------------------------------- /test/site2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - role2 5 | tasks: 6 | - name: show role var 7 | shell: echo $rolevar 8 | -------------------------------------------------------------------------------- /test/site3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - role: role3 5 | name: from site3 6 | -------------------------------------------------------------------------------- /test/site4.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - role3 name="from site4" 5 | -------------------------------------------------------------------------------- /test/site5.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - role4 5 | -------------------------------------------------------------------------------- /test/site6.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - role6 5 | -------------------------------------------------------------------------------- /test/site7.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - roles: 3 | - role7 4 | -------------------------------------------------------------------------------- /test/site8.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - roles: 3 | - role8 4 | tasks: 5 | - name: Use test 6 | test: name="from module" 7 | 8 | -------------------------------------------------------------------------------- /test/site9.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - roles: 3 | - role8 4 | tasks: 5 | - name: Use test 6 | test: 7 | name: from module 8 | 9 | -------------------------------------------------------------------------------- /test/test_script.sh: -------------------------------------------------------------------------------- 1 | echo "hello script" 2 | -------------------------------------------------------------------------------- /test/vagrant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - tasks: 4 | - name: Run on vagrant 5 | tachyon: 6 | target: :vagrant 7 | playbook: on_vagrant.yml 8 | dev: true 9 | -------------------------------------------------------------------------------- /test/vagrant2.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - tasks: 4 | - name: Run on vagrant 5 | tachyon: 6 | target: :vagrant 7 | playbook: on_vagrant2.yml 8 | dev: true 9 | -------------------------------------------------------------------------------- /test/vagrant3.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - tasks: 4 | - name: Run on vagrant 5 | tachyon: 6 | target: :vagrant 7 | playbook: on_vagrant3.yml 8 | dev: true 9 | -------------------------------------------------------------------------------- /upstart/config.go: -------------------------------------------------------------------------------- 1 | package upstart 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | ) 11 | 12 | var InitDir = "/etc/init" 13 | 14 | type Script string 15 | 16 | func (s Script) Indented() string { 17 | return " " + strings.Join(strings.Split(string(s), "\n"), "\n ") 18 | } 19 | 20 | type Code struct { 21 | Exec string 22 | Script Script 23 | } 24 | 25 | func (c Code) Set() bool { 26 | return c.Exec != "" || c.Script != "" 27 | } 28 | 29 | func (c Code) Output(name string) string { 30 | if c.Exec != "" { 31 | return fmt.Sprintf("%s exec %s\n", name, c.Exec) 32 | } 33 | 34 | if c.Script != "" { 35 | return fmt.Sprintf("%s script\n%s\nend script\n", name, c.Script.Indented()) 36 | } 37 | 38 | return "" 39 | } 40 | 41 | type Config struct { 42 | Name string 43 | Type string 44 | 45 | Console string 46 | Directory string 47 | Description string 48 | Emits []string 49 | Env map[string]string 50 | Exec string 51 | Expect string 52 | Instance string 53 | KillSignal []string 54 | KillTimeout int 55 | Limit []string 56 | Manual bool 57 | Nice int 58 | OomScore int 59 | 60 | PostStart Code 61 | PostStop Code 62 | PreStart Code 63 | PreStop Code 64 | 65 | ReloadSignal string 66 | Respawn bool 67 | Script Script 68 | SetGid string 69 | SetUid string 70 | StartOn string 71 | StopOn string 72 | 73 | Umask int 74 | Usage string 75 | Version string 76 | } 77 | 78 | func NewConfig() *Config { 79 | return &Config{ 80 | Nice: -1, 81 | OomScore: -1000, 82 | Umask: -1, 83 | Env: make(map[string]string), 84 | } 85 | } 86 | 87 | func DaemonConfig(name string, cmd string) *Config { 88 | cfg := &Config{ 89 | Nice: -1, 90 | OomScore: -1000, 91 | Umask: -1, 92 | Env: make(map[string]string), 93 | 94 | Name: name, 95 | Type: "daemon", 96 | Console: "log", 97 | Description: fmt.Sprintf("%s service", name), 98 | Exec: cmd, 99 | Expect: "daemon", 100 | Respawn: true, 101 | StartOn: "runlevel [2345]", 102 | StopOn: "runlevel [!2345]", 103 | } 104 | 105 | return cfg 106 | } 107 | 108 | func TaskConfig(name string, cmd string) *Config { 109 | cfg := &Config{ 110 | Nice: -1, 111 | OomScore: -1000, 112 | Umask: -1, 113 | Env: make(map[string]string), 114 | 115 | Name: name, 116 | Type: "task", 117 | Console: "log", 118 | Description: fmt.Sprintf("%s task", name), 119 | Exec: cmd, 120 | StartOn: "runlevel [2345]", 121 | StopOn: "runlevel [!2345]", 122 | } 123 | 124 | return cfg 125 | } 126 | 127 | func (c *Config) UpdateDefaults() { 128 | if c.Description == "" { 129 | c.Description = fmt.Sprintf("%s %s", c.Name, c.Type) 130 | } 131 | } 132 | 133 | func (c *Config) Foreground() { 134 | c.Expect = "" 135 | } 136 | 137 | func (c *Config) Generate() []byte { 138 | var buf bytes.Buffer 139 | 140 | c.UpdateDefaults() 141 | 142 | buf.WriteString(fmt.Sprintf("# %s %s\n\n", c.Name, c.Type)) 143 | 144 | buf.WriteString(fmt.Sprintf("description \"%s\"\n", c.Description)) 145 | 146 | if c.Usage != "" { 147 | buf.WriteString(fmt.Sprintf("usage \"%s\"\n", c.Usage)) 148 | } 149 | 150 | if c.Version != "" { 151 | buf.WriteString(fmt.Sprintf("version \"%s\"\n", c.Version)) 152 | } 153 | 154 | buf.WriteString(fmt.Sprintf("start on %s\n", c.StartOn)) 155 | buf.WriteString(fmt.Sprintf("stop on %s\n", c.StopOn)) 156 | 157 | if c.Type == "task" { 158 | buf.WriteString("task\n") 159 | } 160 | 161 | for _, e := range c.Emits { 162 | buf.WriteString(fmt.Sprintf("emits %s\n", e)) 163 | } 164 | 165 | if c.Instance != "" { 166 | buf.WriteString(fmt.Sprintf("instance %s\n", c.Instance)) 167 | } 168 | 169 | if c.Expect != "" { 170 | buf.WriteString(fmt.Sprintf("expect %s\n", c.Expect)) 171 | } 172 | 173 | if c.Respawn { 174 | buf.WriteString("respawn\n") 175 | } 176 | 177 | if len(c.Limit) > 0 { 178 | ls := strings.Join(c.Limit, " ") 179 | buf.WriteString(fmt.Sprintf("limit %s\n", ls)) 180 | } 181 | 182 | if c.Console != "" { 183 | buf.WriteString(fmt.Sprintf("console %s\n", c.Console)) 184 | } 185 | 186 | if c.Directory != "" { 187 | buf.WriteString(fmt.Sprintf("chdir %s\n", c.Directory)) 188 | } 189 | 190 | for k, v := range c.Env { 191 | buf.WriteString(fmt.Sprintf("env %s=\"%s\"\n", k, v)) 192 | } 193 | 194 | for _, v := range c.KillSignal { 195 | buf.WriteString(fmt.Sprintf("kill signal %s\n", v)) 196 | } 197 | 198 | if c.KillTimeout != 0 { 199 | buf.WriteString(fmt.Sprintf("kill timeout %d\n", c.KillTimeout)) 200 | } 201 | 202 | if c.ReloadSignal != "" { 203 | buf.WriteString(fmt.Sprintf("reload signal %s\n", c.ReloadSignal)) 204 | } 205 | 206 | if c.Manual { 207 | buf.WriteString("manual\n") 208 | } 209 | 210 | if c.Nice != -1 { 211 | buf.WriteString(fmt.Sprintf("nice %d\n", c.Nice)) 212 | } 213 | 214 | if c.OomScore != -1000 { 215 | buf.WriteString(fmt.Sprintf("oom score %d\n", c.OomScore)) 216 | } 217 | 218 | if c.SetGid != "" { 219 | buf.WriteString(fmt.Sprintf("setgid %s\n", c.SetGid)) 220 | } 221 | 222 | if c.SetUid != "" { 223 | buf.WriteString(fmt.Sprintf("setuid %s\n", c.SetUid)) 224 | } 225 | 226 | if c.Umask != -1 { 227 | buf.WriteString(fmt.Sprintf("umask %03o\n", c.Umask)) 228 | } 229 | 230 | if c.PreStart.Set() { 231 | buf.WriteString(c.PreStart.Output("pre-start")) 232 | } 233 | 234 | if c.PostStart.Set() { 235 | buf.WriteString(c.PostStart.Output("post-start")) 236 | } 237 | 238 | if c.PreStop.Set() { 239 | buf.WriteString(c.PreStop.Output("pre-stop")) 240 | } 241 | 242 | if c.PostStop.Set() { 243 | buf.WriteString(c.PostStop.Output("post-stop")) 244 | } 245 | 246 | if c.Script != "" { 247 | s := fmt.Sprintf("script\n%s\nend script\n", c.Script.Indented()) 248 | buf.WriteString(s) 249 | } 250 | 251 | if c.Exec != "" { 252 | buf.WriteString(fmt.Sprintf("exec %s\n", c.Exec)) 253 | } 254 | 255 | return buf.Bytes() 256 | } 257 | 258 | func (c *Config) Install() error { 259 | return InstallConfig(c.Name, c.Generate()) 260 | } 261 | 262 | func (c *Config) Exists() bool { 263 | _, err := os.Stat(filepath.Join(InitDir, c.Name+".conf")) 264 | if err == nil { 265 | return true 266 | } 267 | 268 | return false 269 | } 270 | 271 | func InstallConfig(name string, config []byte) error { 272 | path := filepath.Join(InitDir, name+".conf") 273 | return ioutil.WriteFile(path, config, 0644) 274 | } 275 | -------------------------------------------------------------------------------- /upstart/config_test.go: -------------------------------------------------------------------------------- 1 | package upstart 2 | 3 | import ( 4 | "bytes" 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func TestConfigGenerate(t *testing.T) { 11 | c := DaemonConfig("puma", "puma -c blah.cfg") 12 | 13 | b := c.Generate() 14 | 15 | exp := `# puma daemon 16 | 17 | description "puma service" 18 | start on runlevel [2345] 19 | stop on runlevel [!2345] 20 | expect daemon 21 | respawn 22 | console log 23 | exec puma -c blah.cfg 24 | ` 25 | 26 | if string(b) != exp { 27 | t.Log(exp) 28 | t.Log(string(b)) 29 | t.Fatal("Config did not generate properly") 30 | } 31 | } 32 | 33 | func TestConfigGenerateAllOptions(t *testing.T) { 34 | c := DaemonConfig("puma", "puma -c blah.cfg") 35 | c.Directory = "/tmp" 36 | c.Emits = []string{"fun-times"} 37 | c.Env["FOO"] = "bar" 38 | c.Instance = "$INDEX" 39 | c.KillSignal = []string{"SIGTERM"} 40 | c.KillTimeout = 30 41 | c.Limit = []string{"blah"} 42 | c.Manual = true 43 | c.Nice = 0 44 | c.OomScore = 0 45 | c.ReloadSignal = "SIGUSR2" 46 | c.SetGid = "staff" 47 | c.SetUid = "deploy" 48 | c.Umask = 044 49 | c.Usage = "puma options" 50 | c.Version = "1.0-beta" 51 | 52 | b := c.Generate() 53 | 54 | exp := `# puma daemon 55 | 56 | description "puma service" 57 | usage "puma options" 58 | version "1.0-beta" 59 | start on runlevel [2345] 60 | stop on runlevel [!2345] 61 | emits fun-times 62 | instance $INDEX 63 | expect daemon 64 | respawn 65 | limit blah 66 | console log 67 | chdir /tmp 68 | env FOO="bar" 69 | kill signal SIGTERM 70 | kill timeout 30 71 | reload signal SIGUSR2 72 | manual 73 | nice 0 74 | oom score 0 75 | setgid staff 76 | setuid deploy 77 | umask 044 78 | exec puma -c blah.cfg 79 | ` 80 | 81 | if string(b) != exp { 82 | t.Log(exp) 83 | t.Log(string(b)) 84 | t.Fatal("Config did not generate properly") 85 | } 86 | } 87 | 88 | func TestConfigGeneratePostPreExec(t *testing.T) { 89 | c := DaemonConfig("puma", "puma -c blah.cfg") 90 | c.PreStart.Exec = "foo3" 91 | c.PostStart.Exec = "foo1" 92 | c.PreStop.Exec = "foo4" 93 | c.PostStop.Exec = "foo2" 94 | 95 | b := c.Generate() 96 | 97 | exp := `# puma daemon 98 | 99 | description "puma service" 100 | start on runlevel [2345] 101 | stop on runlevel [!2345] 102 | expect daemon 103 | respawn 104 | console log 105 | pre-start exec foo3 106 | post-start exec foo1 107 | pre-stop exec foo4 108 | post-stop exec foo2 109 | exec puma -c blah.cfg 110 | ` 111 | 112 | if string(b) != exp { 113 | t.Log(exp) 114 | t.Log(string(b)) 115 | t.Fatal("Config did not generate properly") 116 | } 117 | } 118 | 119 | func TestConfigGeneratePostPreScript(t *testing.T) { 120 | c := DaemonConfig("puma", "puma -c blah.cfg") 121 | c.PreStart.Script = "foo3\nbar" 122 | c.PostStart.Script = "foo1\nbar" 123 | c.PreStop.Script = "foo4\nbar" 124 | c.PostStop.Script = "foo2\nbar" 125 | 126 | b := c.Generate() 127 | 128 | exp := `# puma daemon 129 | 130 | description "puma service" 131 | start on runlevel [2345] 132 | stop on runlevel [!2345] 133 | expect daemon 134 | respawn 135 | console log 136 | pre-start script 137 | foo3 138 | bar 139 | end script 140 | post-start script 141 | foo1 142 | bar 143 | end script 144 | pre-stop script 145 | foo4 146 | bar 147 | end script 148 | post-stop script 149 | foo2 150 | bar 151 | end script 152 | exec puma -c blah.cfg 153 | ` 154 | 155 | if string(b) != exp { 156 | t.Log(exp) 157 | t.Log(string(b)) 158 | t.Fatal("Config did not generate properly") 159 | } 160 | } 161 | 162 | func TestConfigGenerateTask(t *testing.T) { 163 | c := TaskConfig("warmup-db", "mysql --warm-up") 164 | 165 | b := c.Generate() 166 | 167 | exp := `# warmup-db task 168 | 169 | description "warmup-db task" 170 | start on runlevel [2345] 171 | stop on runlevel [!2345] 172 | task 173 | console log 174 | exec mysql --warm-up 175 | ` 176 | 177 | if string(b) != exp { 178 | t.Log(exp) 179 | t.Log(string(b)) 180 | t.Fatal("Config did not generate properly") 181 | } 182 | } 183 | 184 | func TestInstallCommand(t *testing.T) { 185 | tmpdir, err := ioutil.TempDir("", "upstart-test") 186 | if err != nil { 187 | panic(err) 188 | } 189 | 190 | defer os.RemoveAll(tmpdir) 191 | 192 | InitDir = tmpdir 193 | 194 | exp := []byte("stuff") 195 | 196 | err = InstallConfig("blah", exp) 197 | if err != nil { 198 | panic(err) 199 | } 200 | 201 | config, err := ioutil.ReadFile(tmpdir + "/blah.conf") 202 | if err != nil { 203 | panic(err) 204 | } 205 | 206 | if !bytes.Equal(exp, config) { 207 | t.Error("Did not write the config") 208 | } 209 | } 210 | 211 | func TestConfigInstall(t *testing.T) { 212 | tmpdir, err := ioutil.TempDir("", "upstart-test") 213 | if err != nil { 214 | panic(err) 215 | } 216 | 217 | defer os.RemoveAll(tmpdir) 218 | 219 | InitDir = tmpdir 220 | 221 | c := DaemonConfig("puma", "puma -c blah.conf") 222 | 223 | exp := c.Generate() 224 | 225 | err = c.Install() 226 | if err != nil { 227 | panic(err) 228 | } 229 | 230 | config, err := ioutil.ReadFile(tmpdir + "/puma.conf") 231 | if err != nil { 232 | panic(err) 233 | } 234 | 235 | if !bytes.Equal(exp, config) { 236 | t.Error("Did not write the config") 237 | } 238 | } 239 | 240 | func TestConfigExists(t *testing.T) { 241 | tmpdir, err := ioutil.TempDir("", "upstart-test") 242 | if err != nil { 243 | panic(err) 244 | } 245 | 246 | defer os.RemoveAll(tmpdir) 247 | 248 | InitDir = tmpdir 249 | 250 | c := DaemonConfig("puma", "puma -c blah.conf") 251 | 252 | c.Generate() 253 | 254 | err = c.Install() 255 | if err != nil { 256 | panic(err) 257 | } 258 | 259 | if !c.Exists() { 260 | t.Error("Didn't find the config already there") 261 | } 262 | } 263 | -------------------------------------------------------------------------------- /upstart/upstart.go: -------------------------------------------------------------------------------- 1 | package upstart 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/guelfey/go.dbus" 7 | "os/exec" 8 | "os/user" 9 | "strings" 10 | ) 11 | 12 | type Conn struct { 13 | conn *dbus.Conn 14 | } 15 | 16 | type Job struct { 17 | u *Conn 18 | path dbus.ObjectPath 19 | } 20 | 21 | const BusName = "com.ubuntu.Upstart" 22 | 23 | func (u *Conn) object(path dbus.ObjectPath) *dbus.Object { 24 | return u.conn.Object(BusName, path) 25 | } 26 | 27 | func userAndHome() (string, string, error) { 28 | u, err := user.Current() 29 | if err != nil { 30 | out, nerr := exec.Command("sh", "-c", "getent passwd `id -u`").Output() 31 | 32 | if nerr != nil { 33 | return "", "", err 34 | } 35 | 36 | fields := bytes.Split(out, []byte(`:`)) 37 | if len(fields) >= 6 { 38 | return string(fields[0]), string(fields[5]), nil 39 | } 40 | 41 | return "", "", fmt.Errorf("Unable to figure out the home dir") 42 | } 43 | 44 | return u.Username, u.HomeDir, nil 45 | } 46 | 47 | func Dial() (*Conn, error) { 48 | conn, err := dbus.SystemBusPrivate() 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | user, home, err := userAndHome() 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | methods := []dbus.Auth{dbus.AuthExternal(user), dbus.AuthCookieSha1(user, home)} 59 | if err = conn.Auth(methods); err != nil { 60 | conn.Close() 61 | return nil, err 62 | } 63 | 64 | if err = conn.Hello(); err != nil { 65 | conn.Close() 66 | conn = nil 67 | } 68 | 69 | return &Conn{conn}, nil 70 | } 71 | 72 | func (u *Conn) Close() error { 73 | return u.conn.Close() 74 | } 75 | 76 | func (u *Conn) Jobs() ([]*Job, error) { 77 | obj := u.object("/com/ubuntu/Upstart") 78 | 79 | var s []dbus.ObjectPath 80 | err := obj.Call("com.ubuntu.Upstart0_6.GetAllJobs", 0).Store(&s) 81 | if err != nil { 82 | return nil, err 83 | } 84 | 85 | var out []*Job 86 | 87 | for _, v := range s { 88 | out = append(out, &Job{u, v}) 89 | } 90 | 91 | return out, nil 92 | } 93 | 94 | func (u *Conn) Job(name string) (*Job, error) { 95 | obj := u.object("/com/ubuntu/Upstart") 96 | 97 | var s dbus.ObjectPath 98 | err := obj.Call("com.ubuntu.Upstart0_6.GetJobByName", 0, name).Store(&s) 99 | if err != nil { 100 | return nil, err 101 | } 102 | 103 | return &Job{u, s}, nil 104 | } 105 | 106 | func (u *Conn) Instance(name string) (*Instance, error) { 107 | parts := strings.SplitN(name, "/", 2) 108 | 109 | job, err := u.Job(parts[0]) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | inst := "" 115 | 116 | if len(parts) == 2 { 117 | inst = parts[1] 118 | } 119 | 120 | return job.Instance(inst) 121 | } 122 | 123 | func (u *Conn) EmitEvent(name string, env []string, wait bool) error { 124 | obj := u.object("/com/ubuntu/Upstart") 125 | return obj.Call("com.ubuntu.Upstart0_6.EmitEvent", 0, name, env, wait).Store() 126 | } 127 | 128 | type Instance struct { 129 | j *Job 130 | path dbus.ObjectPath 131 | } 132 | 133 | func (j *Job) obj() *dbus.Object { 134 | return j.u.object(j.path) 135 | } 136 | 137 | func (i *Instance) obj() *dbus.Object { 138 | return i.j.u.object(i.path) 139 | } 140 | 141 | func (j *Job) Instances() ([]*Instance, error) { 142 | var instances []dbus.ObjectPath 143 | 144 | err := j.obj().Call("com.ubuntu.Upstart0_6.Job.GetAllInstances", 0).Store(&instances) 145 | if err != nil { 146 | return nil, err 147 | } 148 | 149 | var out []*Instance 150 | 151 | for _, inst := range instances { 152 | out = append(out, &Instance{j, inst}) 153 | } 154 | 155 | return out, nil 156 | } 157 | 158 | func (j *Job) Instance(name string) (*Instance, error) { 159 | var path dbus.ObjectPath 160 | 161 | err := j.obj().Call("com.ubuntu.Upstart0_6.Job.GetInstanceByName", 0, name).Store(&path) 162 | if err != nil { 163 | return nil, err 164 | } 165 | 166 | return &Instance{j, path}, nil 167 | } 168 | 169 | func (j *Job) prop(name string) (string, error) { 170 | val, err := j.obj().GetProperty("com.ubuntu.Upstart0_6.Job." + name) 171 | if err != nil { 172 | return "", err 173 | } 174 | 175 | if str, ok := val.Value().(string); ok { 176 | return str, nil 177 | } 178 | 179 | return "", fmt.Errorf("Name was not a string") 180 | } 181 | 182 | func (j *Job) Name() (string, error) { 183 | return j.prop("name") 184 | } 185 | 186 | func (j *Job) Description() (string, error) { 187 | return j.prop("description") 188 | } 189 | 190 | func (j *Job) Author() (string, error) { 191 | return j.prop("author") 192 | } 193 | 194 | func (j *Job) Version() (string, error) { 195 | return j.prop("version") 196 | } 197 | 198 | func (j *Job) Pid() (int32, error) { 199 | insts, err := j.Instances() 200 | if err != nil { 201 | return 0, err 202 | } 203 | 204 | switch len(insts) { 205 | default: 206 | return 0, fmt.Errorf("More than 1 instances running, no single pid") 207 | case 0: 208 | return 0, fmt.Errorf("No instances of job available") 209 | case 1: 210 | procs, err := insts[0].Processes() 211 | if err != nil { 212 | return 0, err 213 | } 214 | 215 | switch len(procs) { 216 | default: 217 | return 0, fmt.Errorf("More than 1 processes running, no single pid") 218 | case 0: 219 | return 0, fmt.Errorf("No process running of any instances") 220 | case 1: 221 | return procs[0].Pid, nil 222 | } 223 | } 224 | } 225 | 226 | func (j *Job) Pids() ([]int32, error) { 227 | insts, err := j.Instances() 228 | if err != nil { 229 | return nil, err 230 | } 231 | 232 | var pids []int32 233 | 234 | for _, inst := range insts { 235 | procs, err := inst.Processes() 236 | if err != nil { 237 | return nil, err 238 | } 239 | 240 | for _, proc := range procs { 241 | pids = append(pids, proc.Pid) 242 | } 243 | } 244 | 245 | return pids, nil 246 | } 247 | 248 | func (j *Job) StartWithOptions(env []string, wait bool) (*Instance, error) { 249 | c := j.obj().Call("com.ubuntu.Upstart0_6.Job.Start", 0, env, wait) 250 | 251 | var path dbus.ObjectPath 252 | err := c.Store(&path) 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | return &Instance{j, path}, nil 258 | } 259 | 260 | func (j *Job) Start() (*Instance, error) { 261 | return j.StartWithOptions([]string{}, true) 262 | } 263 | 264 | func (j *Job) StartAsync() (*Instance, error) { 265 | return j.StartWithOptions([]string{}, false) 266 | } 267 | 268 | func (j *Job) Restart() (*Instance, error) { 269 | wait := true 270 | c := j.obj().Call("com.ubuntu.Upstart0_6.Job.Restart", 0, []string{}, wait) 271 | 272 | var path dbus.ObjectPath 273 | err := c.Store(&path) 274 | if err != nil { 275 | return nil, err 276 | } 277 | 278 | return &Instance{j, path}, nil 279 | } 280 | 281 | func (j *Job) Stop() error { 282 | wait := true 283 | c := j.obj().Call("com.ubuntu.Upstart0_6.Job.Stop", 0, []string{}, wait) 284 | 285 | return c.Store() 286 | } 287 | 288 | func (i *Instance) strprop(name string) (string, error) { 289 | val, err := i.obj().GetProperty("com.ubuntu.Upstart0_6.Instance." + name) 290 | if err != nil { 291 | return "", err 292 | } 293 | 294 | if str, ok := val.Value().(string); ok { 295 | return str, nil 296 | } 297 | 298 | return "", fmt.Errorf("Name was not a string") 299 | } 300 | 301 | func (i *Instance) Name() (string, error) { 302 | return i.strprop("name") 303 | } 304 | 305 | func (i *Instance) Goal() (string, error) { 306 | return i.strprop("goal") 307 | } 308 | 309 | func (i *Instance) State() (string, error) { 310 | return i.strprop("state") 311 | } 312 | 313 | type Process struct { 314 | Name string 315 | Pid int32 316 | } 317 | 318 | func (i *Instance) Processes() ([]Process, error) { 319 | val, err := i.obj().GetProperty("com.ubuntu.Upstart0_6.Instance.processes") 320 | 321 | if err != nil { 322 | return nil, err 323 | } 324 | 325 | var out []Process 326 | 327 | if ary, ok := val.Value().([][]interface{}); ok { 328 | for _, elem := range ary { 329 | out = append(out, Process{elem[0].(string), elem[1].(int32)}) 330 | } 331 | } else { 332 | return nil, fmt.Errorf("Unable to decode processes property") 333 | } 334 | 335 | return out, nil 336 | } 337 | 338 | func (i *Instance) Pid() (int32, error) { 339 | processes, err := i.Processes() 340 | if err != nil { 341 | return 0, err 342 | } 343 | 344 | switch len(processes) { 345 | case 0: 346 | return 0, fmt.Errorf("No running processes for this instance") 347 | case 1: 348 | return processes[0].Pid, nil 349 | default: 350 | return 0, fmt.Errorf("More than one process for this instance") 351 | } 352 | } 353 | 354 | func (i *Instance) Start() error { 355 | c := i.obj().Call("com.ubuntu.Upstart0_6.Instance.Start", 0, true) 356 | 357 | return c.Store() 358 | } 359 | 360 | func (i *Instance) StartAsync() error { 361 | c := i.obj().Call("com.ubuntu.Upstart0_6.Instance.Start", 0, false) 362 | 363 | return c.Store() 364 | } 365 | func (i *Instance) Restart() error { 366 | c := i.obj().Call("com.ubuntu.Upstart0_6.Instance.Restart", 0, true) 367 | 368 | return c.Store() 369 | } 370 | 371 | func (i *Instance) RestartAsync() error { 372 | c := i.obj().Call("com.ubuntu.Upstart0_6.Instance.Restart", 0, false) 373 | 374 | return c.Store() 375 | } 376 | 377 | func (i *Instance) Stop() error { 378 | c := i.obj().Call("com.ubuntu.Upstart0_6.Instance.Stop", 0, true) 379 | 380 | return c.Store() 381 | } 382 | 383 | func (i *Instance) StopAsync() error { 384 | c := i.obj().Call("com.ubuntu.Upstart0_6.Instance.Stop", 0, false) 385 | 386 | return c.Store() 387 | } 388 | -------------------------------------------------------------------------------- /upstart/upstart_test.go: -------------------------------------------------------------------------------- 1 | package upstart 2 | 3 | import ( 4 | "os" 5 | "os/exec" 6 | "strconv" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | var jobName string = "atd" 12 | 13 | var runUpstartTests = false 14 | 15 | func init() { 16 | if s := os.Getenv("TEST_JOB"); s != "" { 17 | jobName = s 18 | } 19 | 20 | c := exec.Command("which", "initctl") 21 | c.Run() 22 | runUpstartTests = c.ProcessState.Success() 23 | } 24 | 25 | func TestJobs(t *testing.T) { 26 | if !runUpstartTests { 27 | t.SkipNow() 28 | } 29 | 30 | u, err := Dial() 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | jobs, err := u.Jobs() 36 | if err != nil { 37 | panic(err) 38 | } 39 | 40 | if len(jobs) == 0 { 41 | t.Fatal("Unable to get jobs") 42 | } 43 | 44 | var job *Job 45 | 46 | for _, j := range jobs { 47 | name, err := j.Name() 48 | if err != nil { 49 | panic(err) 50 | } 51 | 52 | if name == jobName { 53 | job = j 54 | } 55 | } 56 | 57 | if job == nil { 58 | t.Fatalf("Unable to find job: %s", jobName) 59 | } 60 | } 61 | 62 | func TestJob(t *testing.T) { 63 | if !runUpstartTests { 64 | t.SkipNow() 65 | } 66 | 67 | u, err := Dial() 68 | if err != nil { 69 | panic(err) 70 | } 71 | 72 | job, err := u.Job(jobName) 73 | if err != nil { 74 | panic(err) 75 | } 76 | 77 | if job == nil { 78 | t.Fatalf("Unable to find job: %s", jobName) 79 | } 80 | } 81 | 82 | func TestJobName(t *testing.T) { 83 | if !runUpstartTests { 84 | t.SkipNow() 85 | } 86 | 87 | u, err := Dial() 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | job, err := u.Job(jobName) 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | name, err := job.Name() 98 | if err != nil { 99 | panic(err) 100 | } 101 | 102 | if name != jobName { 103 | t.Fatalf("job name didn't work properly: %s != %s", name, jobName) 104 | } 105 | } 106 | 107 | func TestJobPid(t *testing.T) { 108 | if !runUpstartTests { 109 | t.SkipNow() 110 | } 111 | 112 | u, err := Dial() 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | job, err := u.Job(jobName) 118 | if err != nil { 119 | panic(err) 120 | } 121 | 122 | exp, err := job.Pid() 123 | if err != nil { 124 | panic(err) 125 | } 126 | 127 | bytes, err := exec.Command("pgrep", jobName).CombinedOutput() 128 | if err != nil { 129 | panic(err) 130 | } 131 | 132 | act, err := strconv.Atoi(strings.TrimSpace(string(bytes))) 133 | if err != nil { 134 | panic(err) 135 | } 136 | 137 | if exp != int32(act) { 138 | t.Fatalf("pid for job isn't correct: %d != %d", exp, act) 139 | } 140 | } 141 | 142 | func TestJobPidReturnsErrorWhenMultipleInstances(t *testing.T) { 143 | if !runUpstartTests { 144 | t.SkipNow() 145 | } 146 | 147 | u, err := Dial() 148 | if err != nil { 149 | panic(err) 150 | } 151 | 152 | job, err := u.Job("network-interface") 153 | if err != nil { 154 | panic(err) 155 | } 156 | 157 | _, err = job.Pid() 158 | if err == nil { 159 | t.Fatal("Pid didn't return an error") 160 | } 161 | } 162 | 163 | func TestInstanceWithJob(t *testing.T) { 164 | if !runUpstartTests { 165 | t.SkipNow() 166 | } 167 | 168 | u, err := Dial() 169 | if err != nil { 170 | panic(err) 171 | } 172 | 173 | inst, err := u.Instance(jobName) 174 | if err != nil { 175 | panic(err) 176 | } 177 | 178 | if inst == nil { 179 | t.Fatalf("Unable to find inst: %s", jobName) 180 | } 181 | } 182 | 183 | func TestInstanceWithJobAndInstance(t *testing.T) { 184 | if !runUpstartTests { 185 | t.SkipNow() 186 | } 187 | 188 | u, err := Dial() 189 | if err != nil { 190 | panic(err) 191 | } 192 | 193 | instName := "network-interface/lo" 194 | 195 | inst, err := u.Instance(instName) 196 | if err != nil { 197 | panic(err) 198 | } 199 | 200 | if inst == nil { 201 | t.Fatalf("Unable to find inst: %s", instName) 202 | } 203 | } 204 | 205 | func TestInstancePid(t *testing.T) { 206 | if !runUpstartTests { 207 | t.SkipNow() 208 | } 209 | 210 | u, err := Dial() 211 | if err != nil { 212 | panic(err) 213 | } 214 | 215 | inst, err := u.Instance(jobName) 216 | if err != nil { 217 | panic(err) 218 | } 219 | 220 | exp, err := inst.Pid() 221 | if err != nil { 222 | panic(err) 223 | } 224 | 225 | bytes, err := exec.Command("pgrep", jobName).CombinedOutput() 226 | if err != nil { 227 | panic(err) 228 | } 229 | 230 | act, err := strconv.Atoi(strings.TrimSpace(string(bytes))) 231 | if err != nil { 232 | panic(err) 233 | } 234 | 235 | if exp != int32(act) { 236 | t.Fatalf("pid for job isn't correct: %d != %d", exp, act) 237 | } 238 | } 239 | 240 | func TestInstanceRestart(t *testing.T) { 241 | if !runUpstartTests { 242 | t.SkipNow() 243 | } 244 | 245 | u, err := Dial() 246 | if err != nil { 247 | panic(err) 248 | } 249 | 250 | inst, err := u.Instance(jobName) 251 | if err != nil { 252 | panic(err) 253 | } 254 | 255 | start, err := inst.Pid() 256 | if err != nil { 257 | panic(err) 258 | } 259 | 260 | err = inst.Restart() 261 | if err != nil { 262 | panic(err) 263 | } 264 | 265 | cur, err := inst.Pid() 266 | 267 | if start == cur { 268 | t.Fatalf("job did not restart. old:%d, new:%d", start, cur) 269 | } 270 | } 271 | 272 | func TestJobStart(t *testing.T) { 273 | if !runUpstartTests { 274 | t.SkipNow() 275 | } 276 | 277 | u, err := Dial() 278 | if err != nil { 279 | panic(err) 280 | } 281 | 282 | job, err := u.Job(jobName) 283 | if err != nil { 284 | panic(err) 285 | } 286 | 287 | start, err := job.Pid() 288 | if err != nil { 289 | panic(err) 290 | } 291 | 292 | err = job.Stop() 293 | if err != nil { 294 | panic(err) 295 | } 296 | 297 | inst, err := job.Start() 298 | if err != nil { 299 | panic(err) 300 | } 301 | 302 | cur, err := inst.Pid() 303 | 304 | if start == cur { 305 | t.Fatalf("job did not restart. old:%d, new:%d", start, cur) 306 | } 307 | 308 | bytes, err := exec.Command("pgrep", jobName).CombinedOutput() 309 | if err != nil { 310 | panic(err) 311 | } 312 | 313 | act, err := strconv.Atoi(strings.TrimSpace(string(bytes))) 314 | if err != nil { 315 | panic(err) 316 | } 317 | 318 | if cur != int32(act) { 319 | t.Fatalf("pid for job isn't correct: %d != %d", cur, act) 320 | } 321 | } 322 | 323 | func TestJobRestart(t *testing.T) { 324 | if !runUpstartTests { 325 | t.SkipNow() 326 | } 327 | 328 | u, err := Dial() 329 | if err != nil { 330 | panic(err) 331 | } 332 | 333 | job, err := u.Job(jobName) 334 | if err != nil { 335 | panic(err) 336 | } 337 | 338 | start, err := job.Pid() 339 | if err != nil { 340 | panic(err) 341 | } 342 | 343 | inst, err := job.Restart() 344 | if err != nil { 345 | panic(err) 346 | } 347 | 348 | cur, err := inst.Pid() 349 | 350 | if start == cur { 351 | t.Fatalf("job did not restart. old:%d, new:%d", start, cur) 352 | } 353 | 354 | bytes, err := exec.Command("pgrep", jobName).CombinedOutput() 355 | if err != nil { 356 | panic(err) 357 | } 358 | 359 | act, err := strconv.Atoi(strings.TrimSpace(string(bytes))) 360 | if err != nil { 361 | panic(err) 362 | } 363 | 364 | if cur != int32(act) { 365 | t.Fatalf("pid for job isn't correct: %d != %d", cur, act) 366 | } 367 | } 368 | 369 | func TestEmitEvent(t *testing.T) { 370 | if !runUpstartTests { 371 | t.SkipNow() 372 | } 373 | 374 | u, err := Dial() 375 | if err != nil { 376 | panic(err) 377 | } 378 | 379 | u.EmitEvent("test-booted", []string{}, true) 380 | } 381 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/flynn/go-shlex" 8 | "gopkg.in/yaml.v1" 9 | "io/ioutil" 10 | "os" 11 | "os/exec" 12 | "os/user" 13 | "reflect" 14 | "sort" 15 | "strconv" 16 | "strings" 17 | ) 18 | 19 | func HomeDir() (string, error) { 20 | u, err := user.Current() 21 | if err != nil { 22 | su := os.Getenv("SUDO_USER") 23 | 24 | var out []byte 25 | var nerr error 26 | 27 | if su != "" { 28 | out, nerr = exec.Command("sh", "-c", "getent passwd "+su).Output() 29 | } else { 30 | out, nerr = exec.Command("sh", "-c", "getent passwd `id -u`").Output() 31 | } 32 | 33 | if nerr != nil { 34 | return "", err 35 | } 36 | 37 | fields := bytes.Split(out, []byte(`:`)) 38 | if len(fields) >= 6 { 39 | return string(fields[5]), nil 40 | } 41 | 42 | return "", fmt.Errorf("Unable to figure out the home dir") 43 | } 44 | 45 | return u.HomeDir, nil 46 | } 47 | func dbg(format string, args ...interface{}) { 48 | fmt.Printf("[DBG] "+format+"\n", args...) 49 | } 50 | 51 | func yamlFile(path string, v interface{}) error { 52 | data, err := ioutil.ReadFile(path) 53 | 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return yaml.Unmarshal(data, v) 59 | } 60 | 61 | func mapToStruct(m map[string]interface{}, tag string, v interface{}) error { 62 | e := reflect.ValueOf(v).Elem() 63 | 64 | t := e.Type() 65 | 66 | for i := 0; i < t.NumField(); i++ { 67 | f := t.Field(i) 68 | 69 | name := strings.ToLower(f.Name) 70 | required := false 71 | 72 | parts := strings.Split(f.Tag.Get(tag), ",") 73 | 74 | switch len(parts) { 75 | case 0: 76 | // nothing 77 | case 1: 78 | name = parts[0] 79 | case 2: 80 | name = parts[0] 81 | switch parts[1] { 82 | case "required": 83 | required = true 84 | default: 85 | return fmt.Errorf("Unsupported tag flag: %s", parts[1]) 86 | } 87 | } 88 | 89 | if val, ok := m[name]; ok { 90 | e.Field(i).Set(reflect.ValueOf(val)) 91 | } else if required { 92 | return fmt.Errorf("Missing value for %s", f.Name) 93 | } 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func ParseSimpleMap(s Scope, args string) (Vars, error) { 100 | args, err := ExpandVars(s, args) 101 | 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | sm := make(Vars) 107 | 108 | parts, err := shlex.Split(args) 109 | 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | for _, part := range parts { 115 | ec := strings.SplitN(part, "=", 2) 116 | 117 | if len(ec) == 2 { 118 | sm[ec[0]] = Any(inferString(ec[1])) 119 | } else { 120 | sm[part] = Any(true) 121 | } 122 | } 123 | 124 | return sm, nil 125 | } 126 | 127 | func split2(s, sep string) (string, string, bool) { 128 | parts := strings.SplitN(s, sep, 2) 129 | 130 | if len(parts) == 0 { 131 | return "", "", false 132 | } else if len(parts) == 1 { 133 | return parts[0], "", false 134 | } else { 135 | return parts[0], parts[1], true 136 | } 137 | } 138 | 139 | func inferString(s string) interface{} { 140 | switch strings.ToLower(s) { 141 | case "true", "yes": 142 | return true 143 | case "false", "no": 144 | return false 145 | } 146 | 147 | if i, err := strconv.ParseInt(s, 0, 0); err == nil { 148 | return i 149 | } 150 | 151 | return s 152 | } 153 | 154 | func indentedYAML(v interface{}, indent string) (string, error) { 155 | str, err := yaml.Marshal(v) 156 | if err != nil { 157 | return "", err 158 | } 159 | 160 | lines := strings.Split(string(str), "\n") 161 | 162 | out := make([]string, len(lines)) 163 | 164 | for idx, l := range lines { 165 | if l == "" { 166 | out[idx] = l 167 | } else { 168 | out[idx] = indent + l 169 | } 170 | } 171 | 172 | return strings.Join(out, "\n"), nil 173 | } 174 | 175 | func arrayVal(v interface{}, indent string) string { 176 | switch sv := v.(type) { 177 | case string: 178 | var out string 179 | 180 | if strings.Index(sv, "\n") != -1 { 181 | sub := strings.Split(sv, "\n") 182 | out = strings.Join(sub, "\n"+indent+" | ") 183 | return fmt.Sprintf("%s-\\\n%s | %s", indent, indent, out) 184 | } else { 185 | return fmt.Sprintf("%s- \"%s\"", indent, sv) 186 | } 187 | case int, uint, int32, uint32, int64, uint64: 188 | return fmt.Sprintf("%s- %d", indent, sv) 189 | case bool: 190 | return fmt.Sprintf("%s- %t", indent, sv) 191 | case map[string]interface{}: 192 | mv := indentedMap(sv, indent+" ") 193 | return fmt.Sprintf("%s-\n%s", indent, mv) 194 | } 195 | 196 | return fmt.Sprintf("%s- %v", indent, v) 197 | } 198 | 199 | func indentedMap(m map[string]interface{}, indent string) string { 200 | var keys []string 201 | 202 | for k, _ := range m { 203 | keys = append(keys, k) 204 | } 205 | 206 | sort.Strings(keys) 207 | 208 | var lines []string 209 | 210 | for _, k := range keys { 211 | v := m[k] 212 | 213 | switch sv := v.(type) { 214 | case string: 215 | var out string 216 | 217 | if strings.Index(sv, "\n") != -1 { 218 | sub := strings.Split(sv, "\n") 219 | out = strings.Join(sub, "\n"+indent+" | ") 220 | lines = append(lines, fmt.Sprintf("%s%s:\n%s | %s", 221 | indent, k, indent, out)) 222 | } else { 223 | lines = append(lines, fmt.Sprintf("%s%s: \"%s\"", indent, k, sv)) 224 | } 225 | case int, uint, int32, uint32, int64, uint64: 226 | lines = append(lines, fmt.Sprintf("%s%s: %d", indent, k, sv)) 227 | case bool: 228 | lines = append(lines, fmt.Sprintf("%s%s: %t", indent, k, sv)) 229 | case map[string]interface{}: 230 | mv := indentedMap(sv, indent+" ") 231 | lines = append(lines, fmt.Sprintf("%s%s:\n%s", indent, k, mv)) 232 | default: 233 | lines = append(lines, fmt.Sprintf("%s%s: %v", indent, k, sv)) 234 | } 235 | } 236 | 237 | return strings.Join(lines, "\n") 238 | } 239 | 240 | func indentedVars(m Vars, indent string) string { 241 | var keys []string 242 | 243 | for k, _ := range m { 244 | keys = append(keys, k) 245 | } 246 | 247 | sort.Strings(keys) 248 | 249 | var lines []string 250 | 251 | for _, k := range keys { 252 | v := m[k] 253 | 254 | switch sv := v.Read().(type) { 255 | case string: 256 | var out string 257 | 258 | if strings.Index(sv, "\n") != -1 { 259 | sub := strings.Split(sv, "\n") 260 | out = strings.Join(sub, "\n"+indent+" | ") 261 | lines = append(lines, fmt.Sprintf("%s%s:\n%s | %s", 262 | indent, k, indent, out)) 263 | } else { 264 | lines = append(lines, fmt.Sprintf("%s%s: \"%s\"", indent, k, sv)) 265 | } 266 | case int, uint, int32, uint32, int64, uint64: 267 | lines = append(lines, fmt.Sprintf("%s%s: %d", indent, k, sv)) 268 | case bool: 269 | lines = append(lines, fmt.Sprintf("%s%s: %t", indent, k, sv)) 270 | case map[string]interface{}: 271 | mv := indentedMap(sv, indent+" ") 272 | lines = append(lines, fmt.Sprintf("%s%s:\n%s", indent, k, mv)) 273 | default: 274 | lines = append(lines, fmt.Sprintf("%s%s: %v", indent, k, sv)) 275 | } 276 | } 277 | 278 | return strings.Join(lines, "\n") 279 | } 280 | 281 | func inlineMap(m map[string]interface{}) string { 282 | var keys []string 283 | 284 | for k, _ := range m { 285 | keys = append(keys, k) 286 | } 287 | 288 | // Minor special case. If there is only one key and it's 289 | // named "command", just return the value. 290 | if len(keys) == 1 && keys[0] == "command" { 291 | for _, v := range m { 292 | if sv, ok := v.(string); ok { 293 | return sv 294 | } 295 | } 296 | } 297 | 298 | sort.Strings(keys) 299 | 300 | var lines []string 301 | 302 | for _, k := range keys { 303 | v := m[k] 304 | 305 | switch sv := v.(type) { 306 | case string: 307 | lines = append(lines, fmt.Sprintf("%s=%s", k, strconv.Quote(sv))) 308 | case int, uint, int32, uint32, int64, uint64: 309 | lines = append(lines, fmt.Sprintf("%s=%d", k, sv)) 310 | case bool: 311 | lines = append(lines, fmt.Sprintf("%s=%t", k, sv)) 312 | case map[string]interface{}: 313 | lines = append(lines, fmt.Sprintf("%s=(%s)", k, inlineMap(sv))) 314 | default: 315 | lines = append(lines, fmt.Sprintf("%s=`%v`", k, sv)) 316 | } 317 | } 318 | 319 | return strings.Join(lines, " ") 320 | } 321 | 322 | func inlineVars(m Vars) string { 323 | var keys []string 324 | 325 | for k, _ := range m { 326 | keys = append(keys, k) 327 | } 328 | 329 | // Minor special case. If there is only one key and it's 330 | // named "command", just return the value. 331 | if len(keys) == 1 && keys[0] == "command" { 332 | for _, v := range m { 333 | if sv, ok := v.Read().(string); ok { 334 | return sv 335 | } 336 | } 337 | } 338 | 339 | sort.Strings(keys) 340 | 341 | var lines []string 342 | 343 | for _, k := range keys { 344 | v := m[k] 345 | 346 | switch sv := v.Read().(type) { 347 | case string: 348 | lines = append(lines, fmt.Sprintf("%s=%s", k, strconv.Quote(sv))) 349 | case int, uint, int32, uint32, int64, uint64: 350 | lines = append(lines, fmt.Sprintf("%s=%d", k, sv)) 351 | case bool: 352 | lines = append(lines, fmt.Sprintf("%s=%t", k, sv)) 353 | case map[string]interface{}: 354 | lines = append(lines, fmt.Sprintf("%s=(%s)", k, inlineMap(sv))) 355 | default: 356 | lines = append(lines, fmt.Sprintf("%s=`%v`", k, sv)) 357 | } 358 | } 359 | 360 | return strings.Join(lines, " ") 361 | } 362 | 363 | func fileExist(path string) bool { 364 | fi, err := os.Stat(path) 365 | if err != nil { 366 | return false 367 | } 368 | 369 | return !fi.IsDir() 370 | } 371 | 372 | func gmap(args ...interface{}) map[string]interface{} { 373 | m := make(map[string]interface{}) 374 | 375 | if len(args)%2 != 0 { 376 | panic(fmt.Sprintf("Specify an even number of args: %d", len(args))) 377 | } 378 | 379 | i := 0 380 | 381 | for i < len(args) { 382 | m[args[i].(string)] = args[i+1] 383 | i += 2 384 | } 385 | 386 | return m 387 | } 388 | 389 | func ijson(args ...interface{}) []byte { 390 | b, err := json.Marshal(gmap(args...)) 391 | if err != nil { 392 | panic(err) 393 | } 394 | 395 | return b 396 | } 397 | -------------------------------------------------------------------------------- /vagrant-tachyon/roles/build_essential/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update apt-get cache 3 | apt: update_cache=yes cache_time=60m 4 | 5 | - name: Install build packages 6 | apt: pkg=$item state=present 7 | with_items: 8 | - autoconf 9 | - binutils-doc 10 | - bison 11 | - build-essential 12 | - flex 13 | -------------------------------------------------------------------------------- /vagrant-tachyon/roles/golang/files/go.sh: -------------------------------------------------------------------------------- 1 | export PATH=/usr/local/go/bin:$PATH 2 | export GOPATH=$HOME/go 3 | -------------------------------------------------------------------------------- /vagrant-tachyon/roles/golang/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | - name: Install git 4 | apt: pkg=git state=present 5 | 6 | - name: Install mercurial 7 | apt: pkg=mercurial state=present 8 | 9 | - name: Install bzr 10 | apt: pkg=bzr state=present 11 | 12 | - name: Download go 13 | download: 14 | url: https://go.googlecode.com/files/go1.2.1.linux-amd64.tar.gz 15 | dest: /tmp/go.tar.gz 16 | once: true 17 | 18 | - name: Untar go 19 | shell: 20 | command: cd /usr/local && tar xzf /tmp/go.tar.gz 21 | creates: /usr/local/go 22 | 23 | - name: Copy profile 24 | copy: src=go.sh dest=/etc/profile.d/go.sh 25 | -------------------------------------------------------------------------------- /vagrant-tachyon/site.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | roles: 4 | - build_essential 5 | - golang 6 | -------------------------------------------------------------------------------- /vars.go: -------------------------------------------------------------------------------- 1 | package tachyon 2 | 3 | type Vars map[string]Value 4 | 5 | func (v Vars) Copy() Vars { 6 | o := make(Vars) 7 | 8 | for k, v := range v { 9 | o[k] = v 10 | } 11 | 12 | return o 13 | } 14 | 15 | func VarsFromStrMap(sm map[string]string) Vars { 16 | o := make(Vars) 17 | 18 | for k, v := range sm { 19 | o[k] = Any(inferString(v)) 20 | } 21 | 22 | return o 23 | } 24 | --------------------------------------------------------------------------------