Skip to content

Commit

Permalink
[Potentially Breaking] Provisioners responsible for converge action.
Browse files Browse the repository at this point in the history
Potentially Breaking Notes
==========================

This is potentially breaking to Driver authors if all of the following
are true:

* Your Driver currently directly inherits from `Kitchen::Driver::Base`
* Your Driver implements/overrides the `#converge` method

Put another way, your Driver may have issues if it looks like the
following:

    module Kitchen
      module Driver
        class MyDriver < Kitchen::Driver::Base
          def conerge(state)
            # custom converge work
          end
        end
      end
    end

For the vast majority (well over 90%) of OSS Drivers in the wild,
current behavior is maintained as they all inherit from
`Kitchen::Driver::SSHBase`. This class has been cemented to preserve its
current behavior, and Test Kitchen will invoke the `#converge` method
for these Drivers.

**Note:** upgrade path and instructions for Driver authors will be
written, but backwards compatibility is being taken seriously.

A future deprecation process may remove the `SSHBase` backwards
compatibility, but not without plenty of lead time and warning. Due to
the constraints of SemVer, by definition, this wouldn't occur before a
2.x codebase release.

Self-Aware Provisioners
=======================

As of this commit a `#call(state)` method exists in
`Kitchen::Provisioner::Base` which will be invoked by Test Kitchen when
the converge action is performed. For backwards compatibility, the same
convergence "template" is used, relying on a small number of public
methods that return command strings and 3 methods responsible for
sandbox creation and cleanup.

The high-level description of the default `#call(state)` method is as
follows:

1. Create the temporary sandbox on the workstation with "stuff" to
   transfer to the remote instance.
2. Run the `#install_command` on the remote instance, if it is
   implemented.
3. Run the `#init_command` on the remote instance, if it is
   implemented.
4. Transfer all files in the sandbox path to the remote instance's
   configured `:root_path`.
5. Run the `#prepare_command` on the remote instance, if it is
   implemented.
6. Run the `#run_command` on the remote instance, if it is
   implemented.

As a Provisioner author, you may elect to overwrite or partially
re-implement the `#call(state)` method to do whatever you need in
whatever order makes sense. This key difference allows Provisioner
authors to entirely own the `kitchen converge` action and not also rely
on the Driver used to manage the instances.
  • Loading branch information
fnichol committed Mar 9, 2015
1 parent fdd5bbd commit 3196675
Show file tree
Hide file tree
Showing 14 changed files with 558 additions and 86 deletions.
4 changes: 2 additions & 2 deletions features/kitchen_action_commands.feature
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ Feature: Running instance actions
Given a file named ".kitchen.local.yml" with:
"""
---
driver:
fail_converge: true
provisioner:
fail: true
"""
When I successfully run `kitchen create client-beans`
And I successfully run `kitchen list client-beans`
Expand Down
8 changes: 4 additions & 4 deletions features/kitchen_test_command.feature
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ Feature: Running a full test instance test
Given a file named ".kitchen.local.yml" with:
"""
---
driver:
fail_converge: true
provisioner:
fail: true
"""
When I run `kitchen test client-beans --destroy=always`
Then the exit status should not be 0
Expand All @@ -56,8 +56,8 @@ Feature: Running a full test instance test
Given a file named ".kitchen.local.yml" with:
"""
---
driver:
fail_converge: true
provisioner:
fail: true
"""
When I run `kitchen test client-beans --destroy=passing`
Then the exit status should not be 0
Expand Down
2 changes: 1 addition & 1 deletion lib/kitchen/driver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
module Kitchen

# A driver is responsible for carrying out the lifecycle activities of an
# instance, such as creating, converging, and destroying an instance.
# instance, such as creating and destroying an instance.
#
# @author Fletcher Nichol <[email protected]>
module Driver
Expand Down
9 changes: 1 addition & 8 deletions lib/kitchen/driver/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,6 @@ def name
def create(state) # rubocop:disable Lint/UnusedMethodArgument
end

# Converges a running instance.
#
# @param state [Hash] mutable instance and driver state
# @raise [ActionFailed] if the action could not be completed
def converge(state) # rubocop:disable Lint/UnusedMethodArgument
end

# Sets up an instance.
#
# @param state [Hash] mutable instance and driver state
Expand Down Expand Up @@ -128,7 +121,7 @@ class << self
# @param methods [Array<Symbol>] one or more actions as symbols
# @raise [ClientError] if any method is not a valid action method name
def self.no_parallel_for(*methods)
action_methods = [:create, :converge, :setup, :verify, :destroy]
action_methods = [:create, :setup, :verify, :destroy]

Array(methods).each do |meth|
next if action_methods.include?(meth)
Expand Down
5 changes: 0 additions & 5 deletions lib/kitchen/driver/dummy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,6 @@ def create(state)
report(:create, state)
end

# (see Base#converge)
def converge(state)
report(:converge, state)
end

# (see Base#setup)
def setup(state)
report(:setup, state)
Expand Down
38 changes: 35 additions & 3 deletions lib/kitchen/instance.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def create

# Converges this running instance.
#
# @see Driver::Base#converge
# @see Provisioner::Base#call
# @return [self] this instance, used to chain actions
#
# @todo rescue Driver::ActionFailed and return some kind of null object
Expand Down Expand Up @@ -323,11 +323,20 @@ def create_action

# Perform the converge action.
#
# @see Driver::Base#converge
# @see Provisioner::Base#call
# @return [self] this instance, used to chain actions
# @api private
def converge_action
perform_action(:converge, "Converging")
banner "Converging #{to_str}..."
elapsed = action(:converge) do |state|
if legacy_ssh_base_driver?
legacy_ssh_base_converge(state)
else
provisioner.call(state)
end
end
info("Finished converging #{to_str} #{Util.duration(elapsed.real)}.")
self
end

