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.
data/lib/pups/command.rb CHANGED
@@ -1,17 +1,20 @@
1
- class Pups::Command
1
+ # frozen_string_literal: true
2
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
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
- def self.interpolate_params(cmd,params)
11
- Pups::Config.interpolate_params(cmd,params)
12
- end
12
+ def self.interpolate_params(cmd, params)
13
+ Pups::Config.interpolate_params(cmd, params)
14
+ end
13
15
 
14
- def interpolate_params(cmd)
15
- Pups::Command.interpolate_params(cmd,@params)
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
- class Pups::Config
1
+ # frozen_string_literal: true
2
2
 
3
- attr_reader :config, :params
3
+ module Pups
4
+ class Config
5
+ attr_reader :config, :params
4
6
 
5
- def self.load_file(config_file)
6
- new YAML.load_file(config_file)
7
- end
7
+ def initialize(config, ignored = nil)
8
+ @config = config
8
9
 
9
- def self.load_config(config)
10
- new YAML.load(config)
11
- end
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
- 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
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
- def validate!(conf)
25
- # raise proper errors if nodes are missing etc
26
- end
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
- def inject_hooks
29
- return unless hooks = @config["hooks"]
41
+ def self.load_config(config, ignored = nil)
42
+ Config.new(YAML.safe_load(config), ignored)
43
+ end
30
44
 
31
- run = @config["run"]
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
- 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
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
- hooks.each do |full, list|
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
- offset = nil
47
- name = nil
73
+ def inject_hooks
74
+ return unless hooks = @config['hooks']
48
75
 
49
- if full =~ /^after_/
50
- name = full[6..-1]
51
- offset = 1
52
- end
76
+ run = @config['run']
53
77
 
54
- if full =~ /^before_/
55
- name = full[7..-1]
56
- offset = 0
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
- index = run.index(positions[name])
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
- if index && index >= 0
62
- run.insert(index + offset, *list)
63
- else
64
- Pups.log.info "Skipped missing #{full} hook"
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
- 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}"
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
- 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)
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
- def interpolate_params(cmd)
103
- self.class.interpolate_params(cmd,@params)
104
- end
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
- 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)
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
@@ -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
@@ -1,116 +1,128 @@
1
- require 'timeout'
1
+ # frozen_string_literal: true
2
2
 
3
- class Pups::ExecCommand < Pups::Command
4
- attr_reader :commands, :cd
5
- attr_accessor :background, :raise_on_fail, :stdin, :stop_signal
3
+ require 'timeout'
4
+ require 'English'
6
5
 
7
- def self.terminate_async(opts={})
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
- return unless defined? @@asyncs
11
+ def self.terminate_async(opts = {})
12
+ return unless defined? @@asyncs
10
13
 
11
- Pups.log.info("Terminating async processes")
14
+ Pups.log.info('Terminating async processes')
12
15
 
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
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]) rescue nil
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("KILL",async[:pid])
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
- 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)
44
+ def self.from_hash(hash, params)
45
+ cmd = new(params, hash['cd'])
51
46
 
52
- cmd
53
- end
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
- def self.from_str(str, params)
56
- cmd = new(params)
57
- cmd.add(str)
58
- cmd
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
- def initialize(params, cd = nil)
62
- @commands = []
63
- @params = params
64
- @cd = interpolate_params(cd)
65
- @raise_on_fail = true
66
- end
57
+ cmd
58
+ end
67
59
 
68
- def add(cmd)
69
- @commands << process_params(cmd)
70
- end
60
+ def self.from_str(str, params)
61
+ cmd = new(params)
62
+ cmd.add(str)
63
+ cmd
64
+ end
71
65
 
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
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
- 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
73
+ def add(cmd)
74
+ @commands << process_params(cmd)
92
75
  end
93
76
 
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)
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
- raise RuntimeError.new("#{command} failed with return #{$?.inspect}") unless $? == 0
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
- nil
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
- end
120
+ nil
121
+ end
110
122
 
111
- def process_params(cmd)
112
- processed = interpolate_params(cmd)
113
- @cd ? "cd #{cd} && #{processed}" : processed
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