pups 1.0.0 → 1.1.1
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 +5 -5
- data/.github/workflows/ci.yml +29 -0
- data/.github/workflows/lint.yml +27 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG +3 -0
- data/Gemfile +2 -0
- data/Guardfile +3 -1
- data/README.md +108 -29
- data/Rakefile +9 -3
- data/bin/pups +4 -4
- data/lib/pups.rb +25 -11
- data/lib/pups/cli.rb +61 -27
- data/lib/pups/command.rb +14 -11
- data/lib/pups/config.rb +139 -83
- data/lib/pups/docker.rb +69 -0
- data/lib/pups/exec_command.rb +93 -81
- data/lib/pups/file_command.rb +28 -28
- 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 +30 -34
- 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 +4 -2
- metadata +80 -16
data/lib/pups/command.rb
CHANGED
@@ -1,17 +1,20 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
3
|
+
module Pups
|
4
|
+
class Command
|
5
|
+
def self.run(command, params)
|
6
|
+
case command
|
7
|
+
when String then from_str(command, params).run
|
8
|
+
when Hash then from_hash(command, params).run
|
9
|
+
end
|
7
10
|
end
|
8
|
-
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
12
|
+
def self.interpolate_params(cmd, params)
|
13
|
+
Pups::Config.interpolate_params(cmd, params)
|
14
|
+
end
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
+
def interpolate_params(cmd)
|
17
|
+
Pups::Command.interpolate_params(cmd, @params)
|
18
|
+
end
|
16
19
|
end
|
17
20
|
end
|
data/lib/pups/config.rb
CHANGED
@@ -1,115 +1,171 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
3
|
+
module Pups
|
4
|
+
class Config
|
5
|
+
attr_reader :config, :params
|
4
6
|
|
5
|
-
|
6
|
-
|
7
|
-
end
|
7
|
+
def initialize(config, ignored = nil)
|
8
|
+
@config = config
|
8
9
|
|
9
|
-
|
10
|
-
|
11
|
-
|
10
|
+
# remove any ignored config elements prior to any more processing
|
11
|
+
ignored&.each { |e| @config.delete(e) }
|
12
|
+
|
13
|
+
# set some defaults to prevent checks in various functions
|
14
|
+
['env_template', 'env', 'labels', 'params'].each { |key| @config[key] = {} unless @config.has_key?(key) }
|
15
|
+
|
16
|
+
# Order here is important.
|
17
|
+
Pups::Config.combine_template_and_process_env(@config, ENV)
|
18
|
+
Pups::Config.prepare_env_template_vars(@config['env_template'], ENV)
|
19
|
+
|
20
|
+
# Templating is supported in env and label variables.
|
21
|
+
Pups::Config.transform_config_with_templated_vars(@config['env_template'], ENV)
|
22
|
+
Pups::Config.transform_config_with_templated_vars(@config['env_template'], @config['env'])
|
23
|
+
Pups::Config.transform_config_with_templated_vars(@config['env_template'], @config['labels'])
|
12
24
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
ENV.each do |k,v|
|
19
|
-
@params["$ENV_#{k}"] = v
|
25
|
+
@params = @config["params"]
|
26
|
+
ENV.each do |k, v|
|
27
|
+
@params["$ENV_#{k}"] = v
|
28
|
+
end
|
29
|
+
inject_hooks
|
20
30
|
end
|
21
|
-
inject_hooks
|
22
|
-
end
|
23
31
|
|
24
|
-
|
25
|
-
|
26
|
-
|
32
|
+
def self.load_file(config_file, ignored = nil)
|
33
|
+
Config.new(YAML.load_file(config_file), ignored)
|
34
|
+
rescue Exception
|
35
|
+
warn "Failed to parse #{config_file}"
|
36
|
+
warn "This is probably a formatting error in #{config_file}"
|
37
|
+
warn "Cannot continue. Edit #{config_file} and try again."
|
38
|
+
raise
|
39
|
+
end
|
27
40
|
|
28
|
-
|
29
|
-
|
41
|
+
def self.load_config(config, ignored = nil)
|
42
|
+
Config.new(YAML.safe_load(config), ignored)
|
43
|
+
end
|
30
44
|
|
31
|
-
|
45
|
+
def self.prepare_env_template_vars(env_template, env)
|
46
|
+
# Merge env_template variables from env and templates.
|
47
|
+
env.each do |k, v|
|
48
|
+
if k.include?('env_template_')
|
49
|
+
key = k.gsub('env_template_', '')
|
50
|
+
env_template[key] = v.to_s
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
32
54
|
|
33
|
-
|
34
|
-
|
35
|
-
if
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
55
|
+
def self.transform_config_with_templated_vars(env_template, to_transform)
|
56
|
+
# Transform any templated variables prior to copying to params.
|
57
|
+
# This has no effect if no env_template was provided.
|
58
|
+
env_template.each do |k, v|
|
59
|
+
to_transform.each do |key, val|
|
60
|
+
if val.to_s.include?("{{#{k}}}")
|
61
|
+
to_transform[key] = val.gsub("{{#{k}}}", v.to_s)
|
62
|
+
end
|
40
63
|
end
|
41
64
|
end
|
42
65
|
end
|
43
66
|
|
44
|
-
|
67
|
+
def self.combine_template_and_process_env(config, env)
|
68
|
+
# Merge all template env variables and process env variables, so that env
|
69
|
+
# variables can be provided both by configuration and runtime variables.
|
70
|
+
config["env"].each { |k, v| env[k] = v.to_s }
|
71
|
+
end
|
45
72
|
|
46
|
-
|
47
|
-
|
73
|
+
def inject_hooks
|
74
|
+
return unless hooks = @config['hooks']
|
48
75
|
|
49
|
-
|
50
|
-
name = full[6..-1]
|
51
|
-
offset = 1
|
52
|
-
end
|
76
|
+
run = @config['run']
|
53
77
|
|
54
|
-
|
55
|
-
|
56
|
-
|
78
|
+
positions = {}
|
79
|
+
run.each do |row|
|
80
|
+
next unless row.is_a?(Hash)
|
81
|
+
|
82
|
+
command = row.first
|
83
|
+
if command[1].is_a?(Hash)
|
84
|
+
hook = command[1]['hook']
|
85
|
+
positions[hook] = row if hook
|
86
|
+
end
|
57
87
|
end
|
58
88
|
|
59
|
-
|
89
|
+
hooks.each do |full, list|
|
90
|
+
offset = nil
|
91
|
+
name = nil
|
92
|
+
|
93
|
+
if full =~ /^after_/
|
94
|
+
name = full[6..-1]
|
95
|
+
offset = 1
|
96
|
+
end
|
97
|
+
|
98
|
+
if full =~ /^before_/
|
99
|
+
name = full[7..-1]
|
100
|
+
offset = 0
|
101
|
+
end
|
102
|
+
|
103
|
+
index = run.index(positions[name])
|
60
104
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
105
|
+
if index && index >= 0
|
106
|
+
run.insert(index + offset, *list)
|
107
|
+
else
|
108
|
+
Pups.log.info "Skipped missing #{full} hook"
|
109
|
+
end
|
65
110
|
end
|
111
|
+
end
|
66
112
|
|
113
|
+
def generate_docker_run_arguments
|
114
|
+
output = []
|
115
|
+
output << Pups::Docker.generate_env_arguments(config['env'])
|
116
|
+
output << Pups::Docker.generate_link_arguments(config['links'])
|
117
|
+
output << Pups::Docker.generate_expose_arguments(config['expose'])
|
118
|
+
output << Pups::Docker.generate_volume_arguments(config['volumes'])
|
119
|
+
output << Pups::Docker.generate_label_arguments(config['labels'])
|
120
|
+
output.sort!.join(" ").strip
|
67
121
|
end
|
68
|
-
end
|
69
122
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
123
|
+
def run
|
124
|
+
run_commands
|
125
|
+
rescue StandardError => e
|
126
|
+
exit_code = 1
|
127
|
+
exit_code = e.exit_code if e.is_a?(Pups::ExecError)
|
128
|
+
unless exit_code == 77
|
129
|
+
puts
|
130
|
+
puts
|
131
|
+
puts 'FAILED'
|
132
|
+
puts '-' * 20
|
133
|
+
puts "#{e.class}: #{e}"
|
134
|
+
puts "Location of failure: #{e.backtrace[0]}"
|
135
|
+
puts "#{@last_command[:command]} failed with the params #{@last_command[:params].inspect}" if @last_command
|
136
|
+
end
|
137
|
+
exit exit_code
|
81
138
|
end
|
82
|
-
exit 1
|
83
|
-
end
|
84
139
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
140
|
+
def run_commands
|
141
|
+
@config['run']&.each do |item|
|
142
|
+
item.each do |k, v|
|
143
|
+
type = case k
|
144
|
+
when 'exec' then Pups::ExecCommand
|
145
|
+
when 'merge' then Pups::MergeCommand
|
146
|
+
when 'replace' then Pups::ReplaceCommand
|
147
|
+
when 'file' then Pups::FileCommand
|
148
|
+
else raise SyntaxError, "Invalid run command #{k}"
|
149
|
+
end
|
150
|
+
|
151
|
+
@last_command = { command: k, params: v }
|
152
|
+
type.run(v, @params)
|
153
|
+
end
|
98
154
|
end
|
99
155
|
end
|
100
|
-
end
|
101
156
|
|
102
|
-
|
103
|
-
|
104
|
-
|
157
|
+
def interpolate_params(cmd)
|
158
|
+
self.class.interpolate_params(cmd, @params)
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.interpolate_params(cmd, params)
|
162
|
+
return unless cmd
|
105
163
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
processed
|
164
|
+
processed = cmd.dup
|
165
|
+
params.each do |k, v|
|
166
|
+
processed.gsub!("$#{k}", v.to_s)
|
167
|
+
end
|
168
|
+
processed
|
111
169
|
end
|
112
|
-
processed
|
113
170
|
end
|
114
|
-
|
115
171
|
end
|
data/lib/pups/docker.rb
ADDED
@@ -0,0 +1,69 @@
|
|
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
|
+
Shellwords.escape(str)
|
59
|
+
end
|
60
|
+
|
61
|
+
def normalize_output(output)
|
62
|
+
if output.empty?
|
63
|
+
""
|
64
|
+
else
|
65
|
+
output.join(" ")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
data/lib/pups/exec_command.rb
CHANGED
@@ -1,116 +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
|
-
Process.wait(pid)
|
89
|
-
@@asyncs.delete_if{|async| async[:pid] == pid}
|
90
|
-
end
|
91
|
-
return pid
|
73
|
+
def add(cmd)
|
74
|
+
@commands << process_params(cmd)
|
92
75
|
end
|
93
76
|
|
94
|
-
|
95
|
-
|
96
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
else
|
101
|
-
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
|
102
83
|
end
|
84
|
+
rescue StandardError
|
85
|
+
raise if @raise_on_fail
|
103
86
|
end
|
104
87
|
|
105
|
-
|
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
|
102
|
+
|
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
|
106
113
|
|
107
|
-
|
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
|
108
119
|
|
109
|
-
|
120
|
+
nil
|
121
|
+
end
|
110
122
|
|
111
|
-
|
112
|
-
|
113
|
-
|
123
|
+
def process_params(cmd)
|
124
|
+
processed = interpolate_params(cmd)
|
125
|
+
@cd ? "cd #{cd} && #{processed}" : processed
|
126
|
+
end
|
114
127
|
end
|
115
|
-
|
116
128
|
end
|