test-kitchen 2.7.1 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ae69102eac88b3cbb40ad7e39b02fd2a1519e8f9ed9d114fa1c0d4c0ea7319d
4
- data.tar.gz: 48bf850ca5c79fcb0b7af6cc7fce89b05d1c735ad51c80e224285c50581576bd
3
+ metadata.gz: 36e340a7aefc9e4512f05ded9b43003298a3466de705d7c6c6050dd07f433175
4
+ data.tar.gz: 310ae63b637cbd62a88cd5beef8effa437f0d1b2dec6bf9098ba3112e621c8ed
5
5
  SHA512:
6
- metadata.gz: 9164957f96d9e93b04a4a9e32ac475d9b0e239f1e900d02660fa286d4625e312f7d1b47ab5cfa7136b91e514e826f6c915fb9dcc8a56677915714ce9106b6366
7
- data.tar.gz: 3a0968bbe24c08505b12c8bfe3900c450c5f00bc35c3821db18b3535a891256ac9093f2f472a02bcbc7179d0b24f0c87f3a8237e185e87d39b0d741c98629738
6
+ metadata.gz: cc3e9bf0e55d519d757e4339b4e643f038216dedb573225aed9cc1ce2972ad121e7819db643e26e4038ebe5fc05e2bc0a57435588d462bf2d2a4a198cbd0b9b1
7
+ data.tar.gz: 48c9bc01e69691e4096f5ae219638b0d996f069852c3ac14ec143ce583c3f6f610263000e5b24256b3b969314c9903e95b570372c1d3a8d3298d3ba4c65885bd
data/Gemfile CHANGED
@@ -1,4 +1,6 @@
1
1
  source "https://rubygems.org"
2
+
3
+ # Specify your gem"s dependencies in test-kitchen.gemspec
2
4
  gemspec
3
5
 
4
6
  group :integration do
@@ -8,10 +10,6 @@ group :integration do
8
10
  gem "kitchen-vagrant"
9
11
  end
10
12
 
11
- group :changelog do
12
- gem "github_changelog_generator", "1.15.2"
13
- end
14
-
15
13
  group :debug do
16
14
  gem "pry"
17
15
  gem "pry-byebug"
@@ -19,9 +17,5 @@ group :debug do
19
17
  end
20
18
 
21
19
  group :chefstyle do
22
- gem "chefstyle"
23
- end
24
-
25
- group :docs do
26
- gem "yard"
27
- end
20
+ gem "chefstyle", "1.6.2"
21
+ end
data/Rakefile CHANGED
@@ -41,30 +41,8 @@ end
41
41
  desc "Run all quality tasks"
42
42
  task quality: %i{style stats}
43
43
 
44
- begin
45
- require "yard" unless defined?(YARD)
46
- YARD::Rake::YardocTask.new
47
- rescue LoadError
48
- puts "yard is not available. (sudo) gem install yard to generate yard documentation."
49
- end
50
-
51
44
  task default: %i{test quality}
52
45
 
53
- begin
54
- require "github_changelog_generator/task"
55
- require "kitchen/version"
56
-
57
- GitHubChangelogGenerator::RakeTask.new :changelog do |config|
58
- config.future_release = "v#{Kitchen::VERSION}"
59
- config.enhancement_labels = "enhancement,Enhancement,New Feature,Feature,Improvement".split(",")
60
- config.bug_labels = "bug,Bug".split(",")
61
- config.exclude_labels = %w{Duplicate Question Discussion No_Changelog}
62
- end
63
- rescue LoadError
64
- puts "github_changelog_generator is not available." \
65
- " (sudo) gem install github_changelog_generator to generate changelogs"
66
- end
67
-
68
46
  namespace :docs do
69
47
  desc "Deploy docs"
70
48
  task :deploy do
data/lib/kitchen.rb CHANGED
@@ -16,8 +16,6 @@
16
16
  # limitations under the License.
17
17
 
18
18
  require "pathname" unless defined?(Pathname)
19
- require "thread"
20
-
21
19
  require_relative "kitchen/errors"
22
20
  require_relative "kitchen/logger"
23
21
  require_relative "kitchen/logging"
@@ -67,7 +65,7 @@ module Kitchen
67
65
  #
68
66
  # @return [Pathname] root path of gem
69
67
  def source_root
70
- @source_root ||= Pathname.new(File.expand_path("../../", __FILE__))
68
+ @source_root ||= Pathname.new(File.expand_path("..", __dir__))
71
69
  end
72
70
 
73
71
  # Returns a default logger which emits on standard output.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright:: Copyright (c) Chef Software Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ module Kitchen
20
+ # Common Dependency Injection wiring for ChefUtils-related modules
21
+ module ChefUtilsWiring
22
+ private
23
+
24
+ def __config
25
+ # this would need to be some kind of Chef::Config looking thing, which probably requires
26
+ # a translation object from t-k config to Chef::Config layout if that ever becomes necessary.
27
+ # this ISNT the t-k config.
28
+ {}
29
+ end
30
+
31
+ def __log
32
+ @logger
33
+ end
34
+
35
+ def __transport_connection
36
+ # this could be wired up to train at some point, but need to be careful because about local vs. remote
37
+ # uses of helpers with test-kitchen, right now we're using it for local.
38
+ end
39
+ end
40
+ end
data/lib/kitchen/cli.rb CHANGED
@@ -16,7 +16,7 @@
16
16
  # limitations under the License.
