bolt 1.1.0 → 1.2.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.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5955d8922691cc5c2875f1614d69181289c0ba9561e6016fb8c18bcecd09391a
4
- data.tar.gz: 0077fbd497fbbabf23c72a7916517b660a9c21553e8a6bc04ffaddc8cc283e53
3
+ metadata.gz: ca6e259cbc835ea3d8b4f5f67d2b199faf19adfeb2a2d9b0b999582b919a863a
4
+ data.tar.gz: 2152f34ff150aaaf2d1c0a6fb114d401ef3ddf5c2da5a739e8f2fa22ab5565cf
5
5
  SHA512:
6
- metadata.gz: a92a53a9e95ff635ff3adf36e594d6ba38d130cbfd117ecfe7111404cba7f453c63e483269120ecf51d1cd1b58f96cd8bfa6087d88e4ed3c1692c7dfe804ece9
7
- data.tar.gz: f2df408dcd301c87b0d598b0f5848576b832a815f2bf13aa5784e85a2384a098b3a34a0b12e3b7b1d80292e334913ab553848826a52e712cb563fc3814abc24d
6
+ metadata.gz: 69fe10cdd01be7f4965a8e6cca73aa9c65f449b94bb35909399839ca4c506107b611df43f3a0e27fe9fce896d3e63261f3dc90ce329d257583ff427e6c712eb3
7
+ data.tar.gz: 400b7ef4c9b01ad6c5464481ac73f5c3b7774e25aa6780f1aa404e3e6504091c57828d9a0bdaf7a3f4effecd08ff1b35cae4dd8e5b7dd6b6329267805ce0ef77
@@ -133,7 +133,13 @@ module Bolt
133
133
  %w[trusted server_facts facts].each { |k| plan_vars.delete(k) }
134
134
 
135
135
  targets = @inventory.get_targets(args[0])
136
+
136
137
  ast = Puppet::Pops::Serialization::ToDataConverter.convert(apply_body, rich_data: true, symbol_to_string: true)
138
+
139
+ apply_ast(ast, targets, options, plan_vars)
140
+ end
141
+
142
+ def apply_ast(ast, targets, options, plan_vars = {})
137
143
  notify = proc { |_| nil }
138
144
 
139
145
  r = @executor.log_action('apply catalog', targets) do
@@ -30,6 +30,7 @@ Available subcommands:
30
30
  bolt plan show Show list of available plans
31
31
  bolt plan show <plan> Show details for plan
32
32
  bolt plan run <plan> [params] Run a Puppet task plan
33
+ bolt apply <manifest> Apply Puppet manifest code
33
34
  bolt puppetfile install Install modules from a Puppetfile into a Boltdir
34
35
 
35
36
  Run `bolt <subcommand> --help` to view specific examples.
@@ -107,6 +108,13 @@ Install modules into the local Boltdir
107
108
  Available options are:
108
109
  HELP
109
110
 
111
+ APPLY_HELP = <<-HELP
112
+ Usage: bolt apply <manifest.pp> [options]
113
+
114
+ #{examples('apply site.pp', 'apply a manifest on')}
115
+ bolt apply site.pp --nodes foo.example.com,bar.example.com
116
+ HELP
117
+
110
118
  # A helper mixin for OptionParser::Switch instances which will allow
111
119
  # us to show/hide particular switch in the help message produced by
112
120
  # the OptionParser#help method on demand.
@@ -150,6 +158,10 @@ Available options are:
150
158
  "Parameters to a task or plan as json, a json file '@<file>', or on stdin '-'") do |params|
151
159
  @options[:task_options] = parse_params(params)
152
160
  end
161
+ @execute = define('-e', '--execute CODE',
162
+ "Puppet manifest code to apply to the targets") do |code|
163
+ @options[:code] = code
164
+ end.extend(SwitchHider)
153
165
 
154
166
  separator 'Authentication:'
155
167
  define('-u', '--user USER', 'User to authenticate as') do |user|
@@ -270,6 +282,8 @@ Available options are:
270
282
  def update
271
283
  # show the --nodes and --query switches by default
272
284
  @nodes.hide = @query.hide = false
