Skip to content

Commit

Permalink
Run commands from file on Windows when using exec transport
Browse files Browse the repository at this point in the history
Windows limits the length of commands that can be run via powershell which
breaks the exec transport. To work around the issue, when running on a Windows
host, the command to a file and then execute the file, and then remove the file.

This resolves test-kitchen#1630.

Signed-off-by: Lance Albertson <[email protected]>
  • Loading branch information
ramereth committed Nov 30, 2020
1 parent 3b4fa30 commit e3755a8
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 7 deletions.
94 changes: 90 additions & 4 deletions lib/kitchen/transport/exec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class Exec < Kitchen::Transport::Base
plugin_version Kitchen::VERSION

def connection(state, &block)
options = config.to_hash.merge(state)
options = connection_options(config.to_hash.merge(state))
Kitchen::Transport::Exec::Connection.new(options, &block)
end

Expand All @@ -40,19 +40,105 @@ class Connection < Kitchen::Transport::Base::Connection
def execute(command)
return if command.nil?

run_command(command)
if host_os_windows?
run_command(run_from_file_command(command))
close
else
run_command(command)
end
end

def close
if host_os_windows?
FileUtils.remove(exec_script_file)
end
end

# "Upload" the files by copying them locally.
#
# @see Base#upload
def upload(locals, remote)
FileUtils.mkdir_p(remote)
# evaluate $env:temp on Windows
real_remote = remote.to_s == "\$env:TEMP\\kitchen" ? kitchen_temp : remote
FileUtils.mkdir_p(real_remote)
Array(locals).each do |local|
FileUtils.cp_r(local, remote)
FileUtils.cp_r(local, real_remote)
end
end

# (see Base#init_options)
def init_options(options)
super
@instance_name = @options.delete(:instance_name)
@kitchen_root = @options.delete(:kitchen_root)
end

private

# @return [String] display name for the associated instance
# @api private
attr_reader :instance_name

# @return [String] local path to the root of the project
# @api private
attr_reader :kitchen_root

# Takes a long command and saves it to a file and uploads it to
# the test instance. Windows has cli character limits.
#
# @param command [String] a long command to be saved and uploaded
# @return [String] a command that executes the uploaded script
# @api private
def run_from_file_command(command)
if logger.debug?
debug("Creating exec script for #{instance_name} (#{exec_script_file})")
debug("Executing #{exec_script_file}")
end
File.open(exec_script_file, "wb") { |file| file.write(command) }
%{powershell -file "#{exec_script_file}"}
end

# @return [String] evaluated $env:temp variable
# @api private
def kitchen_temp
"#{ENV["temp"]}/kitchen"
end

# @return [String] name of script using instance name
# @api private
def exec_script_name
"#{instance_name}-exec-script.ps1"
end

# @return [String] file path for exec script to be run
# @api private
def exec_script_file
File.join(kitchen_root, ".kitchen", exec_script_name)
end

def host_os_windows?
case RbConfig::CONFIG["host_os"]
when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
true
else
false
end
end
end

private

# Builds the hash of options needed by the Connection object on construction.
#
# @param data [Hash] merged configuration and mutable state data
# @return [Hash] hash of connection options
# @api private
def connection_options(data)
opts = {
instance_name: instance.name,
kitchen_root: Dir.pwd,
}
opts
end
end
end
Expand Down
108 changes: 105 additions & 3 deletions spec/kitchen/transport/exec_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@

require "kitchen/transport/exec"

describe Kitchen::Transport::Ssh do
describe Kitchen::Transport::Exec do

before do
RbConfig::CONFIG.stubs(:[]).with("host_os").returns("blah")
end

let(:logged_output) { StringIO.new }
let(:logger) { Logger.new(logged_output) }
let(:config) { {} }
Expand All @@ -39,15 +44,62 @@
end

describe "#connection" do
it "returns a Kitchen::Transport::Exec::Connection object" do
transport.connection(state).must_be_kind_of Kitchen::Transport::Exec::Connection
let(:klass) { Kitchen::Transport::Exec::Connection }

def self.common_connection_specs
before do
config[:kitchen_root] = "/i/am/root"
end

it "returns a Kitchen::Transport::Exec::Connection object" do
transport.connection(state).must_be_kind_of klass
end

it "sets :instance_name to the instance's name" do
klass.expects(:new).with do |hash|
hash[:instance_name] == "coolbeans"
end

make_connection
end

it "sets :kitchen_root to the transport's kitchen_root" do
klass.expects(:new).with do |hash|
hash[:kitchen_root] == "/i/am/root"
end

make_connection
end

describe "called without a block" do
def make_connection(s = state) # rubocop:disable Lint/NestedMethodDefinition
transport.connection(s)
end

common_connection_specs
end

describe "called with a block" do
def make_connection(s = state) # rubocop:disable Lint/NestedMethodDefinition
transport.connection(s) do |conn|
conn
end
end

common_connection_specs
end
end
end
end

describe Kitchen::Transport::Exec::Connection do
before do
RbConfig::CONFIG.stubs(:[]).with("host_os").returns("blah")
end

let(:logged_output) { StringIO.new }
let(:logger) { Logger.new(logged_output) }
let(:exec_script) { File.join("/tmp/.kitchen/instance-exec-script.ps1") }

let(:options) do
{ logger: logger }
Expand All @@ -67,6 +119,43 @@
connection.expects(:run_command).never
connection.execute(nil)
end
describe "for windows-based workstations" do
before do
RbConfig::CONFIG.stubs(:[]).with("host_os").returns("mingw32")
options[:kitchen_root] = "/tmp"
options[:instance_name] = "instance"
end

it "runs the command" do
stub_file(exec_script, "")
connection.expects(:run_command).with("powershell -file \"#{exec_script}\"")
connection.execute("do the thing")
end

it "ignores nil" do
connection.expects(:run_command).never
connection.execute(nil)
end
end
end

describe "#close" do
it "Does not remove exec script file" do
FileUtils.expects(:remove).with(exec_script).never
connection.close
end

describe "for windows-based workstations" do
before do
RbConfig::CONFIG.stubs(:[]).with("host_os").returns("mingw32")
options[:kitchen_root] = "/tmp"
options[:instance_name] = "instance"
end
it "Removes exec script file" do
FileUtils.expects(:remove).with(exec_script)
connection.close
end
end
end

describe "#upload" do
Expand All @@ -75,5 +164,18 @@
FileUtils.expects(:cp_r).with("/tmp/sandbox/cookbooks", "/tmp/kitchen")
connection.upload(%w{/tmp/sandbox/cookbooks}, "/tmp/kitchen")
end
it "copies files when $env:temp is set" do
ENV["temp"] = "/tmp"
FileUtils.expects(:mkdir_p).with("/tmp/kitchen")
FileUtils.expects(:cp_r).with("/tmp/sandbox/cookbooks", "/tmp/kitchen")
connection.upload(%w{/tmp/sandbox/cookbooks}, "\$env:TEMP\\kitchen")
end
end

private

def stub_file(path, content)
FileUtils.mkdir_p(File.dirname(path))
File.open(path, "wb") { |f| f.write(content) }
end
end

0 comments on commit e3755a8

Please sign in to comment.