pups 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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