bolt 0.14.0 → 0.15.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of bolt might be problematic. Click here for more details.

data/lib/bolt/result.rb CHANGED
@@ -3,9 +3,9 @@ require 'bolt/error'
3
3
 
4
4
  module Bolt
5
5
  class Result
6
- attr_reader :message, :error
6
+ attr_reader :target, :value
7
7
 
8
- def self.from_exception(exception)
8
+ def self.from_exception(target, exception)
9
9
  @exception = exception
10
10
  if @exception.is_a?(Bolt::Error)
11
11
  error = @exception.to_h
@@ -18,121 +18,124 @@ module Bolt
18
18
  }
19
19
  error['details']['stack_trace'] = exception.backtrace.join('\n') if exception.backtrace
20
20
  end
21
- Result.new(error)
21
+ Result.new(target, error: error)
22
22
  end
23
23
 
24
- def initialize(error = nil, message = nil)
25
- @error = error
26
- @message = message
24
+ def self.for_command(target, stdout, stderr, exit_code)
25
+ value = {
26
+ 'stdout' => stdout,
27
+ 'stderr' => stderr,
28
+ 'exit_code' => exit_code
29
+ }
30
+ unless exit_code == 0
31
+ value['_error'] = {
32
+ 'kind' => 'puppetlabs.tasks/command-error',
33
+ 'issue_code' => 'COMMAND_ERROR',
34
+ 'msg' => "The command failed with exit code #{exit_code}",
35
+ 'details' => { 'exit_code' => exit_code }
36
+ }
37
+ end
38
+ new(target, value: value)
27
39
  end
28
40
 
29
- def value
30
- nil
41
+ def self.for_task(target, stdout, stderr, exit_code)
42
+ begin
43
+ value = JSON.parse(stdout)
44
+ unless value.is_a? Hash
45
+ value = nil
46
+ end
47
+ rescue JSON::ParserError
48
+ value = nil
49
+ end
50
+ value ||= { '_output' => stdout }
51
+ if exit_code != 0 && value['_error'].nil?
52
+ msg = if stdout.empty?
53
+ "The task failed with exit code #{exit_code}:\n#{stderr}"
54
+ else
55
+ "The task failed with exit code #{exit_code}"
56
+ end
57
+ value['_error'] = { 'kind' => 'puppetlabs.tasks/task-error',
58
+ 'issue_code' => 'TASK_ERROR',
59
+ 'msg' => msg,
60
+ 'details' => { 'exit_code' => exit_code } }
61
+ end
62
+ new(target, value: value)
63
+ end
64
+
65
+ def self.for_upload(target, source, destination)
66
+ new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'")
31
67
  end
32
68
 
33
- def to_h
34
- h = {}
35
- if value
36
- h['value'] = value
37
- h['value']['_output'] = message if message
38
- elsif message
39
- h['value'] = { '_output' => message }
69
+ def initialize(target, error: nil, message: nil, value: nil)
70
+ @target = target
71
+ @value = value || {}
72
+ @value_set = !value.nil?
73
+ if error && !error.is_a?(Hash)
74
+ raise "TODO: how did we get a string error"
40
75
  end
41
- h['error'] = error if error
42
- h
76
+ @value['_error'] = error if error
77
+ @value['_output'] = message if message
43
78
  end
44
79
 
45
- def to_result
46
- # TODO: This should be to_h but we need to update the plan functions to
47
- # use this hash instead
48
- special_keys = {}
49
- special_keys['_error'] = error if error
50
- special_keys['_output'] = message if message
51
- val = value || {}
52
- val.merge(special_keys)
80
+ def message
81
+ @value['_output']
53
82
  end
54
83
 
55
- def success?
56
- error.nil?
84
+ def status_hash
85
+ { node: @target.name,
86
+ status: ok? ? 'success' : 'failure',
87
+ result: @value }
57
88
  end
58
- end
59
89
 
60
- class CommandResult < Result
61
- attr_reader :stdout, :stderr, :exit_code
90
+ # TODO: what to call this it's the value minus special keys
91
+ # This should be {} if a value was set otherwise it's nil
92
+ def generic_value
93
+ if @value_set
94
+ value.reject { |k, _| %w[_error _output].include? k }
95
+ end
96
+ end
62
97
 
63
- def self.from_output(output)
64
- new(output.stdout.string,
65
- output.stderr.string,
66
- output.exit_code)
98
+ def eql?(other)
99
+ self.class == other.class &&
100
+ target == other.target &&
101
+ value == other.value
67
102
  end
68
103
 
69
- def initialize(stdout, stderr, exit_code)
70
- @stdout = stdout || ""
71
- @stderr = stderr || ""
72
- @exit_code = exit_code
104
+ def [](key)
105
+ value[key]
73
106
  end
74
107
 