17
17
 
18
18
  # CI tests fail without an explicit unconditional require of Thor
19
- require "thor" # rubocop:disable ChefRuby/UnlessDefinedRequire
19
+ require "thor" # rubocop:disable Chef/Ruby/UnlessDefinedRequire
20
20
 
21
21
  require_relative "../kitchen"
22
22
  require_relative "generator/init"
@@ -162,7 +162,7 @@ module Kitchen
162
162
  long_desc <<-DESC
163
163
  The instance states are in order: destroy, create, converge, setup, verify, destroy.
164
164
  Change one or more instances from the current state to the #{action} state. Actions for all
165
- intermediate states will be executed. See http://kitchen.ci for further explanation.
165
+ intermediate states will be executed. See https://kitchen.ci/ for further explanation.
166
166
  DESC
167
167
  method_option :concurrency,
168
168
  aliases: "-c",
@@ -15,8 +15,6 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
 
18
- require "thread"
19
-
20
18
  module Kitchen
21
19
  module Command
22
20
  # Base class for CLI commands.
@@ -18,7 +18,7 @@
18
18
  require_relative "../command"
19
19
  require_relative "../diagnostic"
20
20
 
21
- require "yaml" unless defined?(YAML)
21
+ autoload :YAML, "yaml"
22
22
 
23
23
  module Kitchen
24
24
  module Command
@@ -32,9 +32,9 @@ module Kitchen
32
32
 
33
33
  loader = record_failure { load_loader }
34
34
 
35
- puts Kitchen::Diagnostic.new(
35
+ puts YAML.dump(Kitchen::Diagnostic.new(
36
36
  loader: loader, instances: instances, plugins: plugins?
37
- ).read.to_yaml
37
+ ).read)
38
38
  end
39
39
 
40
40
  private
@@ -213,7 +213,7 @@ module Kitchen
213
213
  provisioner: Provisioner::DEFAULT_PLUGIN,
214
214
  verifier: Verifier::DEFAULT_PLUGIN,
215
215
  transport: lambda do |_suite, platform|
216
- platform =~ /^win/i ? "winrm" : Transport::DEFAULT_PLUGIN
216
+ /^win/i.match?(platform) ? "winrm" : Transport::DEFAULT_PLUGIN
217
217
  end,
218
218
  },
219
219
  kitchen_root: kitchen_root,
@@ -244,16 +244,18 @@ module Kitchen
244
244
  # @return [Instance] a new Instance object
245
245
  # @api private
246
246
  def new_instance(suite, platform, index)
247
+ sf = new_state_file(suite, platform)
248
+
247
249
  Instance.new(
248
250
  driver: new_driver(suite, platform),
249
- lifecycle_hooks: new_lifecycle_hooks(suite, platform),
251
+ lifecycle_hooks: new_lifecycle_hooks(suite, platform, sf),
250
252
  logger: new_instance_logger(suite, platform, index),
251
253
  suite: suite,
252
254
  platform: platform,
253
255
  provisioner: new_provisioner(suite, platform),
254
256
  transport: new_transport(suite, platform),
255
257
  verifier: new_verifier(suite, platform),
256
- state_file: new_state_file(suite, platform)
258
+ state_file: sf
257
259
  )
258
260
  end
259
261
 
@@ -284,11 +286,12 @@ module Kitchen
284
286
  #
285
287
  # @param suite [Suite,#name] a Suite
286
288
  # @param platform [Platform,#name] a Platform
289
+ # @param state_file [Kitchen::StateFile] a SateFile
287
290
  # @return [LifecycleHooks] a new LifecycleHooks object
288
291
  # @api private
289
- def new_lifecycle_hooks(suite, platform)
292
+ def new_lifecycle_hooks(suite, platform, state_file)
290
293
  lhdata = data.lifecycle_hooks_data_for(suite.name, platform.name)
291
- LifecycleHooks.new(lhdata)
294
+ LifecycleHooks.new(lhdata, state_file)
292
295
  end
293
296
 
294
297
  # Builds a newly configured Provisioner object, for a given Suite and