285
+ # Don't show the --execute switch except for `apply`
286
+ @execute.hide = true
273
287
 
274
288
  # Update the banner according to the subcommand
275
289
  self.banner = case @options[:subcommand]
@@ -287,6 +301,9 @@ Available options are:
287
301
  FILE_HELP
288
302
  when 'puppetfile'
289
303
  PUPPETFILE_HELP
304
+ when 'apply'
305
+ @execute.hide = false
306
+ APPLY_HELP
290
307
  else
291
308
  BANNER
292
309
  end
data/lib/bolt/catalog.rb CHANGED
@@ -33,11 +33,11 @@ module Bolt
33
33
  end
34
34
  end
35
35
 
36
- def generate_ast(code)
36
+ def generate_ast(code, filename = nil)
37
37
  with_puppet_settings do
38
38
  Puppet::Pal.in_tmp_environment("bolt_parse") do |pal|
39
39
  pal.with_catalog_compiler do |compiler|
40
- ast = compiler.parse_string(code)
40
+ ast = compiler.parse_string(code, filename)
41
41
  Puppet::Pops::Serialization::ToDataConverter.convert(ast,
42
42
  rich_data: true,
43
43
  symbol_to_string: true)
data/lib/bolt/cli.rb CHANGED
@@ -9,7 +9,6 @@ require 'json'
9
9
  require 'io/console'
10
10
  require 'logging'
11
11
  require 'optparse'
12
- require 'r10k/action/puppetfile/install'
13
12
  require 'bolt/analytics'
14
13
  require 'bolt/bolt_option_parser'
15
14
  require 'bolt/config'
@@ -20,19 +19,19 @@ require 'bolt/logger'
20
19
  require 'bolt/outputter'
21
20
  require 'bolt/puppetdb'
22
21
  require 'bolt/pal'
23
- require 'bolt/r10k_log_proxy'
24
22
  require 'bolt/target'
25
23
  require 'bolt/version'
26
24
 
27
25
  module Bolt
28
26
  class CLIExit < StandardError; end
29
27
  class CLI
30
- COMMANDS = { 'command' => %w[run],
31
- 'script' => %w[run],
32
- 'task' => %w[show run],
33
- 'plan' => %w[show run],
34
- 'file' => %w[upload],
35
- 'puppetfile' => %w[install] }.freeze
28
+ COMMANDS = { 'command' => %w[run],
29
+ 'script' => %w[run],
30
+ 'task' => %w[show run],
31
+ 'plan' => %w[show run],
32
+ 'file' => %w[upload],
33
+ 'puppetfile' => %w[install],
34
+ 'apply' => %w[] }.freeze
36
35
 
37
36
  attr_reader :config, :options
38
37
 
@@ -80,7 +79,10 @@ module Bolt
80
79
 
81
80
  # This section handles parsing non-flag options which are
82
81
  # subcommand specific rather then part of the config
83
- options[:action] = remaining.shift
82
+ actions = COMMANDS[options[:subcommand]]
83
+ if actions && !actions.empty?
84
+ options[:action] = remaining.shift
85
+ end
84
86
  options[:object] = remaining.shift
85
87
 
86
88
  task_options, remaining = remaining.partition { |s| s =~ /.+=/ }
@@ -140,16 +142,18 @@ module Bolt
140
142
  "#{COMMANDS.keys.join(', ')}"
141
143
  end
142
144
 
143
- if options[:action].nil?
144
- raise Bolt::CLIError,
145
- "Expected an action of the form 'bolt #{options[:subcommand]} <action>'"
146
- end
147
-
148
145
  actions = COMMANDS[options[:subcommand]]
149
- unless actions.include?(options[:action])
150
- raise Bolt::CLIError,
151
- "Expected action '#{options[:action]}' to be one of " \
152
- "#{actions.join(', ')}"
146
+ if actions.any?
147
+ if options[:action].nil?
148
+ raise Bolt::CLIError,
149
+ "Expected an action of the form 'bolt #{options[:subcommand]} <action>'"
150
+ end
151
+
152
+ unless actions.include?(options[:action])
153
+ raise Bolt::CLIError,
154
+ "Expected action '#{options[:action]}' to be one of " \
155
+ "#{actions.join(', ')}"
156
+ end
153
157
  end
