bolt 1.29.0 → 1.29.1

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: 8594a6349cae9dc7c45446a40f0f15c7093a2b706361f48a938b6a21471a7ae4
4
- data.tar.gz: 0a4954a8df306277da739d1ae33849719f0071db2ef9ba337ca3a7e045a2861f
3
+ metadata.gz: 319eb2a2f42911eed9c4a7e9aa8e9bd351146f7ab3cdf69b34c9e364a2fbf574
4
+ data.tar.gz: ad22b5b96fc923b68d7109fdeba19f4a1046b327d0dcb0a1934bfc7a774425f6
5
5
  SHA512:
6
- metadata.gz: 63d35051226313b40df680787dd026922fc829480e5cc7e73e34f882ab494a0a7bfb746a2cb561c34ad63f35d382226fd165c45783c2249f23e15763065ba07d
7
- data.tar.gz: 9702e0fe3b12e5f7b7b5531d4dee87a5bd12a39070946c7ea9da678aa8db5d018195c34a1b7f4109c470ef34d98ec6831167cd254c50375df89a8d6dd0a51cb0
6
+ metadata.gz: a2e9e0277e1b7ee2cb7eb86120250e62a2ef91068893e7d86e7fb8ce542b3eaf016f0f7ca8a8245202a6c1c64a4a3d1af82c6e10fd826d5bec8df0f9f42c8d65
7
+ data.tar.gz: 64d7965e9e75abee048a0c251bcaaf2ce6f9a592ac86bc8f73fc330dba77ff847acaf4574ebdd194c6747d90b3d8496ae85c34658d3c0831c85befbe3a8b0aa9
@@ -8,7 +8,7 @@ module Bolt
8
8
  module Transport
9
9
  class Docker < Base
10
10
  def self.options
11
- %w[host service-url service-options tmpdir interpreters shell-command tty]
11
+ %w[host service-url tmpdir interpreters shell-command tty]
12
12
  end
13
13
 
14
14
  def provided_features
@@ -21,12 +21,6 @@ module Bolt
21
21
  raise Bolt::ValidationError, 'service-url must be a string'
22
22
  end
23
23
  end
24
-
25
- if (opts = options['service-options'])
26
- unless opts.instance_of?(Hash)
27
- raise Bolt::ValidationError, 'service-options must be a hash'
28
- end
29
- end
30
24
  end
31
25
 
32
26
  def with_connection(target)
@@ -57,9 +51,8 @@ module Bolt
57
51
  end
58
52
 
59
53
  def run_command(target, command, options = {})
60
- if target.options['tty']
61
- options[:Tty] = true
62
- end
54
+ options[:tty] = target.options['tty']
55
+
63
56
  if target.options['shell-command'] && !target.options['shell-command'].empty?
64
57
  # escape any double quotes in command
65
58
  command = command.gsub('"', '\"')
@@ -8,21 +8,24 @@ module Bolt
8
8
  class Docker < Base
9
9
  class Connection
10
10
  def initialize(target)
11
- # lazy-load expensive gem code
12
- require 'docker'
13
-
14
11
  raise Bolt::ValidationError, "Target #{target.name} does not have a host" unless target.host
15
-
16
12
  @target = target
17
13
  @logger = Logging.logger[target.host]
14
+ @docker_host = @target.options['service-url']
18
15
  end
19
16
 
20
17
  def connect
21
- # Explicitly create the new Connection to avoid relying on global state in the Docker module.
22
- url = @target.options['service-url'] || ::Docker.url
23
- options = ::Docker.options.merge(@target.options['service-options'] || {})
24
- @container = ::Docker::Container.get(@target.host, {}, ::Docker::Connection.new(url, options))
18
+ # We don't actually have a connection, but we do need to
19
+ # check that the container exists and is running.
20
+ output = execute_local_docker_json_command('ps')
21
+ index = output.find_index { |item| item["ID"] == @target.host || item["Names"] == @target.host }
22
+ raise "Could not find a container with name or ID matching '#{@target.host}'" if index.nil?
23
+ # Now find the indepth container information
24
+ output = execute_local_docker_json_command('inspect', [output[index]["ID"]])
25
+ # Store the container information for later
26
+ @container_info = output[0]
25
27
  @logger.debug { "Opened session" }
