From c9a2f41407e1c7c2da8ce63646e7da9cec397b92 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sat, 22 Aug 2026 10:09:47 -0700 Subject: [PATCH 1/2] feat: Support `kitchen login` with the Docker transport `Kitchen::Transport::Base::Connection#login_command` raises "Remote login not supported" unless a transport overrides it, so `kitchen login` has never worked for instances using the Docker transport. Implement it as an interactive `docker exec` against the instance's container: `/bin/bash --login -i` on Linux platforms and `powershell` on Windows ones. Kitchen runs the result through `Kernel.exec` in its multi-argument form, which bypasses the shell, so the argv is built as individual tokens and values are left unquoted. The transport's socket, TLS, username, working_dir, env_variables and privileged settings are all carried over; interactive/tty are forced on and detach forced off, since a detached exec would return a session the user cannot type into. Ported forward from the unmerged #421, reworked to read the connection's own `@options` rather than reaching into the container object with `instance_variable_get`, and extended to cover Windows containers. spec/inspec_helper_spec.rb evaluated `defined?(Kitchen::Verifier::CincAuditor)` at spec-file load time to choose between two examples. RSpec loads every spec file before running any example, so requiring the transport from a new spec file defined that constant and invalidated the already-selected example. Move the check inside the example and load the helper explicitly so it no longer depends on spec load order. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 16 +++++ lib/kitchen/transport/docker.rb | 51 ++++++++++++- spec/inspec_helper_spec.rb | 25 ++++--- spec/transport_docker_spec.rb | 123 ++++++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+), 12 deletions(-) create mode 100644 spec/transport_docker_spec.rb diff --git a/README.md b/README.md index cb0cde7..20afca4 100644 --- a/README.md +++ b/README.md @@ -627,6 +627,22 @@ Examples: docker_platform: linux/amd64 ``` +## Logging into a container + +`kitchen login` opens an interactive shell inside a running container using the +Docker transport, so you do not need to look up the container ID and run +`docker exec` by hand: + +```bash +kitchen login default-ubuntu-2404 +``` + +The session runs `docker exec` against the instance's container. On Linux +platforms it starts `/bin/bash --login -i`; on Windows platforms it starts +`powershell`. The transport's `username`, `working_dir`, `env_variables` and +`privileged` settings are honoured, so the shell matches the environment that +Test Kitchen uses when it runs the provisioner. + ## Development * Source hosted at [GitHub][repo] diff --git a/lib/kitchen/transport/docker.rb b/lib/kitchen/transport/docker.rb index 10a08a9..5f04119 100644 --- a/lib/kitchen/transport/docker.rb +++ b/lib/kitchen/transport/docker.rb @@ -97,13 +97,62 @@ def upload(locals, remote) end def container - @container ||= if @options[:platform].include?("windows") + @container ||= if windows_container? Kitchen::Docker::Container::Windows.new(@options) else Kitchen::Docker::Container::Linux.new(@options) end @container end + + # (see Base::Connection#login_command) + def login_command + argv = build_login_command + LoginCommand.new(argv.first, argv.drop(1)) + end + + private + + def windows_container? + @options[:platform].to_s.include?("windows") + end + + # Builds the argv array for an interactive `docker exec` session. + # + # Kitchen hands the result to `Kernel.exec` in its multi-argument form, + # which bypasses the shell entirely. Every flag and its value therefore + # has to be its own token -- a packed "-H unix:///var/run/docker.sock" + # would reach Docker as a single argument -- and values must not be + # quoted, since there is no shell to strip the quotes back off. + # + # @return [Array] the docker command and its arguments + def build_login_command + docker = [@options[:binary]] + docker.push("-H", @options[:socket]) if @options[:socket] + docker << "--tls" if @options[:tls] + docker << "--tlsverify" if @options[:tls_verify] + docker << "--tlscacert=#{@options[:tls_cacert]}" if @options[:tls_cacert] + docker << "--tlscert=#{@options[:tls_cert]}" if @options[:tls_cert] + docker << "--tlskey=#{@options[:tls_key]}" if @options[:tls_key] + + # Always attached, always a TTY: a detached or non-interactive exec + # would hand back a session the user cannot type into. + cmd = ["exec"] + cmd << "--privileged" if @options[:privileged] + cmd.push("-t", "-i") + Hash(@options[:env_variables]).each { |key, value| cmd.push("-e", "#{key}=#{value}") } + cmd.push("-u", @options[:username]) if @options[:username] + cmd.push("-w", @options[:working_dir]) if @options[:working_dir] + cmd << @options[:container_id] + cmd.concat(login_shell) + + logger.debug("build_login_command: #{(docker + cmd).join(" ")}") + docker + cmd + end + + def login_shell + windows_container? ? ["powershell"] : ["/bin/bash", "--login", "-i"] + end end end end diff --git a/spec/inspec_helper_spec.rb b/spec/inspec_helper_spec.rb index e1400cc..7292e7e 100644 --- a/spec/inspec_helper_spec.rb +++ b/spec/inspec_helper_spec.rb @@ -7,12 +7,15 @@ describe "kitchen-inspec patch" do # Test actual post-load state rather than trying to stub Kernel.require, # which does not intercept require calls made inside a load'd file in Ruby 3.4. - if defined?(Kitchen::Verifier::Inspec) - it "adds runner_options_for_docker to Kitchen::Verifier::Inspec" do + # The availability check has to happen inside the example: RSpec loads every + # spec file before running any example, so a load-time `defined?` would be + # decided by whichever spec file happened to require the verifier first. + it "patches Kitchen::Verifier::Inspec when the gem is available" do + load helper_path + + if defined?(Kitchen::Verifier::Inspec) expect(Kitchen::Verifier::Inspec.method_defined?(:runner_options_for_docker)).to be true - end - else - it "Kitchen::Verifier::Inspec not available — patch correctly skipped" do + else expect(defined?(Kitchen::Verifier::Inspec)).to be_falsy end end @@ -32,13 +35,13 @@ describe "kitchen-cinc-auditor patch" do # Test actual post-load state rather than trying to stub Kernel.require. - if defined?(Kitchen::Verifier::CincAuditor) && - defined?(Kitchen::Verifier::CincAuditor::TransportOptions) - it "adds build_docker to Kitchen::Verifier::CincAuditor::TransportOptions" do + # Checked inside the example for the same load-order reason as above. + it "patches Kitchen::Verifier::CincAuditor::TransportOptions when the gem is available" do + load helper_path + + if defined?(Kitchen::Verifier::CincAuditor::TransportOptions) expect(Kitchen::Verifier::CincAuditor::TransportOptions.method_defined?(:build_docker)).to be true - end - else - it "Kitchen::Verifier::CincAuditor not available — patch correctly skipped" do + else expect(defined?(Kitchen::Verifier::CincAuditor)).to be_falsy end end diff --git a/spec/transport_docker_spec.rb b/spec/transport_docker_spec.rb new file mode 100644 index 0000000..32a6354 --- /dev/null +++ b/spec/transport_docker_spec.rb @@ -0,0 +1,123 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require "spec_helper" +require "kitchen/transport/docker" + +describe Kitchen::Transport::Docker::Connection do + let(:options) do + { + binary: "docker", + container_id: "abc123", + platform: "ubuntu-24.04", + socket: "unix:///var/run/docker.sock", + username: "kitchen", + } + end + + subject(:connection) { described_class.new(options) } + + describe "#login_command" do + subject(:login_command) { connection.login_command } + + it "returns a Kitchen::LoginCommand" do + expect(login_command).to be_a(Kitchen::LoginCommand) + end + + it "execs the docker binary" do + expect(login_command.command).to eq "docker" + end + + it "opens an interactive login shell on a Linux container" do + expect(login_command.arguments).to eq %w{ + -H unix:///var/run/docker.sock + exec -t -i -u kitchen abc123 /bin/bash --login -i + } + end + + it "passes each flag and its value as separate argv tokens" do + expect(login_command.arguments).to include("-H", "unix:///var/run/docker.sock") + expect(login_command.arguments).not_to include("-H unix:///var/run/docker.sock") + end + + context "on a Windows container" do + let(:options) do + { + binary: "docker", + container_id: "abc123", + platform: "windows-2022", + socket: "tcp://localhost:2375", + username: nil, + } + end + + it "opens a PowerShell session instead of bash" do + expect(login_command.arguments).to eq %w{ + -H tcp://localhost:2375 + exec -t -i abc123 powershell + } + end + end + + context "with TLS configured" do + before do + options.merge!( + tls: true, + tls_verify: true, + tls_cacert: "/certs/ca.pem", + tls_cert: "/certs/cert.pem", + tls_key: "/certs/key.pem" + ) + end + + it "includes the TLS flags before the exec subcommand" do + expect(login_command.arguments.take(8)).to eq %w{ + -H unix:///var/run/docker.sock + --tls --tlsverify + --tlscacert=/certs/ca.pem + --tlscert=/certs/cert.pem + --tlskey=/certs/key.pem + exec + } + end + end + + context "with a working directory and environment variables" do + before do + options.merge!(working_dir: "/opt/kitchen", env_variables: { FOO: "bar" }) + end + + it "passes them through to docker exec" do + expect(login_command.arguments).to include("-w", "/opt/kitchen") + expect(login_command.arguments).to include("-e", "FOO=bar") + end + end + + context "when the transport is configured to detach" do + before { options.merge!(detach: true) } + + it "still runs attached so the session is usable" do + expect(login_command.arguments).not_to include("-d") + end + end + + context "when the transport is configured as privileged" do + before { options.merge!(privileged: true) } + + it "keeps the privileged flag" do + expect(login_command.arguments).to include("--privileged") + end + end + end +end From ac7bee93438063d73cd9450600681daf43e3fc91 Mon Sep 17 00:00:00 2001 From: Tim Smith Date: Sat, 22 Aug 2026 10:09:47 -0700 Subject: [PATCH 2/2] chore: Remove dead disabled/ integration specs test/integration/{capabilities,default}/disabled/ held three files whose entire contents were commented out when busser-serverspec was dropped. They are not referenced by kitchen.yml or any workflow. Also ignore bin/* and .idea/*, both from #421. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 2 ++ .../disabled/capabilities_drop_spec.rb | 24 ------------------- .../default/disabled/default_spec.rb | 24 ------------------- .../default/disabled/spec_helper.rb | 21 ---------------- 4 files changed, 2 insertions(+), 69 deletions(-) delete mode 100644 test/integration/capabilities/disabled/capabilities_drop_spec.rb delete mode 100644 test/integration/default/disabled/default_spec.rb delete mode 100644 test/integration/default/disabled/spec_helper.rb diff --git a/.gitignore b/.gitignore index c94a2dd..56803f2 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,5 @@ tmp Dockerfile .DS_Store vendor/ +bin/* +.idea/* diff --git a/test/integration/capabilities/disabled/capabilities_drop_spec.rb b/test/integration/capabilities/disabled/capabilities_drop_spec.rb deleted file mode 100644 index 6d3e7b7..0000000 --- a/test/integration/capabilities/disabled/capabilities_drop_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# -# Copyright 2016, Noah Kantrowitz -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# Disable now busser-serever is gone. -# require 'serverspec' -# set :backend, :exec - -# describe command('/sbin/ifconfig eth0 multicast') do -# its(:exit_status) { is_expected.to_not eq 0 } -# its(:stderr) { is_expected.to match /Operation not permitted/ } -# end diff --git a/test/integration/default/disabled/default_spec.rb b/test/integration/default/disabled/default_spec.rb deleted file mode 100644 index cd48e09..0000000 --- a/test/integration/default/disabled/default_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# -# Copyright 2016, Noah Kantrowitz -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# Disable now busser-server is gone. -# require "serverspec" -# require "spec_helper" -# -# Just make sure the image launched and is reachable. -# describe command("true") do -# its(:exit_status) { is_expected.to eq 0 } -# end diff --git a/test/integration/default/disabled/spec_helper.rb b/test/integration/default/disabled/spec_helper.rb deleted file mode 100644 index c1ce986..0000000 --- a/test/integration/default/disabled/spec_helper.rb +++ /dev/null @@ -1,21 +0,0 @@ -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -# case RbConfig::CONFIG['host_os'] -# when /mswin|msys|mingw|cygwin|bccwin|wince|emc/ -# set :backend, :cmd -# set :os, :family => 'windows' -# else -# set :backend, :exec -# end