154
158
 
155
159
  if options[:subcommand] != 'file' && options[:subcommand] != 'script' &&
@@ -181,9 +185,18 @@ module Bolt
181
185
  raise Bolt::CLIError, "Only one of '--boltdir' or '--configfile' may be specified"
182
186
  end
183
187
 
184
- if options[:noop] && (options[:subcommand] != 'task' || options[:action] != 'run')
188
+ if options[:noop] &&
189
+ !(options[:subcommand] == 'task' && options[:action] == 'run') && options[:subcommand] != 'apply'
185
190
  raise Bolt::CLIError,
186
- "Option '--noop' may only be specified when running a task"
191
+ "Option '--noop' may only be specified when running a task or applying manifest code"
192
+ end
193
+
194
+ if options[:subcommand] == 'apply' && (options[:object] && options[:code])
195
+ raise Bolt::CLIError, "--execute is unsupported when specifying a manifest file"
196
+ end
197
+
198
+ if options[:subcommand] == 'apply' && (!options[:object] && !options[:code])
199
+ raise Bolt::CLIError, "a manifest file or --execute is required"
187
200
  end
188
201
  end
189
202
 
@@ -254,10 +267,17 @@ module Bolt
254
267
  options[:task_options] = pal.parse_params(options[:subcommand], options[:object], options[:task_options])
255
268
  end
256
269
 
257
- if options[:subcommand] == 'plan'
270
+ case options[:subcommand]
271
+ when 'plan'
258
272
  code = run_plan(options[:object], options[:task_options], options[:nodes], options)
259
- elsif options[:subcommand] == 'puppetfile'
273
+ when 'puppetfile'
260
274
  code = install_puppetfile(@config.puppetfile, @config.modulepath)
275
+ when 'apply'
276
+ if options[:object]
277
+ validate_file('manifest', options[:object])
278
+ options[:code] = File.read(File.expand_path(options[:object]))
279
+ end
280
+ code = apply_manifest(options[:code], options[:targets], options[:object], options[:noop])
261
281
  else
262
282
  executor = Bolt::Executor.new(config.concurrency, @analytics, options[:noop], bundled_content: bundled_content)
263
283
  targets = options[:targets]
@@ -364,7 +384,30 @@ module Bolt
364
384
  result.ok? ? 0 : 1
365
385
  end
366
386
 
387
+ def apply_manifest(code, targets, filename = nil, noop = false)
388
+ ast = pal.parse_manifest(code, filename)
389
+
390
+ executor = Bolt::Executor.new(config.concurrency, @analytics, noop, bundled_content: bundled_content)
391
+ # Call start_plan just to enable plan_logging
392
+ executor.start_plan(nil)
393
+
394
+ pal.in_plan_compiler(executor, inventory, puppetdb_client) do |compiler|
395
+ compiler.call_function('apply_prep', targets)
396
+ end
397
+
398
+ results = pal.with_bolt_executor(executor, inventory, puppetdb_client) do
399
+ Puppet.lookup(:apply_executor).apply_ast(ast, targets, '_catch_errors' => true, '_noop' => noop)
400
+ end
401
+
402
+ outputter.print_apply_result(results)
403
+
404
+ results.ok ? 0 : 1
405
+ end
406
+
367
407
  def install_puppetfile(puppetfile, modulepath)
408
+ require 'r10k/action/puppetfile/install'
409
+ require 'bolt/r10k_log_proxy'
410
+
368
411
  if puppetfile.exist?
369
412
  moduledir = modulepath.first.to_s