28
+ true
26
29
  rescue StandardError => e
27
30
  raise Bolt::Node::ConnectError.new(
28
31
  "Failed to connect to #{@target.uri}: #{e.message}",
@@ -30,38 +33,61 @@ module Bolt
30
33
  )
31
34
  end
32
35
 
36
+ # Executes a command inside the target container
37
+ #
38
+ # @param command [Array] The command to run, expressed as an array of strings
39
+ # @param options [Hash] command specific options
40
+ # @option opts [String] :interpreter statements that are prefixed to the command e.g `/bin/bash` or `cmd.exe /c`
41
+ # @option opts [Hash] :environment A hash of environment variables that will be injected into the command
42
+ # @option opts [IO] :stdin An IO object that will be used to redirect STDIN for the docker command
33
43
  def execute(*command, options)
34
44
  command.unshift(options[:interpreter]) if options[:interpreter]
45
+ # Build the `--env` parameters
46
+ envs = []
35
47
  if options[:environment]
36
- envs = options[:environment].map { |env, val| "#{env}=#{val}" }
37
- command = ['env'] + envs + command
48
+ options[:environment].each { |env, val| envs.concat(['--env', "#{env}=#{val}"]) }
38
49
  end
39
50
 
40
- @logger.debug { "Executing: #{command}" }
41
- result = @container.exec(command, options) { |stream, chunk| @logger.debug("#{stream}: #{chunk}") }
42
- if result[2] == 0
51
+ command_options = []
52
+ # Need to be interactive if redirecting STDIN
53
+ command_options << '--interactive' unless options[:stdin].nil?
54
+ command_options << '--tty' if options[:tty]
55
+ command_options.concat(envs) unless envs.empty?
56
+ command_options << container_id
57
+ command_options.concat(command)
58
+
59
+ @logger.debug { "Executing: exec #{command_options}" }
60
+
61
+ stdout_str, stderr_str, status = execute_local_docker_command('exec', command_options, options[:stdin])
62
+
63
+ # The actual result is the exitstatus not the process object
64
+ status = status.nil? ? -32768 : status.exitstatus
65
+ if status == 0
43
66
  @logger.debug { "Command returned successfully" }
44
67
  else
45
- @logger.info { "Command failed with exit code #{result[2]}" }
68
+ @logger.info { "Command failed with exit code #{status}" }
46
69
  end
47
- result[0] = result[0].join.force_encoding('UTF-8')
48
- result[1] = result[1].join.force_encoding('UTF-8')
49
- result
70
+ stdout_str.force_encoding(Encoding::UTF_8)
71
+ stderr_str.force_encoding(Encoding::UTF_8)
72
+ # Normalise line endings
73
+ stdout_str.gsub!("\r\n", "\n")
74
+ stderr_str.gsub!("\r\n", "\n")
75
+ [stdout_str, stderr_str, status]
50
76
  rescue StandardError
51
77
  @logger.debug { "Command aborted" }
52
78
  raise
53
79
  end
54
80
 
55
81
  def write_remote_file(source, destination)
56
- @container.store_file(destination, File.binread(source))
82
+ _, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
83
+ raise "Error writing file to container #{@container_id}: #{stdout_str}" unless status.exitstatus.zero?
57
84
  rescue StandardError => e
58
85
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
59
86
  end
60
87
 
61
88
  def write_remote_directory(source, destination)
62
- tar = ::Docker::Util.create_dir_tar(source)
63
- mkdirs([destination])
64
- @container.archive_in_stream(destination) { tar.read(Excon.defaults[:chunk_size]).to_s }
89
+ _, stdout_str, status = execute_local_docker_command('cp', [source, "#{container_id}:#{destination}"])
90
+ raise "Error writing directory to container #{@container_id}: #{stdout_str}" unless status.exitstatus.zero?
65
91
  rescue StandardError => e