@@ -0,0 +1,78 @@
1
+ module Kitchen
2
+ class LifecycleHook
3
+ class Base
4
+ # @return [Kitchen::LifecycleHooks]
5
+ attr_reader :lifecycle_hooks
6
+
7
+ # return [String]
8
+ attr_reader :phase
9
+
10
+ # return [Hash]
11
+ attr_reader :hook
12
+
13
+ # @param lifecycle_hooks [Kitchen::LifecycleHooks]
14
+ # @param phase [String]
15
+ # @param hook [Hash]
16
+ def initialize(lifecycle_hooks, phase, hook)
17
+ @lifecycle_hooks = lifecycle_hooks
18
+ @phase = phase
19
+ @hook = hook
20
+ end
21
+
22
+ # return [void]
23
+ def run
24
+ raise NotImplementedError
25
+ end
26
+
27
+ # @return [TrueClass, FalseClass]
28
+ def should_run?
29
+ if !includes.empty?
30
+ includes.include?(platform_name)
31
+ elsif !excludes.empty?
32
+ !excludes.include?(platform_name)
33
+ else
34
+ true
35
+ end
36
+ end
37
+
38
+ # @return [Logger] the lifecycle hooks's logger
39
+ # otherwise
40
+ # @api private
41
+ def logger
42
+ lifecycle_hooks.send(:logger)
43
+ end
44
+
45
+ private
46
+
47
+ # @return [Kitchen::Instance]
48
+ def instance
49
+ lifecycle_hooks.instance
50
+ end
51
+
52
+ # @return [Hash]
53
+ def config
54
+ lifecycle_hooks.send(:config)
55
+ end
56
+
57
+ # @return [Kitchen::StateFile]
58
+ def state_file
59
+ lifecycle_hooks.state_file
60
+ end
61
+
62
+ # @return [Array<String>] names of excluded platforms
63
+ def excludes
64
+ @excludes ||= hook.fetch(:excludes, [])
65
+ end
66
+
67
+ # @return [Array<String>] names of only included platforms
68
+ def includes
69
+ @includes ||= hook.fetch(:includes, [])
70
+ end
71
+
72
+ # @return [String]
73
+ def platform_name
74
+ instance.platform.name
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,53 @@
1
+ require_relative "base"
2
+ require_relative "../shell_out"
3
+ require_relative "../logging"
4
+
5
+ module Kitchen
6
+ class LifecycleHook
7
+ class Local < Base
8
+ include ShellOut
9
+ include Logging
10
+
11
+ # Execute a specific local command hook.
12
+ #
13
+ # @return [void]
14
+ def run
15
+ state = state_file.read
16
+ # set up empty user variable
17
+ user = {}
18
+ # Set up some environment variables with instance info.
19
+ environment = {
20
+ "KITCHEN_INSTANCE_NAME" => instance.name,
21
+ "KITCHEN_SUITE_NAME" => instance.suite.name,
22
+ "KITCHEN_PLATFORM_NAME" => instance.platform.name,
23
+ "KITCHEN_INSTANCE_HOSTNAME" => state[:hostname].to_s,
24
+ }
25
+ # If the user specified env vars too, fix them up because symbol keys
26
+ # make mixlib-shellout sad.
27
+ hook[:environment]&.each do |k, v|
28
+ environment[k.to_s] = v.to_s
29
+ end
30
+
31
+ # add user to user hash for later merging
32
+ user[:user] = hook[:user] if hook[:user]
33
+
34
+ # Default the cwd to the kitchen root and resolve a relative input cwd against that.
35
+ cwd = if hook[:cwd]
36
+ File.expand_path(hook[:cwd], config[:kitchen_root])
37
+ else
38
+ config[:kitchen_root]
39
+ end
40
+ # Build the options for mixlib-shellout.
41
+ opts = {}.merge(user).merge(cwd: cwd, environment: environment)
42
+ run_command(command, opts)
43
+ end
44
+
45
+ private
46
+
47
+ # @return [String]
48
+ def command
49
+ hook.fetch(:local)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,33 @@
1
+ require_relative "base"
2
+ require_relative "../errors"
3
+
4
+ module Kitchen
5
+ class LifecycleHook
6
+ class Remote < Base
7
+ # Execute a specific remote command hook.
8
+ #
9
+ # @return [void]
10
+ def run
11
+ # Check if we're in a state that makes sense to even try.
12
+ unless instance.last_action
13
+ if hook[:skippable]
14
+ # Just not even trying.
15
+ return
16
+ else
17
+ raise UserError, "Cannot use remote lifecycle hooks during phases when the instance is not available"
18
+ end
19
+ end
20
+
21
+ conn = instance.transport.connection(state_file.read)
22
+ conn.execute(command)
23
+ end
24
+
25
+ private
26
+
27
+ # return [String]
28
+ def command
29
+ hook.fetch(:remote)
30
+ end
31
+ end
32
+ end
33
+ end
@@ -15,8 +15,11 @@
15
15
  # See the License for the specific language governing permissions and
16
16
  # limitations under the License.
17
17
 
18
+ require_relative "configurable"
18
19
  require_relative "errors"
19
- require_relative "shell_out"
20
+ require_relative "lifecycle_hook/local"
21
+ require_relative "lifecycle_hook/remote"
22
+ require_relative "logging"
20
23
 
21
24
  module Kitchen
22
25
  # A helper object used by {Instance} to coordinate lifecycle hook calls from
@@ -27,10 +30,10 @@ module Kitchen
27
30
  class LifecycleHooks
28
31
  include Configurable
29
32
  include Logging
30
- include ShellOut
31
33
 
32
- def initialize(config)
34
+ def initialize(config, state_file)
33
35
  init_config(config)
36
+ @state_file = state_file
34
37
  end
35
38
 
36
39
  # Run a lifecycle phase with the pre and post hooks.
@@ -40,21 +43,22 @@ module Kitchen
40
43
  # @param block [Proc] Block of code implementing the lifecycle phase.
41
44
  # @return [void]
42
45
  def run_with_hooks(phase, state_file, &block)
43
- run(instance, phase, state_file, :pre)
46
+ run(phase, :pre)
44
47
  yield
45
- run(instance, phase, state_file, :post)
48
+ run(phase, :post)
46
49
  end
47
50
 
51
+ # @return [Kitchen::StateFile]
52
+ attr_reader :state_file
53
+
48
54
  private
