pups 1.0.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +29 -0
- data/.github/workflows/lint.yml +27 -0
- data/.rubocop.yml +3 -0
- data/Gemfile +2 -0
- data/Guardfile +3 -1
- data/README.md +40 -3
- data/Rakefile +8 -6
- data/bin/pups +4 -4
- data/lib/pups.rb +21 -11
- data/lib/pups/cli.rb +61 -26
- data/lib/pups/command.rb +14 -11
- data/lib/pups/config.rb +136 -93
- data/lib/pups/docker.rb +73 -0
- data/lib/pups/exec_command.rb +93 -89
- data/lib/pups/file_command.rb +28 -32
- data/lib/pups/merge_command.rb +48 -46
- data/lib/pups/replace_command.rb +36 -34
- data/lib/pups/runit.rb +23 -24
- data/lib/pups/version.rb +3 -1
- data/pups.gemspec +21 -16
- data/test/cli_test.rb +117 -0
- data/test/config_test.rb +192 -30
- data/test/docker_test.rb +157 -0
- data/test/exec_command_test.rb +29 -33
- data/test/file_command_test.rb +8 -9
- data/test/merge_command_test.rb +32 -32
- data/test/replace_command_test.rb +42 -44
- data/test/test_helper.rb +3 -0
- metadata +70 -6
data/lib/pups/docker.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
class Pups::Docker
|
5
|
+
class << self
|
6
|
+
def generate_env_arguments(config)
|
7
|
+
output = []
|
8
|
+
config&.each do |k, v|
|
9
|
+
if !v.to_s.empty?
|
10
|
+
output << "--env #{k}=#{escape_user_string_literal(v)}"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
normalize_output(output)
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_link_arguments(config)
|
17
|
+
output = []
|
18
|
+
config&.each do |c|
|
19
|
+
output << "--link #{c['link']['name']}:#{c['link']['alias']}"
|
20
|
+
end
|
21
|
+
normalize_output(output)
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate_expose_arguments(config)
|
25
|
+
output = []
|
26
|
+
config&.each do |c|
|
27
|
+
if c.to_s.include?(":")
|
28
|
+
output << "--publish #{c}"
|
29
|
+
else
|
30
|
+
output << "--expose #{c}"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
normalize_output(output)
|
34
|
+
end
|
35
|
+
|
36
|
+
def generate_volume_arguments(config)
|
37
|
+
output = []
|
38
|
+
config&.each do |c|
|
39
|
+
output << "--volume #{c['volume']['host']}:#{c['volume']['guest']}"
|
40
|
+
end
|
41
|
+
normalize_output(output)
|
42
|
+
end
|
43
|
+
|
44
|
+
def generate_label_arguments(config)
|
45
|
+
output = []
|
46
|
+
config&.each do |k, v|
|
47
|
+
output << "--label #{k}=#{escape_user_string_literal(v)}"
|
48
|
+
end
|
49
|
+
normalize_output(output)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
def escape_user_string_literal(str)
|
54
|
+
# We need to escape the following strings as they are more likely to contain
|
55
|
+
# special characters than any of the other config variables on a Linux system:
|
56
|
+
# - the value side of an environment variable
|
57
|
+
# - the value side of a label.
|
58
|
+
if str.to_s.include?(" ")
|
59
|
+
"\"#{Shellwords.escape(str)}\""
|
60
|
+
else
|
61
|
+
Shellwords.escape(str)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def normalize_output(output)
|
66
|
+
if output.empty?
|
67
|
+
""
|
68
|
+
else
|
69
|
+
output.join(" ")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/pups/exec_command.rb
CHANGED
@@ -1,124 +1,128 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
attr_accessor :background, :raise_on_fail, :stdin, :stop_signal
|
3
|
+
require 'timeout'
|
4
|
+
require 'English'
|
6
5
|
|
7
|
-
|
6
|
+
module Pups
|
7
|
+
class ExecCommand < Pups::Command
|
8
|
+
attr_reader :commands, :cd
|
9
|
+
attr_accessor :background, :raise_on_fail, :stdin, :stop_signal
|
8
10
|
|
9
|
-
|
11
|
+
def self.terminate_async(opts = {})
|
12
|
+
return unless defined? @@asyncs
|
10
13
|
|
11
|
-
|
14
|
+
Pups.log.info('Terminating async processes')
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
Process.kill(async[:stop_signal],async[:pid]) rescue nil
|
16
|
-
end
|
17
|
-
|
18
|
-
@@asyncs.map do |async|
|
19
|
-
Thread.new do
|
16
|
+
@@asyncs.each do |async|
|
17
|
+
Pups.log.info("Sending #{async[:stop_signal]} to #{async[:command]} pid: #{async[:pid]}")
|
20
18
|
begin
|
19
|
+
Process.kill(async[:stop_signal], async[:pid])
|
20
|
+
rescue StandardError
|
21
|
+
nil
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
@@asyncs.map do |async|
|
26
|
+
Thread.new do
|
21
27
|
Timeout.timeout(opts[:wait] || 10) do
|
22
|
-
Process.wait(async[:pid])
|
28
|
+
Process.wait(async[:pid])
|
29
|
+
rescue StandardError
|
30
|
+
nil
|
23
31
|
end
|
24
32
|
rescue Timeout::Error
|
25
33
|
Pups.log.info("#{async[:command]} pid:#{async[:pid]} did not terminate cleanly, forcing termination!")
|
26
34
|
begin
|
27
|
-
Process.kill(
|
35
|
+
Process.kill('KILL', async[:pid])
|
28
36
|
Process.wait(async[:pid])
|
29
37
|
rescue Errno::ESRCH
|
30
38
|
rescue Errno::ECHILD
|
31
39
|
end
|
32
|
-
|
33
40
|
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)}
|
41
|
+
end.each(&:join)
|
45
42
|
end
|
46
43
|
|
47
|
-
|
48
|
-
|
49
|
-
cmd.raise_on_fail = hash["raise_on_fail"] if hash.key? "raise_on_fail"
|
50
|
-
cmd.stdin = interpolate_params(hash["stdin"], params)
|
44
|
+
def self.from_hash(hash, params)
|
45
|
+
cmd = new(params, hash['cd'])
|
51
46
|
|
52
|
-
|
53
|
-
|
47
|
+
case c = hash['cmd']
|
48
|
+
when String then cmd.add(c)
|
49
|
+
when Array then c.each { |i| cmd.add(i) }
|
50
|
+
end
|
54
51
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
end
|
52
|
+
cmd.background = hash['background']
|
53
|
+
cmd.stop_signal = hash['stop_signal'] || 'TERM'
|
54
|
+
cmd.raise_on_fail = hash['raise_on_fail'] if hash.key? 'raise_on_fail'
|
55
|
+
cmd.stdin = interpolate_params(hash['stdin'], params)
|
60
56
|
|
61
|
-
|
62
|
-
|
63
|
-
@params = params
|
64
|
-
@cd = interpolate_params(cd)
|
65
|
-
@raise_on_fail = true
|
66
|
-
end
|
57
|
+
cmd
|
58
|
+
end
|
67
59
|
|
68
|
-
|
69
|
-
|
70
|
-
|
60
|
+
def self.from_str(str, params)
|
61
|
+
cmd = new(params)
|
62
|
+
cmd.add(str)
|
63
|
+
cmd
|
64
|
+
end
|
71
65
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
pid
|
66
|
+
def initialize(params, cd = nil)
|
67
|
+
@commands = []
|
68
|
+
@params = params
|
69
|
+
@cd = interpolate_params(cd)
|
70
|
+
@raise_on_fail = true
|
78
71
|
end
|
79
|
-
rescue
|
80
|
-
raise if @raise_on_fail
|
81
|
-
end
|
82
72
|
|
83
|
-
|
84
|
-
|
85
|
-
pid = Process.spawn(command)
|
86
|
-
(@@asyncs ||= []) << {pid: pid, command: command, stop_signal: (stop_signal || "TERM")}
|
87
|
-
Thread.new do
|
88
|
-
begin
|
89
|
-
Process.wait(pid)
|
90
|
-
rescue Errno::ECHILD
|
91
|
-
# already exited so skip
|
92
|
-
end
|
93
|
-
@@asyncs.delete_if{|async| async[:pid] == pid}
|
94
|
-
end
|
95
|
-
return pid
|
73
|
+
def add(cmd)
|
74
|
+
@commands << process_params(cmd)
|
96
75
|
end
|
97
76
|
|
98
|
-
|
99
|
-
|
100
|
-
#
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
else
|
105
|
-
Pups.log.info(f.readlines.join)
|
77
|
+
def run
|
78
|
+
commands.each do |command|
|
79
|
+
Pups.log.info("> #{command}")
|
80
|
+
pid = spawn(command)
|
81
|
+
Pups.log.info(@result.readlines.join("\n")) if @result
|
82
|
+
pid
|
106
83
|
end
|
84
|
+
rescue StandardError
|
85
|
+
raise if @raise_on_fail
|
107
86
|
end
|
108
87
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
88
|
+
def spawn(command)
|
89
|
+
if background
|
90
|
+
pid = Process.spawn(command)
|
91
|
+
(@@asyncs ||= []) << { pid: pid, command: command, stop_signal: (stop_signal || 'TERM') }
|
92
|
+
Thread.new do
|
93
|
+
begin
|
94
|
+
Process.wait(pid)
|
95
|
+
rescue Errno::ECHILD
|
96
|
+
# already exited so skip
|
97
|
+
end
|
98
|
+
@@asyncs.delete_if { |async| async[:pid] == pid }
|
99
|
+
end
|
100
|
+
return pid
|
101
|
+
end
|
114
102
|
|
115
|
-
|
103
|
+
IO.popen(command, 'w+') do |f|
|
104
|
+
if stdin
|
105
|
+
# need a way to get stdout without blocking
|
106
|
+
Pups.log.info(stdin)
|
107
|
+
f.write stdin
|
108
|
+
f.close
|
109
|
+
else
|
110
|
+
Pups.log.info(f.readlines.join)
|
111
|
+
end
|
112
|
+
end
|
116
113
|
|
117
|
-
|
114
|
+
unless $CHILD_STATUS == 0
|
115
|
+
err = Pups::ExecError.new("#{command} failed with return #{$CHILD_STATUS.inspect}")
|
116
|
+
err.exit_code = $CHILD_STATUS.exitstatus
|
117
|
+
raise err
|
118
|
+
end
|
118
119
|
|
119
|
-
|
120
|
-
|
121
|
-
@cd ? "cd #{cd} && #{processed}" : processed
|
122
|
-
end
|
120
|
+
nil
|
121
|
+
end
|
123
122
|
|
123
|
+
def process_params(cmd)
|
124
|
+
processed = interpolate_params(cmd)
|
125
|
+
@cd ? "cd #{cd} && #{processed}" : processed
|
126
|
+
end
|
127
|
+
end
|
124
128
|
end
|
data/lib/pups/file_command.rb
CHANGED
@@ -1,41 +1,37 @@
|
|
1
|
-
|
2
|
-
attr_accessor :path, :contents, :params, :type, :chmod, :chown
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
command.contents = hash["contents"]
|
8
|
-
command.chmod = hash["chmod"]
|
9
|
-
command.chown = hash["chown"]
|
10
|
-
command.params = params
|
3
|
+
module Pups
|
4
|
+
class FileCommand < Pups::Command
|
5
|
+
attr_accessor :path, :contents, :params, :type, :chmod, :chown
|
11
6
|
|
12
|
-
|
13
|
-
|
7
|
+
def self.from_hash(hash, params)
|
8
|
+
command = new
|
9
|
+
command.path = hash['path']
|
10
|
+
command.contents = hash['contents']
|
11
|
+
command.chmod = hash['chmod']
|
12
|
+
command.chown = hash['chown']
|
13
|
+
command.params = params
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
@type = :bash
|
18
|
-
end
|
15
|
+
command
|
16
|
+
end
|
19
17
|
|
20
|
-
|
21
|
-
|
22
|
-
|
18
|
+
def initialize
|
19
|
+
@params = {}
|
20
|
+
@type = :bash
|
21
|
+
end
|
23
22
|
|
24
|
-
|
25
|
-
path = interpolate_params(@path)
|
23
|
+
attr_writer :params
|
26
24
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
`chown #{@chown} #{path}`
|
25
|
+
def run
|
26
|
+
path = interpolate_params(@path)
|
27
|
+
|
28
|
+
`mkdir -p #{File.dirname(path)}`
|
29
|
+
File.open(path, 'w') do |f|
|
30
|
+
f.write(interpolate_params(contents))
|
31
|
+
end
|
32
|
+
`chmod #{@chmod} #{path}` if @chmod
|
33
|
+
`chown #{@chown} #{path}` if @chown
|
34
|
+
Pups.log.info("File > #{path} chmod: #{@chmod} chown: #{@chown}")
|
36
35
|
end
|
37
|
-
Pups.log.info("File > #{path} chmod: #{@chmod} chown: #{@chown}")
|
38
36
|
end
|
39
|
-
|
40
37
|
end
|
41
|
-
|
data/lib/pups/merge_command.rb
CHANGED
@@ -1,48 +1,50 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Pups
|
4
|
+
class MergeCommand < Pups::Command
|
5
|
+
attr_reader :filename, :merge_hash
|
6
|
+
|
7
|
+
def self.from_str(command, params)
|
8
|
+
new(command, params)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.parse_command(command)
|
12
|
+
split = command.split(' ')
|
13
|
+
raise ArgumentError, "Invalid merge command #{command}" unless split[-1][0] == '$'
|
14
|
+
|
15
|
+
[split[0..-2].join(' '), split[-1][1..-1]]
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize(command, params)
|
19
|
+
@params = params
|
20
|
+
|
21
|
+
filename, target_param = Pups::MergeCommand.parse_command(command)
|
22
|
+
@filename = interpolate_params(filename)
|
23
|
+
@merge_hash = params[target_param]
|
24
|
+
end
|
25
|
+
|
26
|
+
def run
|
27
|
+
merged = self.class.deep_merge(YAML.load_file(@filename), @merge_hash)
|
28
|
+
File.open(@filename, 'w') { |f| f.write(merged.to_yaml) }
|
29
|
+
Pups.log.info("Merge: #{@filename} with: \n#{@merge_hash.inspect}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.deep_merge(first, second, *args)
|
33
|
+
args ||= []
|
34
|
+
merge_arrays = args.include? :merge_arrays
|
35
|
+
|
36
|
+
merger = proc { |_key, v1, v2|
|
37
|
+
if v1.is_a?(Hash) && v2.is_a?(Hash)
|
38
|
+
v1.merge(v2, &merger)
|
39
|
+
elsif v1.is_a?(Array) && v2.is_a?(Array)
|
40
|
+
merge_arrays ? v1 + v2 : v2
|
41
|
+
elsif v2.is_a?(NilClass)
|
42
|
+
v1
|
43
|
+
else
|
44
|
+
v2
|
45
|
+
end
|
46
|
+
}
|
47
|
+
first.merge(second, &merger)
|
48
|
+
end
|
28
49
|
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
50
|
end
|