diff --git a/documentation/experimental_features.md b/documentation/experimental_features.md index 4d92acb9b9..6af6915218 100644 --- a/documentation/experimental_features.md +++ b/documentation/experimental_features.md @@ -191,6 +191,30 @@ targets: tmpdir: /root/tmp ``` +## FreeBSD jails support + +Bolt now has experimental support for [FreeBSD +jails](https://docs.freebsd.org/en/books/handbook/jails/), a lightweight virtualization solution +that allow for the creation of isolated environments within a single FreeBSD system. +The jail transport supports connecting to jails running on the local system. +The jail transport accepts many of the same configuration options as the Docker transport. You can +see the full list of supported configuration options [on the transport reference +page](bolt_transports_reference.md). The jail transport doesn't support the `service-url` +configuration options as the transport doesn't support remote connections. If this is a feature +you're interested in, let us know [in Slack](https://slack.puppet.com) or submit a [Github +issue](https://github.com/puppetlabs/bolt/issues). + +The example inventory file below demonstrates connecting to a jail container target named +`postgres_db`. + +``` +targets: + - uri: jail://postgres_db + config: + jail: + user: postgres +``` + ## Streaming output This feature was introduced in [Bolt 3.2.0](https://github.com/puppetlabs/bolt/blob/main/CHANGELOG.md#bolt-320-2021-3-08). diff --git a/lib/bolt/config/options.rb b/lib/bolt/config/options.rb index 8537e784ae..6a6ec53d54 100644 --- a/lib/bolt/config/options.rb +++ b/lib/bolt/config/options.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require_relative '../../bolt/config/transport/docker' +require_relative '../../bolt/config/transport/jail' require_relative '../../bolt/config/transport/local' require_relative '../../bolt/config/transport/lxd' require_relative '../../bolt/config/transport/orch' @@ -16,6 +17,7 @@ module Options # gets passed along to the inventory. TRANSPORT_CONFIG = { 'docker' => Bolt::Config::Transport::Docker, + 'jail' => Bolt::Config::Transport::Jail, 'local' => Bolt::Config::Transport::Local, 'lxd' => Bolt::Config::Transport::LXD, 'pcp' => Bolt::Config::Transport::Orch, @@ -550,6 +552,12 @@ module Options _plugin: true, _example: { "cleanup" => false, "service-url" => "https://docker.example.com" } }, + "jail" => { + description: "A map of configuration options for the jail transport.", + type: Hash, + _plugin: true, + _example: { cleanup: false } + }, "local" => { description: "A map of configuration options for the local transport. The set of available options is "\ "platform dependent.", diff --git a/lib/bolt/config/transport/jail.rb b/lib/bolt/config/transport/jail.rb new file mode 100644 index 0000000000..0cef52cf2c --- /dev/null +++ b/lib/bolt/config/transport/jail.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative '../../../bolt/error' +require_relative '../../../bolt/config/transport/base' + +module Bolt + class Config + module Transport + class Jail < Base + OPTIONS = %w[ + cleanup + host + interpreters + shell-command + tmpdir + user + ].concat(RUN_AS_OPTIONS).sort.freeze + + DEFAULTS = { + 'cleanup' => true + }.freeze + + private def validate + super + + if @config['interpreters'] + @config['interpreters'] = normalize_interpreters(@config['interpreters']) + end + end + end + end + end +end diff --git a/lib/bolt/executor.rb b/lib/bolt/executor.rb index 85f7db4691..bbd926b3da 100644 --- a/lib/bolt/executor.rb +++ b/lib/bolt/executor.rb @@ -14,6 +14,7 @@ require_relative '../bolt/result_set' # Load transports require_relative '../bolt/transport/docker' +require_relative '../bolt/transport/jail' require_relative '../bolt/transport/local' require_relative '../bolt/transport/lxd' require_relative '../bolt/transport/orch' @@ -25,6 +26,7 @@ module Bolt TRANSPORTS = { docker: Bolt::Transport::Docker, + jail: Bolt::Transport::Jail, local: Bolt::Transport::Local, lxd: Bolt::Transport::LXD, pcp: Bolt::Transport::Orch, diff --git a/lib/bolt/shell/bash.rb b/lib/bolt/shell/bash.rb index dd5c5691bf..6317145708 100644 --- a/lib/bolt/shell/bash.rb +++ b/lib/bolt/shell/bash.rb @@ -356,7 +356,7 @@ def execute(command, sudoable: false, **options) if defined? conn.add_env_vars conn.add_env_vars(options[:environment]) else - env_decl = options[:environment].map do |env, val| + env_decl = '/usr/bin/env ' + options[:environment].map do |env, val| "#{env}=#{Shellwords.shellescape(val)}" end.join(' ') end diff --git a/lib/bolt/transport/jail.rb b/lib/bolt/transport/jail.rb new file mode 100644 index 0000000000..26cec33d55 --- /dev/null +++ b/lib/bolt/transport/jail.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require_relative '../../bolt/transport/simple' + +module Bolt + module Transport + class Jail < Simple + def provided_features + ['shell'] + end + + def with_connection(target) + conn = Connection.new(target) + conn.connect + yield conn + end + end + end +end + +require_relative 'jail/connection' diff --git a/lib/bolt/transport/jail/connection.rb b/lib/bolt/transport/jail/connection.rb new file mode 100644 index 0000000000..4d857dd6b0 --- /dev/null +++ b/lib/bolt/transport/jail/connection.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'logging' +require_relative '../../../bolt/node/errors' + +module Bolt + module Transport + class Jail < Simple + class Connection + attr_reader :user, :target + + def initialize(target) + raise Bolt::ValidationError, "Target #{target.safe_name} does not have a host" unless target.host + @target = target + @user = @target.user || ENV['USER'] || Etc.getlogin + @logger = Bolt::Logger.logger(target.safe_name) + @jail_info = {} + @logger.trace("Initializing jail connection to #{target.safe_name}") + end + + def shell + @shell ||= Bolt::Shell::Bash.new(target, self) + end + + def reset_cwd? + true + end + + def jail_id + @jail_info['jid'].to_s + end + + def jail_path + @jail_info['path'] + end + + def connect + output = JSON.parse(`jls --libxo=json`) + @jail_info = output['jail-information']['jail'].select { |jail| jail['hostname'] == target.host }.first + raise "Could not find a jail with name matching #{target.host}" if @jail_info.nil? + @logger.trace { "Opened session" } + true + rescue StandardError => e + raise Bolt::Node::ConnectError.new( + "Failed to connect to #{target.safe_name}: #{e.message}", + 'CONNECT_ERROR' + ) + end + + def execute(command) + args = ['-lU', @user] + + jail_command = %w[jexec] + args + [jail_id] + Shellwords.split(command) + @logger.trace { "Executing #{jail_command.join(' ')}" } + + Open3.popen3({}, *jail_command) + rescue StandardError + @logger.trace { "Command aborted" } + raise + end + + def upload_file(source, destination) + @logger.trace { "Uploading #{source} to #{destination}" } + jail_destination = File.join(jail_path, destination) + FileUtils.cp(source, jail_destination) + rescue StandardError => e + raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR') + end + + def download_file(source, destination, _download) + @logger.trace { "Downloading #{source} to #{destination}" } + jail_source = File.join(jail_path, source) + FileUtils.mkdir_p(destination) + FileUtils.cp(jail_source, destination) + rescue StandardError => e + raise Bolt::Node::FileError.new(e.message, 'WRITE_ERROR') + end + end + end + end +end diff --git a/schemas/bolt-defaults.schema.json b/schemas/bolt-defaults.schema.json index 9c8d444f77..4f1d8eccd4 100644 --- a/schemas/bolt-defaults.schema.json +++ b/schemas/bolt-defaults.schema.json @@ -101,6 +101,9 @@ "docker": { "$ref": "#/definitions/docker" }, + "jail": { + "$ref": "#/definitions/jail" + }, "local": { "$ref": "#/definitions/local" }, @@ -472,6 +475,7 @@ "type": "string", "enum": [ "docker", + "jail", "local", "lxd", "pcp", @@ -609,6 +613,119 @@ } ] }, + "jail": { + "description": "A map of configuration options for the jail transport.", + "oneOf": [ + { + "type": "object", + "properties": { + "cleanup": { + "oneOf": [ + { + "$ref": "#/transport_definitions/cleanup" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "host": { + "oneOf": [ + { + "$ref": "#/transport_definitions/host" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "interpreters": { + "oneOf": [ + { + "$ref": "#/transport_definitions/interpreters" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "run-as": { + "oneOf": [ + { + "$ref": "#/transport_definitions/run-as" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "run-as-command": { + "oneOf": [ + { + "$ref": "#/transport_definitions/run-as-command" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "shell-command": { + "oneOf": [ + { + "$ref": "#/transport_definitions/shell-command" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "sudo-executable": { + "oneOf": [ + { + "$ref": "#/transport_definitions/sudo-executable" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "sudo-password": { + "oneOf": [ + { + "$ref": "#/transport_definitions/sudo-password" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "tmpdir": { + "oneOf": [ + { + "$ref": "#/transport_definitions/tmpdir" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "user": { + "oneOf": [ + { + "$ref": "#/transport_definitions/user" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + } + } + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "local": { "description": "A map of configuration options for the local transport. The set of available options is platform dependent.", "oneOf": [ @@ -635,6 +752,16 @@ } ] }, + "extensions": { + "oneOf": [ + { + "$ref": "#/transport_definitions/extensions" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "interpreters": { "oneOf": [ { @@ -718,6 +845,16 @@ } ] }, + "interpreters": { + "oneOf": [ + { + "$ref": "#/transport_definitions/interpreters" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "remote": { "oneOf": [ { diff --git a/schemas/bolt-inventory.schema.json b/schemas/bolt-inventory.schema.json index 47084c3f74..209dfd0830 100644 --- a/schemas/bolt-inventory.schema.json +++ b/schemas/bolt-inventory.schema.json @@ -42,6 +42,7 @@ "type": "string", "enum": [ "docker", + "jail", "local", "lxd", "pcp", @@ -210,6 +211,148 @@ } ] }, + "jail": { + "description": "A map of configuration options for the jail transport.", + "oneOf": [ + { + "type": "object", + "properties": { + "cleanup": { + "description": "Whether to clean up temporary files created on targets. When running commands on a target, Bolt might create temporary files. After completing the command, these files are automatically deleted. This value can be set to 'false' if you wish to leave these temporary files on the target.", + "oneOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "host": { + "description": "The target's hostname.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "interpreters": { + "description": "A map of an extension name to the absolute path of an executable, enabling you to override the shebang defined in a task executable. The extension can optionally be specified with the `.` character (`.py` and `py` both map to a task executable `task.py`) and the extension is case sensitive. When a target's name is `localhost`, Ruby tasks run with the Bolt Ruby interpreter by default.", + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "array" + ] + }, + "propertyNames": { + "pattern": "^.?[a-zA-Z0-9]+$" + } + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "run-as": { + "description": "The user to run commands as after login. The run-as user must be different than the login user.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "run-as-command": { + "description": "The command to elevate permissions. Bolt appends the user and command strings to the configured `run-as-command` before running it on the target. This command must not require aninteractive password prompt, and the `sudo-password` option is ignored when `run-as-command` is specified. The `run-as-command` must be specified as an array.", + "oneOf": [ + { + "type": "array", + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + } + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "shell-command": { + "description": "A shell command to wrap any Docker exec commands in, such as `bash -lc`.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "sudo-executable": { + "description": "The executable to use when escalating to the configured `run-as` user. This is useful when you want to escalate using the configured `sudo-password`, since `run-as-command` does not use `sudo-password` or support prompting. The command executed on the target is ` -S -u -p custom_bolt_prompt `. **This option is experimental.**", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "sudo-password": { + "description": "The password to use when changing users via `run-as`.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "tmpdir": { + "description": "The directory to upload and execute temporary files on the target.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, + "user": { + "description": "The user name to login as.", + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + } + } + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "local": { "description": "A map of configuration options for the local transport. The set of available options is platform dependent.", "oneOf": [ @@ -231,6 +374,28 @@ } ] }, + "extensions": { + "description": "A list of file extensions that are accepted for scripts or tasks on Windows. Scripts with these file extensions rely on the target's file type association to run. For example, if Python is installed on the system, a `.py` script runs with `python.exe`. The extensions `.ps1`, `.rb`, and `.pp` are always allowed and run via hard-coded executables.", + "oneOf": [ + { + "type": "array", + "uniqueItems": true, + "items": { + "oneOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/_plugin" + } + ] + } + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "interpreters": { "description": "A map of an extension name to the absolute path of an executable, enabling you to override the shebang defined in a task executable. The extension can optionally be specified with the `.` character (`.py` and `py` both map to a task executable `task.py`) and the extension is case sensitive. When a target's name is `localhost`, Ruby tasks run with the Bolt Ruby interpreter by default.", "oneOf": [ @@ -340,6 +505,26 @@ } ] }, + "interpreters": { + "description": "A map of an extension name to the absolute path of an executable, enabling you to override the shebang defined in a task executable. The extension can optionally be specified with the `.` character (`.py` and `py` both map to a task executable `task.py`) and the extension is case sensitive. When a target's name is `localhost`, Ruby tasks run with the Bolt Ruby interpreter by default.", + "oneOf": [ + { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "array" + ] + }, + "propertyNames": { + "pattern": "^.?[a-zA-Z0-9]+$" + } + }, + { + "$ref": "#/definitions/_plugin" + } + ] + }, "remote": { "type": "string", "description": "The LXD remote host to use." diff --git a/spec/fixtures/modules/results/tasks/init.sh b/spec/fixtures/modules/results/tasks/init.sh index 1a6aab2916..a0ec70f294 100644 --- a/spec/fixtures/modules/results/tasks/init.sh +++ b/spec/fixtures/modules/results/tasks/init.sh @@ -1,4 +1,4 @@ -#!/usr/bin/env bash +#!/bin/sh if [ ! -z "$PT_fail" ]; then exit 1 diff --git a/spec/fixtures/modules/sample/tasks/stdin.sh b/spec/fixtures/modules/sample/tasks/stdin.sh index ddf90f15b1..30f8ebcfb8 100644 --- a/spec/fixtures/modules/sample/tasks/stdin.sh +++ b/spec/fixtures/modules/sample/tasks/stdin.sh @@ -1,3 +1,3 @@ -#!/bin/bash +#!/bin/sh grep "message" diff --git a/spec/integration/jail_spec.rb b/spec/integration/jail_spec.rb new file mode 100644 index 0000000000..6a7c067030 --- /dev/null +++ b/spec/integration/jail_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'bolt_spec/conn' +require 'bolt_spec/files' +require 'bolt_spec/integration' +require 'bolt_spec/project' + +describe "when runnning over the jail transport", jail: true do + include BoltSpec::Conn + include BoltSpec::Files + include BoltSpec::Integration + include BoltSpec::Project + + let(:whoami) { "whoami" } + let(:modulepath) { fixtures_path('modules') } + let(:stdin_task) { "sample::stdin" } + let(:uri) { conn_uri('jail') } + let(:user) { conn_info('jail')[:user] } + let(:password) { conn_info('jail')[:password] } + + after(:each) { Puppet.settings.send(:clear_everything_for_tests) } + + context 'when using CLI options' do + let(:config_flags) { + %W[--targets #{uri} --no-host-key-check --format json --modulepath #{modulepath} --password #{password}] + } + + it 'runs a command' do + result = run_one_node(%W[command run #{whoami}] + config_flags) + expect(result['stdout'].strip).to eq('root') + end + + it 'reports errors when command fails' do + result = run_failed_node(%w[command run boop] + config_flags) + expect(result['_error']['kind']).to eq('puppetlabs.tasks/command-error') + expect(result['_error']['msg']).to eq('The command failed with exit code 1') + end + + it 'runs a task', :reset_puppet_settings do + result = run_one_node(%W[task run #{stdin_task} message=somemessage] + config_flags) + expect(result['message'].strip).to eq("somemessage") + end + + it 'reports errors when task fails', :reset_puppet_settings do + result = run_failed_node(%w[task run results fail=true] + config_flags) + expect(result['_error']['kind']).to eq('puppetlabs.tasks/task-error') + expect(result['_error']['msg']).to eq("The task failed with exit code 1 and no output") + end + + it 'passes noop to a task that supports noop', :reset_puppet_settings do + result = run_one_node(%w[task run sample::noop message=somemessage --noop] + config_flags) + expect(result['_output'].strip).to eq("somemessage with noop true") + end + + it 'passes noop to a plan that runs a task with noop', :reset_puppet_settings do + result = run_cli_json(%w[plan run sample::noop] + config_flags)[0]['value'] + expect(result['_output'].strip).to eq("This works with noop true") + end + + it 'does not pass noop to a task by default', :reset_puppet_settings do + result = run_one_node(%w[task run sample::noop message=somemessage] + config_flags) + expect(result['_output'].strip).to eq("somemessage with noop") + end + + it 'escalates privileges when passed --run-as' do + result = run_one_node(%W[command run #{whoami} --run-as root --sudo-password #{password}] + config_flags) + expect(result['stdout'].strip).to eq("root") + result = run_one_node(%W[command run #{whoami} --run-as #{user} --sudo-password #{password}] + config_flags) + expect(result['stdout'].strip).to eq(user) + end + end + + context 'when using a project', :reset_puppet_settings do + let(:config) do + { + 'format' => 'json', + 'future' => future_config, + 'modulepath' => modulepath + } + end + + let(:future_config) { {} } + + let(:default_inv) do + { + 'config' => { + 'jail' => { + } + } + } + end + + let(:inv) { default_inv } + let(:uri) { (1..2).map { |i| "#{conn_uri('jail')}?id=#{i}" }.join(',') } + let(:project) { @project } + let(:config_flags) { %W[--targets #{uri} --project #{project.path}] } + let(:single_target_conf) { %W[--targets #{conn_uri('jail')} --project #{project.path}] } + let(:interpreter_task) { 'sample::interpreter' } + let(:interpreter_script) { 'sample/scripts/script.py' } + + let(:run_as_conf) do + { + 'config' => { + 'jail' => { + } + } + } + end + + let(:interpreter_ext) do + { + 'config' => { + 'jail' => { + 'interpreters' => { + '.py' => '/usr/local/bin/python3.9' + } + } + } + } + end + + let(:interpreter_no_ext) do + { + 'config' => { + 'jail' => { + 'interpreters' => { + 'py' => '/usr/local/bin/python3.9' + } + } + } + } + end + + let(:interpreter_array) do + { + 'config' => { + 'jail' => { + 'interpreters' => { + 'py' => ['/usr/local/bin/python3.9', '-d'] + } + } + } + } + end + + around :each do |example| + with_project(config: config, inventory: inv) do |project| + @project = project + example.run + end + end + + shared_examples 'script interpreter' do + it 'does not run script with specified interpreter' do + result = run_cli_json(%W[script run #{interpreter_script}] + config_flags)['items'][0] + expect(result['status']).to eq('failure') + expect(result['value']['exit_code']).to eq(2) + expect(result['value']['stderr']).to match(/word unexpected/) + end + + context 'with future.script_interpreter configured' do + let(:future_config) do + { + 'script_interpreter' => true + } + end + + it 'runs script with specified interpreter' do + result = run_cli_json(%W[script run #{interpreter_script}] + config_flags)['items'][0] + expect(result['status']).to eq('success') + expect(result['value']['exit_code']).to eq(0) + expect(result['value']['stdout']).to match(/Hello, world!/) + end + end + end + + it 'runs multiple commands' do + result = run_nodes(%W[command run #{whoami}] + config_flags) + expect(result.map { |r| r['stdout'].strip }).to eq([user, user]) + end + + it 'runs multiple tasks' do + result = run_nodes(%W[task run #{stdin_task} message=short] + config_flags) + expect(result.map { |r| r['message'].strip }).to eq(%w[short short]) + end + + context 'with run-as configured' do + let(:inv) { Bolt::Util.deep_merge(default_inv, run_as_conf) } + + it 'runs multiple tasks as a specified user' do + result = run_nodes(%W[command run #{whoami} --sudo-password #{password}] + config_flags) + expect(result.map { |r| r['stdout'].strip }).to eq([user, user]) + end + end + + context 'with interpreters without dots configured' do + let(:inv) { Bolt::Util.deep_merge(default_inv, interpreter_no_ext) } + + include_examples 'script interpreter' + + it 'runs task with specified interpreter key py' do + result = run_nodes(%W[task run #{interpreter_task} message=short] + config_flags) + expect(result.map { |r| r['env'].strip }).to eq(%w[short short]) + expect(result.map { |r| r['stdin'].strip }).to eq(%w[short short]) + end + + it 'runs task with specified interpreter that with run-as set' do + result = run_nodes(%W[task run #{interpreter_task} message=short + --run-as root --sudo-password #{password}] + config_flags) + expect(result.map { |r| r['env'].strip }).to eq(%w[short short]) + expect(result.map { |r| r['stdin'].strip }).to eq(%w[short short]) + end + end + + context 'with interpreters with dots configured' do + let(:inv) { Bolt::Util.deep_merge(default_inv, interpreter_ext) } + + include_examples 'script interpreter' + + it 'runs task with interpreter key .py' do + result = run_nodes(%W[task run #{interpreter_task} message=short] + config_flags) + expect(result.map { |r| r['env'].strip }).to eq(%w[short short]) + expect(result.map { |r| r['stdin'].strip }).to eq(%w[short short]) + end + end + + context 'with interpreters as an array' do + let(:inv) { Bolt::Util.deep_merge(default_inv, interpreter_array) } + + include_examples 'script interpreter' + + it 'runs task with interpreter value as array' do + result = run_nodes(%W[task run #{interpreter_task} message=short] + config_flags) + expect(result.map { |r| r['env'].strip }).to eq(%w[short short]) + expect(result.map { |r| r['stdin'].strip }).to eq(%w[short short]) + end + end + + it 'task fails when bad shebang is not overriden' do + result = run_failed_node(%W[task run #{interpreter_task} message=short] + single_target_conf) + expect(result['_error']['msg']).to match(/interpreter.py: not found/) + end + end +end diff --git a/spec/lib/bolt_spec/conn.rb b/spec/lib/bolt_spec/conn.rb index 79eb566f5d..5ed54a14c4 100644 --- a/spec/lib/bolt_spec/conn.rb +++ b/spec/lib/bolt_spec/conn.rb @@ -29,8 +29,11 @@ def conn_info(transport) default_host = 'ubuntu_node' when 'lxd' default_host = 'testlxd' + when 'jail' + default_user = 'root' + default_host = 'bolt' else - raise Error, "The transport must be either 'ssh', 'winrm', 'docker', 'podman', or 'lxd'." + raise Error, "The transport must be either 'ssh', 'winrm', 'docker', 'podman', 'lxd' or 'jail'." end additional_config.merge( diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 92b7c18689..fbd7e738cb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,6 +48,7 @@ end config.filter_run_excluding windows: true unless Bolt::Util.windows? + config.filter_run_excluding jail: true unless RUBY_PLATFORM =~ /-freebsd\d+\z/ # rspec-mocks config config.mock_with :rspec do |mocks| diff --git a/spec/unit/shell/bash_spec.rb b/spec/unit/shell/bash_spec.rb index e827d6dc7f..d7bd200862 100644 --- a/spec/unit/shell/bash_spec.rb +++ b/spec/unit/shell/bash_spec.rb @@ -131,7 +131,7 @@ def echo_result end it "sets environment variables if requested" do - expect(connection).to receive(:execute).with('FOO=bar sh -c echo\\ \\$FOO') + expect(connection).to receive(:execute).with('/usr/bin/env FOO=bar sh -c echo\\ \\$FOO') shell.run_command('echo $FOO', env_vars: { 'FOO' => 'bar' }) end