49
55
 
50
56
  # Execute a specific lifecycle hook.
51
57
  #
52
- # @param instance [Instance] The instance object to run against.
53
58
  # @param phase [String] Lifecycle phase which is being executed.
54
- # @param state_file [StateFile] Instance state file object.
55
59
  # @param hook_timing [Symbol] `:pre` or `:post` to indicate which hook to run.
56
60
  # @return [void]
57
- def run(instance, phase, state_file, hook_timing)
61
+ def run(phase, hook_timing)
58
62
  # Yes this has to be a symbol because of how data munger works.
59
63
  hook_key = :"#{hook_timing}_#{phase}"
60
64
  # No hooks? We're outta here.
@@ -65,80 +69,24 @@ module Kitchen
65
69
  # Coerce the common case of a bare string to be a local command. This
66
70
  # is to match the behavior of the old `pre_create_command` semi-hook.
67
71
  hook = { local: hook } if hook.is_a?(String)
68
- if hook.include?(:local)
69
- # Local command execution on the workstation.
70
- run_local_hook(instance, state_file, hook)
71
- elsif hook.include?(:remote)
72
- # Remote command execution on the test instance.
73
- run_remote_hook(instance, state_file, hook)
74
- else
75
- raise UserError, "Unknown lifecycle hook target #{hook.inspect}"
76
- end
72
+ hook = generate_hook(phase, hook)
73
+ hook.run if hook.should_run?
77
74
  end
78
75
  end
79
76
 
80
- # Execute a specific local command hook.
81
- #
82
- # @param instance [Instance] The instance object to run against.
83
- # @param state_file [StateFile] Instance state file object.
84
- # @param hook [Hash] Hook configration to use.
85
- # @return [void]
86
- def run_local_hook(instance, state_file, hook)
87
- cmd = hook.fetch(:local)
88
- state = state_file.read
89
- # set up empty user variable
90
- user = {}
91
- # Set up some environment variables with instance info.
92
- environment = {
93
- "KITCHEN_INSTANCE_NAME" => instance.name,
94
- "KITCHEN_SUITE_NAME" => instance.suite.name,
95
- "KITCHEN_PLATFORM_NAME" => instance.platform.name,
96
- "KITCHEN_INSTANCE_HOSTNAME" => state[:hostname].to_s,
97
- }
98
- # If the user specified env vars too, fix them up because symbol keys
99
- # make mixlib-shellout sad.
100
- if hook[:environment]
101
- hook[:environment].each do |k, v|
102
- environment[k.to_s] = v.to_s
103
- end
104
- end
105
-
106
- # add user to user hash for later merging
107
- if hook[:user]
108
- user[:user] = hook[:user]
77
+ # @param phase [String]
78
+ # @param hook [Hash]
79
+ # @return [Kitchen::LifecycleHook::Local, Kitchen::LifecycleHook::Remote]
80
+ def generate_hook(phase, hook)
81
+ if hook.include?(:local)
82
+ # Local command execution on the workstation.
83
+ Kitchen::LifecycleHook::Local.new(self, phase, hook)
84
+ elsif hook.include?(:remote)
85
+ # Remote command execution on the test instance.
86
+ Kitchen::LifecycleHook::Remote.new(self, phase, hook)
87
+ else
88
+ raise UserError, "Unknown lifecycle hook target #{hook.inspect}"
109
89
  end
110
-
111
- # Default the cwd to the kitchen root and resolve a relative input cwd against that.
112
- cwd = if hook[:cwd]
113
- File.expand_path(hook[:cwd], config[:kitchen_root])
114
- else
115
- config[:kitchen_root]
116
- end
117
- # Build the options for mixlib-shellout.
118
- opts = {}.merge(user).merge(cwd: cwd, environment: environment)
119
- run_command(cmd, opts)
120
- end
121
-
122
- # Execute a specific remote command hook.
123
- #
124
- # @param instance [Instance] The instance object to run against.
125
- # @param state_file [StateFile] Instance state file object.
126
- # @param hook [Hash] Hook configration to use.
127
- # @return [void]
128
- def run_remote_hook(instance, state_file, hook)
129
- # Check if we're in a state that makes sense to even try.
130
- unless instance.last_action
131
- if hook[:skippable]
132
- # Just not even trying.
133
- return
134
- else
135
- raise UserError, "Cannot use remote lifecycle hooks during phases when the instance is not available"
136
- end
137
- end
138
-
139
- cmd = hook.fetch(:remote)
140
- conn = instance.transport.connection(state_file.read)
141
- conn.execute(cmd)
142
90
  end
143
91
  end
144
92
  end
@@ -17,6 +17,7 @@
17
17
 
18
18
  require "erb" unless defined?(Erb)
19
19
  require_relative "../../vendor/hash_recursive_merge"
20
+ require "psych" unless defined?(Psych)
20
21
  require "yaml" unless defined?(YAML)
21
22
 
22
23
  module Kitchen
@@ -51,6 +51,7 @@ module Kitchen
51
51
 
52
52
  default_config :command_prefix, nil
53
53
 
54
+ default_config :uploads, {}
54
55
  default_config :downloads, {}
55
56
 
56
57
  expand_path_for :test_base_path
