bolt 1.14.0 → 1.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.

@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/pal/yaml_plan'
4
+ require 'bolt/pal/yaml_plan/evaluator'
5
+ require 'psych'
6
+
7
+ module Bolt
8
+ class PAL
9
+ class YamlPlan
10
+ class Loader
11
+ class PuppetVisitor < Psych::Visitors::NoAliasRuby
12
+ def self.create_visitor
13
+ class_loader = Psych::ClassLoader::Restricted.new([], [])
14
+ scanner = Psych::ScalarScanner.new(class_loader)
15
+ new(scanner, class_loader)
16
+ end
17
+
18
+ def deserialize(node)
19
+ if node.quoted
20
+ case node.style
21
+ when Psych::Nodes::Scalar::SINGLE_QUOTED
22
+ # Single-quoted strings are treated literally
23
+ # @ss is a ScalarScanner, from the base ToRuby visitor class
24
+ node.value
25
+ when Psych::Nodes::Scalar::DOUBLE_QUOTED
26
+ DoubleQuotedString.new(node.value)
27
+ # | style string or > style string
28
+ when Psych::Nodes::Scalar::LITERAL, Psych::Nodes::Scalar::FOLDED
29
+ CodeLiteral.new(node.value)
30
+ # This one shouldn't be possible
31
+ else
32
+ @ss.tokenize(node.value)
33
+ end
34
+ else
35
+ value = @ss.tokenize(node.value)
36
+ if value.is_a?(String)
37
+ BareString.new(value)
38
+ else
39
+ value
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def self.parse_plan(yaml_string, source_ref)
46
+ # This passes the filename as the second arg for compatibility with Psych used with ruby < 2.6
47
+ # This can be removed when we remove support for ruby 2.5
48
+ parse_tree = if Psych.method(:parse).parameters.include?('legacy_filename')
49
+ Psych.parse(yaml_string, filename: source_ref)
50
+ else
51
+ Psych.parse(yaml_string, source_ref)
52
+ end
53
+ PuppetVisitor.create_visitor.accept(parse_tree)
54
+ end
55
+
56
+ def self.create(loader, typed_name, source_ref, yaml_string)
57
+ result = parse_plan(yaml_string, source_ref)
58
+ unless result.is_a?(Hash)
59
+ type = result.class.name
60
+ raise ArgumentError, "The data loaded from #{source_ref} does not contain an object - its type is #{type}"
61
+ end
62
+
63
+ begin
64
+ plan_definition = YamlPlan.new(typed_name, result).freeze
65
+ rescue Bolt::Error => e
66
+ raise Puppet::ParseError.new(e.message, source_ref)
67
+ end
68
+
69
+ created = create_function_class(plan_definition)
70
+ closure_scope = nil
71
+
72
+ created.new(closure_scope, loader.private_loader)
73
+ end
74
+
75
+ def self.create_function_class(plan_definition)
76
+ Puppet::Functions.create_function(plan_definition.name, Puppet::Functions::PuppetFunction) do
77
+ closure = Puppet::Pops::Evaluator::Closure::Named.new(plan_definition.name,
78
+ YamlPlan::Evaluator.new,
79
+ plan_definition)
80
+ init_dispatch(closure)
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
data/lib/bolt/result.rb CHANGED
@@ -5,7 +5,7 @@ require 'bolt/error'
5
5
 
6
6
  module Bolt
7
7
  class Result
8
- attr_reader :target, :value
8
+ attr_reader :target, :value, :type, :object
9
9
 
10
10
  def self.from_exception(target, exception)
11
11
  @exception = exception
@@ -23,7 +23,7 @@ module Bolt
23
23
  Result.new(target, error: error)
24
24
  end
25
25
 
26
- def self.for_command(target, stdout, stderr, exit_code)
26
+ def self.for_command(target, stdout, stderr, exit_code, type, command)
27
27
  value = {
28
28
  'stdout' => stdout,
29
29
  'stderr' => stderr,
@@ -37,10 +37,10 @@ module Bolt
37
37
  'details' => { 'exit_code' => exit_code }
38
38
  }
39
39
  end
40
- new(target, value: value)
40
+ new(target, value: value, type: type, object: command)
41
41
  end
42
42
 
43
- def self.for_task(target, stdout, stderr, exit_code)
43
+ def self.for_task(target, stdout, stderr, exit_code, task)
44
44
  begin
45
45
  value = JSON.parse(stdout)
46
46
  unless value.is_a? Hash
@@ -61,11 +61,11 @@ module Bolt
61
61
  'msg' => msg,
62
62
  'details' => { 'exit_code' => exit_code } }
63
63
  end
64
- new(target, value: value)
64
+ new(target, value: value, type: 'task', object: task)
65
65
  end