66
92
  raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR')
67
93
  end
@@ -75,7 +101,7 @@ module Bolt
75
101
  end
76
102
 
77
103
  def make_tempdir
78
- tmpdir = @target.options.fetch('tmpdir', '/tmp')
104
+ tmpdir = @target.options.fetch('tmpdir', container_tmpdir)
79
105
  tmppath = "#{tmpdir}/#{SecureRandom.uuid}"
80
106
 
81
107
  stdout, stderr, exitcode = execute('mkdir', '-m', '700', tmppath, {})
@@ -112,6 +138,70 @@ module Bolt
112
138
  raise Bolt::Node::FileError.new(message, 'CHMOD_ERROR')
113
139
  end
114
140
  end
141
+
142
+ private
143
+
144
+ # Converts the JSON encoded STDOUT string from the docker cli into ruby objects
145
+ #
146
+ # @param stdout_string [String] The string to convert
147
+ # @return [Object] Ruby object representation of the JSON string
148
+ def extract_json(stdout_string)
149
+ # The output from the docker format command is a JSON string per line.
150
+ # We can't do a direct convert but this helper method will convert it into
151
+ # an array of Objects
152
+ stdout_string.split("\n")
153
+ .reject { |str| str.strip.empty? }
154
+ .map { |str| JSON.parse(str) }
155
+ end
156
+
157
+ # rubocop:disable Metrics/LineLength
158
+ # Executes a Docker CLI command
159
+ #
160
+ # @param subcommand [String] The docker subcommand to run e.g. 'inspect' for `docker inspect`
161
+ # @param command_options [Array] Additional command options e.g. ['--size'] for `docker inspect --size`
162
+ # @param redir_stdin [IO] IO object which will be use to as STDIN in the docker command. Default is nil, which does not perform redirection
163
+ # @return [String, String, Process::Status] The output of the command: STDOUT, STDERR, Process Status
164
+ # rubocop:enable Metrics/LineLength
165
+ def execute_local_docker_command(subcommand, command_options = [], redir_stdin = nil)
166
+ env_hash = {}
167
+ # Set the DOCKER_HOST if we are using a non-default service-url
168
+ env_hash['DOCKER_HOST'] = @docker_host unless @docker_host.nil?
169
+
170
+ command_options = [] if command_options.nil?
171
+ docker_command = [subcommand].concat(command_options)
172
+
173
+ # Always use binary mode for any text data
174
+ capture_options = { binmode: true }
175
+ capture_options[:stdin_data] = redir_stdin unless redir_stdin.nil?
176
+ stdout_str, stderr_str, status = Open3.capture3(env_hash, 'docker', *docker_command, capture_options)
177
+ [stdout_str, stderr_str, status]
178
+ end
179
+
180
+ # Executes a Docker CLI command and parses the output in JSON format
181
+ #
182
+ # @param subcommand [String] The docker subcommand to run e.g. 'inspect' for `docker inspect`
183
+ # @param command_options [Array] Additional command options e.g. ['--size'] for `docker inspect --size`
184
+ # @return [Object] Ruby object representation of the JSON string
185
+ def execute_local_docker_json_command(subcommand, command_options = [])
186
+ command_options = [] if command_options.nil?
187
+ command_options = ['--format', '{{json .}}'].concat(command_options)
188
+ stdout_str, _stderr_str, _status = execute_local_docker_command(subcommand, command_options)
189
+ extract_json(stdout_str)
190
+ end
191
+
192
+ # The full ID of the target container
193
+ #
194
+ # @return [String] The full ID of the target container
195
+ def container_id
196
+ @container_info["Id"]
197
+ end
198
+
199
+ # The temp path inside the target container
200
+ #
201
+ # @return [String] The absolute path to the temp directory
202
+ def container_tmpdir
203
+ '/tmp'
204
+ end
115
205
  end
116
206
  end
117
207
  end
