Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ tmp
Dockerfile
.DS_Store
vendor/
bin/*
.idea/*
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
51 changes: 50 additions & 1 deletion lib/kitchen/transport/docker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>] 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
Expand Down
25 changes: 14 additions & 11 deletions spec/inspec_helper_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
123 changes: 123 additions & 0 deletions spec/transport_docker_spec.rb
Original file line number Diff line number Diff line change
@@ -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
24 changes: 0 additions & 24 deletions test/integration/capabilities/disabled/capabilities_drop_spec.rb

This file was deleted.

24 changes: 0 additions & 24 deletions test/integration/default/disabled/default_spec.rb

This file was deleted.

21 changes: 0 additions & 21 deletions test/integration/default/disabled/spec_helper.rb

This file was deleted.

Loading