75
- def value
76
- {
77
- 'stdout' => @stdout,
78
- 'stderr' => @stderr,
79
- 'exit_code' => @exit_code
80
- }
108
+ def ==(other)
109
+ eql?(other)
81
110
  end
82
111
 
112
+ # TODO: remove in favor of ok?
83
113
  def success?
84
- @exit_code == 0
114
+ ok?
85
115
  end
86
116
 
87
- def error
88
- unless success?
89
- {
90
- 'kind' => 'puppetlabs.tasks/command-error',
91
- 'issue_code' => 'COMMAND_ERROR',
92
- 'msg' => "The command failed with exit code #{@exit_code}",
93
- 'details' => { 'exit_code' => @exit_code }
94
- }
95
- end
117
+ def ok?
118
+ error_hash.nil?
96
119
  end
97
- end
98
-
99
- class TaskResult < CommandResult
100
- attr_reader :value
120
+ alias ok ok?
101
121
 
102
- def initialize(stdout, stderr, exit_code)
103
- super(stdout, stderr, exit_code)
104
- @value = parse_output(stdout)
105
- @message = @value.delete('_output')
106
- @error = @value.delete('_error')
122
+ # This allows access to errors outside puppet compilation
123
+ # it should be prefered over error in bolt code
124
+ def error_hash
125
+ value['_error']
107
126
  end
108
127
 
128
+ # Warning: This will fail outside of a compilation.
129
+ # Use error_hash inside bolt.
130
+ # Is it crazy for this to behave differently outside a compiler?
109
131
  def error
110
- unless success?
111
- return @error if @error
112
- msg = if !@stdout.empty?
113
- "The task failed with exit code #{@exit_code}"
114
- else
115
- "The task failed with exit code #{@exit_code}:\n#{@stderr}"
116
- end
117
- { 'kind' => 'puppetlabs.tasks/task-error',
118
- 'issue_code' => 'TASK_ERROR',
119
- 'msg' => msg,
120
- 'details' => { 'exit_code' => @exit_code } }
121
- end
122
- end
132
+ if error_hash
133
+ Puppet::DataTypes::Error.new(error_hash['msg'],
134
+ error_hash['kind'],
135
+ nil, nil,
136
+ error_hash['details'])
123
137
 
124
- private
125
-
126
- def parse_output(output)
127
- begin
128
- obj = JSON.parse(output)
129
- unless obj.is_a? Hash
130
- obj = nil
131
- end
132
- rescue JSON::ParserError
133
- obj = nil
134
138
  end
135
- obj || { '_output' => output }
136
139
  end
137
140
  end
138
141
  end
@@ -0,0 +1,94 @@
1
+ module Bolt
2
+ class ResultSet
3
+ attr_reader :results
4
+
5
+ include Enumerable
6
+
7
+ # We only want want to include these when puppet is loaded
8
+ def self.include_iterable
9
+ include(Puppet::Pops::Types::Iterable)
10
+ include(Puppet::Pops::Types::IteratorProducer)
11
+ end
12
+
13
+ def iterator
14
+ if Object.const_defined?(:Puppet) && Puppet.const_defined?(:Pops) &&
15
+ self.class.included_modules.include?(Puppet::Pops::Types::Iterable)
16
+ Puppet::Pops::Types::Iterable.on(@results, Bolt::Result)
17
+ else
18
+ raise NotImplementedError, "iterator requires puppet code to be loaded."
19
+ end
20
+ end
21
+
22
+ def initialize(results)
23
+ @results = results
24
+ end
25
+
26
+ def each
27
+ @results.each { |r| yield r }
28
+ end
29
+
30
+ def result_hash
31
+ @result_hash ||= @results.each_with_object({}) do |result, acc|
32
+ acc[result.target.name] = result
33
+ end
34
+ end
35
+
36
+ def count
37
+ @results.size
38
+ end
39
+ alias length count
40
+ alias size count
41
+
42
+ def empty
43
+ @results.empty?
44
+ end
45
+ alias empty? empty
46
+
47
+ def targets
48
+ results.map(&:target)
49
+ end
50
+
51
+ def names
52
+ @results.map { |r| r.target.name }
53
+ end
54
+
55
+ def ok
56
+ @results.all?(&:ok?)
57
+ end
58
+ alias ok? ok
59
+
60
+ def error_set
61
+ filtered = @results.reject(&:ok?)
62
+ ResultSet.new(filtered)
63
+ end
64
+
65
+ def ok_set
66
+ filtered = @results.select(&:success?)
67
+ self.class.new(filtered)
68
+ end
69
+
70
+ def find(target_name)
71
+ result_hash[target_name]
72
+ end
73
+
74
+ def first
75
+ @results.first
76
+ end
77
+
78
+ def eql?(other)
79
+ self.class == other.class && @results == other.results
80
+ end
81
+
82
+ def to_json(opts = nil)
83
+ @results.map(&:status_hash).to_json(opts)
84
+ end
85
+
86
+ def to_s
87
+ to_json
88
+ end
89
+
90
+ def ==(other)
91
+ eql?(other)
92
+ end
93
+ end
94
+ end
data/lib/bolt/target.rb CHANGED
@@ -1,32 +1,80 @@
1
+ require 'addressable/uri'
2
+ require 'bolt/error'
3
+
1
4
  module Bolt
