puppet-debugger 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.document +5 -0
  3. data/.gitignore +54 -0
  4. data/.gitlab-ci.yml +129 -0
  5. data/.rspec +3 -0
  6. data/CHANGELOG.md +61 -0
  7. data/Gemfile +18 -0
  8. data/Gemfile.lock +67 -0
  9. data/LICENSE.txt +20 -0
  10. data/README.md +276 -0
  11. data/Rakefile +32 -0
  12. data/bin/pdb +4 -0
  13. data/lib/awesome_print/ext/awesome_puppet.rb +40 -0
  14. data/lib/puppet-debugger/cli.rb +247 -0
  15. data/lib/puppet-debugger/code/code_file.rb +98 -0
  16. data/lib/puppet-debugger/code/code_range.rb +69 -0
  17. data/lib/puppet-debugger/code/loc.rb +80 -0
  18. data/lib/puppet-debugger/debugger_code.rb +318 -0
  19. data/lib/puppet-debugger/support/compiler.rb +20 -0
  20. data/lib/puppet-debugger/support/environment.rb +38 -0
  21. data/lib/puppet-debugger/support/errors.rb +75 -0
  22. data/lib/puppet-debugger/support/facts.rb +78 -0
  23. data/lib/puppet-debugger/support/functions.rb +72 -0
  24. data/lib/puppet-debugger/support/input_responders.rb +136 -0
  25. data/lib/puppet-debugger/support/node.rb +90 -0
  26. data/lib/puppet-debugger/support/play.rb +91 -0
  27. data/lib/puppet-debugger/support/scope.rb +42 -0
  28. data/lib/puppet-debugger/support.rb +176 -0
  29. data/lib/puppet-debugger.rb +55 -0
  30. data/lib/trollop.rb +861 -0
  31. data/lib/version.rb +3 -0
  32. data/puppet-debugger.gemspec +36 -0
  33. data/run_container_test.sh +12 -0
  34. data/spec/facts_spec.rb +86 -0
  35. data/spec/fixtures/environments/production/manifests/site.pp +1 -0
  36. data/spec/fixtures/invalid_node_obj.yaml +8 -0
  37. data/spec/fixtures/node_obj.yaml +298 -0
  38. data/spec/fixtures/sample_manifest.pp +2 -0
  39. data/spec/fixtures/sample_start_debugger.pp +13 -0
  40. data/spec/pdb_spec.rb +50 -0
  41. data/spec/puppet-debugger_spec.rb +492 -0
  42. data/spec/remote_node_spec.rb +170 -0
  43. data/spec/spec_helper.rb +57 -0
  44. data/spec/support_spec.rb +190 -0
  45. data/test_matrix.rb +42 -0
  46. metadata +148 -0