66
66
 
67
67
  def self.for_upload(target, source, destination)
68
- new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'")
68
+ new(target, message: "Uploaded '#{source}' to '#{target.host}:#{destination}'", type: 'upload', object: source)
69
69
  end
70
70
 
71
71
  # Satisfies the Puppet datatypes API
@@ -73,9 +73,11 @@ module Bolt
73
73
  new(target, value: value)
74
74
  end
75
75
 
76
- def initialize(target, error: nil, message: nil, value: nil)
76
+ def initialize(target, error: nil, message: nil, value: nil, type: nil, object: nil)
77
77
  @target = target
78
78
  @value = value || {}
79
+ @type = type
80
+ @object = object
79
81
  @value_set = !value.nil?
80
82
  if error && !error.is_a?(Hash)
81
83
  raise "TODO: how did we get a string error"
@@ -90,12 +92,12 @@ module Bolt
90
92
 
91
93
  def status_hash
92
94
  { node: @target.name,
95
+ type: type,
96
+ object: object,
93
97
  status: ok? ? 'success' : 'failure',
94
98
  result: @value }
95
99
  end
96
100
 
97
- # TODO: what to call this it's the value minus special keys
98
- # This should be {} if a value was set otherwise it's nil
99
101
  def generic_value
100
102
  if @value_set
101
103
  value.reject { |k, _| %w[_error _output].include? k }
@@ -124,15 +126,11 @@ module Bolt
124
126
  to_json
125
127
  end
126
128
 
127
- # TODO: remove in favor of ok?
128
- def success?
129
- ok?
130
- end
131
-
132
129
  def ok?
133
130
  error_hash.nil?
134
131
  end
135
132
  alias ok ok?
133
+ alias success? ok?
136
134
 
137
135
  # This allows access to errors outside puppet compilation
138
136
  # it should be prefered over error in bolt code
data/lib/bolt/task.rb CHANGED
@@ -21,12 +21,20 @@ module Bolt
21
21
  :metadata
22
22
  ) do
23
23
 
24
- def initialize(task)
24
+ attr_reader :remote
25
+
26
+ def initialize(task, remote: false)
25
27
  super(nil, nil, [], {})
26
28
 
29
+ @remote = remote
30
+
27
31
  task.reject { |k, _| k == 'parameters' }.each { |k, v| self[k] = v }
28
32
  end
29
33
 
34
+ def remote_instance
35
+ self.class.new(to_h.each_with_object({}) { |(k, v), h| h[k.to_s] = v }, remote: true)
36
+ end
37
+
30
38
  def description
31
39
  metadata['description']
32
40
  end
@@ -67,13 +75,18 @@ module Bolt
67
75
  def select_implementation(target, additional_features = [])
68
76
  impl = if (impls = implementations)
69
77
  available_features = target.features + additional_features
70
- impl = impls.find { |imp| Set.new(imp['requirements']).subset?(available_features) }
78
+ impl = impls.find do |imp|
79
+ remote_impl = imp['remote']
80
+ remote_impl = metadata['remote'] if remote_impl.nil?
81
+ Set.new(imp['requirements']).subset?(available_features) && !!remote_impl == @remote
82
+ end
71
83
  raise NoImplementationError.new(target, self) unless impl
72
84
  impl = impl.dup
73
85
  impl['path'] = file_path(impl['name'])
74
86
  impl.delete('requirements')
75
87
  impl
76
88
  else
89
+ raise NoImplementationError.new(target, self) unless !!metadata['remote'] == @remote
77
90
  name = files.first['name']
78
91
  { 'name' => name, 'path' => file_path(name) }
79
92
  end
@@ -1,14 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bolt/task'
4
-
5
3
  module Bolt
6
4
  class Task
7
5
  class PuppetServer < Bolt::Task
8
- def initialize(task_data, file_cache)
6
+ def remote_instance
7
+ self.class.new(to_h.each_with_object({}) { |(k, v), h| h[k.to_s] = v },
8
+ @file_cache,
9
+ remote: true)
10
+ end
11
+
12
+ def initialize(task, file_cache, **opts)
13
+ super(task, **opts)
9
14
  @file_cache = file_cache
10
- task_data = update_file_data(task_data)
11
- super(task_data)
15
+ update_file_data(task)
12
16
  end
13
17
 
14
18
  # puppetserver file entries have 'filename' rather then 'name'
@@ -17,7 +21,6 @@ module Bolt
17
21
  task_data
18
22
  end
19
23
 
20
- # Compute local path and download files from puppetserver as needed
21
24
  def file_path(file_name)
22
25
  file = file_map[file_name]
23
26
  file['path'] ||= @file_cache.update_file(file)