# Perform the setup action.
Expand Down Expand Up @@ -472,6 +481,29 @@ def failure_message(what)
"#{what.capitalize} failed on instance #{to_str}."
end

# Invokes `Driver#converge` on a legacy Driver, which inherits from
# `Kitchen::Driver::SSHBase`.
#
# @param state [Hash] mutable instance state
# @deprecated When legacy Driver::SSHBase support is removed, the
# `#converge` method will no longer be called on the Driver.
# @api private
def legacy_ssh_base_converge(state)
warn("Running legacy converge for '#{driver.name}' Driver")
# TODO: Document upgrade path and provide link
# warn("Driver authors: please read http://example.com for more details.")
driver.converge(state)
end

# @return [TrueClass,FalseClass] whether or not the Driver inherits from
# `Kitchen::Driver::SSHBase`
# @deprecated When legacy Driver::SSHBase support is removed, the
# `#converge` method will no longer be called on the Driver.
# @api private
def legacy_ssh_base_driver?
driver.class < Kitchen::Driver::SSHBase
end

# The simplest finite state machine pseudo-implementation needed to manage
# an Instance.
#
Expand Down
23 changes: 23 additions & 0 deletions lib/kitchen/provisioner/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,29 @@ def initialize(config = {})
init_config(config)
end

# Runs the provisioner on the instance.
#
# @param state [Hash] mutable instance state
# @raise [ActionFailed] if the action could not be completed
def call(state)
create_sandbox
sandbox_dirs = Dir.glob(File.join(sandbox_path, "*"))

instance.transport.connection(state) do |conn|
conn.execute(install_command)
conn.execute(init_command)
info("Transferring files to #{instance.to_str}")
conn.upload(sandbox_dirs, config[:root_path])
debug("Transfer complete")
conn.execute(prepare_command)
conn.execute(run_command)
end
rescue Kitchen::Transport::TransportFailed => ex
raise ActionFailed, ex.message
ensure
cleanup_sandbox
end

# Performs any final configuration required for the provisioner to do its
# work. A reference to an Instance is required as configuration dependant
# data may need access through an Instance. This also acts as a hook
Expand Down
47 changes: 47 additions & 0 deletions lib/kitchen/provisioner/dummy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,54 @@ module Kitchen

module Provisioner

# Dummy provisioner for Kitchen. This driver does nothing but report what
# would happen if this provisioner did anything of consequence. As a result
# it may be a useful provisioner to use when debugging or developing new
# features or plugins.
#
# @author Fletcher Nichol <[email protected]>
class Dummy < Kitchen::Provisioner::Base

default_config :sleep, 0
default_config :random_failure, false

# (see Base#call)
def call(state)
info("[#{name}] Converge on instance=#{instance} with state=#{state}")
sleep_if_set
failure_if_set
debug("[#{name}] Converge completed (#{config[:sleep]}s).")
end

private

# Sleep for a period of time, if a value is set in the config.
#
# @api private
def sleep_if_set
sleep(config[:sleep].to_f) if config[:sleep].to_f > 0.0
end

# Simulate a failure in an action, if set in the config.
#
# @api private
def failure_if_set
if config[:"fail"]
debug("Failure for Provisioner #{name}.")
raise ActionFailed, "Action #converge failed for #{instance.to_str}."
elsif config[:random_failure] && randomly_fail?
debug("Random failure for Provisioner #{name}.")
raise ActionFailed, "Action #converge failed for #{instance.to_str}."
end
end

# Determine whether or not to randomly fail.
#
# @return [true, false]
# @api private
def randomly_fail?
[true, false].sample
end
end
end
end
6 changes: 3 additions & 3 deletions spec/kitchen/driver/base_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Speedy < Base

class Dodgy < Base

no_parallel_for :converge
no_parallel_for :setup
end

class Slow < Base
Expand Down Expand Up @@ -105,7 +105,7 @@ class Slow < Base
logged_output.string.must_match(/yo\n/)
end

[:create, :converge, :setup, :verify, :destroy].each do |action|
[:create, :setup, :verify, :destroy].each do |action|

it "has a #{action} method that takes state" do
state = Hash.new
Expand All @@ -132,7 +132,7 @@ class Slow < Base
end

it "registers a single serial action method" do
Kitchen::Driver::Dodgy.serial_actions.must_equal [:converge]
Kitchen::Driver::Dodgy.serial_actions.must_equal [:setup]
end

it "registers multiple serial action methods" do
Expand Down
29 changes: 0 additions & 29 deletions spec/kitchen/driver/dummy_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -95,35 +95,6 @@
end
end

describe "#converge" do

it "calls sleep if :sleep value is greater than 0" do
config[:sleep] = 12.5
driver.expects(:sleep).with(12.5).returns(true)

driver.create(state)
end

it "raises ActionFailed if :fail_converge is set" do
config[:fail_converge] = true

proc { driver.converge(state) }.must_raise Kitchen::ActionFailed
end

it "randomly raises ActionFailed if :random_failure is set" do
config[:random_failure] = true
driver.stubs(:randomly_fail?).returns(true)

proc { driver.converge(state) }.must_raise Kitchen::ActionFailed
end

it "logs a converge event to INFO" do
driver.converge(state)

logged_output.string.must_match(/^.+ INFO .+ \[Dummy\] Converge on .+$/)
end
end

describe "#setup" do

it "calls sleep if :sleep value is greater than 0" do
Expand Down
Loading

0 comments on commit 3196675

Please sign in to comment.