@@ -20,17 +20,18 @@ module Bolt
20
20
  @user = ENV['USER'] || Etc.getlogin
21
21
  @run_as = target.options['run-as']
22
22
  @logger = Logging.logger[self]
23
+ @sudo_id = SecureRandom.uuid
23
24
  end
24
25
 
25
26
  # If prompted for sudo password, send password to stdin and return an
26
27
  # empty string. Otherwise, check for sudo errors and raise Bolt error.
28
+ # If sudo_id is detected, that means the task needs to have stdin written.
27
29
  # If error is not sudo-related, return the stderr string to be added to
28
30
  # node output
29
- def handle_sudo(stdin, err, pid)
31
+ def handle_sudo(stdin, err, pid, sudo_stdin)
30
32
  if err.include?(Sudoable.sudo_prompt)
31
33
  # A wild sudo prompt has appeared!
32
34
  if @target.options['sudo-password']
33
- # Hopefully no one's sudo-password is > 64kb
34
35
  stdin.write("#{@target.options['sudo-password']}\n")
35
36
  ''
36
37
  else
@@ -39,6 +40,12 @@ module Bolt
39
40
  'NO_PASSWORD'
40
41
  )
41
42
  end
43
+ elsif err =~ /^#{@sudo_id}/
44
+ if sudo_stdin
45
+ stdin.write("#{sudo_stdin}\n")
46
+ stdin.close
47
+ end
48
+ ''
42
49
  else
43
50
  handle_sudo_errors(err, pid)
44
51
  end
@@ -96,12 +103,12 @@ module Bolt
96
103
 
97
104
  # See if there's a sudo prompt in the output
98
105
  # If not, return the output
99
- def check_sudo(out, inp, pid)
106
+ def check_sudo(out, inp, pid, stdin)
100
107
  buffer = out.readpartial(CHUNK_SIZE)
101
108
  # Split on newlines, including the newline
102
109
  lines = buffer.split(/(?<=[\n])/)
103
110
  # handle_sudo will return the line if it is not a sudo prompt or error
104
- lines.map! { |line| handle_sudo(inp, line, pid) }
111
+ lines.map! { |line| handle_sudo(inp, line, pid, stdin) }
105
112
  lines.join("")
106
113
  # If stream has reached EOF, no password prompt is expected
107
114
  # return an empty string
@@ -114,33 +121,25 @@ module Bolt
114
121
  escalate = sudoable && run_as && @user != run_as
115
122
  use_sudo = escalate && @target.options['run-as-command'].nil?
116
123
 
117
- if options[:interpreter]
118
- if command.is_a?(Array)
119
- command.unshift(options[:interpreter])
120
- else
121
- command = [options[:interpreter], command]
122
- end
123
- end
124
-
125
- command_str = command.is_a?(String) ? command : Shellwords.shelljoin(command)
124
+ command_str = inject_interpreter(options[:interpreter], command)
126
125
 
127
126
  if escalate
128
127
  if use_sudo
129
128
  sudo_flags = ["sudo", "-k", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
130
129
  sudo_flags += ["-E"] if options[:environment]
131
130
  sudo_str = Shellwords.shelljoin(sudo_flags)
132
- command_str = "#{sudo_str} #{command_str}"
133
131
  else
134
- run_as_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
135
- command_str = "#{run_as_str} #{command_str}"
132
+ sudo_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
136
133
  end
134
+ command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options)
137
135
  end
138
136
 
139
137
  command_arr = options[:environment].nil? ? [command_str] : [options[:environment], command_str]
140
138
 
141
139
  # Prepare the variables!
142
140
  result_output = Bolt::Node::Output.new
143
- in_buffer = options[:stdin] || ''
141
+ # Sudo handler will pass stdin if needed.
142
+ in_buffer = !use_sudo && options[:stdin] ? options[:stdin] : ''
144
143
  # Chunks of this size will be read in one iteration
145
144
  index = 0
146
145
  timeout = 0.1
@@ -153,7 +152,7 @@ module Bolt
153
152
  # See if there's a sudo prompt