@@ -59,7 +59,7 @@ module Bolt
59
59
  def run_command(target, command, _options = {})
60
60
  with_connection(target) do |conn|
61
61
  stdout, stderr, exitcode = conn.execute(*Shellwords.split(command), {})
62
- Bolt::Result.for_command(target, stdout, stderr, exitcode)
62
+ Bolt::Result.for_command(target, stdout, stderr, exitcode, 'command', command)
63
63
  end
64
64
  end
65
65
 
@@ -71,7 +71,7 @@ module Bolt
71
71
  conn.with_remote_tempdir do |dir|
72
72
  remote_path = conn.write_remote_executable(dir, script)
73
73
  stdout, stderr, exitcode = conn.execute(remote_path, *arguments, {})
74
- Bolt::Result.for_command(target, stdout, stderr, exitcode)
74
+ Bolt::Result.for_command(target, stdout, stderr, exitcode, 'script', script)
75
75
  end
76
76
  end
77
77
  end
@@ -112,7 +112,7 @@ module Bolt
112
112
  end
113
113
 
114
114
  stdout, stderr, exitcode = conn.execute(remote_task_path, execute_options)
115
- Bolt::Result.for_task(target, stdout, stderr, exitcode)
115
+ Bolt::Result.for_task(target, stdout, stderr, exitcode, task.name)
116
116
  end
117
117
  end
118
118
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'docker'
4
3
  require 'logging'
5
4
  require 'bolt/node/errors'
6
5
 
@@ -9,6 +8,9 @@ module Bolt
9
8
  class Docker < Base
10
9
  class Connection
11
10
  def initialize(target)
11
+ # lazy-load expensive gem code
12
+ require 'docker'
13
+
12
14
  @target = target
13
15
  @logger = Logging.logger[target.host]
14
16
  end
@@ -78,7 +78,11 @@ module Bolt
78
78
  def run_command(target, command, _options = {})
79
79
  in_tmpdir(target.options['tmpdir']) do |dir|
80
80
  output = @conn.execute(command, dir: dir)
81
- Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code)
81
+ Bolt::Result.for_command(target,
82
+ output.stdout.string,
83
+ output.stderr.string,
84
+ output.exit_code,
85
+ 'command', command)
82
86
  end
83
87
  end
84
88
 
@@ -106,7 +110,11 @@ module Bolt
106
110
  end
107
111
  output = @conn.execute(file, *arguments, dir: dir)
108
112
  end
109
- Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code)
113
+ Bolt::Result.for_command(target,
114
+ output.stdout.string,
115
+ output.stderr.string,
116
+ output.exit_code,
117
+ 'script', script)
110
118
  end
111
119
  end
112
120
 
@@ -181,7 +189,7 @@ module Bolt
181
189
  env = ENVIRONMENT_METHODS.include?(input_method) ? envify_params(unwrapped_arguments) : nil
182
190
  output = @conn.execute(script, stdin: stdin, env: env, dir: dir, interpreter: interpreter)
183
191
  end
184
- Bolt::Result.for_task(target, output.stdout.string, output.stderr.string, output.exit_code)
192
+ Bolt::Result.for_task(target, output.stdout.string, output.stderr.string, output.exit_code, task.name)
185
193
  end
186
194
  end
187
195
 
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'base64'
4
- require 'concurrent'
5
4
  require 'find'
6
5
  require 'json'
7
6
  require 'minitar'
8
- require 'orchestrator_client'
9
7
  require 'pathname'
10
8
  require 'zlib'
11
9
  require 'bolt/transport/base'
@@ -40,6 +38,9 @@ module Bolt
40
38
  def self.validate(options); end
41
39
 
42
40
  def initialize(*args)
41
+ # lazy-load expensive gem code
42
+ require 'orchestrator_client'
43
+
43
44
  @connections = {}
44
45
  super
45
46
  end
@@ -66,7 +67,7 @@ module Bolt
66
67
  conn
67
68
  end
68
69
 
69
- def process_run_results(targets, results)
70
+ def process_run_results(targets, results, task_name)
70
71
  targets_by_name = Hash[targets.map(&:host).zip(targets)]
71
72
  results.map do |node_result|
72
73
  target = targets_by_name[node_result['name']]
@@ -76,7 +77,7 @@ module Bolt
76
77
  # If it's finished or already has a proper error simply pass it to the
77
78
  # the result otherwise make sure an error is generated
78
79
  if state == 'finished' || (result && result['_error'])
79
- Bolt::Result.new(target, value: result)
80
+ Bolt::Result.new(target, value: result, type: 'task', object: task_name)
80
81
  elsif state == 'skipped'