2
5
  class Target
3
- attr_reader :host, :options
6
+ attr_reader :uri, :options
4
7
 
8
+ # Satisfies the Puppet datatypes API
5
9
  def self.from_asserted_hash(hash)
6
- new(hash['host'], hash['options'])
10
+ new(hash['uri'], hash['options'])
11
+ end
12
+
13
+ def self.from_uri(uri)
14
+ new(uri)
15
+ end
16
+
17
+ def self.parse_urls(urls)
18
+ urls.split(/[[:space:],]+/).reject(&:empty?).uniq.map { |url| from_uri(url) }
19
+ end
20
+
21
+ def initialize(uri, options = nil)
22
+ @uri = uri
23
+ @uri_obj = parse(uri)
24
+ @options = options || {}
7
25
  end
8
26
 
9
- def initialize(host, options = {})
10
- @host = host
11
- @options = options
27
+ def parse(string)
28
+ if string =~ %r{^[^:]+://}
29
+ Addressable::URI.parse(string)
30
+ else
31
+ # Initialize with an empty scheme to ensure we parse the hostname correctly
32
+ Addressable::URI.parse("//#{string}")
33
+ end
12
34
  end
35
+ private :parse
13
36
 
14
37
  def eql?(other)
15
- self.class.equal?(other.class) && @host == other.host && @options == other.options
38
+ self.class.equal?(other.class) && @uri == other.uri && @options == other.options
16
39
  end
17
40
  alias == eql?
18
41
 
19
42
  def hash
20
- @host.hash ^ @options.hash
43
+ @uri.hash ^ @options.hash
21
44
  end
22
45
 
23
46
  def to_s
24
- # Use Puppet::Pops::Types::StringConverter if it is available
25
- if Object.const_defined?(:Puppet) && Puppet.const_defined?(:Pops)
26
- Puppet::Pops::Types::StringConverter.singleton.convert(self)
27
- else
28
- "Target('#{@host}', #{@options})"
29
- end
47
+ "Target('#{@uri}', #{@options})"
48
+ end
49
+
50
+ def host
51
+ @uri_obj.hostname
52
+ end
53
+
54
+ def port
55
+ @uri_obj.port
56
+ end
57
+
58
+ # name is currently just uri but should be be used instead to identify the
59
+ # Target ouside the transport or uri options.
60
+ def name
61
+ uri
62
+ end
63
+
64
+ def user
65
+ Addressable::URI.unencode_component(
66
+ @uri_obj.user
67
+ )
68
+ end
69
+
70
+ def password
71
+ Addressable::URI.unencode_component(
72
+ @uri_obj.password
73
+ )
74
+ end
75
+
76
+ def protocol
77
+ @uri_obj.scheme
30
78
  end
31
79
  end
32
80
  end
data/lib/bolt/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Bolt
2
- VERSION = '0.14.0'.freeze
2
+ VERSION = '0.15.0'.freeze
3
3
  end
@@ -0,0 +1,18 @@
1
+ Puppet::DataTypes.create_type('Result') do
2
+ interface <<-PUPPET
3
+ attributes => {
4
+ 'value' => Hash[String[1], Data],
5
+ 'target' => Target
6
+ },
7
+ functions => {
8
+ error => Callable[[], Optional[Error]],
9
+ message => Callable[[], Optional[String]],
10
+ ok => Callable[[], Boolean],
11
+ '[]' => Callable[[String[1]], Data]
12
+ }
13
+ PUPPET
14
+
15
+ load_file('bolt/result')
16
+
17
+ implementation_class Bolt::Result
18
+ end
@@ -0,0 +1,22 @@
1
+ Puppet::DataTypes.create_type('ResultSet') do
2
+ interface <<-PUPPET
3
+ attributes => {
4
+ 'results' => Array[Result],
5
+ },
6
+ functions => {
7
+ count => Callable[[], Integer],
8
+ empty => Callable[[], Boolean],
9
+ error_set => Callable[[], ResultSet],
10
+ find => Callable[[String[1]], Optional[Result]],
11
+ first => Callable[[], Optional[Result]],
12
+ names => Callable[[], Array[String[1]]],
13
+ ok => Callable[[], Boolean],
14
+ ok_set => Callable[[], ResultSet],
15
+ targets => Callable[[], Array[Target]],
16
+ }
17
+ PUPPET
18
+
19
+ load_file('bolt/result_set')
20
+
21
+ implementation_class Bolt::ResultSet
22
+ end