154
153
  if use_sudo
155
154
  ready_read = select([err], nil, nil, timeout * 5)
156
- read_streams[err] << check_sudo(err, inp, t.pid) if ready_read
155
+ read_streams[err] << check_sudo(err, inp, t.pid, options[:stdin]) if ready_read
157
156
  end
158
157
 
159
158
  # True while the process is running or waiting for IO input
@@ -166,7 +165,7 @@ module Bolt
166
165
  begin
167
166
  # Check for sudo prompt
168
167
  read_streams[stream] << if use_sudo
169
- check_sudo(stream, inp, t.pid)
168
+ check_sudo(stream, inp, t.pid, options[:stdin])
170
169
  else
171
170
  stream.readpartial(CHUNK_SIZE)
172
171
  end
@@ -201,5 +201,3 @@ module Bolt
201
201
  end
202
202
  end
203
203
  end
204
-
205
- require 'bolt/transport/local/shell'
@@ -20,6 +20,7 @@ module Bolt
20
20
  require 'net/ssh/proxy/jump'
21
21
 
22
22
  raise Bolt::ValidationError, "Target #{target.name} does not have a host" unless target.host
23
+ @sudo_id = SecureRandom.uuid
23
24
 
24
25
  @target = target
25
26
  @load_config = target.options['load-config']
@@ -139,10 +140,10 @@ module Bolt
139
140
  end
140
141
  end
141
142
 
142
- def handled_sudo(channel, data)
143
+ def handled_sudo(channel, data, stdin)
143
144
  if data.lines.include?(Sudoable.sudo_prompt)
144
145
  if target.options['sudo-password']
145
- channel.send_data "#{target.options['sudo-password']}\n"
146
+ channel.send_data("#{target.options['sudo-password']}\n")
146
147
  channel.wait
147
148
  return true
148
149
  else
@@ -153,6 +154,12 @@ module Bolt
153
154
  'NO_PASSWORD'
154
155
  )
155
156
  end
157
+ elsif data =~ /^#{@sudo_id}/
158
+ if stdin
159
+ channel.send_data(stdin)
160
+ channel.eof!
161
+ end
162
+ return true
156
163
  elsif data =~ /^#{@user} is not in the sudoers file\./
157
164
  @logger.debug { data }