81
82
  Bolt::Result.new(
82
83
  target,
@@ -84,11 +85,12 @@ module Bolt
84
85
  'kind' => 'puppetlabs.tasks/skipped-node',
85
86
  'msg' => "Node #{target.host} was skipped",
86
87
  'details' => {}
87
- } }
88
+ } },
89
+ type: 'task', object: task_name
88
90
  )
89
91
  else
90
92
  # Make a generic error with a unkown exit_code
91
- Bolt::Result.for_task(target, result.to_json, '', 'unknown')
93
+ Bolt::Result.for_task(target, result.to_json, '', 'unknown', task_name)
92
94
  end
93
95
  end
94
96
  end
@@ -103,7 +105,7 @@ module Bolt
103
105
  options,
104
106
  &callback)
105
107
  callback ||= proc {}
106
- results.map! { |result| unwrap_bolt_result(result.target, result) }
108
+ results.map! { |result| unwrap_bolt_result(result.target, result, 'command', command) }
107
109
  results.each do |result|
108
110
  callback.call(type: :node_result, result: result)
109
111
  end
@@ -119,7 +121,7 @@ module Bolt
119
121
  }
120
122
  callback ||= proc {}
121
123
  results = run_task_job(targets, BOLT_SCRIPT_TASK, params, options, &callback)
122
- results.map! { |result| unwrap_bolt_result(result.target, result) }
124
+ results.map! { |result| unwrap_bolt_result(result.target, result, 'script', script) }
123
125
  results.each do |result|
124
126
  callback.call(type: :node_result, result: result)
125
127
  end
@@ -198,7 +200,7 @@ module Bolt
198
200
  arguments = unwrap_sensitive_args(arguments)
199
201
  results = get_connection(targets.first.options).run_task(targets, task, arguments, options)
200
202
 
201
- process_run_results(targets, results)
203
+ process_run_results(targets, results, task.name)
202
204
  rescue OrchestratorClient::ApiError => e
203
205
  targets.map do |target|
204
206
  Bolt::Result.new(target, error: e.data)
@@ -226,13 +228,17 @@ module Bolt
226
228
  # run_task generates a result that makes sense for a generic task which
227
229
  # needs to be unwrapped to extract stdout/stderr/exitcode.
228
230
  #
229
- def unwrap_bolt_result(target, result)
231
+ def unwrap_bolt_result(target, result, type, obj)
230
232
  if result.error_hash
231
233
  # something went wrong return the failure
232
234
  return result
233
235
  end
234
236
 
235
- Bolt::Result.for_command(target, result.value['stdout'], result.value['stderr'], result.value['exit_code'])
237
+ Bolt::Result.for_command(target,
238
+ result.value['stdout'],
239
+ result.value['stderr'],
240
+ result.value['exit_code'],
241
+ type, obj)
236
242
  end
237
243
  end
238
244
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bolt/task/remote'
3
+ require 'bolt/task'
4
4
  require 'bolt/transport/base'
5
5
 
6
6
  module Bolt
@@ -47,7 +47,7 @@ module Bolt
47
47
  transport = @executor.transport(proxy_target.protocol)
48
48
  arguments = arguments.merge('_target' => target.to_h.reject { |_, v| v.nil? })
49
49
 
50
- remote_task = Bolt::Task::Remote.new(task.to_h)
50
+ remote_task = task.remote_instance
51
51
 
52
52
  result = transport.run_task(proxy_target, remote_task, arguments, options)
53
53
  Bolt::Result.new(target, value: result.value)
@@ -108,7 +108,11 @@ module Bolt
108
108
  with_connection(target) do |conn|
109
109
  conn.running_as(options['_run_as']) do
110
110
  output = conn.execute(command, sudoable: true)
111
- Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code)
111
+ Bolt::Result.for_command(target,
112
+ output.stdout.string,
113
+ output.stderr.string,
114
+ output.exit_code,
115
+ 'command', command)
112
116
  end
113
117
  end
114
118
  end
@@ -123,7 +127,11 @@ module Bolt
123
127
  remote_path = conn.write_remote_executable(dir, script)
124
128
  dir.chown(conn.run_as)
125
129
  output = conn.execute([remote_path, *arguments], sudoable: true)
126
- Bolt::Result.for_command(target, output.stdout.string, output.stderr.string, output.exit_code)
130
+ Bolt::Result.for_command(target,
131
+ output.stdout.string,
132
+ output.stderr.string,
133
+ output.exit_code,
134
+ 'script', script)
127
135
  end
128
136
  end
129
137
  end
@@ -184,7 +192,8 @@ module Bolt
184
192
  end
185
193
  Bolt::Result.for_task(target, output.stdout.string,
186
194
  output.stderr.string,
187
- output.exit_code)
195
+ output.exit_code,
196
+ task.name)
188
197
  end
189
198
  end
190
199
  end