pups 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 055f645c7b240abd8d9d8206029043d1097611c8
4
+ data.tar.gz: 9a0c6803b35b3813058ed89c33f937a959dc9fcb
5
+ SHA512:
6
+ metadata.gz: 2b64563d45db0ec09bb16465e09d81ca4ef39ac92dee8c1b6dd894a24ea4eda5ecc08e6890391e57e3653365f29a39fdcf17399ee383b7bcb9e7e5978ec44b52
7
+ data.tar.gz: 0069a7d13b2070240fc23b17befc9dc2a34d1a51c6e7573b575acf892d22251fbf1bd4c44a94820cc86c3d1dbdb5999e0b048fec6dc528a2ce44f0a81d62f9f7
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ *.swp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pups.gemspec
4
+ gemspec
@@ -0,0 +1,6 @@
1
+ guard :minitest do
2
+ # with Minitest::Unit
3
+ watch(%r{^test/(.*)\/?(.*)_test\.rb$})
4
+ watch(%r{^lib/(.*/)?([^/]+)\.rb$}) { |m| "test/#{m[2]}_test.rb" }
5
+ watch(%r{^test/test_helper\.rb$}) { 'test' }
6
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Sam Saffron
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,126 @@
1
+ # pups
2
+
3
+ Simple yaml based bootstrapper
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'pups'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install pups
18
+
19
+ ## Usage
20
+
21
+ pups is a small library that allows you to automate the process of creating Unix images.
22
+
23
+ Example:
24
+
25
+ ```
26
+ # somefile.yaml
27
+ params:
28
+ hello: hello world
29
+
30
+ run:
31
+ exec: /bin/bash -c 'echo $hello >>> hello'
32
+ ```
33
+
34
+ Running: `pups somefile.yaml` will execute the shell script resulting in a file called "hello" with the "hello world" contents
35
+
36
+ ### Features:
37
+
38
+ ####Execution
39
+
40
+ Run multiple commands in one path:
41
+
42
+ ```
43
+ run:
44
+ exec:
45
+ cd: some/path
46
+ cmd:
47
+ - echo 1
48
+ - echo 2
49
+ ```
50
+
51
+ Run commands in the background (for services etc)
52
+
53
+ ```
54
+ run:
55
+ exec:
56
+ cmd: /usr/bin/sshd
57
+ background: true
58
+ ```
59
+
60
+ Suppress exceptions on certain commands
61
+
62
+ ```
63
+ run:
64
+ exec:
65
+ cmd: /test
66
+ raise_on_faile: false
67
+ ```
68
+
69
+ ####Replacements:
70
+
71
+ ```
72
+ run:
73
+ replace:
74
+ filename: "/etc/redis/redis.conf"
75
+ from: /^pidfile.*$/
76
+ to: ""
77
+ ```
78
+
79
+ Will substitued the regex with blank, removing the pidfile line
80
+
81
+ ```
82
+ run:
83
+ - replace:
84
+ filename: "/etc/nginx/conf.d/discourse.conf"
85
+ from: /upstream[^\}]+\}/m
86
+ to: "upstream discourse {
87
+ server 127.0.0.1:3000;
88
+ }"
89
+ ```
90
+
91
+ Multiline replace using regex
92
+
93
+ ####Merge yaml files:
94
+
95
+ ```
96
+ home: /var/www/my_app
97
+ params:
98
+ database_yml:
99
+ production:
100
+ username: discourse
101
+ password: foo
102
+
103
+ run:
104
+ - merge: $home/config/database.yml $database_yml
105
+
106
+ ```
107
+
108
+ Will merge the yaml file with the inline contents
109
+
110
+ ####A common environment
111
+
112
+ ```
113
+ env:
114
+ MY_ENV: 1
115
+ ```
116
+
117
+ All executions will get this environment set up
118
+
119
+
120
+ ## Contributing
121
+
122
+ 1. Fork it
123
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
124
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
125
+ 4. Push to the branch (`git push origin my-new-feature`)
126
+ 5. Create new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.pattern = "test/*_test.rb"
6
+ end
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.dirname(__FILE__) + "/../lib"
4
+
5
+ require "pups"
6
+ require "pups/cli"
7
+
8
+ Pups::Cli.run(ARGV)
9
+
@@ -0,0 +1,23 @@
1
+ require "logger"
2
+ require "yaml"
3
+
4
+ require "pups/version"
5
+ require "pups/config"
6
+ require "pups/command"
7
+ require "pups/exec_command"
8
+ require "pups/merge_command"
9
+ require "pups/replace_command"
10
+ require "pups/file_command"
11
+
12
+ require "pups/runit"
13
+
14
+ module Pups
15
+ def self.log
16
+ # at the moment docker likes this
17
+ @logger ||= Logger.new(STDERR)
18
+ end
19
+
20
+ def self.log=(logger)
21
+ @logger = logger
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ class Pups::Cli
2
+
3
+ def self.usage
4
+ puts "Usage: pups FILE or pups --stdin"
5
+ exit 1
6
+ end
7
+ def self.run(args)
8
+ if args.length != 1
9
+ usage
10
+ end
11
+
12
+ Pups.log.info("Loading #{args[0]}")
13
+ if args[0] == "--stdin"
14
+ conf = STDIN.readlines.join
15
+ split = conf.split("_FILE_SEPERATOR_")
16
+
17
+ conf = nil
18
+ split.each do |data|
19
+ current = YAML.load(data.strip)
20
+ if conf
21
+ conf = Pups::MergeCommand.deep_merge(conf, current, :merge_arrays)
22
+ else
23
+ conf = current
24
+ end
25
+ end
26
+
27
+ config = Pups::Config.new(conf)
28
+ else
29
+ config = Pups::Config.load_file(args[0])
30
+ end
31
+ config.run
32
+
33
+ ensure
34
+ Pups::ExecCommand.terminate_async
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ class Pups::Command
2
+
3
+ def self.run(command,params)
4
+ case command
5
+ when String then self.from_str(command,params).run
6
+ when Hash then self.from_hash(command,params).run
7
+ end
8
+ end
9
+
10
+ def self.interpolate_params(cmd,params)
11
+ Pups::Config.interpolate_params(cmd,params)
12
+ end
13
+
14
+ def interpolate_params(cmd)
15
+ Pups::Command.interpolate_params(cmd,@params)
16
+ end
17
+ end
@@ -0,0 +1,115 @@
1
+ class Pups::Config
2
+
3
+ attr_reader :config, :params
4
+
5
+ def self.load_file(config_file)
6
+ new YAML.load_file(config_file)
7
+ end
8
+
9
+ def self.load_config(config)
10
+ new YAML.load(config)
11
+ end
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ validate!(@config)
16
+ @params = @config["params"]
17
+ @params ||= {}
18
+ ENV.each do |k,v|
19
+ @params["$ENV_#{k}"] = v
20
+ end
21
+ inject_hooks
22
+ end
23
+
24
+ def validate!(conf)
25
+ # raise proper errors if nodes are missing etc
26
+ end
27
+
28
+ def inject_hooks
29
+ return unless hooks = @config["hooks"]
30
+
31
+ run = @config["run"]
32
+
33
+ positions = {}
34
+ run.each do |row|
35
+ if Hash === row
36
+ command = row.first
37
+ if Hash === command[1]
38
+ hook = command[1]["hook"]
39
+ positions[hook] = row if hook
40
+ end
41
+ end
42
+ end
43
+
44
+ hooks.each do |full, list|
45
+
46
+ offset = nil
47
+ name = nil
48
+
49
+ if full =~ /^after_/
50
+ name = full[6..-1]
51
+ offset = 1
52
+ end
53
+
54
+ if full =~ /^before_/
55
+ name = full[7..-1]
56
+ offset = 0
57
+ end
58
+
59
+ index = run.index(positions[name])
60
+
61
+ if index && index >= 0
62
+ run.insert(index + offset, *list)
63
+ else
64
+ Pups.log.info "Skipped missing #{full} hook"
65
+ end
66
+
67
+ end
68
+ end
69
+
70
+ def run
71
+ run_commands
72
+ rescue => e
73
+ puts
74
+ puts
75
+ puts "FAILED"
76
+ puts "-" * 20
77
+ puts "#{e.class}: #{e}"
78
+ puts "Location of failure: #{e.backtrace[0]}"
79
+ if @last_command
80
+ puts "#{@last_command[:command]} failed with the params #{@last_command[:params].inspect}"
81
+ end
82
+ exit 1
83
+ end
84
+
85
+ def run_commands
86
+ @config["run"].each do |item|
87
+ item.each do |k,v|
88
+ type = case k
89
+ when "exec" then Pups::ExecCommand
90
+ when "merge" then Pups::MergeCommand
91
+ when "replace" then Pups::ReplaceCommand
92
+ when "file" then Pups::FileCommand
93
+ else raise SyntaxError.new("Invalid run command #{k}")
94
+ end
95
+
96
+ @last_command = { command: k, params: v }
97
+ type.run(v, @params)
98
+ end
99
+ end
100
+ end
101
+
102
+ def interpolate_params(cmd)
103
+ self.class.interpolate_params(cmd,@params)
104
+ end
105
+
106
+ def self.interpolate_params(cmd, params)
107
+ return unless cmd
108
+ processed = cmd.dup
109
+ params.each do |k,v|
110
+ processed.gsub!("$#{k}", v.to_s)
111
+ end
112
+ processed
113
+ end
114
+
115
+ end
@@ -0,0 +1,116 @@
1
+ require 'timeout'
2
+
3
+ class Pups::ExecCommand < Pups::Command
4
+ attr_reader :commands, :cd
5
+ attr_accessor :background, :raise_on_fail, :stdin, :stop_signal
6
+
7
+ def self.terminate_async(opts={})
8
+
9
+ return unless defined? @@asyncs
10
+
11
+ Pups.log.info("Terminating async processes")
12
+
13
+ @@asyncs.each do |async|
14
+ Pups.log.info("Sending #{async[:stop_signal]} to #{async[:command]} pid: #{async[:pid]}")
15
+ Process.kill(async[:stop_signal],async[:pid]) rescue nil
16
+ end
17
+
18
+ @@asyncs.map do |async|
19
+ Thread.new do
20
+ begin
21
+ Timeout.timeout(opts[:wait] || 10) do
22
+ Process.wait(async[:pid]) rescue nil
23
+ end
24
+ rescue Timeout::Error
25
+ Pups.log.info("#{async[:command]} pid:#{async[:pid]} did not terminate cleanly, forcing termination!")
26
+ begin
27
+ Process.kill("KILL",async[:pid])
28
+ Process.wait(async[:pid])
29
+ rescue Errno::ESRCH
30
+ rescue Errno::ECHILD
31
+ end
32
+
33
+ end
34
+ end
35
+ end.each(&:join)
36
+
37
+ end
38
+
39
+ def self.from_hash(hash, params)
40
+ cmd = new(params, hash["cd"])
41
+
42
+ case c = hash["cmd"]
43
+ when String then cmd.add(c)
44
+ when Array then c.each{|i| cmd.add(i)}
45
+ end
46
+
47
+ cmd.background = hash["background"]
48
+ cmd.stop_signal = hash["stop_signal"] || "TERM"
49
+ cmd.raise_on_fail = hash["raise_on_fail"] if hash.key? "raise_on_fail"
50
+ cmd.stdin = interpolate_params(hash["stdin"], params)
51
+
52
+ cmd
53
+ end
54
+
55
+ def self.from_str(str, params)
56
+ cmd = new(params)
57
+ cmd.add(str)
58
+ cmd
59
+ end
60
+
61
+ def initialize(params, cd = nil)
62
+ @commands = []
63
+ @params = params
64
+ @cd = interpolate_params(cd)
65
+ @raise_on_fail = true
66
+ end
67
+
68
+ def add(cmd)
69
+ @commands << process_params(cmd)
70
+ end
71
+
72
+ def run
73
+ commands.each do |command|
74
+ Pups.log.info("> #{command}")
75
+ pid = spawn(command)
76
+ Pups.log.info(@result.readlines.join("\n")) if @result
77
+ pid
78
+ end
79
+ rescue
80
+ raise if @raise_on_fail
81
+ end
82
+
83
+ def spawn(command)
84
+ if background
85
+ pid = Process.spawn(command)
86
+ (@@asyncs ||= []) << {pid: pid, command: command, stop_signal: (stop_signal || "TERM")}
87
+ Thread.new do
88
+ Process.wait(pid)
89
+ @@asyncs.delete_if{|async| async[:pid] == pid}
90
+ end
91
+ return pid
92
+ end
93
+
94
+ IO.popen(command, "w+") do |f|
95
+ if stdin
96
+ # need a way to get stdout without blocking
97
+ Pups.log.info(stdin)
98
+ f.write stdin
99
+ f.close
100
+ else
101
+ Pups.log.info(f.readlines.join)
102
+ end
103
+ end
104
+
105
+ raise RuntimeError.new("#{command} failed with return #{$?.inspect}") unless $? == 0
106
+
107
+ nil
108
+
109
+ end
110
+
111
+ def process_params(cmd)
112
+ processed = interpolate_params(cmd)
113
+ @cd ? "cd #{cd} && #{processed}" : processed
114
+ end
115
+
116
+ end
@@ -0,0 +1,37 @@
1
+ class Pups::FileCommand < Pups::Command
2
+ attr_accessor :path, :contents, :params, :type, :chmod
3
+
4
+ def self.from_hash(hash, params)
5
+ command = new
6
+ command.path = hash["path"]
7
+ command.contents = hash["contents"]
8
+ command.chmod = hash["chmod"]
9
+ command.params = params
10
+
11
+ command
12
+ end
13
+
14
+ def initialize
15
+ @params = {}
16
+ @type = :bash
17
+ end
18
+
19
+ def params=(p)
20
+ @params = p
21
+ end
22
+
23
+ def run
24
+ path = interpolate_params(@path)
25
+
26
+ `mkdir -p #{File.dirname(path)}`
27
+ File.open(path, "w") do |f|
28
+ f.write(interpolate_params(contents))
29
+ end
30
+ if @chmod
31
+ `chmod #{@chmod} #{path}`
32
+ end
33
+ Pups.log.info("File > #{path} chmod: #{@chmod}")
34
+ end
35
+
36
+ end
37
+
@@ -0,0 +1,48 @@
1
+ class Pups::MergeCommand < Pups::Command
2
+ attr_reader :filename
3
+ attr_reader :merge_hash
4
+
5
+ def self.from_str(command, params)
6
+ new(command,params)
7
+ end
8
+
9
+ def self.parse_command(command)
10
+ split = command.split(" ")
11
+ raise ArgumentError.new("Invalid merge command #{command}") unless split[-1][0] == "$"
12
+
13
+ [split[0..-2].join(" ") , split[-1][1..-1]]
14
+ end
15
+
16
+ def initialize(command, params)
17
+ @params = params
18
+
19
+ filename, target_param = Pups::MergeCommand.parse_command(command)
20
+ @filename = interpolate_params(filename)
21
+ @merge_hash = params[target_param]
22
+ end
23
+
24
+ def run
25
+ merged = self.class.deep_merge(YAML.load_file(@filename), @merge_hash)
26
+ File.open(@filename,"w"){|f| f.write(merged.to_yaml) }
27
+ Pups.log.info("Merge: #{@filename} with: \n#{@merge_hash.inspect}")
28
+ end
29
+
30
+ def self.deep_merge(first,second, *args)
31
+ args ||= []
32
+ merge_arrays = args.include? :merge_arrays
33
+
34
+ merger = proc { |key, v1, v2|
35
+ if Hash === v1 && Hash === v2
36
+ v1.merge(v2, &merger)
37
+ elsif Array === v1 && Array === v2
38
+ merge_arrays ? v1 + v2 : v2
39
+ elsif NilClass === v2
40
+ v1
41
+ else
42
+ v2
43
+ end
44
+ }
45
+ first.merge(second, &merger)
46
+ end
47
+
48
+ end
@@ -0,0 +1,43 @@
1
+ class Pups::ReplaceCommand < Pups::Command
2
+ attr_accessor :text, :from, :to, :filename, :direction, :global
3
+
4
+ def self.from_hash(hash, params)
5
+ replacer = new(params)
6
+ replacer.from = guess_replace_type(hash["from"])
7
+ replacer.to = guess_replace_type(hash["to"])
8
+ replacer.text = File.read(hash["filename"])
9
+ replacer.filename = hash["filename"]
10
+ replacer.direction = hash["direction"].to_sym if hash["direction"]
11
+ replacer.global = hash["global"].to_s == "true"
12
+ replacer
13
+ end
14
+
15
+ def self.guess_replace_type(item)
16
+ # evaling to get all the regex flags easily
17
+ item[0] == "/" ? eval(item) : item
18
+ end
19
+
20
+ def initialize(params)
21
+ @params = params
22
+ end
23
+
24
+ def replaced_text
25
+ new_to = to
26
+ if String === to
27
+ new_to = interpolate_params(to)
28
+ end
29
+ if global
30
+ text.gsub(from,new_to)
31
+ elsif direction == :reverse
32
+ index = text.rindex(from)
33
+ text[0..index-1] << text[index..-1].sub(from,new_to)
34
+ else
35
+ text.sub(from,new_to)
36
+ end
37
+ end
38
+
39
+ def run
40
+ Pups.log.info("Replacing #{from.to_s} with #{to.to_s} in #{filename}")
41
+ File.open(filename, "w"){|f| f.write replaced_text }
42
+ end
43
+ end
@@ -0,0 +1,40 @@
1
+ class Pups::Runit
2
+
3
+ attr_accessor :env, :exec, :cd, :name
4
+
5
+
6
+ def initialize(name)
7
+ @name = name
8
+ end
9
+
10
+ def setup
11
+ `mkdir -p /etc/service/#{name}`
12
+ run = "/etc/service/#{name}/run"
13
+ File.open(run, "w") do |f|
14
+ f.write(run_script)
15
+ end
16
+ `chmod +x #{run}`
17
+ end
18
+
19
+ def run_script
20
+ "#!/bin/bash
21
+ exec 2>&1
22
+ #{env_script}
23
+ #{cd_script}
24
+ #{exec}
25
+ "
26
+ end
27
+
28
+ def cd_script
29
+ "cd #{@cd}" if @cd
30
+ end
31
+
32
+ def env_script
33
+ if @env
34
+ @env.map do |k,v|
35
+ "export #{k}=#{v}"
36
+ end.join("\n")
37
+ end
38
+ end
39
+
40
+ end
@@ -0,0 +1,3 @@
1
+ module Pups
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'pups/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "pups"
8
+ spec.version = Pups::VERSION
9
+ spec.authors = ["Sam Saffron"]
10
+ spec.email = ["sam.saffron@gmail.com"]
11
+ spec.description = %q{Process orchestrator}
12
+ spec.summary = %q{Process orchestrator}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "minitest"
24
+ spec.add_development_dependency "guard"
25
+ spec.add_development_dependency "guard-minitest"
26
+ end
@@ -0,0 +1,55 @@
1
+ require 'test_helper'
2
+ require 'tempfile'
3
+
4
+ module Pups
5
+ class ConfigTest < MiniTest::Test
6
+
7
+ def test_config_from_env
8
+ ENV["HELLO"] = "world"
9
+ config = Config.new({})
10
+ assert_equal("world", config.params["$ENV_HELLO"])
11
+ end
12
+
13
+ def test_integration
14
+
15
+ f = Tempfile.new("test")
16
+ f.close
17
+
18
+ config = <<YAML
19
+ params:
20
+ run: #{f.path}
21
+ run:
22
+ - exec: echo hello world >> #{f.path}
23
+ YAML
24
+
25
+ Config.new(YAML.load(config)).run
26
+ assert_equal("hello world", File.read(f.path).strip)
27
+
28
+ ensure
29
+ f.unlink
30
+ end
31
+
32
+ def test_hooks
33
+ yaml = <<YAML
34
+ run:
35
+ - exec: 1
36
+ - exec:
37
+ hook: middle
38
+ cmd: 2
39
+ - exec: 3
40
+ hooks:
41
+ after_middle:
42
+ - exec: 2.1
43
+ before_middle:
44
+ - exec: 1.9
45
+ YAML
46
+
47
+ config = Config.load_config(yaml).config
48
+ assert_equal({ "exec" => 1.9 }, config["run"][1])
49
+ assert_equal({ "exec" => 2.1 }, config["run"][3])
50
+
51
+
52
+ end
53
+ end
54
+ end
55
+
@@ -0,0 +1,113 @@
1
+ require 'test_helper'
2
+ require 'tempfile'
3
+
4
+ module Pups
5
+ class ExecCommandTest < MiniTest::Test
6
+
7
+ def from_str(str, params={})
8
+ ExecCommand.from_str(str, params).commands
9
+ end
10
+
11
+ def from_hash(hash, params={})
12
+ ExecCommand.from_hash(hash, params).commands
13
+ end
14
+
15
+ def test_simple_str_command
16
+ assert_equal(["do_something"],
17
+ from_str("do_something"))
18
+ end
19
+
20
+ def test_simple_str_command_with_param
21
+ assert_equal(["hello world"],
22
+ from_str("hello $bob", {"bob" => "world"}))
23
+ end
24
+
25
+ def test_nested_command
26
+ assert_equal(["first"],
27
+ from_hash("cmd" => "first"))
28
+ end
29
+
30
+ def test_multi_commands
31
+ assert_equal(["first","second"],
32
+ from_hash("cmd" => ["first","second"]))
33
+ end
34
+
35
+ def test_multi_commands_with_home
36
+ assert_equal(["cd /home/sam && first",
37
+ "cd /home/sam && second"],
38
+ from_hash("cmd" => ["first","second"],
39
+ "cd" => "/home/sam"))
40
+ end
41
+
42
+ def test_exec_works
43
+ ExecCommand.from_str("ls",{}).run
44
+ end
45
+
46
+ def test_fails_for_bad_command
47
+ assert_raises(Errno::ENOENT) do
48
+ ExecCommand.from_str("boom",{}).run
49
+ end
50
+ end
51
+
52
+ def test_backgroud_task_do_not_fail
53
+ cmd = ExecCommand.new({})
54
+ cmd.background = true
55
+ cmd.add("sleep 10 && exit 1")
56
+ cmd.run
57
+ end
58
+
59
+ def test_raise_on_fail
60
+ cmd = ExecCommand.new({})
61
+ cmd.add("chgrp -a")
62
+ cmd.raise_on_fail = false
63
+ cmd.run
64
+ end
65
+
66
+ def test_stdin
67
+
68
+ `touch test_file`
69
+ cmd = ExecCommand.new({})
70
+ cmd.add("read test ; echo $test > test_file")
71
+ cmd.stdin = "hello"
72
+ cmd.run
73
+
74
+ assert_equal("hello\n", File.read("test_file"))
75
+
76
+ ensure
77
+ File.delete("test_file")
78
+ end
79
+
80
+ def test_fails_for_non_zero_exit
81
+ assert_raises(RuntimeError) do
82
+ ExecCommand.from_str("chgrp -a",{}).run
83
+ end
84
+ end
85
+
86
+
87
+ def test_can_terminate_async
88
+ cmd = ExecCommand.new({})
89
+ cmd.background = true
90
+ pid = cmd.spawn("sleep 10 && exit 1")
91
+ ExecCommand.terminate_async
92
+ assert_raises(Errno::ECHILD) do
93
+ Process.waitpid(pid,Process::WNOHANG)
94
+ end
95
+ end
96
+
97
+ def test_can_terminate_rogues
98
+ cmd = ExecCommand.new({})
99
+ cmd.background = true
100
+ pid = cmd.spawn("trap \"echo TERM && sleep 100\" TERM ; sleep 100")
101
+ # we need to give bash enough time to trap
102
+ sleep 0.01
103
+
104
+ ExecCommand.terminate_async(wait: 0.1)
105
+
106
+ assert_raises(Errno::ECHILD) do
107
+ Process.waitpid(pid,Process::WNOHANG)
108
+ end
109
+
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,27 @@
1
+ require 'test_helper'
2
+ require 'tempfile'
3
+
4
+ module Pups
5
+ class FileCommandTest < MiniTest::Test
6
+
7
+ def test_simple_file_creation
8
+ tmp = Tempfile.new("test")
9
+ tmp.write("x")
10
+ tmp.close
11
+
12
+
13
+ cmd = FileCommand.new
14
+ cmd.path = tmp.path
15
+ cmd.contents = "hello $world"
16
+ cmd.params = {"world" => "world"}
17
+ cmd.run
18
+
19
+ assert_equal("hello world",
20
+ File.read(tmp.path))
21
+ ensure
22
+ tmp.close
23
+ tmp.unlink
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,58 @@
1
+ require 'test_helper'
2
+ require 'tempfile'
3
+
4
+ module Pups
5
+ class MergeCommandTest < MiniTest::Test
6
+ def test_deep_merge_arrays
7
+ a = {a: {a: ["hi",1]}}
8
+ b = {a: {a: ["hi",2]}}
9
+ c = {a: {}}
10
+
11
+ d = Pups::MergeCommand.deep_merge(a,b,:merge_arrays)
12
+ d = Pups::MergeCommand.deep_merge(d,c,:merge_arrays)
13
+
14
+ assert_equal(["hi", 1,"hi", 2], d[:a][:a])
15
+ end
16
+
17
+ def test_merges
18
+
19
+ source = <<YAML
20
+ user:
21
+ name: "bob"
22
+ password: "xyz"
23
+ YAML
24
+
25
+ f = Tempfile.new("test")
26
+ f.write source
27
+ f.close
28
+
29
+ merge = <<YAML
30
+ user:
31
+ name: "bob2"
32
+ YAML
33
+
34
+ MergeCommand.from_str("#{f.path} $yaml", {"yaml" => YAML.load(merge) }).run
35
+
36
+ changed = YAML.load_file(f.path)
37
+
38
+ assert_equal({"user" => {
39
+ "name" => "bob2",
40
+ "password" => "xyz"
41
+ }}, changed)
42
+
43
+ def test_deep_merge_nil
44
+ a = {param: {venison: "yes please"}}
45
+ b = {param: nil}
46
+
47
+ r1 = Pups::MergeCommand.deep_merge(a,b)
48
+ r2 = Pups::MergeCommand.deep_merge(b,a)
49
+
50
+ assert_equal({venison: "yes please"}, r1[:param])
51
+ assert_equal({venison: "yes please"}, r2[:param])
52
+ end
53
+
54
+ ensure
55
+ f.unlink
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,112 @@
1
+ require 'test_helper'
2
+ require 'tempfile'
3
+
4
+ module Pups
5
+ class ReplaceCommandTest < MiniTest::Test
6
+
7
+ def test_simple
8
+ command = ReplaceCommand.new({})
9
+ command.text = "hello world"
10
+ command.from = /he[^o]+o/
11
+ command.to = "world"
12
+
13
+ assert_equal("world world", command.replaced_text)
14
+ end
15
+
16
+ def test_reverse
17
+ source = <<SCR
18
+ 1 one thousand 1
19
+ 1 one thousand 1
20
+ 1 one thousand 1
21
+ SCR
22
+
23
+ f = Tempfile.new("test")
24
+ f.write source
25
+ f.close
26
+
27
+ hash = {
28
+ "filename" => f.path,
29
+ "from" => "/one t.*d/",
30
+ "to" => "hello world",
31
+ "direction" => "reverse"
32
+ }
33
+
34
+ command = ReplaceCommand.from_hash(hash, {})
35
+
36
+ assert_equal("1 one thousand 1\n1 one thousand 1\n1 hello world 1\n", command.replaced_text)
37
+ ensure
38
+ f.unlink
39
+ end
40
+
41
+ def test_global
42
+ source = <<SCR
43
+ one
44
+ one
45
+ one
46
+ SCR
47
+
48
+ f = Tempfile.new("test")
49
+ f.write source
50
+ f.close
51
+
52
+ hash = {
53
+ "filename" => f.path,
54
+ "from" => "/one/",
55
+ "to" => "two",
56
+ "global" => "true"
57
+ }
58
+
59
+ command = ReplaceCommand.from_hash(hash, {})
60
+
61
+ assert_equal("two\ntwo\ntwo\n", command.replaced_text)
62
+ ensure
63
+ f.unlink
64
+
65
+ end
66
+
67
+ def test_replace_with_env
68
+ source = "123"
69
+
70
+ f = Tempfile.new("test")
71
+ f.write source
72
+ f.close
73
+
74
+ hash = {
75
+ "filename" => f.path,
76
+ "from" => "123",
77
+ "to" => "hello $hellos"
78
+ }
79
+
80
+ command = ReplaceCommand.from_hash(hash, {"hello" => "world"})
81
+ assert_equal("hello worlds", command.replaced_text)
82
+
83
+ ensure
84
+ f.unlink
85
+ end
86
+
87
+ def test_parse
88
+
89
+ source = <<SCR
90
+ this {
91
+ is a test
92
+ }
93
+ SCR
94
+
95
+ f = Tempfile.new("test")
96
+ f.write source
97
+ f.close
98
+
99
+ hash = {
100
+ "filename" => f.path,
101
+ "from" => "/this[^\}]+\}/m",
102
+ "to" => "hello world"
103
+ }
104
+
105
+ command = ReplaceCommand.from_hash(hash, {})
106
+
107
+ assert_equal("hello world", command.replaced_text.strip)
108
+ ensure
109
+ f.unlink
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,4 @@
1
+ require 'pups'
2
+ require 'minitest/pride'
3
+
4
+
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pups
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Sam Saffron
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-10-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.3'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.3'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: guard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: guard-minitest
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description: Process orchestrator
84
+ email:
85
+ - sam.saffron@gmail.com
86
+ executables:
87
+ - pups
88
+ extensions: []
89
+ extra_rdoc_files: []
90
+ files:
91
+ - ".gitignore"
92
+ - Gemfile
93
+ - Guardfile
94
+ - LICENSE.txt
95
+ - README.md
96
+ - Rakefile
97
+ - bin/pups
98
+ - lib/pups.rb
99
+ - lib/pups/cli.rb
100
+ - lib/pups/command.rb
101
+ - lib/pups/config.rb
102
+ - lib/pups/exec_command.rb
103
+ - lib/pups/file_command.rb
104
+ - lib/pups/merge_command.rb
105
+ - lib/pups/replace_command.rb
106
+ - lib/pups/runit.rb
107
+ - lib/pups/version.rb
108
+ - pups.gemspec
109
+ - test/config_test.rb
110
+ - test/exec_command_test.rb
111
+ - test/file_command_test.rb
112
+ - test/merge_command_test.rb
113
+ - test/replace_command_test.rb
114
+ - test/test_helper.rb
115
+ homepage: ''
116
+ licenses:
117
+ - MIT
118
+ metadata: {}
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ requirements:
125
+ - - ">="
126
+ - !ruby/object:Gem::Version
127
+ version: '0'
128
+ required_rubygems_version: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - ">="
131
+ - !ruby/object:Gem::Version
132
+ version: '0'
133
+ requirements: []
134
+ rubyforge_project:
135
+ rubygems_version: 2.2.2
136
+ signing_key:
137
+ specification_version: 4
138
+ summary: Process orchestrator
139
+ test_files:
140
+ - test/config_test.rb
141
+ - test/exec_command_test.rb
142
+ - test/file_command_test.rb
143
+ - test/merge_command_test.rb
144
+ - test/replace_command_test.rb
145
+ - test/test_helper.rb