@@ -72,6 +73,10 @@ module Kitchen
72
73
  sandbox_dirs = Util.list_directory(sandbox_path)
73
74
 
74
75
  instance.transport.connection(state) do |conn|
76
+ config[:uploads].to_h.each do |locals, remote|
77
+ debug("Uploading #{Array(locals).join(", ")} to #{remote}")
78
+ conn.upload(locals.to_s, remote)
79
+ end
75
80
  conn.execute(install_command)
76
81
  conn.execute(init_command)
77
82
  info("Transferring files to #{instance.to_str}")
@@ -21,6 +21,7 @@ require "rbconfig" unless defined?(RbConfig)
21
21
  require_relative "../../errors"
22
22
  require_relative "../../logging"
23
23
  require_relative "../../shell_out"
24
+ require_relative "../../which"
24
25
 
25
26
  module Kitchen
26
27
  module Provisioner
@@ -31,6 +32,7 @@ module Kitchen
31
32
  class Policyfile
32
33
  include Logging
33
34
  include ShellOut
35
+ include Which
34
36
 
35
37
  # Creates a new cookbook resolver.
36
38
  #
@@ -40,9 +42,9 @@ module Kitchen
40
42
  # @param logger [Kitchen::Logger] a logger to use for output, defaults
41
43
  # to `Kitchen.logger`
42
44
  def initialize(policyfile, path, logger: Kitchen.logger, always_update: false)
43
- @policyfile = policyfile
44
- @path = path
45
- @logger = logger
45
+ @policyfile = policyfile
46
+ @path = path
47
+ @logger = logger
46
48
  @always_update = always_update
47
49
  end
48
50
 
@@ -51,30 +53,29 @@ module Kitchen
51
53
  # @param logger [Kitchen::Logger] a logger to use for output, defaults
52
54
  # to `Kitchen.logger`
53
55
  def self.load!(logger: Kitchen.logger)
54
- detect_chef_command!(logger)
56
+ # intentionally left blank
55
57
  end
56
58
 
57
59
  # Performs the cookbook resolution and vendors the resulting cookbooks
58
60
  # in the desired path.
59
61
  def resolve
60
- info("Exporting cookbook dependencies from Policyfile #{path}...")
61
- run_command("chef export #{escape_path(policyfile)} #{escape_path(path)} --force")
62
+ info("Exporting cookbook dependencies from Policyfile #{path} using `#{cli_path} export`...")
63
+ run_command("#{cli_path} export #{escape_path(policyfile)} #{escape_path(path)} --force")
62
64
  end
63
65
 
64
66
  # Runs `chef install` to determine the correct cookbook set and
65
67
  # generate the policyfile lock.
66
68
  def compile
67
69
  if File.exist?(lockfile)
68
- info("Installing cookbooks for Policyfile #{policyfile} using `chef install`")
70
+ info("Installing cookbooks for Policyfile #{policyfile} using `#{cli_path} install`")
69
71
  else
70
- info("Policy lock file doesn't exist, running `chef install` for "\
71
- "Policyfile #{policyfile}...")
72
+ info("Policy lock file doesn't exist, running `#{cli_path} install` for Policyfile #{policyfile}...")
72
73
  end
73
- run_command("chef install #{escape_path(policyfile)}")
74
+ run_command("#{cli_path} install #{escape_path(policyfile)}")
74
75
 
75
76
  if always_update
76
- info("Updating policy lock using `chef update`")
77
- run_command("chef update #{escape_path(policyfile)}")
77
+ info("Updating policy lock using `#{cli_path} update`")
78
+ run_command("#{cli_path} update #{escape_path(policyfile)}")
78
79
  end
79
80
  end
80
81
 
@@ -110,13 +111,13 @@ module Kitchen
110
111
  # @return [String]
111
112
  # @api private
112
113
  def escape_path(path)
113
- if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
114
+ if /mswin|mingw/.match?(RbConfig::CONFIG["host_os"])
114
115
  # I know what you're thinking: "just use Shellwords.escape". That
115
116
  # method produces incorrect results on Windows with certain input
116
117
  # which would be a metacharacter in Sh but is not for one or more of
117
118
  # Windows command line parsing libraries. This covers the 99% case of
118
119
  # spaces in the path without breaking other stuff.