370
413
  r10k_config = {
data/lib/bolt/error.rb CHANGED
@@ -17,7 +17,7 @@ module Bolt
17
17
  end
18
18
 
19
19
  def to_h
20
- h = { 'kind' => kind,
20
+ h = { 'kind' => kind,
21
21
  'msg' => message,
22
22
  'details' => details }
23
23
  h['issue_code'] = issue_code if issue_code
@@ -53,7 +53,7 @@ module Bolt
53
53
  def initialize(result_set, action, object)
54
54
  details = {
55
55
  'action' => action,
56
- 'object' => object,
56
+ 'object' => object,
57
57
  'result_set' => result_set
58
58
  }
59
59
  message = "Plan aborted: #{action} '#{object}' failed on #{result_set.error_set.length} nodes"
@@ -118,19 +118,19 @@ module Bolt
118
118
  end
119
119
 
120
120
  def group_data
121
- { 'config' => @config,
122
- 'vars' => @vars,
123
- 'facts' => @facts,
121
+ { 'config' => @config,
122
+ 'vars' => @vars,
123
+ 'facts' => @facts,
124
124
  'features' => @features,
125
- 'groups' => [@name] }
125
+ 'groups' => [@name] }
126
126
  end
127
127
 
128
128
  def empty_data
129
- { 'config' => {},
130
- 'vars' => {},
131
- 'facts' => {},
129
+ { 'config' => {},
130
+ 'vars' => {},
131
+ 'facts' => {},
132
132
  'features' => [],
133
- 'groups' => [] }
133
+ 'groups' => [] }
134
134
  end
135
135
 
136
136
  def data_merge(data1, data2)
@@ -143,8 +143,8 @@ module Bolt
143
143
  # Shallow merge instead of deep merge so that vars with a hash value
144
144
  # are assigned a new hash, rather than merging the existing value
145
145
  # with the value meant to replace it
146
- 'vars' => data1['vars'].merge(data2['vars']),
147
- 'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
146
+ 'vars' => data1['vars'].merge(data2['vars']),
147
+ 'facts' => Bolt::Util.deep_merge(data1['facts'], data2['facts']),
148
148
  'features' => data1['features'] | data2['features'],
149
149
  'groups' => data2['groups'] + data1['groups']
150
150
  }
@@ -177,6 +177,12 @@ module Bolt
177
177
  @stream.puts(plan_info)
178
178
  end
179
179
 
180
+ # @param [Bolt::ResultSet] apply_result A ResultSet object representing the result of a `bolt apply`
181
+ def print_apply_result(apply_result)
182
+ apply_result.each { |result| print_result(result) }
183
+ print_summary(apply_result)
184
+ end
185
+
180
186
  # @param [Bolt::PlanResult] plan_result A PlanResult object
181
187
  def print_plan_result(plan_result)
182
188
  value = plan_result.value
@@ -63,6 +63,10 @@ module Bolt
63
63
  @stream.puts plan.to_json
64
64
  end
65
65
 
66
+ def print_apply_result(apply_result)
67
+ @stream.puts apply_result.to_json
68
+ end
69
+
66
70
  def print_plan_result(result)
67
71
  # Ruby JSON patches most objects to have a to_json method.
68
72
  @stream.puts result.to_json
data/lib/bolt/pal.rb CHANGED
@@ -57,6 +57,7 @@ module Bolt
57
57
 
58
58
  # Puppet logging is global so this is class method to avoid confusion
59
59
  def self.configure_logging
60
+ Puppet::Util::Log.destinations.clear
60
61
  Puppet::Util::Log.newdestination(Logging.logger['Puppet'])
61
62
  # Defer all log level decisions to the Logging library by telling Puppet
62
63
  # to log everything
@@ -176,6 +177,15 @@ module Bolt
176
177
  end
177
178
  end
178
179
 
180
+ # Parses a snippet of Puppet manifest code and returns the AST represented
181
+ # in JSON.
182
+ def parse_manifest(code, filename)
183
+ raw_ast = Puppet::Pops::Parser::EvaluatingParser.new.parse_string(code, filename)
184
+ Puppet::Pops::Serialization::ToDataConverter.convert(raw_ast, rich_data: true, symbol_to_string: true)
185
+ rescue Puppet::Error => e
186
+ raise Bolt::PAL::PALError, "Failed to parse manifest: #{e}"
187
+ end
188
+
179
189
  def list_tasks
180
190
  in_bolt_compiler do |compiler|
181
191
  tasks = compiler.list_tasks
data/lib/bolt/target.rb CHANGED
@@ -8,6 +8,8 @@ module Bolt
8
8
  attr_reader :uri, :options
9
9
  attr_writer :inventory
10
10
 
11
+ PRINT_OPTS ||= %w[host user port protocol].freeze
12
+
11
13
  # Satisfies the Puppet datatypes API
12
14
  def self.from_asserted_hash(hash)
13
15
  new(hash['uri'], hash['options'])
@@ -79,7 +81,8 @@ module Bolt
79
81
  end
80
82
 
81
83
  def to_s
82
- "Target('#{@uri}', #{@options})"
84
+ opts = @options.select { |k, _| PRINT_OPTS.include? k }
85
+ "Target('#{@uri}', #{opts})"
83
86
  end
84
87
 
85
88
  def host
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bolt/task'
4
+
5
+ module Bolt
6
+ class Task
7
+ class PuppetServer < Bolt::Task
8
+ def initialize(task_data, file_cache)
9
+ @file_cache = file_cache
10
+ task_data = update_file_data(task_data)
11
+ super(task_data)
12
+ end
13
+
14
+ # puppetserver file entries have 'filename' rather then 'name'
15
+ def update_file_data(task_data)
16
+ task_data['files'].each { |f| f['name'] = f['filename'] }
17
+ task_data
18
+ end
19
+
20
+ # Compute local path and download files from puppetserver as needed
21
+ def file_path(file_name)
22
+ file = file_map[file_name]
23
+ file['path'] ||= @file_cache.update_file(file)
24
+ end
25
+ end
26
+ end
27
+ end
data/lib/bolt/task.rb CHANGED
@@ -32,26 +32,39 @@ module Bolt
32
32
  metadata['supports_noop']
33
33
  end
34
34
 
35
+ def module_name
36
+ name.split('::').first
37
+ end
38
+
39
+ def tasks_dir
40
+ File.join(module_name, 'tasks')
41
+ end
42
+
35
43
  def file_map
36
- @file_map ||= files.each_with_object({}) { |file, hsh| hsh[file['name']] = file['path'] }
44
+ @file_map ||= files.each_with_object({}) { |file, hsh| hsh[file['name']] = file }
37
45
  end
38
46
  private :file_map
39
47
 
48
+ # This provides a method we can override in subclasses if the 'path' needs
49
+ # to be fetched or computed.
50
+ def file_path(file_name)
51
+ file_map[file_name]['path']
52
+ end
53
+
40
54
  # Returns a hash of implementation name, path to executable, input method (if defined),
41
55
  # and any additional files (name and path)
42
56
  def select_implementation(target, additional_features = [])
43
- raise 'select_implementation only supported with multiple files' if files.nil? || files.empty?
44
-
45
57
  impl = if (impls = metadata['implementations'])
46
58
  available_features = target.features + additional_features
47
59
  impl = impls.find { |imp| Set.new(imp['requirements']).subset?(available_features) }
48
60
  raise "No suitable implementation of #{name} for #{target.name}" unless impl
49
61
  impl = impl.dup
50
- impl['path'] = file_map[impl['name']]
62
+ impl['path'] = file_path(impl['name'])
51
63
  impl.delete('requirements')
52
64
  impl
53
65
  else
54
- files.first.dup
66
+ name = files.first['name']
67
+ { 'name' => name, 'path' => file_path(name) }
55
68
  end
56
69
 
57
70
  inmethod = impl['input_method'] || metadata['input_method']
@@ -60,15 +73,16 @@ module Bolt
60
73
  mfiles = impl.fetch('files', []) + metadata.fetch('files', [])
61
74
  dirnames, filenames = mfiles.partition { |file| file.end_with?('/') }
62
75
  impl['files'] = filenames.map do |file|
63
- path = file_map[file]
76
+ path = file_path(file)
64
77
  raise "No file found for reference #{file}" if path.nil?
65
78
  { 'name' => file, 'path' => path }
66
79
  end
67
80
 
68
81
  unless dirnames.empty?
69
82
  files.each do |file|
70
- if dirnames.any? { |dirname| file['name'].start_with?(dirname) }
71
- impl['files'] << file
83
+ name = file['name']
84
+ if dirnames.any? { |dirname| name.start_with?(dirname) }
85
+ impl['files'] << { 'name' => name, 'path' => file_path(name) }
72
86
  end
73
87
  end
74
88
  end
@@ -162,10 +162,6 @@ module Bolt
162
162
  targets.map { |target| [target] }
163
163
  end
164
164
 
165
- def from_api?(task)
166
- !task.file.nil?
167
- end
168
-
169
165
  # Transports should override this method with their own implementation of running a command.
170
166
  def run_command(*_args)
171
167
  raise NotImplementedError, "run_command() must be implemented by the transport class"
@@ -85,17 +85,25 @@ module Bolt
85
85
  input_method = implementation['input_method'] || 'both'
86
86
  extra_files = implementation['files']
87
87
 
88
- with_tmpscript(executable, target.options['tmpdir']) do |script, dir|
89
- unless extra_files.empty?
90
- installdir = File.join(dir, '_installdir')
91
- arguments['_installdir'] = installdir
92
- FileUtils.mkdir_p(extra_files.map { |file| File.join(installdir, File.dirname(file['name'])) })
93
-
88
+ in_tmpdir(target.options['tmpdir']) do |dir|
89
+ if extra_files.empty?
90
+ script = File.join(dir, File.basename(executable))
91
+ else
92
+ arguments['_installdir'] = dir
93
+ script_dest = File.join(dir, task.tasks_dir)
94
+ FileUtils.mkdir_p([script_dest] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
95
+
96
+ script = File.join(script_dest, File.basename(executable))
94
97
  extra_files.each do |file|
95
- copy_file(file['path'], File.join(installdir, file['name']))
98
+ dest = File.join(dir, file['name'])
99
+ copy_file(file['path'], dest)
100
+ File.chmod(0o750, dest)
96
101
  end
97
102
  end
98
103
 
104
+ copy_file(executable, script)
105
+ File.chmod(0o750, script)
106
+
99
107
  # unpack any Sensitive data, write it to a separate variable because
100
108
  # we log 'arguments' below
101
109
  unwrapped_arguments = unwrap_sensitive_args(arguments)
@@ -122,17 +122,10 @@ module Bolt
122
122
  end
123
123
 
124
124
  def run_task(target, task, arguments, options = {})
125
- if from_api?(task)
126
- executable = task.file['filename']
127
- file_content = Base64.decode64(task.file['file_content'])
128
- input_method = task.metadata['input_method']
129
- extra_files = []
130
- else
131
- implementation = task.select_implementation(target, PROVIDED_FEATURES)
132
- executable = implementation['path']
133
- input_method = implementation['input_method']
134
- extra_files = implementation['files']
135
- end
125
+ implementation = task.select_implementation(target, PROVIDED_FEATURES)
126
+ executable = implementation['path']
127
+ input_method = implementation['input_method']
128
+ extra_files = implementation['files']
136
129
  input_method ||= 'both'
137
130
 
138
131
  # unpack any Sensitive data
@@ -144,22 +137,20 @@ module Bolt
144
137
  execute_options = {}
145
138
 
146
139
  conn.with_remote_tempdir do |dir|
147
- remote_task_path = if from_api?(task)
148
- conn.write_executable_from_content(dir, file_content, executable)
149
- else
150
- conn.write_remote_executable(dir, executable)
151
- end
152
-
153
- unless extra_files.empty?
140
+ if extra_files.empty?
141
+ task_dir = dir
142
+ else
154
143
  # TODO: optimize upload of directories
155
- installdir = File.join(dir.to_s, '_installdir')
156
- arguments['_installdir'] = installdir
157
- dir.mkdirs(extra_files.map { |file| File.join('_installdir', File.dirname(file['name'])) })
144
+ arguments['_installdir'] = dir.to_s
145
+ task_dir = File.join(dir.to_s, task.tasks_dir)
146
+ dir.mkdirs([task.tasks_dir] + extra_files.map { |file| File.dirname(file['name']) })
158
147
  extra_files.each do |file|
159
- conn.write_remote_file(file['path'], File.join(installdir, file['name']))
148
+ conn.write_remote_file(file['path'], File.join(dir.to_s, file['name']))
160
149
  end
161
150
  end
162
151
 
152
+ remote_task_path = conn.write_remote_executable(task_dir, executable)
153
+
163
154
  if STDIN_METHODS.include?(input_method)
164
155
  stdin = JSON.dump(arguments)
165
156
  end
@@ -108,39 +108,30 @@ catch
108
108
  end
109
109
 
110
110
  def run_task(target, task, arguments, _options = {})
111
- if from_api?(task)
112
- executable = task.file['filename']
113
- file_content = StringIO.new(Base64.decode64(task.file['file_content']))
114
- input_method = task.metadata['input_method']
115
- extra_files = []
116
- else
117
- implementation = task.select_implementation(target, PROVIDED_FEATURES)
118
- executable = implementation['path']
119
- input_method = implementation['input_method']
120
- extra_files = implementation['files']
121
- end
111
+ implementation = task.select_implementation(target, PROVIDED_FEATURES)
112
+ executable = implementation['path']
113
+ input_method = implementation['input_method']
114
+ extra_files = implementation['files']
122
115
  input_method ||= powershell_file?(executable) ? 'powershell' : 'both'
123
116
 
124
117
  # unpack any Sensitive data
125
118
  arguments = unwrap_sensitive_args(arguments)
126
119
  with_connection(target) do |conn|
127
120
  conn.with_remote_tempdir do |dir|
128
- remote_task_path = if from_api?(task)
129
- conn.write_executable_from_content(dir, file_content, executable)
130
- else
131
- conn.write_remote_executable(dir, executable)
132
- end
133
-
134
- unless extra_files.empty?
121
+ if extra_files.empty?
122
+ task_dir = dir
123
+ else
135
124
  # TODO: optimize upload of directories
136
- installdir = File.join(dir, '_installdir')
137
- arguments['_installdir'] = installdir
138
- conn.mkdirs(extra_files.map { |file| File.join(installdir, File.dirname(file['name'])) })
125
+ arguments['_installdir'] = dir
126
+ task_dir = File.join(dir, task.tasks_dir)
127
+ conn.mkdirs([task_dir] + extra_files.map { |file| File.join(dir, File.dirname(file['name'])) })
139
128
  extra_files.each do |file|
140
- conn.write_remote_file(file['path'], File.join(installdir, file['name']))
129
+ conn.write_remote_file(file['path'], File.join(dir, file['name']))
141
130
  end
142
131
  end
143
132
 
133
+ remote_task_path = conn.write_remote_executable(task_dir, executable)
134
+
144
135
  if STDIN_METHODS.include?(input_method)
145
136
  stdin = JSON.dump(arguments)
146
137
  end
data/lib/bolt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Bolt
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/auth/rack'
4
+
5
+ module BoltServer
6
+ class ACL < Rails::Auth::ErrorPage::Middleware
7
+ class X509Matcher
8
+ def initialize(options)
9
+ @options = options.freeze
10
+ end
11
+
12
+ def match(env)
13
+ certificate = Rails::Auth::X509::Certificate.new(env['puma.peercert'])
14
+ # This can be extended fairly easily to search OpenSSL::X509::Certificate#extensions for subjectAltNames.
15
+ @options.all? { |name, value| certificate[name] == value }
16
+ end
17
+ end
18
+
19
+ def initialize(app, whitelist)
20
+ acls = []
21
+ whitelist.each do |entry|
22
+ acls << {
23
+ 'resources' => [
24
+ {
25
+ 'method' => 'ALL',
26
+ 'path' => '/.*'
27
+ }
28
+ ],
29
+ 'allow_x509_subject' => {
30
+ 'cn' => entry
31
+ }
32
+ }
33
+ end
34
+ acl = Rails::Auth::ACL.new(acls, matchers: { allow_x509_subject: X509Matcher })
35
+ mid = Rails::Auth::ACL::Middleware.new(app, acl: acl)
36
+ super(mid, page_body: 'Access denied')
37
+ end
38
+ end
39
+ end