diff --git a/README.md b/README.md index e546823d..48d1ba7a 100644 --- a/README.md +++ b/README.md @@ -284,6 +284,7 @@ Each of these accepts a single value or a list. | `binary` | `docker` | Docker CLI to invoke — e.g. `docker.io`, or an absolute path. | | `socket` | `$DOCKER_HOST`, else `unix:///var/run/docker.sock` (`npipe:////./pipe/docker_engine` on Windows) | Daemon to talk to. A `tcp://` value also supplies the host used for SSH to the container. | | `use_sudo` | `false` | Run every `docker` command through `sudo`. | +| `sudo_command` | `sudo -E` | The command `use_sudo` prefixes, for hosts that use something else (`doas`, say). | | `tls` | `false` | Use TLS when connecting. | | `tls_verify` | `false` | Verify the daemon's certificate. | | `tls_cacert` | *(none)* | Path to the CA certificate. | @@ -314,11 +315,13 @@ These options go under `transport:`, not `driver:`. | `privileged` | `false` | Run commands with `--privileged`. | | `interactive` | `false` | Pass `-i`. | | `tty` | `false` | Pass `-t`. | +| `use_sudo` | `false` | Run every `docker` command through `sudo`. | +| `sudo_command` | `sudo -E` | The command `use_sudo` prefixes. | | `tls`, `tls_verify`, `tls_cacert`, `tls_cert`, `tls_key` | as for the driver | TLS settings for the daemon connection. | The driver and transport each read their own copy of `binary`, `socket`, -`username`, and the TLS settings. If you point one at a non-default daemon, -point the other at it too. +`username`, `use_sudo`, and the TLS settings. If you point one at a non-default +daemon, or need `sudo` to reach it, configure the other the same way. ## Logging into a container @@ -534,7 +537,8 @@ bootstrap. Set it to a supported family, or supply your own to `false` against a remote daemon. **Permission denied talking to the daemon.** Either add your user to the -`docker` group, or set `use_sudo: true`. +`docker` group, or set `use_sudo: true` under **both** `driver:` and +`transport:` -- the transport runs its own `docker exec` and `docker cp`. **Anything else.** Run with `-l debug`: diff --git a/lib/kitchen/docker/helpers/cli_helper.rb b/lib/kitchen/docker/helpers/cli_helper.rb index fac8f79f..8e3086b5 100644 --- a/lib/kitchen/docker/helpers/cli_helper.rb +++ b/lib/kitchen/docker/helpers/cli_helper.rb @@ -64,11 +64,39 @@ def docker_command(cmd, options = {}) docker << " --tlscacert=#{shell_escape(config[:tls_cacert])}" if config[:tls_cacert] docker << " --tlscert=#{shell_escape(config[:tls_cert])}" if config[:tls_cert] docker << " --tlskey=#{shell_escape(config[:tls_key])}" if config[:tls_key] + options = docker_sudo_opts(options) logger.debug("docker_command: #{docker} #{cmd} shell_opts: #{docker_shell_opts(options)}") run_command("#{docker} #{cmd}", docker_shell_opts(options)) end # rubocop:enable Metrics/AbcSize + # Adds the sudo options {#run_command} reads, when +use_sudo+ is set. + # + # Sudo is a property of the call rather than of the configuration as far + # as the shell-out layer is concerned: it reads +:use_sudo+ from the + # options hash it is handed and knows nothing about +config+. So a + # docker command only runs through sudo if these are passed to it. + # + # Without this, +use_sudo+ reached exactly one command -- the + # `docker` probe in +verify_dependencies+ -- and every build, run, + # exec, cp, and rm still ran as the invoking user. On a host where the + # daemon socket needs root, that made the documented answer to + # "permission denied while trying to connect to the Docker daemon + # socket" do nothing at all. + # + # A copy is returned rather than the hash being edited in place, so a + # caller that reuses its options hash does not accumulate sudo. + # + # @param options [Hash] shell-out options + # @return [Hash] those options, with sudo added when configured + def docker_sudo_opts(options = {}) + return options unless config[:use_sudo] + + options = options.merge(use_sudo: true) + options[:sudo_command] = config[:sudo_command] if config[:sudo_command] + options + end + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize # Runs a shell command, returning stderr as well as stdout. diff --git a/lib/kitchen/driver/docker.rb b/lib/kitchen/driver/docker.rb index f6ee692a..40b04b98 100644 --- a/lib/kitchen/driver/docker.rb +++ b/lib/kitchen/driver/docker.rb @@ -52,6 +52,7 @@ class Docker < Kitchen::Driver::Base default_config :remove_images, false default_config :run_options, nil default_config :security_opt, nil + default_config :sudo_command, nil default_config :tls, false default_config :tls_cacert, nil default_config :tls_cert, nil diff --git a/lib/kitchen/transport/docker.rb b/lib/kitchen/transport/docker.rb index ba85ed12..4967a7ac 100644 --- a/lib/kitchen/transport/docker.rb +++ b/lib/kitchen/transport/docker.rb @@ -38,12 +38,14 @@ class DockerFailed < TransportFailed; end default_config :env_variables, nil default_config :interactive, false default_config :privileged, false + default_config :sudo_command, nil default_config :tls, false default_config :tls_cacert, nil default_config :tls_cert, nil default_config :tls_key, nil default_config :tls_verify, false default_config :tty, false + default_config :use_sudo, false default_config :working_dir, nil default_config :socket do |transport| diff --git a/spec/cli_helper_spec.rb b/spec/cli_helper_spec.rb index 016fde44..93bc18b4 100644 --- a/spec/cli_helper_spec.rb +++ b/spec/cli_helper_spec.rb @@ -298,6 +298,55 @@ def exec_argv(config = {}, command: "whoami") end end + describe "#docker_command" do + # `binary` is set to `echo` and `sudo_command` to `echo SUDO`, so the + # assembled command line is observable as output without needing real + # sudo -- or a real docker -- on the machine running the specs. + def echoed(config = {}) + helper({ binary: "echo", socket: nil, sudo_command: "echo SUDO" }.merge(config)) + .docker_command("ps -a") + end + + it "runs the command as the invoking user by default" do + expect(echoed).to eq "ps -a\n" + end + + # Cases from the README's "Permission denied talking to the daemon" + # advice. `use_sudo` reached only the `verify_dependencies` probe, so + # every command that actually touches the daemon still ran unprivileged + # and the documented fix did nothing. + it "runs the command through sudo when use_sudo is set" do + expect(echoed(use_sudo: true)).to eq "SUDO echo ps -a\n" + end + + it "leaves the sudo command at its default when none is configured" do + expect(helper(binary: "echo", socket: nil, use_sudo: true).docker_sudo_opts({})) + .to eq(use_sudo: true) + end + end + + describe "#docker_sudo_opts" do + it "adds nothing when use_sudo is unset" do + expect(helper.docker_sudo_opts(suppress_output: true)).to eq(suppress_output: true) + end + + it "adds the sudo options the shell-out layer reads" do + expect(helper(use_sudo: true, sudo_command: "doas").docker_sudo_opts({})) + .to eq(use_sudo: true, sudo_command: "doas") + end + + it "keeps the options it was given" do + expect(helper(use_sudo: true).docker_sudo_opts(suppress_output: true)) + .to eq(suppress_output: true, use_sudo: true) + end + + it "does not mutate the options it was given" do + options = {} + helper(use_sudo: true).docker_sudo_opts(options) + expect(options).to eq({}) + end + end + describe "#docker_shell_opts" do it "translates suppress_output into silencing the live stream" do expect(helper.docker_shell_opts(suppress_output: true)).to eq(live_stream: nil)