119
- if path =~ /[ \t\n\v"]/
120
+ if /[ \t\n\v"]/.match?(path)
120
121
  "\"#{path.gsub(/[ \t\n\v\"\\]/) { |m| '\\' + m[0] }}\""
121
122
  else
122
123
  path
@@ -126,33 +127,24 @@ module Kitchen
126
127
  end
127
128
  end
128
129
 
129
- class << self
130
- private
131
-
132
- # Ensure the `chef` command is in the path.
133
- #
134
- # @param logger [Kitchen::Logger] the logger to use
135
- # @raise [UserError] if the `chef` command is not in the PATH
136
- # @api private
137
- def detect_chef_command!(logger)
138
- unless ENV["PATH"].split(File::PATH_SEPARATOR).any? do |path|
139
- if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/
140
- # Windows could have different extentions: BAT, EXE or NONE
141
- %w{chef chef.exe chef.bat}.each do |bin|
142
- File.exist?(File.join(path, bin))
143
- end
144
- else
145
- File.exist?(File.join(path, "chef"))
146
- end
147
- end
148
- logger.fatal("The `chef` executable cannot be found in your " \
149
- "PATH. Ensure you have installed ChefDK or Chef Workstation " \
150
- "from https://downloads.chef.io and that your PATH " \
151
- "setting includes the path to the `chef` command.")
152
- raise UserError,
153
- "Could not find the chef executable in your PATH."
154
- end
155
- end
130
+ # Find the `chef` or `chef-cli` commands in the path or raise `chef` is present in
131
+ # ChefDK / Workstation releases, but is no longer shipped in any gems now that we
132
+ # use a Go based wrapper for the `chef` command in Workstation. The Ruby CLI has been
133
+ # renamed `chef-cli` under the hood and is shipped in the `chef-cli` gem.
134
+ #
135
+ # @api private
136
+ # @returns [String]
137
+ def cli_path
138
+ @cli_path ||= which("chef-cli") || which("chef") || no_cli_found_error
139
+ end
140
+
141
+ # @api private
142
+ def no_cli_found_error
143
+ @logger.fatal("The `chef` or `chef-cli` executables cannot be found in your " \
144
+ "PATH. Ensure you have installed Chef Workstation " \
145
+ "from https://downloads.chef.io and that your PATH " \
146
+ "setting includes the path to the `chef` or `chef-cli` commands.")
147
+ raise UserError, "Could not find the chef or chef-cli executables in your PATH."
156
148
  end
157
149
 
158
150
  end
@@ -156,7 +156,7 @@ module Kitchen
156
156
  product_name: <chef or chef-workstation>
157
157
  install_strategy: skip
158
158
  MSG
159
- when provisioner[:require_chef_omnibus].to_s.match(/\d/)
159
+ when provisioner[:require_chef_omnibus].to_s.match?(/\d/)
160
160
  Util.outdent!(<<-MSG)
161
161
  The 'require_chef_omnibus' attribute with version values will change
162
162
  to use the new 'product_version' attribute.
@@ -28,7 +28,7 @@ module Kitchen
28
28
  plugin_version Kitchen::VERSION
29
29
 
30
30
  def connection(state, &block)
31
- options = config.to_hash.merge(state)
31
+ options = connection_options(config.to_hash.merge(state))
32
32
  Kitchen::Transport::Exec::Connection.new(options, &block)
33
33
  end
34
34
 
@@ -40,19 +40,105 @@ module Kitchen
40
40
  def execute(command)
41
41
  return if command.nil?
42
42
 
43
- run_command(command)
43
+ if host_os_windows?
44
+ run_command(run_from_file_command(command))
45
+ close
46
+ else
47
+ run_command(command)
48
+ end
49
+ end
50
+
51
+ def close
52
+ if host_os_windows?
53
+ FileUtils.remove(exec_script_file)
54
+ end
44
55
  end
45
56
 
46
57
  # "Upload" the files by copying them locally.
47
58
  #
48
59
  # @see Base#upload
49
60
  def upload(locals, remote)
50
- FileUtils.mkdir_p(remote)
61
+ # evaluate $env:temp on Windows
62
+ real_remote = remote.to_s == "\$env:TEMP\\kitchen" ? kitchen_temp : remote
63
+ FileUtils.mkdir_p(real_remote)
51
64
  Array(locals).each do |local|
52
- FileUtils.cp_r(local, remote)
65
+ FileUtils.cp_r(local, real_remote)
66
+ end
67
+ end
68
+
69
+ # (see Base#init_options)
70
+ def init_options(options)
71
+ super
72
+ @instance_name = @options.delete(:instance_name)
73
+ @kitchen_root = @options.delete(:kitchen_root)
74
+ end
75
+
76
+ private
77
+
78
+ # @return [String] display name for the associated instance
79
+ # @api private
80
+ attr_reader :instance_name
81
+
82
+ # @return [String] local path to the root of the project
83
+ # @api private
84
+ attr_reader :kitchen_root
85
+
86
+ # Takes a long command and saves it to a file and uploads it to
87
+ # the test instance. Windows has cli character limits.
88
+ #
89
+ # @param command [String] a long command to be saved and uploaded
90
+ # @return [String] a command that executes the uploaded script
91
+ # @api private
92
+ def run_from_file_command(command)
93
+ if logger.debug?
94
+ debug("Creating exec script for #{instance_name} (#{exec_script_file})")
95
+ debug("Executing #{exec_script_file}")
96
+ end
97
+ File.open(exec_script_file, "wb") { |file| file.write(command) }
98
+ %{powershell -file "#{exec_script_file}"}
99
+ end
100
+
101
+ # @return [String] evaluated $env:temp variable
102
+ # @api private
103
+ def kitchen_temp
104
+ "#{ENV["temp"]}/kitchen"
105
+ end
106
+
107
+ # @return [String] name of script using instance name
108
+ # @api private
109
+ def exec_script_name
110
+ "#{instance_name}-exec-script.ps1"
111
+ end
112
+
113
+ # @return [String] file path for exec script to be run
114
+ # @api private
115
+ def exec_script_file
116
+ File.join(kitchen_root, ".kitchen", exec_script_name)
117
+ end
118
+
119
+ def host_os_windows?
120
+ case RbConfig::CONFIG["host_os"]
121
+ when /mswin|msys|mingw|cygwin|bccwin|wince|emc/
122
+ true
123
+ else
124
+ false
53
125
  end
54
126
  end
127
+ end
128
+
129
+ private
55
130
 
131
+ # Builds the hash of options needed by the Connection object on construction.
132
+ #
133
+ # @param data [Hash] merged configuration and mutable state data
134
+ # @return [Hash] hash of connection options
135
+ # @api private
136
+ def connection_options(data)
137
+ opts = {
138
+ instance_name: instance.name,
139
+ kitchen_root: Dir.pwd,
140
+ }
141
+ opts
56
142
  end
57
143
  end
58
144
  end
@@ -195,18 +195,16 @@ module Kitchen
195
195
  FileUtils.mkdir_p(File.dirname(local))
196
196
 
197
197
  Array(remotes).each do |file|
198
+ logger.debug("Attempting to download '#{file}' as file")
199
+ session.scp.download!(file, local)
200
+ rescue Net::SCP::Error
198
201
  begin
199
- logger.debug("Attempting to download '#{file}' as file")
200
- session.scp.download!(file, local)
202
+ logger.debug("Attempting to download '#{file}' as directory")
203
+ session.scp.download!(file, local, recursive: true)
201
204
  rescue Net::SCP::Error
202
- begin
203
- logger.debug("Attempting to download '#{file}' as directory")
204
- session.scp.download!(file, local, recursive: true)
205
- rescue Net::SCP::Error
206
- logger.warn(
207
- "SCP download failed for file or directory '#{file}', perhaps it does not exist?"
208
- )
209
- end
205
+ logger.warn(
206
+ "SCP download failed for file or directory '#{file}', perhaps it does not exist?"
207
+ )
210
208
  end
211
209
  end
212
210
  rescue Net::SSH::Exception => ex
@@ -308,16 +308,20 @@ module Kitchen
308
308
 
309
309
  # Builds a `LoginCommand` for use by Linux-based platforms.
310
310
  #
311
- # TODO: determine whether or not `desktop` exists
312
- #
313
311
  # @return [LoginCommand] a login command
314
312
  # @api private
315
313
  def login_command_for_linux
316
- args = %W{-u #{options[:user]}}
317
- args += %W{-p #{options[:password]}} if options.key?(:password)
318
- args += %W{#{URI.parse(options[:endpoint]).host}:#{rdp_port}}
314
+ xfreerdp = Util.command_exists? "xfreerdp"
315
+ unless xfreerdp
316
+ raise WinrmFailed, "xfreerdp binary not found. Please install freerdp2-x11 on Debian-based systems or freerdp on Redhat-based systems."
317
+ end
318
+
319
+ args = %W{/u:#{options[:user]}}
320
+ args += %W{/p:#{options[:password]}} if options.key?(:password)
321
+ args += %W{/v:#{URI.parse(options[:endpoint]).host}:#{rdp_port}}
322
+ args += %W{/cert-tofu} # always accept certificate
319
323
 
320
- LoginCommand.new("rdesktop", args)
324
+ LoginCommand.new(xfreerdp, args)
321
325
  end
322
326
 
323
327
  # Builds a `LoginCommand` for use by Mac-based platforms.
data/lib/kitchen/util.rb CHANGED
@@ -110,7 +110,7 @@ module Kitchen
110
110
  def self.wrap_command(cmd)
111
111
  cmd = "false" if cmd.nil?
112
112
  cmd = "true" if cmd.to_s.empty?
113
- cmd = cmd.sub(/\n\Z/, "") if cmd =~ /\n\Z/
113
+ cmd = cmd.sub(/\n\Z/, "") if /\n\Z/.match?(cmd)
114
114
 
115
115
  "sh -c '\n#{cmd}\n'"
116
116
  end
@@ -215,5 +215,15 @@ module Kitchen
215
215
  def self.snake_case(a_string)
216
216
  Thor::Util.snake_case(a_string)
217
217
  end
218
+
219
+ # Check if a cmd exists on the PATH
220
+ def self.command_exists?(cmd)
221
+ paths = ENV["PATH"].split(File::PATH_SEPARATOR) + [ "/bin", "/usr/bin", "/sbin", "/usr/sbin" ]
222
+ paths.each do |path|
223
+ filename = File.join(path, cmd)
224
+ return filename if File.executable?(filename)
225
+ end
226
+ false
227
+ end
218
228
  end
219
229
  end
@@ -168,7 +168,7 @@ module Kitchen
168
168
  # @api private
169
169
  def gem_install_args
170
170
  gem, version = config[:version].split("@")
171
- if gem =~ /^\d+\.\d+\.\d+/
171
+ if /^\d+\.\d+\.\d+/.match?(gem)
172
172
  version = gem
173
173
  gem = "busser"
174
174
  end
@@ -88,6 +88,7 @@ module Kitchen
88
88
  env_state[:environment]["KITCHEN_INSTANCE"] = instance.name
89
89
  env_state[:environment]["KITCHEN_PLATFORM"] = instance.platform.name
90
90
  env_state[:environment]["KITCHEN_SUITE"] = instance.suite.name
91
+ env_state[:environment]["KITCHEN_USERNAME"] = instance.transport[:username] if instance.respond_to?(:transport)
91
92
  state.each_pair do |key, value|
92
93
  env_state[:environment]["KITCHEN_" + key.to_s.upcase] = value.to_s
93
94
  end
@@ -16,5 +16,5 @@
16
16
  # limitations under the License.
17
17
 
18
18
  module Kitchen
19
- VERSION = "2.7.1".freeze
19
+ VERSION = "2.11.0".freeze
20
20
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ #
3
+ # Copyright:: Copyright (c) Chef Software Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+
18
+ require "chef-utils/dsl/which" unless defined?(ChefUtils::DSL::Which)
19
+ require_relative "chef_utils_wiring" unless defined?(Ohai::Mixin::ChefUtilsWiring)
20
+
21
+ module Kitchen
22
+ module Which
23
+ include ChefUtils::DSL::Which
24
+ include ChefUtilsWiring
25
+ end
26
+ end
data/test-kitchen.gemspec CHANGED
@@ -1,4 +1,4 @@
1
- lib = File.expand_path("../lib", __FILE__)
1
+ lib = File.expand_path("lib", __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require "kitchen/version"
4
4
  require "English"
@@ -20,7 +20,7 @@ Gem::Specification.new do |gem|
20
20
  gem.executables = %w{kitchen}
21
21
  gem.require_paths = ["lib"]
22
22
 
23
- gem.required_ruby_version = ">= 2.4"
23
+ gem.required_ruby_version = ">= 2.5"
24
24
 
25
25
  gem.add_dependency "mixlib-shellout", ">= 1.2", "< 4.0"
26
26
  gem.add_dependency "net-scp", ">= 1.1", "< 4.0" # pinning until we can confirm 4+ works
@@ -33,6 +33,7 @@ Gem::Specification.new do |gem|
33
33
  gem.add_dependency "winrm", "~> 2.0"
34
34
  gem.add_dependency "winrm-elevated", "~> 1.0"
35
35
  gem.add_dependency "winrm-fs", "~> 1.1"
36
+ gem.add_dependency "chef-utils", ">= 16.4.35"
36
37
  # Required to run the Chef provisioner local license check for remote systems
37
38
  # TK is not under Chef EULA
38
39
  gem.add_dependency "license-acceptance", ">= 1.0.11", "< 3.0" # pinning until we can confirm 3+ works
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test-kitchen
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.1
4
+ version: 2.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fletcher Nichol
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-09-15 00:00:00.000000000 Z
11
+ date: 2021-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mixlib-shellout
@@ -194,6 +194,20 @@ dependencies:
194
194
  - - "~>"
195
195
  - !ruby/object:Gem::Version
196
196
  version: '1.1'
197
+ - !ruby/object:Gem::Dependency
198
+ name: chef-utils
199
+ requirement: !ruby/object:Gem::Requirement
200
+ requirements:
201
+ - - ">="
202
+ - !ruby/object:Gem::Version
203
+ version: 16.4.35
204
+ type: :runtime
205
+ prerelease: false
206
+ version_requirements: !ruby/object:Gem::Requirement
207
+ requirements:
208
+ - - ">="
209
+ - !ruby/object:Gem::Version
210
+ version: 16.4.35
197
211
  - !ruby/object:Gem::Dependency
198
212
  name: license-acceptance
199
213
  requirement: !ruby/object:Gem::Requirement
@@ -387,6 +401,7 @@ files:
387
401
  - bin/kitchen
388
402
  - lib/kitchen.rb
389
403
  - lib/kitchen/base64_stream.rb
404
+ - lib/kitchen/chef_utils_wiring.rb
390
405
  - lib/kitchen/cli.rb
391
406
  - lib/kitchen/collection.rb
392
407
  - lib/kitchen/color.rb
@@ -415,6 +430,9 @@ files:
415
430
  - lib/kitchen/generator/init.rb
416
431
  - lib/kitchen/instance.rb
417
432
  - lib/kitchen/lazy_hash.rb
433
+ - lib/kitchen/lifecycle_hook/base.rb
434
+ - lib/kitchen/lifecycle_hook/local.rb
435
+ - lib/kitchen/lifecycle_hook/remote.rb
418
436
  - lib/kitchen/lifecycle_hooks.rb
419
437
  - lib/kitchen/loader/yaml.rb
420
438
  - lib/kitchen/logger.rb
@@ -454,6 +472,7 @@ files:
454
472
  - lib/kitchen/verifier/dummy.rb
455
473
  - lib/kitchen/verifier/shell.rb
456
474
  - lib/kitchen/version.rb
475
+ - lib/kitchen/which.rb
457
476
  - lib/vendor/hash_recursive_merge.rb
458
477
  - support/busser_install_command.ps1
459
478
  - support/busser_install_command.sh
@@ -493,14 +512,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
493
512
  requirements:
494
513
  - - ">="
495
514
  - !ruby/object:Gem::Version
496
- version: '2.4'
515
+ version: '2.5'
497
516
  required_rubygems_version: !ruby/object:Gem::Requirement
498
517
  requirements:
499
518
  - - ">="
500
519
  - !ruby/object:Gem::Version
501
520
  version: '0'
502
521
  requirements: []
503
- rubygems_version: 3.1.2
522
+ rubygems_version: 3.1.4
504
523
  signing_key:
505
524
  specification_version: 4
506
525
  summary: Test Kitchen is an integration tool for developing and testing infrastructure