158
165
  raise Bolt::Node::EscalateError.new(
@@ -175,21 +182,17 @@ module Bolt
175
182
  escalate = sudoable && run_as && @user != run_as
176
183
  use_sudo = escalate && @target.options['run-as-command'].nil?
177
184
 
178
- if options[:interpreter]
179
- command.is_a?(Array) ? command.unshift(options[:interpreter]) : [options[:interpreter], command]
180
- end
185
+ command_str = inject_interpreter(options[:interpreter], command)
181
186
 
182
- command_str = command.is_a?(String) ? command : Shellwords.shelljoin(command)
183
187
  if escalate
184
188
  if use_sudo
185
189
  sudo_flags = ["sudo", "-S", "-u", run_as, "-p", Sudoable.sudo_prompt]
186
190
  sudo_flags += ["-E"] if options[:environment]
187
191
  sudo_str = Shellwords.shelljoin(sudo_flags)
188
- command_str = "#{sudo_str} #{command_str}"
189
192
  else
190
- run_as_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
191
- command_str = "#{run_as_str} #{command_str}"
193
+ sudo_str = Shellwords.shelljoin(@target.options['run-as-command'] + [run_as])
192
194
  end
195
+ command_str = build_sudoable_command_str(command_str, sudo_str, @sudo_id, options)
193
196
  end
194
197
 
195
198
  # Including the environment declarations in the shelljoin will escape
@@ -216,14 +219,14 @@ module Bolt
216
219
  end
217
220
 
218
221
  channel.on_data do |_, data|
219
- unless use_sudo && handled_sudo(channel, data)
222
+ unless use_sudo && handled_sudo(channel, data, options[:stdin])
220
223
  result_output.stdout << data
221
224
  end
222
225
  @logger.debug { "stdout: #{data.strip}" }
223
226
  end
224
227
 
225
228
  channel.on_extended_data do |_, _, data|
226
- unless use_sudo && handled_sudo(channel, data)
229
+ unless use_sudo && handled_sudo(channel, data, options[:stdin])
227
230
  result_output.stderr << data
228
231
  end
229
232
  @logger.debug { "stderr: #{data.strip}" }
@@ -232,8 +235,8 @@ module Bolt
232
235
  channel.on_request("exit-status") do |_, data|
233
236
  result_output.exit_code = data.read_long
234
237
  end
235
-
236
- if options[:stdin]
238
+ # A wrapper is used to direct stdin when elevating privilage or using tty
239
+ if options[:stdin] && !use_sudo && !options[:wrapper]
237
240
  channel.send_data(options[:stdin])
238
241
  channel.eof!
239
242
  end
@@ -79,7 +79,6 @@ module Bolt
79
79
  with_connection(target) do |conn|
80
80
  conn.running_as(options['_run_as']) do
81
81
  stdin, output = nil
82
- command = []
83
82
  execute_options = {}
84
83
  execute_options[:interpreter] = select_interpreter(executable, target.options['interpreters'])
85
84
  interpreter_debug = if execute_options[:interpreter]
@@ -103,8 +102,6 @@ module Bolt
103
102
  end
104
103
  end
105
104
 
106
- remote_task_path = conn.write_executable(task_dir, executable)
107
-
108
105
  if STDIN_METHODS.include?(input_method)
109
106
  stdin = JSON.dump(arguments)
110
107
  end
@@ -113,20 +110,22 @@ module Bolt
113
110
  execute_options[:environment] = envify_params(arguments)
114
111
  end
115
112
 
116
- if conn.run_as && stdin
117
- # Inject interpreter in to wrapper script and remove from execute options
113
+ remote_task_path = conn.write_executable(task_dir, executable)
114
+
115
+ # Avoid the horrors of passing data on stdin via a tty on multiple platforms
116
+ # by writing a wrapper script that directs stdin to the task.
117
+ if stdin && target.options['tty']
118
118
  wrapper = make_wrapper_stringio(remote_task_path, stdin, execute_options[:interpreter])
119
119
  execute_options.delete(:interpreter)
120
- remote_wrapper_path = conn.write_executable(dir, wrapper, 'wrapper.sh')
121
- command << remote_wrapper_path
122
- else
123
- command << remote_task_path
124
- execute_options[:stdin] = stdin
120
+ execute_options[:wrapper] = true
121
+ remote_task_path = conn.write_executable(dir, wrapper, 'wrapper.sh')
125
122
  end
123
+
126
124
  dir.chown(conn.run_as)
127
125
 
126
+ execute_options[:stdin] = stdin
128
127
  execute_options[:sudoable] = true if conn.run_as
129
- output = conn.execute(command, execute_options)
128
+ output = conn.execute(remote_task_path, execute_options)
130
129
  end
131
130
  Bolt::Result.for_task(target, output.stdout.string,
132
131
  output.stderr.string,
@@ -70,6 +70,39 @@ module Bolt
70
70
  message = "#{self.class.name} must implement #{method} to execute commands"
71
71
  raise NotImplementedError, message
72
72
  end
73
+
74
+ # In the case where a task is run with elevated privilege and needs stdin
75
+ # a random string is echoed to stderr indicating that the stdin is available
76
+ # for task input data because the sudo password has already either been
77
+ # provided on stdin or was not needed.
78
+ def prepend_sudo_success(sudo_id, command_str)
79
+ "sh -c 'echo #{sudo_id} 1>&2; #{command_str}'"
80
+ end
81
+
82
+ # A helper to build up a single string that contains all of the options for
83
+ # privilege escalation. A wrapper script is used to direct task input to stdin
84
+ # when a tty is allocated and thus we do not need to prepend_sudo_success when
85
+ # using the wrapper or when the task does not require stdin data.
86
+ def build_sudoable_command_str(command_str, sudo_str, sudo_id, options)
87
+ if options[:stdin] && !options[:wrapper]
88
+ "#{sudo_str} #{prepend_sudo_success(sudo_id, command_str)}"
89
+ else
90
+ "#{sudo_str} #{command_str}"
91
+ end
92
+ end
93
+
94
+ # Returns string with the interpreter conditionally prepended
95
+ def inject_interpreter(interpreter, command)
96
+ if interpreter
97
+ if command.is_a?(Array)
98
+ command.unshift(interpreter)
99
+ else
100
+ command = [interpreter, command]
101
+ end
102
+ end
103
+
104
+ command.is_a?(String) ? command : Shellwords.shelljoin(command)
105
+ end
73
106
  end
74
107
  end
75
108
  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.29.0'
4
+ VERSION = '1.29.1'
5
5
  end
@@ -0,0 +1,12 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-04/schema#",
3
+ "title": "run_command request",
4
+ "description": "POST <transport>/run_command request schema",
5
+ "type": "object",
6
+ "properties": {
7
+ "command": { "type": "string" },
8
+ "target": { "$ref": "partial:target-any" }
9
+ },
10
+ "required": ["command", "target"],
11
+ "additionalProperties": false
12
+ }
@@ -19,7 +19,7 @@ module BoltServer
19
19
  PARTIAL_SCHEMAS = %w[target-any target-ssh target-winrm task].freeze
20
20
 
21
21
  # These schemas combine shared schemas to describe client requests
22
- REQUEST_SCHEMAS = %w[action-run_task transport-ssh transport-winrm].freeze
22
+ REQUEST_SCHEMAS = %w[action-run_task action-run_command transport-ssh transport-winrm].freeze
23
23
 
24
24
  def initialize(config)
25
25
  @config = config
@@ -65,6 +65,14 @@ module BoltServer
65
65
  @executor.run_task(target, task, parameters)
66
66
  end
67
67
 
68
+ def run_command(target, body)
69
+ error = validate_schema(@schemas["action-run_command"], body)
70
+ return [400, error.to_json] unless error.nil?
71
+
72
+ command = body['command']
73
+ @executor.run_command(target, command)
74
+ end
75
+
68
76
  get '/' do
69
77
  200
70
78
  end
@@ -84,7 +92,7 @@ module BoltServer
84
92
  raise 'Unexpected error'
85
93
  end
86
94
 
87
- ACTIONS = %w[run_task].freeze
95
+ ACTIONS = %w[run_task run_command].freeze
88
96
 
89
97
  post '/ssh/:action' do
90
98
  not_found unless ACTIONS.include?(params[:action])
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bolt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.29.0
4
+ version: 1.29.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Puppet
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-08-15 00:00:00.000000000 Z
11
+ date: 2019-08-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: addressable
@@ -66,20 +66,6 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '1.0'
69
- - !ruby/object:Gem::Dependency
70
- name: docker-api
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.34'
76
- type: :runtime
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.34'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: hiera-eyaml
85
71
  requirement: !ruby/object:Gem::Requirement
@@ -454,6 +440,7 @@ files:
454
440
  - lib/bolt_server/base_config.rb
455
441
  - lib/bolt_server/config.rb
456
442
  - lib/bolt_server/file_cache.rb
443
+ - lib/bolt_server/schemas/action-run_command.json
457
444
  - lib/bolt_server/schemas/action-run_task.json
458
445
  - lib/bolt_server/schemas/partials/target-any.json
459
446
  - lib/bolt_server/schemas/partials/target-ssh.json
@@ -509,8 +496,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
509
496
  - !ruby/object:Gem::Version
510
497
  version: '0'
511
498
  requirements: []
512
- rubyforge_project:
513
- rubygems_version: 2.7.7
499
+ rubygems_version: 3.0.4
514
500
  signing_key:
515
501
  specification_version: 4
516
502
  summary: Execute commands remotely over SSH and WinRM