@@ -0,0 +1,40 @@
1
+ module AwesomePrint
2
+ module Puppet
3
+ def self.included(base)
4
+ base.send :alias_method, :cast_without_puppet_resource, :cast
5
+ base.send :alias_method, :cast, :cast_with_puppet_resource
6
+ end
7
+
8
+ # this tells ap how to cast our object so we can be specific
9
+ # about printing different puppet objects
10
+ def cast_with_puppet_resource(object, type)
11
+ cast = cast_without_puppet_resource(object, type)
12
+ # check the object to see if it has an acestor (< ) of the specified type
13
+ if (defined?(::Puppet::Type)) && (object.class < ::Puppet::Type)
14
+ cast = :puppet_type
15
+ elsif (defined?(::Puppet::Pops::Types)) && (object.class < ::Puppet::Pops::Types)
16
+ cast = :puppet_type
17
+ elsif (defined?(::Puppet::Parser::Resource)) && (object.class < ::Puppet::Parser::Resource)
18
+ cast = :puppet_resource
19
+ elsif /Puppet::Pops::Types/.match(object.class.to_s)
20
+ cast = :puppet_type
21
+ end
22
+ cast
23
+ end
24
+
25
+ def awesome_puppet_resource(object)
26
+ return '' if object.nil?
27
+ awesome_puppet_type(object.to_ral)
28
+ end
29
+
30
+ def awesome_puppet_type(object)
31
+ return '' if object.nil?
32
+ return object.to_s unless object.respond_to?(:name) && object.respond_to?(:title)
33
+ h = object.to_hash.merge(:name => object.name, :title => object.title)
34
+ res_str = awesome_hash(h)
35
+ "#{object.class} #{res_str.gsub(':', '')}"
36
+ end
37
+ end
38
+ end
39
+
40
+ AwesomePrint::Formatter.send(:include, AwesomePrint::Puppet)
@@ -0,0 +1,247 @@
1
+ require 'puppet'
2
+ require 'readline'
3
+ require 'json'
4
+ require_relative 'support'
5
+
6
+ module PuppetDebugger
7
+ class Cli
8
+ include PuppetDebugger::Support
9
+
10
+ attr_accessor :settings, :log_level, :in_buffer, :out_buffer, :html_mode
11
+
12
+ def initialize(options={})
13
+ @log_level = 'notice'
14
+ @out_buffer = options[:out_buffer] || $stdout
15
+ @html_mode = options[:html_mode] || false
16
+ @source_file = options[:source_file] || nil
17
+ @source_line_num = options[:source_line] || nil
18
+ @in_buffer = options[:in_buffer] || $stdin
19
+ comp = Proc.new do |s|
20
+ key_words.grep(/^#{Regexp.escape(s)}/)
21
+ end
22
+ Readline.completion_append_character = ""
23
+ Readline.basic_word_break_characters = " "
24
+ Readline.completion_proc = comp
25
+ AwesomePrint.defaults = {
26
+ :html => @html_mode,
27
+ :sort_keys => true,
28
+ :indent => 2
29
+ }
30
+ do_initialize
31
+ end
32
+
33
+ # returns a cached list of key words
34
+ def key_words
35
+ # because dollar signs don't work we can't display a $ sign in the keyword
36
+ # list so its not explicitly clear what the keyword
37
+ variables = scope.to_hash.keys
38
+ # prepend a :: to topscope variables
39
+ scoped_vars = variables.map { |k,v| scope.compiler.topscope.exist?(k) ? "$::#{k}" : "$#{k}" }
40
+ # append a () to functions so we know they are functions
41
+ funcs = function_map.keys.map { |k| "#{k.split('::').last}()"}
42
+ (scoped_vars + funcs + static_responder_list).uniq.sort
43
+ end
44
+
45
+ # looks up the type in the catalog by using the type and title
46
+ # and returns the resource in ral format
47
+ def to_resource_declaration(type)
48
+ if type.respond_to?(:type_name) and type.respond_to?(:title)
49
+ title = type.title
50
+ type_name = type.type_name
51
+ elsif type_result = /(\w+)\['?(\w+)'?\]/.match(type.to_s)
52
+ # not all types have a type_name and title so we
53
+ # output to a string and parse the results
54
+ title = type_result[2]
55
+ type_name = type_result[1]
56
+ else
57
+ return type
58
+ end
59
+ res = scope.catalog.resource(type_name, title)
60
+ if res
61
+ return res.to_ral
62
+ end
63
+ # don't return anything or returns nil if item is not in the catalog
64
+ end
65
+
66
+ # ruturns a formatted array
67
+ def expand_resource_type(types)
68
+ output = [types].flatten.map do |t|
69
+ if t.class.to_s =~ /Puppet::Pops::Types/
70
+ to_resource_declaration(t)
71
+ else
72
+ t
73
+ end
74
+ end
75
+ output
76
+ end
77
+
78
+ def normalize_output(result)
79
+ if result.instance_of?(Array)
80
+ output = expand_resource_type(result)
81
+ if output.count == 1
82
+ return output.first
83
+ end
84
+ return output
85
+ elsif result.class.to_s =~ /Puppet::Pops::Types/
86
+ return to_resource_declaration(result)
87
+ end
88
+ result
89
+ end
90
+
91
+ # this method handles all input and expects a string of text.
92
+ #
93
+ def handle_input(input)
94
+ raise ArgumentError unless input.instance_of?(String)
95
+ begin
96
+ output = ''
97
+ case input
98
+ when /^play|^classification|^whereami|^facterdb_filter|^facts|^vars|^functions|^classes|^resources|^krt|^environment|^reset|^help/
99
+ args = input.split(' ')
100
+ command = args.shift.to_sym
101
+ if self.respond_to?(command)
102
+ output = self.send(command, args)
103
+ end
104
+ return out_buffer.puts output
105
+ when /exit/
106
+ exit 0
107
+ when /^:set/
108
+ output = handle_set(input)
109
+ when '_'
110
+ output = " => #{@last_item}"
111
+ else
112
+ result = puppet_eval(input)
113
+ @last_item = result
114
+ output = normalize_output(result)
115
+ if output.nil?
116
+ output = ""
117
+ else
118
+ output = output.ai
119
+ end
120
+ end
121
+ rescue LoadError => e
122
+ output = e.message.fatal
123
+ rescue Errno::ETIMEDOUT => e
124
+ output = e.message.fatal
125
+ rescue ArgumentError => e
126
+ output = e.message.fatal
127
+ rescue Puppet::ResourceError => e
128
+ output = e.message.fatal
129
+ rescue Puppet::Error => e
130
+ output = e.message.fatal
131
+ rescue Puppet::ParseErrorWithIssue => e
132
+ output = e.message.fatal
133
+ rescue PuppetDebugger::Exception::FatalError => e
134
+ output = e.message.fatal
135
+ out_buffer.puts output
136
+ exit 1 # this can sometimes causes tests to fail
137
+ rescue PuppetDebugger::Exception::Error => e
138
+ output = e.message.fatal
139
+ end
140
+ unless output.empty?
141
+ out_buffer.print " => "
142
+ out_buffer.puts output unless output.empty?
143
+ end
144
+ end
145
+
146
+ def self.print_repl_desc
147
+ output = <<-EOT
148
+ Ruby Version: #{RUBY_VERSION}
149
+ Puppet Version: #{Puppet.version}
150
+ Puppet Debugger Version: #{PuppetDebugger::VERSION}
151
+ Created by: NWOps <corey@nwops.io>
152
+ Type "exit", "functions", "vars", "krt", "whereami", "facts", "resources", "classes",
153
+ "play", "classification", "reset", or "help" for more information.
154
+
155
+ EOT
156
+ output
157
+ end
158
+
159
+ # tries to determine if the input is going to be a multiline input
160
+ # by reading the parser error message
161
+ def multiline_input?(e)
162
+ case e.message
163
+ when /Syntax error at end of file/i
164
+ true
165
+ else
166
+ false
167
+ end
168
+ end
169
+
170
+ # reads input from stdin, since readline requires a tty
171
+ # we cannot read from other sources as readline requires a file object
172
+ # we parse the string after each input to determine if the input is a multiline_input
173
+ # entry. If it is multiline we run through the loop again and concatenate the
174
+ # input
175
+ def read_loop
176
+ line_number = 1
177
+ full_buffer = ''
178
+ while buf = Readline.readline("#{line_number}:>> ", true)
179
+ begin
180
+ full_buffer += buf
181
+ # unless this is puppet code, otherwise skip repl keywords
182
+ unless keyword_expression.match(buf)
183
+ line_number = line_number.next
184
+ parser.parse_string(full_buffer)
185
+ end
186
+ rescue Puppet::ParseErrorWithIssue => e
187
+ if multiline_input?(e)
188
+ out_buffer.print ' '
189
+ full_buffer += "\n"
190
+ next
191
+ end
192
+ end
193
+ handle_input(full_buffer)
194
+ full_buffer = ''
195
+ end
196
+ end
197
+
198
+ # used to start a repl without attempting to read from stdin
199
+ # or
200
+ # @param [Hash] must contain at least the puppet scope object
201
+ def self.start_without_stdin(options={:scope => nil})
202
+ puts print_repl_desc unless options[:quiet]
203
+ repl_obj = PuppetDebugger::Cli.new(options)
204
+ repl_obj.remote_node_name = options[:node_name] if options[:node_name]
205
+ repl_obj.initialize_from_scope(options[:scope])
206
+ puts repl_obj.whereami if options[:source_file] and options[:source_line]
207
+ if options[:play]
208
+ repl_obj.play_back(options)
209
+ elsif ! options[:run_once]
210
+ repl_obj.read_loop
211
+ end
212
+ end
213
+
214
+ # start reads from stdin or from a file
215
+ # if from stdin, the repl will process the input and exit
216
+ # if from a file, the repl will process the file and continue to prompt
217
+ # @param [Hash] puppet scope object
218
+ def self.start(options={:scope => nil})
219
+ opts = Trollop::options do
220
+ opt :play, "Url or file to load from", :required => false, :type => String
221
+ opt :run_once, "Evaluate and quit", :required => false, :default => false
222
+ opt :node_name, "Remote Node to grab facts from", :required => false, :type => String
223
+ opt :quiet, "Do not display banner", :required => false, :default => false
224
+ end
225
+ options = opts.merge(options)
226
+ puts print_repl_desc unless options[:quiet]
227
+ repl_obj = PuppetDebugger::Cli.new(options)
228
+ repl_obj.remote_node_name = opts[:node_name] if opts[:node_name]
229
+ repl_obj.initialize_from_scope(options[:scope])
230
+ if options[:play]
231
+ repl_obj.play_back(opts)
232
+ # when the user supplied a file name without using the args (stdin)
233
+ elsif ARGF.filename != "-"
234
+ path = File.expand_path(ARGF.filename)
235
+ repl_obj.play_back(:play => path)
236
+ # when the user supplied a file content using stdin, aka. cat,pipe,echo or redirection
237
+ elsif ARGF.filename == "-" and (not STDIN.tty? and not STDIN.closed?)
238
+ input = ARGF.read
239
+ repl_obj.handle_input(input)
240
+ end
241
+ # helper code to make tests exit the loop
242
+ unless options[:run_once]
243
+ repl_obj.read_loop
244
+ end
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,98 @@
1
+
2
+ class CodeFile
3
+ class SourceNotFound < Exception
4
+ end
5
+
6
+ DEFAULT_EXT = '.pp'
7
+
8
+ # List of all supported languages.
9
+ # @return [Hash]
10
+ EXTENSIONS = {
11
+ %w(.py) => :python,
12
+ %w(.js) => :javascript,
13
+ %w(.pp) => :puppet,
14
+ %w(.css) => :css,
15
+ %w(.xml) => :xml,
16
+ %w(.php) => :php,
17
+ %w(.html) => :html,
18
+ %w(.diff) => :diff,
19
+ %w(.java) => :java,
20
+ %w(.json) => :json,
21
+ %w(.c .h) => :c,
22
+ %w(.rhtml) => :rhtml,
23
+ %w(.yaml .yml) => :yaml,
24
+ %w(.cpp .hpp .cc .h cxx) => :cpp,
25
+ %w(.rb .ru .irbrc .gemspec .pryrc) => :ruby,
26
+ }
27
+
28
+ # @return [Symbol] The type of code stored in this wrapper.
29
+ attr_reader :code_type
30
+
31
+ # @param [String] filename The name of a file with code to be detected
32
+ # @param [Symbol] code_type The type of code the `filename` contains
33
+ def initialize(filename, code_type = type_from_filename(filename))
34
+ @filename = filename
35
+ @code_type = code_type
36
+ end
37
+
38
+ # @return [String] The code contained in the current `@filename`.
39
+ def code
40
+ path = abs_path
41
+ @code_type = type_from_filename(path)
42
+ File.read(path)
43
+ end
44
+
45
+ private
46
+
47
+ # @raise [MethodSource::SourceNotFoundError] if the `filename` is not
48
+ # readable for some reason.
49
+ # @return [String] absolute path for the given `filename`.
50
+ def abs_path
51
+ code_path.detect { |path| readable?(path) } or
52
+ raise SourceNotFound,
53
+ "Cannot open #{ @filename.inspect } for reading."
54
+ end
55
+
56
+ # @param [String] path
57
+ # @return [Boolean] if the path, with or without the default ext,
58
+ # is a readable file then `true`, otherwise `false`.
59
+ def readable?(path)
60
+ File.readable?(path) && !File.directory?(path) or
61
+ File.readable?(path << DEFAULT_EXT)
62
+ end
63
+
64
+ # @return [Array] All the paths that contain code that Pry can use for its
65
+ # API's. Skips directories.
66
+ def code_path
67
+ [from_pwd, from_pry_init_pwd, *from_load_path]
68
+ end
69
+
70
+ # @param [String] filename
71
+ # @param [Symbol] default (:unknown) the file type to assume if none could be
72
+ # detected.
73
+ # @return [Symbol, nil] The CodeRay type of a file from its extension, or
74
+ # `nil` if `:unknown`.
75
+ def type_from_filename(filename, default = :unknown)
76
+ _, @code_type = EXTENSIONS.find do |k, _|
77
+ k.any? { |ext| ext == File.extname(filename) }
78
+ end
79
+
80
+ code_type || default
81
+ end
82
+
83
+ # @return [String]
84
+ def from_pwd
85
+ File.expand_path(@filename, Dir.pwd)
86
+ end
87
+
88
+ # @return [String]
89
+ def from_pry_init_pwd
90
+ File.expand_path(@filename, Dir.pwd)
91
+ end
92
+
93
+ # @return [String]
94
+ def from_load_path
95
+ $LOAD_PATH.map { |path| File.expand_path(@filename, path) }
96
+ end
97
+
98
+ end
@@ -0,0 +1,69 @@
1
+ class DebuggerCode
2
+
3
+ # Represents a range of lines in a code listing.
4
+ #
5
+ # @api private
6
+ class CodeRange
7
+
8
+ # @param [Integer] start_line
9
+ # @param [Integer?] end_line
10
+ def initialize(start_line, end_line = nil)
11
+ @start_line = start_line
12
+ @end_line = end_line
13
+ force_set_end_line
14
+ end
15
+
16
+ # @param [Array<LOC>] lines
17
+ # @return [Range]
18
+ def indices_range(lines)
19
+ Range.new(*indices(lines))
20
+ end
21
+
22
+ private
23
+
24
+ def start_line; @start_line; end
25
+ def end_line; @end_line; end
26
+
27
+ # If `end_line` is equal to `nil`, then calculate it from the first
28
+ # parameter, `start_line`. Otherwise, leave it as it is.
29
+ # @return [void]
30
+ def force_set_end_line
31
+ if start_line.is_a?(Range)
32
+ set_end_line_from_range
33
+ else
34
+ @end_line ||= start_line
35
+ end
36
+ end
37
+
38
+ # Finds indices of `start_line` and `end_line` in the given Array of
39
+ # +lines+.
40
+ #
41
+ # @param [Array<LOC>] lines
42
+ # @return [Array<Integer>]
43
+ def indices(lines)
44
+ [find_start_index(lines), find_end_index(lines)]
45
+ end
46
+
47
+ # @return [Integer]
48
+ def find_start_index(lines)
49
+ return start_line if start_line < 0
50
+ lines.index { |loc| loc.lineno >= start_line } || lines.length
51
+ end
52
+
53
+ # @return [Integer]
54
+ def find_end_index(lines)
55
+ return end_line if end_line < 0
56
+ (lines.index { |loc| loc.lineno > end_line } || 0) - 1
57
+ end
58
+
59
+ # For example, if the range is 4..10, then `start_line` would be equal to
60
+ # 4 and `end_line` to 10.
61
+ # @return [void]
62
+ def set_end_line_from_range
63
+ @end_line = start_line.last
64
+ @end_line -= 1 if start_line.exclude_end?
65
+ @start_line = start_line.first
66
+ end
67
+ end
68
+
69
+ end
@@ -0,0 +1,80 @@
1
+ class DebuggerCode
2
+
3
+ # Represents a line of code. A line of code is a tuple, which consists of a
4
+ # line and a line number. A `LOC` object's state (namely, the line
5
+ # parameter) can be changed via instance methods. `Pry::Code` heavily uses
6
+ # this class.
7
+ #
8
+ # @api private
9
+ # @example
10
+ # loc = LOC.new("def example\n :example\nend", 1)
11
+ # puts loc.line
12
+ # def example
13
+ # :example
14
+ # end
15
+ # #=> nil
16
+ #
17
+ # loc.indent(3)
18
+ # loc.line #=> " def example\n :example\nend"
19
+ class LOC
20
+
21
+ # @return [Array<String, Integer>]
22
+ attr_reader :tuple
23
+
24
+ # @param [String] line The line of code.
25
+ # @param [Integer] lineno The position of the +line+.
26
+ def initialize(line, lineno)
27
+ @tuple = [line.chomp, lineno.to_i]
28
+ end
29
+
30
+ # @return [Boolean]
31
+ def ==(other)
32
+ other.tuple == tuple
33
+ end
34
+
35
+ def dup
36
+ self.class.new(line, lineno)
37
+ end
38
+
39
+ # @return [String]
40
+ def line
41
+ tuple.first
42
+ end
43
+
44
+ # @return [Integer]
45
+ def lineno
46
+ tuple.last
47
+ end
48
+
49
+ #Prepends the line number `lineno` to the `line`.
50
+ # @param [Integer] max_width
51
+ # @return [void]
52
+ def add_line_number(max_width = 0)
53
+ padded = lineno.to_s.rjust(max_width)
54
+ tuple[0] = "#{ padded }: #{ line }"
55
+ end
56
+
57
+ # Prepends a marker "=>" or an empty marker to the +line+.
58
+ #
59
+ # @param [Integer] marker_lineno If it is equal to the `lineno`, then
60
+ # prepend a hashrocket. Otherwise, an empty marker.
61
+ # @return [void]
62
+ def add_marker(marker_lineno)
63
+ tuple[0] =
64
+ if lineno == marker_lineno
65
+ " => #{ line }".cyan
66
+ else
67
+ " #{ line }"
68
+ end
69
+ end
70
+
71
+ # Indents the `line` with +distance+ spaces.
72
+ #
73
+ # @param [Integer] distance
74
+ # @return [void]
75
+ def indent(distance)
76
+ tuple[0] = "#{ ' ' * distance }#{ line }"
77
+ end
78
+ end
79
+
80
+ end