summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/chef/config.rb14
-rw-r--r--lib/chef/file_access_control/unix.rb10
-rw-r--r--lib/chef/provider.rb13
-rw-r--r--lib/chef/provider/cookbook_file.rb83
-rw-r--r--lib/chef/provider/directory.rb34
-rw-r--r--lib/chef/provider/file.rb368
-rw-r--r--lib/chef/provider/file/content.rb75
-rw-r--r--lib/chef/provider/file/content/cookbook_file.rb50
-rw-r--r--lib/chef/provider/file/content/file.rb48
-rw-r--r--lib/chef/provider/file/content/remote_file.rb112
-rw-r--r--lib/chef/provider/file/content/template.rb58
-rw-r--r--lib/chef/provider/file/deploy/cp_unix.rb52
-rw-r--r--lib/chef/provider/file/deploy/mv_unix.rb60
-rw-r--r--lib/chef/provider/file/deploy/mv_windows.rb51
-rw-r--r--lib/chef/provider/file_strategies.rb7
-rw-r--r--lib/chef/provider/link.rb2
-rw-r--r--lib/chef/provider/remote_directory.rb3
-rw-r--r--lib/chef/provider/remote_file.rb124
-rw-r--r--lib/chef/provider/template.rb87
-rw-r--r--lib/chef/providers.rb10
-rw-r--r--lib/chef/resource/file.rb21
-rw-r--r--lib/chef/util/backup.rb84
-rw-r--r--lib/chef/util/diff.rb103
-rw-r--r--spec/functional/resource/cookbook_file_spec.rb2
-rw-r--r--spec/functional/resource/directory_spec.rb2
-rw-r--r--spec/functional/resource/file_spec.rb16
-rw-r--r--spec/functional/resource/remote_directory_spec.rb2
-rw-r--r--spec/functional/resource/remote_file_spec.rb16
-rw-r--r--spec/functional/resource/template_spec.rb17
-rw-r--r--spec/unit/provider/file_spec.rb867
-rw-r--r--spec/unit/util/backup_spec.rb151
-rw-r--r--spec/unit/util/diff_spec.rb254
32 files changed, 1833 insertions, 963 deletions
diff --git a/lib/chef/config.rb b/lib/chef/config.rb
index ca912b65ab..0be2eb7d31 100644
--- a/lib/chef/config.rb
+++ b/lib/chef/config.rb
@@ -344,5 +344,19 @@ class Chef
# returns a platform specific path to the user home dir
windows_home_path = ENV['SYSTEMDRIVE'] + ENV['HOMEPATH'] if ENV['SYSTEMDRIVE'] && ENV['HOMEPATH']
user_home(ENV['HOME'] || windows_home_path || ENV['USERPROFILE'])
+
+ # selinux command to restore file contexts
+ selinux_restorecon_command "/sbin/restorecon -R"
+ # guess if you're running selinux or not -- override this if it guesses wrong
+ selinux_enabled system( "/usr/sbin/selinuxenabled" )
+
+ # set this to something like Chef::Provider::File::Deploy::CpUnix if you want to override behavior globally
+ file_deployment_strategy nil
+
+ # do we create /tmp or %TEMP% files, or do we create temp files in the destination directory of the file?
+ # - on windows this avoids issues with permission inheritance with the %TEMP% directory (do not set this to false)
+ # - on unix this creates temp files like /etc/.sudoers.X-Y-Z and may create noise and make for itchy neckbeards
+ # - with selinux and other ACLs approaches it may still be useful or to avoid copying across filesystems
+ file_deployment_uses_destdir ( RUBY_PLATFORM =~ /mswin|mingw|windows/ )
end
end
diff --git a/lib/chef/file_access_control/unix.rb b/lib/chef/file_access_control/unix.rb
index 1dbfe40f2f..dbfe67c0fa 100644
--- a/lib/chef/file_access_control/unix.rb
+++ b/lib/chef/file_access_control/unix.rb
@@ -59,8 +59,10 @@ class Chef
uid_from_resource(current_resource)
end
+ # target_uid.nil? means the new_resource.owner is nil and the requesting owner doesn't care
+ # current_uid.nil? means the file does not exist
def should_update_owner?
- !target_uid.nil? && target_uid != current_uid
+ !target_uid.nil? && ( current_uid.nil? || target_uid != current_uid )
end
def set_owner!
@@ -103,7 +105,7 @@ class Chef
end
def should_update_group?
- !target_gid.nil? && target_gid != current_gid
+ !target_gid.nil? && ( current_gid.nil? || target_gid != current_gid )
end
def set_group!
@@ -136,7 +138,7 @@ class Chef
end
def should_update_mode?
- !target_mode.nil? && current_mode != target_mode
+ !target_mode.nil? && ( current_mode.nil? || current_mode != target_mode )
end
def set_mode!
@@ -192,7 +194,7 @@ class Chef
end
end
- def uid_from_resource(resource)
+ def uid_from_resource(resource)
return nil if resource == nil or resource.owner.nil?
if resource.owner.kind_of?(String)
diminished_radix_complement( Etc.getpwnam(resource.owner).uid )
diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb
index 3fe057bd9b..302a9fb106 100644
--- a/lib/chef/provider.rb
+++ b/lib/chef/provider.rb
@@ -103,7 +103,7 @@ class Chef
define_resource_requirements
process_resource_requirements
- # user-defined providers including LWRPs may
+ # user-defined providers including LWRPs may
# not include whyrun support - if they don't support it
# we can't execute any actions while we're running in
# whyrun mode. Instead we 'fake' whyrun by documenting that
@@ -133,7 +133,7 @@ class Chef
events.resource_up_to_date(@new_resource, @action)
else
events.resource_updated(@new_resource, @action)
- new_resource.updated_by_last_action(true)
+ new_resource.updated_by_last_action(true)
end
end
@@ -141,17 +141,16 @@ class Chef
@requirements ||= ResourceRequirements.new(@new_resource, run_context)
end
+ def converge_by(descriptions, &block)
+ converge_actions.add_action(descriptions, &block)
+ end
+
protected
def converge_actions
@converge_actions ||= ConvergeActions.new(@new_resource, run_context, @action)
end
- def converge_by(descriptions, &block)
- converge_actions.add_action(descriptions, &block)
- end
-
-
def recipe_eval(&block)
# This block has new resource definitions within it, which
# essentially makes it an in-line Chef run. Save our current
diff --git a/lib/chef/provider/cookbook_file.rb b/lib/chef/provider/cookbook_file.rb
index cde8dade61..e80da7593d 100644
--- a/lib/chef/provider/cookbook_file.rb
+++ b/lib/chef/provider/cookbook_file.rb
@@ -16,18 +16,15 @@
# limitations under the License.
#
-require 'chef/file_access_control'
require 'chef/provider/file'
-require 'tempfile'
class Chef
class Provider
class CookbookFile < Chef::Provider::File
- include Chef::Mixin::EnforceOwnershipAndPermissions
-
- def whyrun_supported?
- true
+ def initialize(new_resource, run_context)
+ @content_class = Chef::Provider::File::Content::CookbookFile
+ super
end
def load_current_resource
@@ -35,79 +32,7 @@ class Chef
super
end
- def action_create
- if file_cache_location && content_stale?
- description = []
- description << "create a new cookbook_file #{@new_resource.path}"
- description << diff_current(file_cache_location)
- converge_by(description) do
- Chef::Log.debug("#{@new_resource} has new contents")
- backup_new_resource
- deploy_tempfile do |tempfile|
- Chef::Log.debug("#{@new_resource} staging #{file_cache_location} to #{tempfile.path}")
- tempfile.close
- FileUtils.cp(file_cache_location, tempfile.path)
- enforce_tempfile_inheritance(tempfile.path)
- end
- update_new_file_state
- Chef::Log.info("#{@new_resource} created file #{@new_resource.path}")
- end
- else
- set_all_access_controls
- end
- end
-
- def file_cache_location
- @file_cache_location ||= begin
- cookbook = run_context.cookbook_collection[resource_cookbook]
- cookbook.preferred_filename_on_disk_location(node, :files, @new_resource.source, @new_resource.path)
- end
- end
-
- # Determine the cookbook to get the file from. If new resource sets an
- # explicit cookbook, use it, otherwise fall back to the implicit cookbook
- # i.e., the cookbook the resource was declared in.
- def resource_cookbook
- @new_resource.cookbook || @new_resource.cookbook_name
- end
-
- def backup_new_resource
- if ::File.exists?(@new_resource.path)
- backup @new_resource.path
- end
- end
-
- def content_stale?
- ( ! ::File.exist?(@new_resource.path)) || ( ! compare_content)
- end
-
- protected
-
- def enforce_tempfile_inheritance(tempfile_path)
- # On the Windows platform, files in the temp directory
- # default to not inherit unless the new resource
- # specifies rights of
- # some sort. Here we ensure that even when no rights
- # are
- # specified, the dacl's inheritance flag is set.
- if Chef::Platform.windows? &&
- @new_resource.rights.nil? &&
- @new_resource.group.nil? &&
- @new_resource.owner.nil? &&
- @new_resource.deny_rights.nil?
-
- securable_tempfile = Chef::ReservedNames::Win32::Security::SecurableObject.new(tempfile_path)
-
- # No rights were specified, so the dacl will have
- # no explicit aces
- default_dacl = Chef::ReservedNames::Win32::Security::ACL.create([])
-
- # In setting this default dacl, set inheritance to
- # true
- securable_tempfile.set_dacl(default_dacl, true)
- end
- end
-
end
end
end
+
diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb
index 8fdc070c77..e6886dde59 100644
--- a/lib/chef/provider/directory.rb
+++ b/lib/chef/provider/directory.rb
@@ -27,24 +27,16 @@ class Chef
class Provider
class Directory < Chef::Provider::File
- include Chef::Mixin::EnforceOwnershipAndPermissions
-
def whyrun_supported?
true
end
def load_current_resource
@current_resource = Chef::Resource::Directory.new(@new_resource.name)
- @current_resource.path(@new_resource.path)
- setup_acl
-
- @current_resource
+ super
end
def define_resource_requirements
- # this must be evaluated before whyrun messages are printed
- access_controls.requires_changes?
-
requirements.assert(:create) do |a|
# Make sure the parent dir exists, or else fail.
# for why run, print a message explaining the potential error.
@@ -61,8 +53,8 @@ class Chef
# find the lowest-level directory in @new_resource.path that already exists
# make sure we have write permissions to that directory
is_parent_writable = lambda do |base_dir|
- base_dir = ::File.dirname(base_dir)
- if ::File.exist?(base_dir)
+ base_dir = ::File.dirname(base_dir)
+ if ::File.exist?(base_dir)
::File.writable?(base_dir)
else
is_parent_writable.call(base_dir)
@@ -72,27 +64,27 @@ class Chef
else
# in why run mode & parent directory does not exist no permissions check is required
# If not in why run, permissions must be valid and we rely on prior assertion that dir exists
- if !whyrun_mode? || ::File.exist?(parent_directory)
+ if !whyrun_mode? || ::File.exist?(parent_directory)
::File.writable?(parent_directory)
else
true
end
end
end
- a.failure_message(Chef::Exceptions::InsufficientPermissions,
+ a.failure_message(Chef::Exceptions::InsufficientPermissions,
"Cannot create #{@new_resource} at #{@new_resource.path} due to insufficient permissions")
end
- requirements.assert(:delete) do |a|
- a.assertion do
+ requirements.assert(:delete) do |a|
+ a.assertion do
if ::File.exist?(@new_resource.path)
- ::File.directory?(@new_resource.path) && ::File.writable?(@new_resource.path)
+ ::File.directory?(@new_resource.path) && ::File.writable?(@new_resource.path)
else
true
end
end
a.failure_message(RuntimeError, "Cannot delete #{@new_resource} at #{@new_resource.path}!")
- # No why-run handling here:
+ # No why-run handling here:
# * if we don't have permissions, this is unlikely to be changed earlier in the run
# * if the target is a file (not a dir), there's no reasonable path by which this would have been changed
end
@@ -100,17 +92,17 @@ class Chef
def action_create
unless ::File.exist?(@new_resource.path)
- converge_by("create new directory #{@new_resource.path}") do
+ converge_by("create new directory #{@new_resource.path}") do
if @new_resource.recursive == true
::FileUtils.mkdir_p(@new_resource.path)
else
::Dir.mkdir(@new_resource.path)
end
Chef::Log.info("#{@new_resource} created directory #{@new_resource.path}")
- end
+ end
end
- set_all_access_controls
- update_new_file_state
+ do_acl_changes
+ load_resource_attributes_from_file(@new_resource)
end
def action_delete
diff --git a/lib/chef/provider/file.rb b/lib/chef/provider/file.rb
index 26c1618b60..b21651ee05 100644
--- a/lib/chef/provider/file.rb
+++ b/lib/chef/provider/file.rb
@@ -1,6 +1,7 @@
#
# Author:: Adam Jacob (<adam@opscode.com>)
-# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2008-2013 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -19,230 +20,74 @@
require 'chef/config'
require 'chef/log'
require 'chef/resource/file'
-require 'chef/mixin/checksum'
require 'chef/provider'
require 'etc'
require 'fileutils'
require 'chef/scan_access_control'
-require 'chef/mixin/shell_out'
+require 'chef/mixin/checksum'
+require 'chef/util/backup'
+require 'chef/util/diff'
+
+# The Tao of File Providers:
+# - the content provider must always return a tempfile that we can delete/mv
+# - do_create_file shall always create the file first and obey umask when perms are not specified
+# - do_contents_changes may assume the destination file exists (simplifies exception checking,
+# and always gives us something to diff against)
+# - do_contents_changes must restore the perms to the dest file and not obliterate them with
+# random tempfile permissions
+# - do_acl_changes may assume perms were not modified between lcr and when it runs (although the
+# file may have been created)
class Chef
-
class Provider
class File < Chef::Provider
include Chef::Mixin::EnforceOwnershipAndPermissions
include Chef::Mixin::Checksum
- include Chef::Mixin::ShellOut
- def negative_complement(big)
- if big > 1073741823 # Fixnum max
- big -= (2**32) # diminished radix wrap to negative
- end
- big
- end
+ attr_reader :deployment_strategy
- def octal_mode(mode)
- ((mode.respond_to?(:oct) ? mode.oct : mode.to_i) & 007777)
- end
-
- private :negative_complement, :octal_mode
-
- def diff_current_from_content(new_content)
- result = nil
- Tempfile.open("chef-diff") do |file|
- file.write new_content
- file.close
- result = diff_current file.path
- end
- result
+ def initialize(new_resource, run_context)
+ @content_class ||= Chef::Provider::File::Content::File
+ @deployment_strategy = new_resource.deployment_strategy.new() if new_resource.respond_to?(:deployment_strategy)
+ super
end
- def is_binary?(path)
- ::File.open(path) do |file|
-
- buff = file.read(Chef::Config[:diff_filesize_threshold])
- buff = "" if buff.nil?
- return buff !~ /^[\r[:print:]]*$/
- end
- end
-
-
- def diff_current(temp_path)
- suppress_resource_reporting = false
-
- return [ "(diff output suppressed by config)" ] if Chef::Config[:diff_disabled]
- return [ "(no temp file with new content, diff output suppressed)" ] unless ::File.exists?(temp_path) # should never happen?
-
- # solaris does not support diff -N, so create tempfile to diff against if we are creating a new file
- target_path = if ::File.exists?(@current_resource.path)
- @current_resource.path
- else
- suppress_resource_reporting = true # suppress big diffs going to resource reporting service
- tempfile = Tempfile.new('chef-tempfile')
- tempfile.path
- end
-
- diff_filesize_threshold = Chef::Config[:diff_filesize_threshold]
- diff_output_threshold = Chef::Config[:diff_output_threshold]
-
- if ::File.size(target_path) > diff_filesize_threshold || ::File.size(temp_path) > diff_filesize_threshold
- return [ "(file sizes exceed #{diff_filesize_threshold} bytes, diff output suppressed)" ]
- end
-
- # MacOSX(BSD?) diff will *sometimes* happily spit out nasty binary diffs
- return [ "(current file is binary, diff output suppressed)"] if is_binary?(target_path)
- return [ "(new content is binary, diff output suppressed)"] if is_binary?(temp_path)
-
- begin
- # -u: Unified diff format
- result = shell_out("diff -u #{target_path} #{temp_path}" )
- rescue Exception => e
- # Should *not* receive this, but in some circumstances it seems that
- # an exception can be thrown even using shell_out instead of shell_out!
- return [ "Could not determine diff. Error: #{e.message}" ]
- end
-
- # diff will set a non-zero return code even when there's
- # valid stdout results, if it encounters something unexpected
- # So as long as we have output, we'll show it.
- if not result.stdout.empty?
- if result.stdout.length > diff_output_threshold
- [ "(long diff of over #{diff_output_threshold} characters, diff output suppressed)" ]
- else
- val = result.stdout.split("\n")
- val.delete("\\ No newline at end of file")
- @new_resource.diff(val.join("\\n")) unless suppress_resource_reporting
- val
- end
- elsif not result.stderr.empty?
- [ "Could not determine diff. Error: #{result.stderr}" ]
- else
- [ "(no diff)" ]
- end
- end
-
def whyrun_supported?
true
end
def load_current_resource
- # Every child should be specifying their own constructor, so this
- # should only be run in the file case.
+ # Let children resources override constructing the @current_resource
@current_resource ||= Chef::Resource::File.new(@new_resource.name)
@new_resource.path.gsub!(/\\/, "/") # for Windows
@current_resource.path(@new_resource.path)
- if !::File.directory?(@new_resource.path)
- if ::File.exist?(@new_resource.path)
- if @action != :create_if_missing
- @current_resource.checksum(checksum(@new_resource.path))
- end
- end
- end
- setup_acl
-
+ load_resource_attributes_from_file(@current_resource)
@current_resource
end
- def setup_acl
- return if Chef::Platform.windows?
- acl_scanner = ScanAccessControl.new(@new_resource, @current_resource)
- acl_scanner.set_all!
- end
-
def define_resource_requirements
- # this must be evaluated before whyrun messages are printed
- access_controls.requires_changes?
-
+ # Make sure the parent directory exists, otherwise fail. For why-run assume it would have been created.
requirements.assert(:create, :create_if_missing, :touch) do |a|
- # Make sure the parent dir exists, or else fail.
- # for why run, print a message explaining the potential error.
parent_directory = ::File.dirname(@new_resource.path)
-
a.assertion { ::File.directory?(parent_directory) }
a.failure_message(Chef::Exceptions::EnclosingDirectoryDoesNotExist, "Parent directory #{parent_directory} does not exist.")
a.whyrun("Assuming directory #{parent_directory} would have been created")
end
- # Make sure the file is deletable if it exists. Otherwise, fail.
- requirements.assert(:delete) do |a|
- a.assertion do
- if ::File.exists?(@new_resource.path)
- ::File.writable?(@new_resource.path)
- else
- true
- end
- end
- a.failure_message(Chef::Exceptions::InsufficientPermissions,"File #{@new_resource.path} exists but is not writable so it cannot be deleted")
- end
- end
-
- # Compare the content of a file. Returns true if they are the same, false if they are not.
- def compare_content
- checksum(@current_resource.path) == new_resource_content_checksum
- end
-
- # Set the content of the file, assuming it is not set correctly already.
- def set_content
- unless compare_content
- description = []
- description << "update content in file #{@new_resource.path} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(new_resource_content_checksum)}"
- description << diff_current_from_content(@new_resource.content)
- converge_by(description) do
- backup @new_resource.path if ::File.exists?(@new_resource.path)
- ::File.open(@new_resource.path, "w") {|f| f.write @new_resource.content }
- Chef::Log.info("#{@new_resource} contents updated")
+ # Make sure the file is deletable if it exists, otherwise fail.
+ if ::File.exists?(@new_resource.path)
+ requirements.assert(:delete) do |a|
+ a.assertion { ::File.writable?(@new_resource.path) }
+ a.failure_message(Chef::Exceptions::InsufficientPermissions,"File #{@new_resource.path} exists but is not writable so it cannot be deleted")
end
end
end
- # if you are using a tempfile before creating, you must
- # override the default with the tempfile, since the
- # file at @new_resource.path will not be updated on converge
- def update_new_file_state(path=@new_resource.path)
- if !::File.directory?(path)
- @new_resource.checksum(checksum(path))
- end
-
- if Chef::Platform.windows?
- # TODO: To work around CHEF-3554, add support for Windows
- # equivalent, or implicit resource reporting won't work for
- # Windows.
- return
- end
-
- acl_scanner = ScanAccessControl.new(@new_resource, @new_resource)
- acl_scanner.set_all!
- end
-
def action_create
- if !::File.exists?(@new_resource.path)
- description = []
- desc = "create new file #{@new_resource.path}"
- desc << " with content checksum #{short_cksum(new_resource_content_checksum)}" if new_resource.content
- description << desc
- description << diff_current_from_content(@new_resource.content)
-
- converge_by(description) do
- Chef::Log.info("entered create")
- ::File.open(@new_resource.path, "w+") {|f| f.write @new_resource.content }
- access_controls.set_all
- Chef::Log.info("#{@new_resource} created file #{@new_resource.path}")
- update_new_file_state
- end
- else
- set_content unless @new_resource.content.nil?
- set_all_access_controls
- end
- end
-
- def set_all_access_controls
- if access_controls.requires_changes?
- converge_by(access_controls.describe_changes) do
- access_controls.set_all
- #Update file state with new access values
- update_new_file_state
- end
- end
+ do_create_file
+ do_contents_changes
+ do_acl_changes
+ load_resource_attributes_from_file(@new_resource)
end
def action_create_if_missing
@@ -255,7 +100,7 @@ class Chef
def action_delete
if ::File.exists?(@new_resource.path)
- converge_by("delete file #{@new_resource.path}") do
+ converge_by("delete file #{@new_resource.path}") do
backup unless ::File.symlink?(@new_resource.path)
::File.delete(@new_resource.path)
Chef::Log.info("#{@new_resource} deleted file at #{@new_resource.path}")
@@ -272,57 +117,132 @@ class Chef
end
end
- def backup(file=nil)
- file ||= @new_resource.path
- if @new_resource.backup != false && @new_resource.backup > 0 && ::File.exist?(file)
- time = Time.now
- savetime = time.strftime("%Y%m%d%H%M%S")
- backup_filename = "#{@new_resource.path}.chef-#{savetime}"
- backup_filename = backup_filename.sub(/^([A-Za-z]:)/, "") #strip drive letter on Windows
- # if :file_backup_path is nil, we fallback to the old behavior of
- # keeping the backup in the same directory. We also need to to_s it
- # so we don't get a type error around implicit to_str conversions.
- prefix = Chef::Config[:file_backup_path].to_s
- backup_path = ::File.join(prefix, backup_filename)
- FileUtils.mkdir_p(::File.dirname(backup_path)) if Chef::Config[:file_backup_path]
- FileUtils.cp(file, backup_path, :preserve => true)
- Chef::Log.info("#{@new_resource} backed up to #{backup_path}")
-
- # Clean up after the number of backups
- slice_number = @new_resource.backup
- backup_files = Dir[::File.join(prefix, ".#{@new_resource.path}.chef-*")].sort { |a,b| b <=> a }
- if backup_files.length >= @new_resource.backup
- remainder = backup_files.slice(slice_number..-1)
- remainder.each do |backup_to_delete|
- FileUtils.rm(backup_to_delete)
- Chef::Log.info("#{@new_resource} removed backup at #{backup_to_delete}")
- end
+ # deprecated methods to support
+
+ def set_content
+ end
+
+ def compare_content
+ end
+
+ def diff_current
+ end
+
+ def diff_current_from_content
+ end
+
+ def is_binary?(path)
+ end
+
+ def update_new_file_state
+ end
+
+ def whyrun_mode?
+ Chef::Log.warn("The method Chef::Provider::File#whyrun_mode? is deprecated and will be removed in Chef 12")
+ Chef::Config[:why_run]
+ end
+
+ private
+
+ def content
+ @content ||= begin
+ load_current_resource if @current_resource.nil?
+ @content_class.new(@new_resource, @current_resource, @run_context)
+ end
+ end
+
+ def do_create_file
+ @file_created = false
+ unless ::File.exists?(@new_resource.path)
+ description = "create new file #{@new_resource.path}"
+ converge_by(description) do
+ deployment_strategy.create(@new_resource.path)
+ Chef::Log.info("#{@new_resource} created file #{@new_resource.path}")
+ @file_created = true
end
end
end
- def deploy_tempfile
- Tempfile.open(::File.basename(@new_resource.name)) do |tempfile|
- yield tempfile
+ # do_contents_changes needs to know if do_create_file created a file or not
+ def file_created?
+ @file_created == true
+ end
- temp_res = Chef::Resource::CookbookFile.new(@new_resource.name)
- temp_res.path(tempfile.path)
- ac = Chef::FileAccessControl.new(temp_res, @new_resource, self)
- ac.set_all!
- FileUtils.mv(tempfile.path, @new_resource.path)
+ def backup(file = nil)
+ Chef::Util::Backup.new(@new_resource, file).backup!
+ end
+
+ def diff
+ @diff ||= Chef::Util::Diff.new
+ end
+
+ def do_contents_changes
+ # a nil tempfile is okay, means the resource has no content or no new content
+ return if tempfile.nil?
+ # but a tempfile that has no path or doesn't exist should not happen
+ if tempfile.path.nil? || !::File.exists?(tempfile.path)
+ raise "chef-client is confused, trying to deploy a file that has no path or does not exist..."
end
+ if contents_changed?
+ diff.diff(@current_resource.path, tempfile.path)
+ @new_resource.diff( diff.for_reporting ) unless file_created?
+ description = [ "update content in file #{@new_resource.path} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(checksum(tempfile.path))}" ]
+ description << diff.for_output
+ converge_by(description) do
+ backup unless file_created?
+ deployment_strategy.deploy(tempfile.path, @new_resource.path)
+ Chef::Log.info("#{@new_resource} updated file contents #{@new_resource.path}")
+ end
+ end
+ # unlink necessary to clean up in why-run mode
+ tempfile.unlink
end
- private
+ def do_acl_changes
+ if access_controls.requires_changes?
+ converge_by(access_controls.describe_changes) do
+ access_controls.set_all
+ end
+ end
+ end
+
+ def contents_changed?
+ checksum(tempfile.path) != @current_resource.checksum
+ end
+
+ def tempfile
+ content.tempfile
+ end
def short_cksum(checksum)
return "none" if checksum.nil?
checksum.slice(0,6)
end
- def new_resource_content_checksum
- @new_resource.content && Digest::SHA2.hexdigest(@new_resource.content)
+ # if you are using a tempfile before creating, you must
+ # override the default with the tempfile, since the
+ # file at @new_resource.path will not be updated on converge
+ def load_resource_attributes_from_file(resource)
+ if resource.respond_to?(:checksum)
+ if ::File.exists?(resource.path) && !::File.directory?(resource.path)
+ if @action != :create_if_missing # XXX: don't we break current_resource semantics by skipping this?
+ resource.checksum(checksum(resource.path))
+ end
+ end
+ end
+
+ if Chef::Platform.windows?
+ # TODO: To work around CHEF-3554, add support for Windows
+ # equivalent, or implicit resource reporting won't work for
+ # Windows.
+ return
+ end
+
+ acl_scanner = ScanAccessControl.new(@new_resource, resource)
+ acl_scanner.set_all!
end
+
end
end
end
+
diff --git a/lib/chef/provider/file/content.rb b/lib/chef/provider/file/content.rb
new file mode 100644
index 0000000000..dd5d98852c
--- /dev/null
+++ b/lib/chef/provider/file/content.rb
@@ -0,0 +1,75 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+class Chef
+ class Provider
+ class File
+ class Content
+
+ def initialize(new_resource, current_resource, run_context)
+ @new_resource = new_resource
+ @current_resource = current_resource
+ @run_context = run_context
+ end
+
+ def run_context
+ @run_context
+ end
+
+ def new_resource
+ @new_resource
+ end
+
+ def current_resource
+ @current_resource
+ end
+
+ def tempfile
+ @tempfile ||= file_for_provider
+ end
+
+ private
+
+ #
+ # Return something that looks like a File or Tempfile and
+ # you must assume the provider will unlink this file. Copy
+ # the contents to a Tempfile if you need to.
+ #
+ def file_for_provider
+ raise "class must implement file_for_provider!"
+ end
+
+ #
+ # These are important for windows to get permissions right, and may
+ # be useful for SELinux and other ACL approaches. Please use them
+ # as the arguments to Tempfile.new() consistently.
+ #
+ def tempfile_basename
+ basename = ::File.basename(@new_resource.name)
+ basename.insert 0, "." unless Chef::Platform.windows? # dotfile if we're not on windows
+ basename
+ end
+
+ def tempfile_dirname
+ Chef::Config[:file_deployment_uses_destdir] ? ::File.dirname(@new_resource.path) : Dir::tmpdir
+ end
+
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/file/content/cookbook_file.rb b/lib/chef/provider/file/content/cookbook_file.rb
new file mode 100644
index 0000000000..9071aa4a3a
--- /dev/null
+++ b/lib/chef/provider/file/content/cookbook_file.rb
@@ -0,0 +1,50 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+require 'chef/provider/file/content'
+
+class Chef
+ class Provider
+ class File
+ class Content
+ class CookbookFile < Chef::Provider::File::Content
+
+ private
+
+ def file_for_provider
+ cookbook = run_context.cookbook_collection[resource_cookbook]
+ file_cache_location = cookbook.preferred_filename_on_disk_location(run_context.node, :files, @new_resource.source, @new_resource.path)
+ if file_cache_location.nil?
+ nil
+ else
+ tempfile = Tempfile.open(tempfile_basename, tempfile_dirname)
+ tempfile.close
+ Chef::Log.debug("#{@new_resource} staging #{file_cache_location} to #{tempfile.path}")
+ FileUtils.cp(file_cache_location, tempfile.path)
+ tempfile
+ end
+ end
+
+ def resource_cookbook
+ @new_resource.cookbook || @new_resource.cookbook_name
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/file/content/file.rb b/lib/chef/provider/file/content/file.rb
new file mode 100644
index 0000000000..b0a1234956
--- /dev/null
+++ b/lib/chef/provider/file/content/file.rb
@@ -0,0 +1,48 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+require 'chef/provider/file/content'
+
+class Chef
+ class Provider
+ class File
+ class Content
+ class File < Chef::Provider::File::Content
+ def file_for_provider
+ if @new_resource.content
+ tempfile = Tempfile.open(tempfile_basename, tempfile_dirname)
+ tempfile.write(@new_resource.content)
+ tempfile.close
+ tempfile
+ else
+ nil
+ end
+ end
+
+ private
+
+ def tempfile_basename
+ basename = ::File.basename(@new_resource.name)
+ basename.insert 0, "." unless Chef::Platform.windows? # dotfile if we're not on windows
+ basename
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/file/content/remote_file.rb b/lib/chef/provider/file/content/remote_file.rb
new file mode 100644
index 0000000000..7e006d5415
--- /dev/null
+++ b/lib/chef/provider/file/content/remote_file.rb
@@ -0,0 +1,112 @@
+#
+# Author:: Jesse Campbell (<hikeit@gmail.com>)
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+require 'rest_client'
+require 'uri'
+require 'tempfile'
+require 'chef/provider/file/content'
+
+class Chef
+ class Provider
+ class File
+ class Content
+ class RemoteFile < Chef::Provider::File::Content
+
+ attr_reader :raw_file_source
+
+ private
+
+ def file_for_provider
+ Chef::Log.debug("#{@new_resource} checking for changes")
+
+ if current_resource_matches_target_checksum?
+ Chef::Log.debug("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating")
+ else
+ sources = @new_resource.source
+ raw_file, @raw_file_source = try_multiple_sources(sources)
+ end
+ raw_file
+ end
+
+ private
+
+ # Given an array of source uris, iterate through them until one does not fail
+ def try_multiple_sources(sources)
+ sources = sources.dup
+ source = sources.shift
+ begin
+ uri = URI.parse(source)
+ raw_file = grab_file_from_uri(uri)
+ rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPFatalError, Net::FTPError => e
+ Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e.to_s}")
+ if source = sources.shift
+ Chef::Log.info("#{@new_resource} trying to download from another mirror")
+ retry
+ else
+ raise e
+ end
+ end
+ if uri.userinfo
+ uri.password = "********"
+ end
+ return raw_file, uri.to_s
+ end
+
+ # Given a source uri, return a Tempfile, or a File that acts like a Tempfile (close! method)
+ def grab_file_from_uri(uri)
+ if_modified_since = @new_resource.last_modified
+ if_none_match = @new_resource.etag
+ uri_dup = uri.dup
+ if uri_dup.userinfo
+ uri_dup.password = "********"
+ end
+ if uri_dup.to_s == @current_resource.source[0]
+ if_modified_since ||= @current_resource.last_modified
+ if_none_match ||= @current_resource.etag
+ end
+ if URI::HTTP === uri
+ #HTTP or HTTPS
+ raw_file, mtime, etag = Chef::Provider::RemoteFile::HTTP.fetch(uri, if_modified_since, if_none_match)
+ elsif URI::FTP === uri
+ #FTP
+ raw_file, mtime = Chef::Provider::RemoteFile::FTP.fetch(uri, @new_resource.ftp_active_mode, if_modified_since)
+ etag = nil
+ elsif uri.scheme == "file"
+ #local/network file
+ raw_file, mtime = Chef::Provider::RemoteFile::LocalFile.fetch(uri, if_modified_since)
+ etag = nil
+ else
+ raise ArgumentError, "Invalid uri. Only http(s), ftp, and file are currently supported"
+ end
+ unless raw_file.nil?
+ @new_resource.etag etag unless @new_resource.etag
+ @new_resource.last_modified mtime unless @new_resource.last_modified
+ end
+ return raw_file
+ end
+
+ def current_resource_matches_target_checksum?
+ @new_resource.checksum && @current_resource.checksum && @current_resource.checksum =~ /^#{Regexp.escape(@new_resource.checksum)}/
+ end
+
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/file/content/template.rb b/lib/chef/provider/file/content/template.rb
new file mode 100644
index 0000000000..68673e2116
--- /dev/null
+++ b/lib/chef/provider/file/content/template.rb
@@ -0,0 +1,58 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+require 'chef/mixin/template'
+require 'chef/provider/file/content'
+
+class Chef
+ class Provider
+ class File
+ class Content
+ class Template < Chef::Provider::File::Content
+
+ include Chef::Mixin::Template
+
+ def template_location
+ @template_file_cache_location ||= begin
+ template_finder.find(@new_resource.source, :local => @new_resource.local, :cookbook => @new_resource.cookbook)
+ end
+ end
+
+ private
+
+ def file_for_provider
+ context = {}
+ context.merge!(@new_resource.variables)
+ context[:node] = @run_context.node
+ context[:template_finder] = template_finder
+ file = nil
+ render_template(IO.read(template_location), context) { |t| file = t }
+ file
+ end
+
+ def template_finder
+ @template_finder ||= begin
+ TemplateFinder.new(run_context, @new_resource.cookbook_name, @run_context.node)
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/provider/file/deploy/cp_unix.rb b/lib/chef/provider/file/deploy/cp_unix.rb
new file mode 100644
index 0000000000..1640c68538
--- /dev/null
+++ b/lib/chef/provider/file/deploy/cp_unix.rb
@@ -0,0 +1,52 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+#
+# PURPOSE: this strategy should be cross-platform and maintain SELinux contexts
+# and windows ACL inheritance, but it uses cp and is both slower and is
+# not atomic and may result in a corrupted destination file in low
+# disk or power outage situations.
+#
+
+class Chef
+ class Provider
+ class File
+ class Deploy
+ class CpUnix
+ def create(file)
+ Chef::Log.debug("touching #{file} to create it")
+ FileUtils.touch(file)
+ end
+
+ def deploy(src, dst)
+ # we are only responsible for content so restore the dst files perms
+ mode = ::File.stat(dst).mode & 07777
+ uid = ::File.stat(dst).uid
+ gid = ::File.stat(dst).gid
+ Chef::Log.debug("saved mode = #{mode.to_s(8)}, uid = #{uid}, gid = #{gid} from #{dst}")
+ Chef::Log.debug("copying temporary file #{src} into place at #{dst}")
+ FileUtils.cp(src, dst)
+ ::File.chmod(mode, dst)
+ ::File.chown(uid, gid, dst)
+ Chef::Log.debug("restored mode = #{mode.to_s(8)}, uid = #{uid}, gid = #{gid} to #{dst}")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/file/deploy/mv_unix.rb b/lib/chef/provider/file/deploy/mv_unix.rb
new file mode 100644
index 0000000000..5e8e5ce72e
--- /dev/null
+++ b/lib/chef/provider/file/deploy/mv_unix.rb
@@ -0,0 +1,60 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+#
+# PURPOSE: this strategy is atomic, does not mutate file modes, and supports selinux
+#
+# Note the FileUtils.mv does not have a preserve flag, and the preserve behavior of it is different
+# on different rubies (1.8.7 vs 1.9.x) so we are explicit about making certain the tempfile metadata
+# is not deployed (technically implementing preserve = false ourselves).
+#
+
+class Chef
+ class Provider
+ class File
+ class Deploy
+ class MvUnix
+ def create(file)
+ Chef::Log.debug("touching #{file} to create it")
+ FileUtils.touch(file)
+ end
+
+ def deploy(src, dst)
+ # we are only responsible for content so restore the dst files perms
+ Chef::Log.debug("reading modes from #{dst} file")
+ mode = ::File.stat(dst).mode & 07777
+ uid = ::File.stat(dst).uid
+ gid = ::File.stat(dst).gid
+ Chef::Log.debug("applying mode = #{mode.to_s(8)}, uid = #{uid}, gid = #{gid} to #{src}")
+ ::File.chmod(mode, src)
+ ::File.chown(uid, gid, src)
+ Chef::Log.debug("moving temporary file #{src} into place at #{dst}")
+ FileUtils.mv(src, dst)
+
+ # handle selinux if we need to run restorecon
+ if Chef::Config[:selinux_enabled]
+ Chef::Log.debug("selinux is enabled, fixing selinux permissions")
+ cmd = "#{Chef::Config[:selinux_restorecon_comand]} #{dst}"
+ shell_out!(cmd)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/file/deploy/mv_windows.rb b/lib/chef/provider/file/deploy/mv_windows.rb
new file mode 100644
index 0000000000..2c4d01c6ae
--- /dev/null
+++ b/lib/chef/provider/file/deploy/mv_windows.rb
@@ -0,0 +1,51 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+#
+# PURPOSE: this strategy is atomic and preserves default umasks, but on windows you must
+# not be copying from the temp directory, and will not correctly restore
+# SELinux contexts.
+#
+
+class Chef
+ class Provider
+ class File
+ class Deploy
+ class MvWindows
+ def create(file)
+ Chef::Log.debug("touching #{file} to create it")
+ FileUtils.touch(file)
+ end
+
+ def deploy(src, dst)
+ if ::File.dirname(src) != ::File.dirname(dst)
+ # internal warning for now - in a Windows/SElinux/ACLs world its better to write
+ # a tempfile to your destination directory and then rename it
+ Chef::Log.debug("WARNING: moving tempfile across different directories -- this may break permissions")
+ end
+
+ # FIXME: save all the windows perms off the dst
+ FileUtils.mv(src, dst)
+ # FIXME: restore all the windows perms onto the dst
+
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/provider/file_strategies.rb b/lib/chef/provider/file_strategies.rb
new file mode 100644
index 0000000000..ca2e9d7be2
--- /dev/null
+++ b/lib/chef/provider/file_strategies.rb
@@ -0,0 +1,7 @@
+require 'chef/provider/file_strategy/content_from_resource'
+require 'chef/provider/file_strategy/content_from_cookbook_file'
+require 'chef/provider/file_strategy/content_from_remote_file'
+require 'chef/provider/file_strategy/content_from_template'
+require 'chef/provider/file_strategy/deploy_cp'
+require 'chef/provider/file_strategy/deploy_mv'
+require 'chef/provider/file_strategy/deploy_mv_with_restorecon'
diff --git a/lib/chef/provider/link.rb b/lib/chef/provider/link.rb
index b8017c4558..5a29aff623 100644
--- a/lib/chef/provider/link.rb
+++ b/lib/chef/provider/link.rb
@@ -112,7 +112,7 @@ class Chef
end
if @new_resource.link_type == :symbolic
if access_controls.requires_changes?
- converge_by(access_controls.describe_changes) do
+ converge_by(access_controls.describe_changes) do
access_controls.set_all
end
end
diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb
index 0412e8a668..bd4243cd84 100644
--- a/lib/chef/provider/remote_directory.rb
+++ b/lib/chef/provider/remote_directory.rb
@@ -31,7 +31,6 @@ class Chef
class Provider
class RemoteDirectory < Chef::Provider::Directory
- include Chef::Mixin::EnforceOwnershipAndPermissions
include Chef::Mixin::FileClass
def action_create
@@ -67,7 +66,7 @@ class Chef
if @new_resource.purge
unmanaged_files.sort.reverse.each do |f|
# file_class comes from Chef::Mixin::FileClass
- if ::File.directory?(f) && !Chef::Platform.windows? && !file_class.symlink?(f.dup)
+ if ::File.directory?(f) && !Chef::Platform.windows? && !file_class.symlink?(f.dup)
# Linux treats directory symlinks as files
# Remove a directory as a directory when not on windows if it is not a symlink
purge_directory(f)
diff --git a/lib/chef/provider/remote_file.rb b/lib/chef/provider/remote_file.rb
index 655a21a75c..df95be176b 100644
--- a/lib/chef/provider/remote_file.rb
+++ b/lib/chef/provider/remote_file.rb
@@ -18,14 +18,15 @@
#
require 'chef/provider/file'
-require 'uri'
-require 'tempfile'
class Chef
class Provider
class RemoteFile < Chef::Provider::File
- include Chef::Mixin::EnforceOwnershipAndPermissions
+ def initialize(new_resource, run_context)
+ @content_class = Chef::Provider::File::Content::RemoteFile
+ super
+ end
def load_current_resource
@current_resource = Chef::Resource::RemoteFile.new(@new_resource.name)
@@ -39,124 +40,12 @@ class Chef
end
def action_create
- Chef::Log.debug("#{@new_resource} checking for changes")
-
- if current_resource_matches_target_checksum?
- Chef::Log.debug("#{@new_resource} checksum matches target checksum (#{@new_resource.checksum}) - not updating")
- else
- sources = @new_resource.source
- raw_file, raw_file_source = try_multiple_sources(sources)
- if raw_file.nil?
- Chef::Log.info("#{@new_resource} matched #{raw_file_source}, not updating")
- elsif matches_current_checksum?(raw_file)
- Chef::Log.info("#{@new_resource} downloaded from #{raw_file_source}, checksums match, not updating")
- raw_file.close!
- else
- Chef::Log.info("#{@new_resource} downloaded from #{raw_file_source}")
- description = []
- description << "copy file downloaded from #{raw_file_source} into #{@new_resource.path}"
- description << diff_current(raw_file.path)
- converge_by(description) do
- backup_new_resource
- FileUtils.cp raw_file.path, @new_resource.path
- Chef::Log.info "#{@new_resource} updated"
- raw_file.close!
- save_fileinfo(raw_file_source)
- end
- # whyrun mode cleanup - the temp file will never be used,
- # so close/unlink it here.
- if whyrun_mode?
- raw_file.close!
- end
- end
- end
- set_all_access_controls
- update_new_file_state
- end
-
- def current_resource_matches_target_checksum?
- @new_resource.checksum && @current_resource.checksum && @current_resource.checksum =~ /^#{Regexp.escape(@new_resource.checksum)}/
- end
-
- def matches_current_checksum?(candidate_file)
- Chef::Log.debug "#{@new_resource} checking for file existence of #{@new_resource.path}"
- @new_resource.checksum(checksum(candidate_file.path))
- if ::File.exists?(@new_resource.path)
- Chef::Log.debug "#{@new_resource} file exists at #{@new_resource.path}"
- Chef::Log.debug "#{@new_resource} target checksum: #{@current_resource.checksum}"
- Chef::Log.debug "#{@new_resource} source checksum: #{@new_resource.checksum}"
-
- @new_resource.checksum == @current_resource.checksum
- else
- Chef::Log.debug "#{@new_resource} creating #{@new_resource.path}"
- false
- end
- end
-
- def backup_new_resource
- if ::File.exists?(@new_resource.path)
- Chef::Log.debug "#{@new_resource} checksum changed from #{@current_resource.checksum} to #{@new_resource.checksum}"
- backup @new_resource.path
- end
+ super
+ save_fileinfo(@content.raw_file_source)
end
private
- # Given an array of source uris, iterate through them until one does not fail
- def try_multiple_sources(sources)
- sources = sources.dup
- source = sources.shift
- begin
- uri = URI.parse(source)
- raw_file = grab_file_from_uri(uri)
- rescue SocketError, Errno::ECONNREFUSED, Errno::ENOENT, Errno::EACCES, Timeout::Error, Net::HTTPFatalError, Net::FTPError => e
- Chef::Log.warn("#{@new_resource} cannot be downloaded from #{source}: #{e.to_s}")
- if source = sources.shift
- Chef::Log.info("#{@new_resource} trying to download from another mirror")
- retry
- else
- raise e
- end
- end
- if uri.userinfo
- uri.password = "********"
- end
- return raw_file, uri.to_s
- end
-
- # Given a source uri, return a Tempfile, or a File that acts like a Tempfile (close! method)
- def grab_file_from_uri(uri)
- if_modified_since = @new_resource.last_modified
- if_none_match = @new_resource.etag
- uri_dup = uri.dup
- if uri_dup.userinfo
- uri_dup.password = "********"
- end
- if uri_dup.to_s == @current_resource.source[0]
- if_modified_since ||= @current_resource.last_modified
- if_none_match ||= @current_resource.etag
- end
- if URI::HTTP === uri
- #HTTP or HTTPS
- raw_file, mtime, etag = RemoteFile::HTTP.fetch(uri, if_modified_since, if_none_match)
- elsif URI::FTP === uri
- #FTP
- raw_file, mtime = RemoteFile::FTP.fetch(uri, @new_resource.ftp_active_mode, if_modified_since)
- etag = nil
- elsif uri.scheme == "file"
- #local/network file
- raw_file, mtime = RemoteFile::LocalFile.fetch(uri, if_modified_since)
- etag = nil
- else
- raise ArgumentError, "Invalid uri. Only http(s), ftp, and file are currently supported"
- end
- unless raw_file.nil?
- @new_resource.etag etag unless @new_resource.etag
- @new_resource.last_modified mtime unless @new_resource.last_modified
- end
- return raw_file
- end
-
def load_fileinfo
begin
Chef::JSONCompat.from_json(Chef::FileCache.load("remote_file/#{new_resource.name}"))
@@ -177,3 +66,4 @@ class Chef
end
end
end
+
diff --git a/lib/chef/provider/template.rb b/lib/chef/provider/template.rb
index 6df671c8e5..064f9fde74 100644
--- a/lib/chef/provider/template.rb
+++ b/lib/chef/provider/template.rb
@@ -19,18 +19,15 @@
require 'chef/provider/template_finder'
require 'chef/provider/file'
-require 'chef/mixin/template'
-require 'chef/mixin/checksum'
-require 'chef/file_access_control'
class Chef
class Provider
-
class Template < Chef::Provider::File
- include Chef::Mixin::EnforceOwnershipAndPermissions
- include Chef::Mixin::Checksum
- include Chef::Mixin::Template
+ def initialize(new_resource, run_context)
+ @content_class = Chef::Provider::File::Content::Template
+ super
+ end
def load_current_resource
@current_resource = Chef::Resource::Template.new(@new_resource.name)
@@ -40,81 +37,15 @@ class Chef
def define_resource_requirements
super
- requirements.assert(:create, :create_if_missing) do |a|
- a.assertion { ::File::exist?(template_location) }
- a.failure_message "Template source #{template_location} could not be found."
- a.whyrun "Template source #{template_location} does not exist. Assuming it would have been created."
+ requirements.assert(:create, :create_if_missing) do |a|
+ a.assertion { ::File::exist?(content.template_location) }
+ a.failure_message "Template source #{content.template_location} could not be found."
+ a.whyrun "Template source #{content.template_location} does not exist. Assuming it would have been created."
a.block_action!
end
end
- def action_create
- render_with_context(template_location) do |rendered_template|
- rendered(rendered_template)
- if file_already_exists? && content_matches?
- Chef::Log.debug("#{@new_resource} content has not changed.")
- set_all_access_controls
- update_new_file_state(@new_resource.path)
- else
- description = []
- action_message = if file_already_exists?
- "update #{@current_resource} from #{short_cksum(@current_resource.checksum)} to #{short_cksum(@new_resource.checksum)}"
- else
- "create #{@new_resource}"
- end
- description << action_message
- description << diff_current(rendered_template.path)
- converge_by(description) do
- backup
- FileUtils.cp(rendered_template.path, @new_resource.path)
- Chef::Log.info("#{@new_resource} updated content")
- access_controls.set_all!
- update_new_file_state(@new_resource.path)
- end
- end
- end
- end
-
- def template_finder
- @template_finder ||= begin
- TemplateFinder.new(run_context, cookbook_name, node)
- end
- end
-
- def template_location
- @template_file_cache_location ||= begin
- template_finder.find(@new_resource.source, :local => @new_resource.local, :cookbook => @new_resource.cookbook)
- end
- end
-
- def resource_cookbook
- @new_resource.cookbook || @new_resource.cookbook_name
- end
-
- def rendered(rendered_template)
- @new_resource.checksum(checksum(rendered_template.path))
- Chef::Log.debug("Current content's checksum: #{@current_resource.checksum}")
- Chef::Log.debug("Rendered content's checksum: #{@new_resource.checksum}")
- end
-
- def content_matches?
- @current_resource.checksum == @new_resource.checksum
- end
-
- private
-
- def file_already_exists?
- ::File.exist?(@new_resource.path)
- end
-
- def render_with_context(template_location, &block)
- context = {}
- context.merge!(@new_resource.variables)
- context[:node] = node
- context[:template_finder] = template_finder
- render_template(IO.read(template_location), context, &block)
- end
-
end
end
end
+
diff --git a/lib/chef/providers.rb b/lib/chef/providers.rb
index f4b3175546..500bc727c8 100644
--- a/lib/chef/providers.rb
+++ b/lib/chef/providers.rb
@@ -107,3 +107,13 @@ require 'chef/provider/remote_file/local_file'
require "chef/provider/lwrp_base"
require 'chef/provider/registry_key'
+
+require 'chef/provider/file/content'
+require 'chef/provider/file/content/file'
+require 'chef/provider/file/content/remote_file'
+require 'chef/provider/file/content/cookbook_file'
+require 'chef/provider/file/content/template'
+require 'chef/provider/file/deploy/cp_unix'
+require 'chef/provider/file/deploy/mv_unix'
+require 'chef/provider/file/deploy/mv_windows'
+
diff --git a/lib/chef/resource/file.rb b/lib/chef/resource/file.rb
index 0b92f3332d..cec70857cd 100644
--- a/lib/chef/resource/file.rb
+++ b/lib/chef/resource/file.rb
@@ -46,6 +46,13 @@ class Chef
@action = "create"
@allowed_actions.push(:create, :delete, :touch, :create_if_missing)
@provider = Chef::Provider::File
+ @deployment_strategy = Chef::Config[:file_deployment_strategy]
+ @deployment_strategy ||= if Chef::Platform.windows?
+ Chef::Provider::File::Deploy::MvWindows
+ else
+ Chef::Provider::File::Deploy::MvUnix
+ end
+
@diff = nil
end
@@ -81,7 +88,7 @@ class Chef
:kind_of => String
)
end
-
+
def diff(arg=nil)
set_or_return(
:diff,
@@ -90,6 +97,18 @@ class Chef
)
end
+ def deployment_strategy(arg=nil)
+ klass = if arg.kind_of?(String) || arg.kind_of?(Symbol)
+ lookup_provider_constant(arg)
+ else
+ arg
+ end
+ set_or_return(
+ :deployment_strategy,
+ klass,
+ :kind_of => [ Class ]
+ )
+ end
end
end
diff --git a/lib/chef/util/backup.rb b/lib/chef/util/backup.rb
new file mode 100644
index 0000000000..95c85d9751
--- /dev/null
+++ b/lib/chef/util/backup.rb
@@ -0,0 +1,84 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+class Chef
+ class Util
+ class Backup
+ attr_reader :new_resource
+ attr_accessor :path
+
+ def initialize(new_resource, path = nil)
+ @new_resource = new_resource
+ @path = path.nil? ? new_resource.path : path
+ end
+
+ def backup!
+ if @new_resource.backup != false && @new_resource.backup > 0 && ::File.exist?(path)
+ do_backup
+ # Clean up after the number of backups
+ slice_number = @new_resource.backup
+ backup_files = sorted_backup_files
+ if backup_files.length >= @new_resource.backup
+ remainder = backup_files.slice(slice_number..-1)
+ remainder.each do |backup_to_delete|
+ delete_backup(backup_to_delete)
+ end
+ end
+ end
+ end
+
+ private
+
+ def backup_filename
+ @backup_filename ||= begin
+ time = Time.now
+ savetime = time.strftime("%Y%m%d%H%M%S")
+ backup_filename = "#{path}.chef-#{savetime}"
+ backup_filename = backup_filename.sub(/^([A-Za-z]:)/, "") #strip drive letter on Windows
+ end
+ end
+
+ def prefix
+ # if :file_backup_path is nil, we fallback to the old behavior of
+ # keeping the backup in the same directory. We also need to to_s it
+ # so we don't get a type error around implicit to_str conversions.
+ @prefix ||= Chef::Config[:file_backup_path].to_s
+ end
+
+ def backup_path
+ @backup_path ||= ::File.join(prefix, backup_filename)
+ end
+
+ def do_backup
+ FileUtils.mkdir_p(::File.dirname(backup_path)) if Chef::Config[:file_backup_path]
+ FileUtils.cp(path, backup_path, :preserve => true)
+ Chef::Log.info("#{@new_resource} backed up to #{backup_path}")
+ end
+
+ def delete_backup(backup_file)
+ FileUtils.rm(backup_file)
+ Chef::Log.info("#{@new_resource} removed backup at #{backup_file}")
+ end
+
+ def sorted_backup_files
+ Dir[::File.join(prefix, ".#{path}.chef-*")].sort { |a,b| b <=> a }
+ end
+ end
+ end
+end
+
diff --git a/lib/chef/util/diff.rb b/lib/chef/util/diff.rb
new file mode 100644
index 0000000000..72459642de
--- /dev/null
+++ b/lib/chef/util/diff.rb
@@ -0,0 +1,103 @@
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+require 'chef/mixin/shell_out'
+
+class Chef
+ class Util
+ class Diff
+ include Chef::Mixin::ShellOut
+
+ def for_output
+ # formatted output to a terminal uses arrays of strings and returns error strings
+ @diff.nil? ? [ @error ] : @diff
+ end
+
+ def for_reporting
+ # caller needs to ensure that new files aren't posted to resource reporting
+ return nil if @diff.nil?
+ @diff.join("\\n")
+ end
+
+ def diff(old_file, new_file)
+ # indicates calling code bug: caller is reponsible for making certain both
+ # files exist
+ raise "old file #{old_file} does not exist" unless File.exists?(old_file)
+ raise "new file #{new_file} does not exist" unless File.exists?(new_file)
+ @error = catch (:nodiff) do
+ do_diff(old_file, new_file)
+ end
+ end
+
+ private
+
+ def do_diff(old_file, new_file)
+ if Chef::Config[:diff_disabled]
+ throw :nodiff, "(diff output suppressed by config)"
+ end
+
+ diff_filesize_threshold = Chef::Config[:diff_filesize_threshold]
+ diff_output_threshold = Chef::Config[:diff_output_threshold]
+
+ if ::File.size(old_file) > diff_filesize_threshold || ::File.size(new_file) > diff_filesize_threshold
+ throw :nodiff, "(file sizes exceed #{diff_filesize_threshold} bytes, diff output suppressed)"
+ end
+
+ # MacOSX(BSD?) diff will *sometimes* happily spit out nasty binary diffs
+ throw :nodiff, "(current file is binary, diff output suppressed)" if is_binary?(old_file)
+ throw :nodiff, "(new content is binary, diff output suppressed)" if is_binary?(new_file)
+
+ begin
+ # -u: Unified diff format
+ result = shell_out("diff -u #{old_file} #{new_file}")
+ rescue Exception => e
+ # Should *not* receive this, but in some circumstances it seems that
+ # an exception can be thrown even using shell_out instead of shell_out!
+ throw :nodiff, "Could not determine diff. Error: #{e.message}"
+ end
+
+ # diff will set a non-zero return code even when there's
+ # valid stdout results, if it encounters something unexpected
+ # So as long as we have output, we'll show it.
+ if not result.stdout.empty?
+ if result.stdout.length > diff_output_threshold
+ throw :nodiff, "(long diff of over #{diff_output_threshold} characters, diff output suppressed)"
+ else
+ @diff = result.stdout.split("\n")
+ @diff.delete("\\ No newline at end of file")
+ # XXX: successful return of the diff is here, we return nil as no error... ugh...
+ return nil
+ end
+ elsif not result.stderr.empty?
+ throw :nodiff, "Could not determine diff. Error: #{result.stderr}"
+ else
+ throw :nodiff, "(no diff)"
+ end
+ end
+
+ def is_binary?(path)
+ ::File.open(path) do |file|
+ buff = file.read(Chef::Config[:diff_filesize_threshold])
+ buff = "" if buff.nil?
+ return buff !~ /^[\r[:print:]]*$/
+ end
+ end
+
+ end
+ end
+end
+
diff --git a/spec/functional/resource/cookbook_file_spec.rb b/spec/functional/resource/cookbook_file_spec.rb
index 9977cd6c99..d61668853c 100644
--- a/spec/functional/resource/cookbook_file_spec.rb
+++ b/spec/functional/resource/cookbook_file_spec.rb
@@ -32,7 +32,7 @@ describe Chef::Resource::CookbookFile do
content
end
- let(:default_mode) { "600" }
+ let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) }
it_behaves_like "a securable resource with reporting"
diff --git a/spec/functional/resource/directory_spec.rb b/spec/functional/resource/directory_spec.rb
index 9ae0503336..0401f506c3 100644
--- a/spec/functional/resource/directory_spec.rb
+++ b/spec/functional/resource/directory_spec.rb
@@ -23,7 +23,7 @@ describe Chef::Resource::Directory do
let(:directory_base) { "directory_spec" }
- let(:default_mode) { "755" }
+ let(:default_mode) { ((0100777 - File.umask) & 07777).to_s(8) }
def create_resource
events = Chef::EventDispatch::Dispatcher.new
diff --git a/spec/functional/resource/file_spec.rb b/spec/functional/resource/file_spec.rb
index 2c18d07520..7da15ff2e5 100644
--- a/spec/functional/resource/file_spec.rb
+++ b/spec/functional/resource/file_spec.rb
@@ -58,6 +58,22 @@ describe Chef::Resource::File do
it_behaves_like "a securable resource with reporting"
+ describe "when running action :create without content" do
+ before do
+ resource_without_content.run_action(:create)
+ end
+
+ context "and the target file does not exist" do
+ it "creates the file" do
+ File.should exist(path)
+ end
+
+ it "is marked updated by last action" do
+ resource_without_content.should be_updated_by_last_action
+ end
+ end
+ end
+
describe "when running action :touch" do
context "and the target file does not exist" do
before do
diff --git a/spec/functional/resource/remote_directory_spec.rb b/spec/functional/resource/remote_directory_spec.rb
index b4e26a59b2..10d4d973e9 100644
--- a/spec/functional/resource/remote_directory_spec.rb
+++ b/spec/functional/resource/remote_directory_spec.rb
@@ -22,7 +22,7 @@ describe Chef::Resource::RemoteDirectory do
include_context Chef::Resource::Directory
let(:directory_base) { "directory_spec" }
- let(:default_mode) { "755" }
+ let(:default_mode) { ((0100777 - File.umask) & 07777).to_s(8) }
def create_resource
cookbook_repo = File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks"))
diff --git a/spec/functional/resource/remote_file_spec.rb b/spec/functional/resource/remote_file_spec.rb
index fbb921d48c..0354cc1aa3 100644
--- a/spec/functional/resource/remote_file_spec.rb
+++ b/spec/functional/resource/remote_file_spec.rb
@@ -37,21 +37,7 @@ describe Chef::Resource::RemoteFile do
create_resource
end
- let(:default_mode) do
- # TODO: Lots of ugly here :(
- # RemoteFile uses FileUtils.cp. FileUtils does a copy by opening the
- # destination file and writing to it. Before 1.9.3, it does not preserve
- # the mode of the copied file. In 1.9.3 and after, it does. So we have to
- # figure out what the default mode ought to be via heuristic.
-
- t = Tempfile.new("get-the-mode")
- path = t.path
- path_2 = t.path + "fileutils-mode-test"
- FileUtils.cp(path, path_2)
- t.close
- m = File.stat(path_2).mode
- (07777 & m).to_s(8)
- end
+ let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) }
before(:all) do
@server = TinyServer::Manager.new
diff --git a/spec/functional/resource/template_spec.rb b/spec/functional/resource/template_spec.rb
index 0987aabf05..ae568a496a 100644
--- a/spec/functional/resource/template_spec.rb
+++ b/spec/functional/resource/template_spec.rb
@@ -49,22 +49,7 @@ describe Chef::Resource::Template do
create_resource
end
- let(:default_mode) do
- # TODO: Lots of ugly here :(
- # RemoteFile uses FileUtils.cp. FileUtils does a copy by opening the
- # destination file and writing to it. Before 1.9.3, it does not preserve
- # the mode of the copied file. In 1.9.3 and after, it does. So we have to
- # figure out what the default mode ought to be via heuristic.
-
- t = Tempfile.new("get-the-mode")
- path = t.path
- path_2 = t.path + "fileutils-mode-test"
- FileUtils.cp(path, path_2)
- t.close
- m = File.stat(path_2).mode
- (07777 & m).to_s(8)
- end
-
+ let(:default_mode) { ((0100666 - File.umask) & 07777).to_s(8) }
it_behaves_like "a file resource"
diff --git a/spec/unit/provider/file_spec.rb b/spec/unit/provider/file_spec.rb
index 5b1cbbbdb8..7e99e6755b 100644
--- a/spec/unit/provider/file_spec.rb
+++ b/spec/unit/provider/file_spec.rb
@@ -1,6 +1,7 @@
#
# Author:: Adam Jacob (<adam@opscode.com>)
-# Copyright:: Copyright (c) 2008 Opscode, Inc.
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2008-2013 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,499 +17,575 @@
# limitations under the License.
#
-
require 'spec_helper'
require 'tmpdir'
describe Chef::Provider::File do
- before(:each) do
- @node = Chef::Node.new
- @node.name "latte"
- @events = Chef::EventDispatch::Dispatcher.new
- @run_context = Chef::RunContext.new(@node, {}, @events)
- @resource = Chef::Resource::File.new("seattle")
- @resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "seattle.txt")))
+ # Mocksplosion
+
+ let(:node) { double('Chef::Node') }
+ let(:events) { double('Chef::Events').as_null_object } # mock all the methods
+ let(:run_context) { double('Chef::RunContext', :node => node, :events => events) }
+ let(:enclosing_directory) { File.expand_path(File.join(CHEF_SPEC_DATA, "templates")) }
+ let(:resource_path) { File.expand_path(File.join(enclosing_directory, "seattle.txt")) }
- @provider = Chef::Provider::File.new(@resource, @run_context)
+ let(:resource) do
+ # need to check for/against mutating state within the new_resource, so don't mock
+ resource = Chef::Resource::File.new("seattle")
+ resource.path(resource_path)
+ resource
end
- it "should return a Chef::Provider::File" do
- @provider.should be_a_kind_of(Chef::Provider::File)
+ # Subject
+
+ let(:provider) do
+ Chef::Provider::File.new(resource, run_context)
end
- it "should store the resource passed to new as new_resource" do
- @provider.new_resource.should eql(@resource)
+ # Filesystem stubs
+
+ def setup_normal_file
+ File.stub!(:exists?).with(resource_path).and_return(true)
+ File.stub!(:directory?).with(resource_path).and_return(false)
+ File.stub!(:directory?).with(enclosing_directory).and_return(true)
+ File.stub!(:writable?).with(resource_path).and_return(true)
+ File.stub!(:symlink?).with(resource_path).and_return(false)
end
- it "should store the node passed to new as node" do
- @provider.node.should eql(@node)
+ def setup_missing_file
+ File.stub!(:exists?).with(resource_path).and_return(false)
+ File.stub!(:directory?).with(resource_path).and_return(false)
+ File.stub!(:directory?).with(enclosing_directory).and_return(true)
+ File.stub!(:writable?).with(resource_path).and_return(false)
+ File.stub!(:symlink?).with(resource_path).and_return(false)
end
- it "should load a current resource based on the one specified at construction" do
- @provider.load_current_resource
- @provider.current_resource.should be_a_kind_of(Chef::Resource::File)
- @provider.current_resource.name.should eql(@resource.name)
- @provider.current_resource.path.should eql(@resource.path)
- @provider.current_resource.content.should eql(nil)
+ def setup_symlink
+ File.stub!(:exists?).with(resource_path).and_return(true)
+ File.stub!(:directory?).with(resource_path).and_return(false)
+ File.stub!(:directory?).with(enclosing_directory).and_return(true)
+ File.stub!(:writable?).with(resource_path).and_return(true)
+ File.stub!(:symlink?).with(resource_path).and_return(true)
end
- describe "examining file security metadata on Unix" do
- before do
- Chef::Platform.stub!(:windows?).and_return(false)
- end
- it "should collect the current state of the file on the filesystem and populate current_resource" do
- # test setup
- stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000)
- ::File.should_receive(:stat).exactly(1).times.with(@resource.path).and_return(stat_struct)
+ def setup_unwritable_file
+ File.stub!(:exists?).with(resource_path).and_return(true)
+ File.stub!(:directory?).with(resource_path).and_return(false)
+ File.stub!(:directory?).with(enclosing_directory).and_return(true)
+ File.stub!(:writable?).with(resource_path).and_return(false)
+ File.stub!(:symlink?).with(resource_path).and_return(false)
+ end
- # test execution
+ def setup_missing_enclosing_directory
+ File.stub!(:exists?).with(resource_path).and_return(false)
+ File.stub!(:directory?).with(resource_path).and_return(false)
+ File.stub!(:directory?).with(enclosing_directory).and_return(false)
+ File.stub!(:writable?).with(resource_path).and_return(false)
+ File.stub!(:symlink?).with(resource_path).and_return(false)
+ end
- Etc.should_receive(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel"))
- Etc.should_receive(:getpwuid).with(0).and_return(mock("User Ent", :name => "root"))
+ # Tests
- # test execution
- @provider.load_current_resource
+ it "should return a Chef::Provider::File" do
+ provider.should be_a_kind_of(Chef::Provider::File)
+ end
- # post-condition checks
- @provider.current_resource.mode.should == "0600"
- @provider.current_resource.owner.should == "root"
- @provider.current_resource.group.should == "wheel"
- end
+ it "should store the resource passed to new as new_resource" do
+ provider.new_resource.should eql(resource)
+ end
- it "should NOT update the new_resource state with the current_resourse state if new_resource state is already specified" do
- # test setup
- stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000)
- ::File.should_receive(:stat).exactly(1).times.with(@resource.path).and_return(stat_struct)
+ it "should store the node passed to new as node" do
+ provider.node.should eql(node)
+ end
- @provider.new_resource.group(1)
- @provider.new_resource.owner(1)
- @provider.new_resource.mode(0644)
+ context "when loading the current resource" do
- # test execution
- @provider.load_current_resource
+ context "when running load_current_resource and the file exists" do
+ before do
+ File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(true)
+ provider.load_current_resource
+ end
- # post-condition checks
- @provider.new_resource.group.should == 1
- @provider.new_resource.owner.should == 1
- @provider.new_resource.mode.should == 0644
- end
+ it "should load a current resource based on the one specified at construction" do
+ provider.current_resource.should be_a_kind_of(Chef::Resource::File)
+ end
- context "when the new_resource does not specify the desired access control" do
- it "records access control information in the new resource after modifying the file" do
- # test setup
- stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000)
- # called once in update_new_file_state and once in checksum
- ::File.should_receive(:stat).once.with(@provider.new_resource.path).and_return(stat_struct)
- ::File.should_receive(:directory?).once.with(@provider.new_resource.path).and_return(false)
+ it "the loaded current_resource name should be the same as the resource name" do
+ provider.current_resource.name.should eql(resource.name)
+ end
- Etc.should_receive(:getpwuid).with(0).and_return(mock("User Ent", :name => "root"))
- Etc.should_receive(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel"))
+ it "the loaded current_resource path should be the same as the resoure path" do
+ provider.current_resource.path.should eql(resource.path)
+ end
- @provider.new_resource.group(nil)
- @provider.new_resource.owner(nil)
- @provider.new_resource.mode(nil)
+ it "the loaded current_resource content should be nil" do
+ provider.current_resource.content.should eql(nil)
+ end
+ end
- # test exectution
- @provider.update_new_file_state
+ context "when running load_current_resource and the file does not exist" do
+ before do
+ File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(false)
+ provider.load_current_resource
+ end
- # post-condition checks
- @provider.new_resource.group.should == "wheel"
- @provider.new_resource.owner.should == "root"
- @provider.new_resource.mode.should == "0600"
+ it "the current_resource should be a Chef::Resource::File" do
+ provider.current_resource.should be_a_kind_of(Chef::Resource::File)
end
- end
- end
- describe "when reporting security metadata on windows" do
+ it "the current_resource name should be the same as the resource name" do
+ provider.current_resource.name.should eql(resource.name)
+ end
- it "records the file owner" do
- pending
- end
+ it "the current_resource path should be the same as the resource path" do
+ provider.current_resource.path.should eql(resource.path)
+ end
- it "records rights for each user in the ACL" do
- pending
+ it "the loaded current_resource content should be nil" do
+ provider.current_resource.content.should eql(nil)
+ end
end
- it "records deny_rights for each user in the ACL" do
- pending
- end
- end
+ context "examining file security metadata on Unix with a file that exists" do
+ before do
+ # fake that we're on unix even if we're on windows
+ Chef::Platform.stub!(:windows?).and_return(false)
+ # mock up the filesystem to behave like unix
+ stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000)
+ File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(true)
+ File.should_receive(:stat).with(resource.path).at_least(:once).and_return(stat_struct)
+ Etc.stub!(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel"))
+ Etc.stub!(:getpwuid).with(0).and_return(mock("User Ent", :name => "root"))
+ end
- it "should load a mostly blank current resource if the file specified in new_resource doesn't exist/isn't readable" do
- resource = Chef::Resource::File.new("seattle")
- resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "woot.txt")))
- node = Chef::Node.new
- node.name "latte"
- provider = Chef::Provider::File.new(resource, @run_context)
- provider.load_current_resource
- provider.current_resource.should be_a_kind_of(Chef::Resource::File)
- provider.current_resource.name.should eql(resource.name)
- provider.current_resource.path.should eql(resource.path)
- end
+ context "when the new_resource does not specify any state" do
+ before do
+ provider.load_current_resource
+ end
- it "should not backup symbolic links on delete" do
- path = File.expand_path(File.join(CHEF_SPEC_DATA, "detroit.txt"))
- ::File.open(path, "w") do |file|
- file.write("Detroit's not so nice, so you should come to Seattle instead and buy me a beer instead.")
- end
- @resource = Chef::Resource::File.new("detroit")
- @resource.path(path)
- @node = Chef::Node.new
- @node.name "latte"
- @provider = Chef::Provider::File.new(@resource, @run_context)
-
- ::File.stub!(:symlink?).and_return(true)
- @provider.should_not_receive(:backup)
- @provider.run_action(:delete)
- @resource.should be_updated_by_last_action
- end
+ it "should load the permissions into the current_resource" do
+ provider.current_resource.mode.should == "0600"
+ provider.current_resource.owner.should == "root"
+ provider.current_resource.group.should == "wheel"
+ end
- it "should compare the current content with the requested content" do
- @provider.load_current_resource
+ it "should not set the new_resource permissions" do
+ provider.new_resource.group.should be_nil
+ provider.new_resource.owner.should be_nil
+ provider.new_resource.mode.should be_nil
+ end
+ end
- @provider.new_resource.content "foobar"
- @provider.compare_content.should eql(false)
+ context "when the new_resource explicitly specifies resource state as numbers" do
+ before do
+ resource.owner(1)
+ resource.group(1)
+ resource.mode(0644)
+ provider.load_current_resource
+ end
- @provider.new_resource.content IO.read(@resource.path)
- @provider.compare_content.should eql(true)
- end
+ it "should load the permissions into the current_resource as numbers (BUT DOESN'T, BUG?)" do
+ # FIXME: inconsistency, hmmmm....
+ provider.current_resource.mode.should == "0600"
+ provider.current_resource.owner.should == 0
+ provider.current_resource.group.should == 0
+ end
- it "should set the content of the file to the requested content" do
- io = StringIO.new
- @provider.load_current_resource
- @provider.new_resource.content "foobar"
- @provider.should_receive(:diff_current_from_content).and_return("")
- @provider.should_receive(:backup)
- # checksum check
- File.should_receive(:open).with(@provider.new_resource.path, "rb").and_yield(io)
- File.should_receive(:open).with(@provider.new_resource.path, "w").and_yield(io)
- @provider.set_content
- io.string.should == "foobar"
- end
+ it "should not set the new_resource permissions" do
+ provider.new_resource.group.should == 1
+ provider.new_resource.owner.should == 1
+ provider.new_resource.mode.should == 0644
+ end
+ end
- it "should not set the content of the file if it already matches the requested content" do
- @provider.load_current_resource
- @provider.new_resource.content IO.read(@resource.path)
- # Checksum check:
- File.should_receive(:open).with(@resource.path, "rb").and_yield(StringIO.new(@resource.content))
- File.should_not_receive(:open).with(@provider.new_resource.path, "w")
- lambda { @provider.set_content }.should_not raise_error
- @resource.should_not be_updated_by_last_action
- end
+ context "when the new_resource explicitly specifies resource state as symbols" do
+ before do
+ resource.owner("macklemore")
+ resource.group("seattlehiphop")
+ resource.mode("0321")
+ provider.load_current_resource
+ end
- it "should create the file if it is missing, then set the attributes on action_create" do
- @provider.load_current_resource
- @provider.stub!(:update_new_file_state)
- @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo"))
- @provider.access_controls.should_receive(:set_all)
- @provider.should_receive(:diff_current_from_content).and_return("")
- File.stub!(:open).and_return(1)
- #File.should_receive(:directory?).with("/tmp").and_return(true)
- File.should_receive(:open).with(@provider.new_resource.path, "w+")
- @provider.run_action(:create)
- @resource.should be_updated_by_last_action
- end
+ it "should load the permissions into the current_resource as symbols" do
+ provider.current_resource.mode.should == "0600"
+ provider.current_resource.owner.should == "root"
+ provider.current_resource.group.should == "wheel"
+ end
- it "should create the file with the proper content if it is missing, then set attributes on action_create" do
- io = StringIO.new
- @provider.load_current_resource
- @provider.new_resource.content "foobar"
- @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo"))
- @provider.should_receive(:diff_current_from_content).and_return("")
- @provider.stub!(:update_new_file_state)
- File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io)
- @provider.access_controls.should_receive(:set_all)
- @provider.run_action(:create)
- io.string.should == "foobar"
- @resource.should be_updated_by_last_action
- end
+ it "should not set the new_resource permissions" do
+ provider.new_resource.group.should == "seattlehiphop"
+ provider.new_resource.owner.should == "macklemore"
+ provider.new_resource.mode.should == "0321"
+ end
+ end
- it "should delete the file if it exists and is writable on action_delete" do
- @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo"))
- @provider.stub!(:backup).and_return(true)
- File.should_receive("exists?").exactly(2).times.with(@provider.new_resource.path).and_return(true)
- File.should_receive("writable?").with(@provider.new_resource.path).and_return(true)
- File.should_receive(:delete).with(@provider.new_resource.path).and_return(true)
- @provider.run_action(:delete)
- @resource.should be_updated_by_last_action
- end
+ end
- it "should not raise an error if it cannot delete the file because it does not exist" do
- @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo"))
- @provider.stub!(:backup).and_return(true)
- File.should_receive("exists?").exactly(2).times.with(@provider.new_resource.path).and_return(false)
- lambda { @provider.run_action(:delete) }.should_not raise_error()
- @resource.should_not be_updated_by_last_action
- end
+ context "examining file security metadata on Unix with a file that does not exist" do
+ before do
+ # fake that we're on unix even if we're on windows
+ Chef::Platform.stub!(:windows?).and_return(false)
+ File.should_receive(:exist?).with(resource_path).at_least(:once).and_return(false)
+ end
- it "should update the atime/mtime on action_touch" do
- @provider.load_current_resource
- @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo"))
- @provider.should_receive(:diff_current_from_content).and_return("")
- @provider.stub!(:update_new_file_state)
- File.should_receive(:utime).once.and_return(1)
- File.stub!(:open).and_return(1)
- @provider.access_controls.should_receive(:set_all).once
- @provider.run_action(:touch)
- @resource.should be_updated_by_last_action
- end
+ context "when the new_resource does not specify any state" do
+ before do
+ provider.load_current_resource
+ end
- it "should keep 1 backup copy if specified" do
- @provider.load_current_resource
- @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233")
- @provider.new_resource.stub!(:backup).and_return(1)
- Dir.stub!(:[]).and_return([ "/tmp/s-20080705111233", "/tmp/s-20080705111232", "/tmp/s-20080705111223"])
- FileUtils.should_receive(:rm).with("/tmp/s-20080705111223").once.and_return(true)
- FileUtils.should_receive(:rm).with("/tmp/s-20080705111232").once.and_return(true)
- FileUtils.stub!(:cp).and_return(true)
- FileUtils.stub!(:mkdir_p).and_return(true)
- File.stub!(:exist?).and_return(true)
- @provider.backup
- end
+ it "the current_resource permissions should be nil" do
+ provider.current_resource.mode.should be_nil
+ provider.current_resource.owner.should be_nil
+ provider.current_resource.group.should be_nil
+ end
- it "should backup a file no more than :backup times" do
- @provider.load_current_resource
- @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233")
- @provider.new_resource.stub!(:backup).and_return(2)
- Dir.stub!(:[]).and_return([ "/tmp/s-20080705111233", "/tmp/s-20080705111232", "/tmp/s-20080705111223"])
- FileUtils.should_receive(:rm).with("/tmp/s-20080705111223").once.and_return(true)
- FileUtils.stub!(:cp).and_return(true)
- FileUtils.stub!(:mkdir_p).and_return(true)
- File.stub!(:exist?).and_return(true)
- @provider.backup
- end
+ it "should not set the new_resource permissions" do
+ provider.new_resource.group.should be_nil
+ provider.new_resource.owner.should be_nil
+ provider.new_resource.mode.should be_nil
+ end
+ end
- it "should not attempt to backup a file if :backup == 0" do
- @provider.load_current_resource
- @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233")
- @provider.new_resource.stub!(:backup).and_return(0)
- FileUtils.stub!(:cp).and_return(true)
- File.stub!(:exist?).and_return(true)
- FileUtils.should_not_receive(:cp)
- @provider.backup
- end
+ context "when the new_resource explicitly specifies resource state" do
+ before do
+ resource.owner(63945)
+ resource.group(51948)
+ resource.mode(0123)
+ provider.load_current_resource
+ end
- it "should put the backup backup file in the directory specified by Chef::Config[:file_backup_path]" do
- @provider.load_current_resource
- @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233")
- @provider.new_resource.stub!(:backup).and_return(1)
- Chef::Config.stub!(:[]).with(:file_backup_path).and_return("/some_prefix")
- Dir.stub!(:[]).and_return([ "/some_prefix/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111232", "/some_prefix/tmp/s-20080705111223"])
- FileUtils.should_receive(:mkdir_p).with("/some_prefix/tmp").once
- FileUtils.should_receive(:rm).with("/some_prefix/tmp/s-20080705111232").once.and_return(true)
- FileUtils.should_receive(:rm).with("/some_prefix/tmp/s-20080705111223").once.and_return(true)
- FileUtils.stub!(:cp).and_return(true)
- File.stub!(:exist?).and_return(true)
- @provider.backup
- end
+ it "the current_resource permissions should be nil" do
+ provider.current_resource.mode.should be_nil
+ provider.current_resource.owner.should be_nil
+ provider.current_resource.group.should be_nil
+ end
- it "should strip the drive letter from the backup resource path (for Windows platforms)" do
- @provider.load_current_resource
- @provider.new_resource.stub!(:path).and_return("C:/tmp/s-20080705111233")
- @provider.new_resource.stub!(:backup).and_return(1)
- Chef::Config.stub!(:[]).with(:file_backup_path).and_return("C:/some_prefix")
- Dir.stub!(:[]).and_return([ "C:/some_prefix/tmp/s-20080705111233", "C:/some_prefix/tmp/s-20080705111232", "C:/some_prefix/tmp/s-20080705111223"])
- FileUtils.should_receive(:mkdir_p).with("C:/some_prefix/tmp").once
- FileUtils.should_receive(:rm).with("C:/some_prefix/tmp/s-20080705111232").once.and_return(true)
- FileUtils.should_receive(:rm).with("C:/some_prefix/tmp/s-20080705111223").once.and_return(true)
- FileUtils.stub!(:cp).and_return(true)
- File.stub!(:exist?).and_return(true)
- @provider.backup
+ it "should not set the new_resource permissions" do
+ provider.new_resource.group.should == 51948
+ provider.new_resource.owner.should == 63945
+ provider.new_resource.mode.should == 0123
+ end
+ end
+ end
end
- it "should keep the same ownership on backed up files" do
- @provider.load_current_resource
- @provider.new_resource.stub!(:path).and_return("/tmp/s-20080705111233")
- @provider.new_resource.stub!(:backup).and_return(1)
- Chef::Config.stub!(:[]).with(:file_backup_path).and_return("/some_prefix")
- Dir.stub!(:[]).and_return([ "/some_prefix/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111232", "/some_prefix/tmp/s-20080705111223"])
- FileUtils.stub!(:mkdir_p).and_return(true)
- FileUtils.stub!(:rm).and_return(true)
- File.stub!(:exist?).and_return(true)
- Time.stub!(:now).and_return(Time.at(1272147455).getgm)
- FileUtils.should_receive(:cp).with("/tmp/s-20080705111233", "/some_prefix/tmp/s-20080705111233.chef-20100424221735", {:preserve => true}).and_return(true)
- @provider.backup
- end
+ context "when loading the new_resource after the run" do
- describe "when the enclosing directory does not exist" do
before do
- @resource.path("/tmp/no-such-path/file.txt")
+ # fake that we're on unix even if we're on windows
+ Chef::Platform.stub!(:windows?).and_return(false)
+ # mock up the filesystem to behave like unix
+ stat_struct = mock("::File.stat", :mode => 0600, :uid => 0, :gid => 0, :mtime => 10000)
+ File.stub!(:stat).with(resource.path).and_return(stat_struct)
+ Etc.stub!(:getgrgid).with(0).and_return(mock("Group Ent", :name => "wheel"))
+ Etc.stub!(:getpwuid).with(0).and_return(mock("User Ent", :name => "root"))
+ provider.send(:load_resource_attributes_from_file, resource)
end
- it "raises a specific error describing the problem" do
- lambda {@provider.run_action(:create)}.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist)
+ it "new_resource should record the new permission information" do
+ provider.new_resource.group.should == "wheel"
+ provider.new_resource.owner.should == "root"
+ provider.new_resource.mode.should == "0600"
end
end
- describe "when creating a file which may be missing" do
- it "should not call action create if the file exists" do
- @resource.path(File.expand_path(File.join(CHEF_SPEC_DATA, "templates", "seattle.txt")))
- @provider = Chef::Provider::File.new(@resource, @run_context)
- File.should_not_receive(:open)
- @provider.run_action(:create_if_missing)
- @resource.should_not be_updated_by_last_action
+ context "when reporting security metadata on windows (FIXME: moar tests)" do
+
+ it "records the file owner" do
+ pending
+ end
+
+ it "records rights for each user in the ACL" do
+ pending
end
- it "should call action create if the does not file exist" do
- @resource.path("/tmp/example-dir/non_existant_file")
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.should_receive(:diff_current_from_content).and_return("")
- ::File.stub!(:exists?).with(@resource.path).and_return(false)
- ::File.stub!(:directory?).with("/tmp/example-dir/non_existant_file").and_return(false)
- ::File.stub!(:directory?).with("/tmp/example-dir").and_return(true)
- @provider.stub!(:update_new_file_state)
- io = StringIO.new
- File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io)
- #@provider.should_receive(:action_create).and_return(true)
- @provider.run_action(:create_if_missing)
- @resource.should be_updated_by_last_action
+ it "records deny_rights for each user in the ACL" do
+ pending
end
end
- describe "when a diff is requested", :uses_diff => true do
-
- before(:each) do
- @original_config = Chef::Config.hash_dup
+ context "define_resource_requirements" do
+ context "when the enclosing directory does not exist" do
+ before { setup_missing_enclosing_directory }
+
+ [:create, :create_if_missing, :touch].each do |action|
+ describe "action #{action}" do
+ it "raises EnclosingDirectoryDoesNotExist" do
+ lambda {provider.run_action(action)}.should raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist)
+ end
+
+ it "does not raise an exception in why-run mode" do
+ Chef::Config[:why_run] = true
+ lambda {provider.run_action(action)}.should_not raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist)
+ Chef::Config[:why_run] = false
+ end
+ end
+ end
end
- after(:each) do
- Chef::Config.configuration = @original_config if @original_config
+ context "when the file exists but is not deletable" do
+ before { setup_unwritable_file }
+
+ it "action delete raises InsufficientPermissions" do
+ lambda {provider.run_action(:delete)}.should raise_error(Chef::Exceptions::InsufficientPermissions)
+ end
+
+ it "action delete also raises InsufficientPermissions in why-run mode" do
+ Chef::Config[:why_run] = true
+ lambda {provider.run_action(:delete)}.should raise_error(Chef::Exceptions::InsufficientPermissions)
+ Chef::Config[:why_run] = false
+ end
end
+ end
- describe "when identifying files as binary or text" do
+ context "action create" do
+ it "should create the file, update its contents and then set the acls on the file" do
+ setup_missing_file
+ provider.should_receive(:do_create_file)
+ provider.should_receive(:do_contents_changes)
+ provider.should_receive(:do_acl_changes)
+ provider.should_receive(:load_resource_attributes_from_file).twice # current_resource + new_resource
+ provider.run_action(:create)
+ end
- it "should identify zero-length files as text" do
- Tempfile.open("some-temp") do |file|
- @resource.path(file.path)
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.is_binary?(file.path).should be_false
+ context "do_create_file" do
+ context "when the file exists" do
+ before { setup_normal_file }
+ it "should not create the file" do
+ provider.deployment_strategy.should_not_receive(:create).with(resource_path)
+ provider.send(:do_create_file)
+ provider.send(:file_created?).should == false
end
end
-
- it "should correctly identify text files as being text" do
- Tempfile.open("some-temp") do |file|
- @resource.path(file.path)
- file.puts("This is a text file.")
- file.puts("That has a couple of lines in it.")
- file.puts("And lets make sure that other printable chars work too: ~!@\#$%^&*()`:\"<>?{}|_+,./;'[]\\-=")
- file.close
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.is_binary?(file.path).should be_false
+ context "when the file does not exist" do
+ before { setup_missing_file }
+ it "should create the file" do
+ provider.deployment_strategy.should_receive(:create).with(resource_path)
+ provider.send(:do_create_file)
+ provider.send(:file_created?).should == true
end
end
+ end
+
+ context "do_contents_changes" do
+ context "when there is content to deploy" do
+ before do
+ tempfile = double('Tempfile', :path => "/tmp/foo-bar-baz")
+ provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile)
+ File.should_receive(:exists?).with("/tmp/foo-bar-baz").and_return(true)
+ tempfile.should_receive(:unlink).once
+ end
- it "should identify a null-terminated string as binary" do
- Tempfile.open("some-temp") do |file|
- @resource.path(file.path)
- file.write("This is a binary file.\0")
- file.close
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.is_binary?(file.path).should be_true
+ context "when the contents have changed" do
+ let (:tempfile_path) { "/tmp/foo-bar-baz" }
+ let (:tempfile_md5) { "71f3811d0472fbef15d90a779615b254" }
+ let (:diff_for_reporting) { "+++\n---\n+foo\n-bar\n" }
+ before do
+ provider.stub!(:contents_changed?).and_return(true)
+ diff = double('Diff', :for_output => ['+++','---','+foo','-bar'],
+ :for_reporting => diff_for_reporting )
+ diff.stub!(:diff).with(resource_path, tempfile_path).and_return(true)
+ provider.should_receive(:diff).at_least(:once).and_return(diff)
+ provider.should_receive(:checksum).with(tempfile_path).and_return(tempfile_md5)
+ provider.deployment_strategy.should_receive(:deploy).with(tempfile_path, resource_path)
+ end
+ context "when the file was created" do
+ before { provider.should_receive(:file_created?).at_least(:once).and_return(true) }
+ it "does not backup the file and does not produce a diff for reporting" do
+ provider.should_not_receive(:backup)
+ provider.send(:do_contents_changes)
+ resource.diff.should be_nil
+ end
+ end
+ context "when the file was not created" do
+ before { provider.should_receive(:file_created?).at_least(:once).and_return(false) }
+ it "backs up the file and produces a diff for reporting" do
+ provider.should_receive(:backup)
+ provider.send(:do_contents_changes)
+ resource.diff.should == diff_for_reporting
+ end
+ end
end
+
+ it "does nothing when the contents have not changed" do
+ provider.stub!(:contents_changed?).and_return(false)
+ provider.should_not_receive(:diff)
+ provider.send(:do_contents_changes)
+ end
+ end
+
+ it "does nothing when there is no content to deploy (tempfile returned from contents is nil)" do
+ provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(nil)
+ provider.should_not_receive(:diff)
+ lambda{ provider.send(:do_contents_changes) }.should_not raise_error
end
+ it "raises an exception when the content object returns a tempfile with a nil path" do
+ tempfile = double('Tempfile', :path => nil)
+ provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile)
+ lambda{ provider.send(:do_contents_changes) }.should raise_error
+ end
+
+ it "raises an exception when the content object returns a tempfile that does not exist" do
+ tempfile = double('Tempfile', :path => "/tmp/foo-bar-baz")
+ provider.send(:content).should_receive(:tempfile).at_least(:once).and_return(tempfile)
+ File.should_receive(:exists?).with("/tmp/foo-bar-baz").and_return(false)
+ lambda{ provider.send(:do_contents_changes) }.should raise_error
+ end
end
- it "should not return diff output when chef config has disabled it" do
- Chef::Config[:diff_disabled] = true
- Tempfile.open("some-temp") do |file|
- @resource.path(file.path)
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.load_current_resource
- result = @provider.diff_current_from_content "foo baz"
- result.should == [ "(diff output suppressed by config)" ]
- @resource.diff.should be_nil
+ context "do_acl_changes" do
+ it "needs tests" do
+ pending
end
end
- it "should not return diff output when there is no new file to compare it to" do
- Tempfile.open("some-temp") do |file|
- Tempfile.open("other-temp") do |missing_file|
- missing_path = missing_file.path
- missing_file.close
- missing_file.unlink
- @resource.path(file.path)
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.load_current_resource
- result = @provider.diff_current missing_path
- result.should == [ "(no temp file with new content, diff output suppressed)" ]
- @resource.diff.should be_nil
+ # it "should compare the current content with the requested content" do
+ # @provider.load_current_resource
+ #
+ # @provider.new_resource.content "foobar"
+ # @provider.compare_content.should eql(false)
+ #
+ # @provider.new_resource.content IO.read(@resource.path)
+ # @provider.compare_content.should eql(true)
+ # end
+ #
+ # it "should set the content of the file to the requested content" do
+ # io = StringIO.new
+ # @provider.load_current_resource
+ # @provider.new_resource.content "foobar"
+ # @provider.should_receive(:diff_current_from_content).and_return("")
+ # @provider.should_receive(:backup)
+ # # checksum check
+ # File.should_receive(:open).with(@provider.new_resource.path, "rb").and_yield(io)
+ # File.should_receive(:open).with(@provider.new_resource.path, "w").and_yield(io)
+ # @provider.set_content
+ # io.string.should == "foobar"
+ # end
+ #
+ # it "should not set the content of the file if it already matches the requested content" do
+ # @provider.load_current_resource
+ # @provider.new_resource.content IO.read(@resource.path)
+ # # Checksum check:
+ # File.should_receive(:open).with(@resource.path, "rb").and_yield(StringIO.new(@resource.content))
+ # File.should_not_receive(:open).with(@provider.new_resource.path, "w")
+ # lambda { @provider.set_content }.should_not raise_error
+ # @resource.should_not be_updated_by_last_action
+ # end
+ #
+ # it "should create the file if it is missing, then set the attributes on action_create" do
+ # @provider.load_current_resource
+ # @provider.stub!(:update_new_file_state)
+ # @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo"))
+ # @provider.access_controls.should_receive(:set_all)
+ # @provider.should_receive(:diff_current_from_content).and_return("")
+ # File.stub!(:open).and_return(1)
+ # #File.should_receive(:directory?).with("/tmp").and_return(true)
+ # File.should_receive(:open).with(@provider.new_resource.path, "w+")
+ # @provider.run_action(:create)
+ # @resource.should be_updated_by_last_action
+ # end
+ #
+ # it "should create the file with the proper content if it is missing, then set attributes on action_create" do
+ # io = StringIO.new
+ # @provider.load_current_resource
+ # @provider.new_resource.content "foobar"
+ # @provider.new_resource.stub!(:path).and_return(File.join(Dir.tmpdir, "monkeyfoo"))
+ # @provider.should_receive(:diff_current_from_content).and_return("")
+ # @provider.stub!(:update_new_file_state)
+ # File.should_receive(:open).with(@provider.new_resource.path, "w+").and_yield(io)
+ # @provider.access_controls.should_receive(:set_all)
+ # @provider.run_action(:create)
+ # io.string.should == "foobar"
+ # @resource.should be_updated_by_last_action
+ # end
+ end
+
+ context "action delete" do
+ context "when the file exists" do
+ context "when the file is writable" do
+ context "when the file is not a symlink" do
+ before { setup_normal_file }
+ it "should backup and delete the file and be updated by the last action" do
+ provider.should_receive(:backup).at_least(:once).and_return(true)
+ File.should_receive(:delete).with(resource_path).and_return(true)
+ provider.run_action(:delete)
+ resource.should be_updated_by_last_action
+ end
+ end
+ context "when the file is a symlink" do
+ before { setup_symlink }
+ it "should not backup the symlink" do
+ provider.should_not_receive(:backup)
+ File.should_receive(:delete).with(resource_path).and_return(true)
+ provider.run_action(:delete)
+ resource.should be_updated_by_last_action
+ end
+ end
+ end
+ context "when the file is not writable" do
+ before { setup_unwritable_file }
+ it "should not try to backup or delete the file, and should not be updated by last action" do
+ provider.should_not_receive(:backup)
+ File.should_not_receive(:delete)
+ lambda { provider.run_action(:delete) }.should raise_error()
+ resource.should_not be_updated_by_last_action
end
end
end
- it "should produce diff output when the file does not exist yet, but suppress reporting it" do
- Tempfile.open("some-temp") do |file|
- @resource.path(file.path)
- file.close
- file.unlink
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.load_current_resource
- result = @provider.diff_current_from_content "foo baz"
- result.length.should == 4
- @resource.diff.should be_nil
+ context "when the file does not exist" do
+ before { setup_missing_file }
+
+ it "should not try to backup or delete the file, and should not be updated by last action" do
+ provider.should_not_receive(:backup)
+ File.should_not_receive(:delete)
+ lambda { provider.run_action(:delete) }.should_not raise_error()
+ resource.should_not be_updated_by_last_action
end
end
+ end
- it "should not produce a diff when the current resource file is above the filesize threshold" do
- Chef::Config[:diff_filesize_threshold] = 5
- Tempfile.open("some-temp") do |file|
- @resource.path(file.path)
- file.puts("this is a line which is longer than 5 characters")
- file.flush
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.load_current_resource
- result = @provider.diff_current_from_content "foo" # not longer than 5
- result.should == [ "(file sizes exceed 5 bytes, diff output suppressed)" ]
- @resource.diff.should be_nil
+ context "action touch" do
+ context "when the file does not exist" do
+ before { setup_missing_file }
+ it "should update the atime/mtime on action_touch" do
+ File.should_receive(:utime).once
+ provider.should_receive(:action_create)
+ provider.run_action(:touch)
+ resource.should be_updated_by_last_action
end
end
-
- it "should not produce a diff when the new content is above the filesize threshold" do
- Chef::Config[:diff_filesize_threshold] = 5
- Tempfile.open("some-temp") do |file|
- @resource.path(file.path)
- file.puts("foo")
- file.flush
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.load_current_resource
- result = @provider.diff_current_from_content "this is a line that is longer than 5 characters"
- result.should == [ "(file sizes exceed 5 bytes, diff output suppressed)" ]
- @resource.diff.should be_nil
+ context "when the file exists" do
+ before { setup_normal_file }
+ it "should update the atime/mtime on action_touch" do
+ File.should_receive(:utime).once
+ provider.should_receive(:action_create)
+ provider.run_action(:touch)
+ resource.should be_updated_by_last_action
end
end
+ end
- it "should not produce a diff when the generated diff size is above the diff size threshold" do
- Chef::Config[:diff_output_threshold] = 5
- Tempfile.open("some-temp") do |file|
- @resource.path(file.path)
- file.puts("some text to increase the size of the diff")
- file.flush
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.load_current_resource
- result = @provider.diff_current_from_content "this is a line that is longer than 5 characters"
- result.should == [ "(long diff of over 5 characters, diff output suppressed)" ]
- @resource.diff.should be_nil
+ context "action create_if_missing" do
+ context "when the file does not exist" do
+ before { setup_missing_file }
+ it "should call action_create" do
+ provider.should_receive(:action_create)
+ provider.run_action(:create_if_missing)
end
end
- it "should return valid diff output when content does not match the string content provided" do
- Tempfile.open("some-temp") do |file|
- @resource.path file.path
- @provider = Chef::Provider::File.new(@resource, @run_context)
- @provider.load_current_resource
- result = @provider.diff_current_from_content "foo baz"
- # remove the file name info which varies.
- result.shift(2)
- # Result appearance seems to vary slightly under solaris diff
- # So we'll compare the second line which is common to both.
- # Solaris: -1,1 +1,0 @@, "+foo baz"
- # Linux/Mac: -1,0, +1 @@, "+foo baz"
- result.length.should == 2
- result[1].should == "+foo baz"
- @resource.diff.should_not be_nil
- end
+ context "when the file exists" do
+ before { setup_normal_file }
+ it "should not call action_create" do
+ provider.should_not_receive(:action_create)
+ provider.run_action(:create_if_missing)
+ end
end
+
end
+
end
+
diff --git a/spec/unit/util/backup_spec.rb b/spec/unit/util/backup_spec.rb
new file mode 100644
index 0000000000..e08d5c846f
--- /dev/null
+++ b/spec/unit/util/backup_spec.rb
@@ -0,0 +1,151 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+
+require 'spec_helper'
+require 'tmpdir'
+
+describe Chef::Util::Backup do
+ before(:all) do
+ @original_config = Chef::Config.configuration
+ end
+
+ after(:all) do
+ Chef::Config.configuration.replace(@original_config)
+ end
+
+ let (:tempfile) do
+ Tempfile.new("chef-util-backup-spec-test")
+ end
+
+ before(:each) do
+ @new_resource = mock("new_resource")
+ @new_resource.should_receive(:path).at_least(:once).and_return(tempfile.path)
+ @backup = Chef::Util::Backup.new(@new_resource)
+ end
+
+ it "should store the resource passed to new as new_resource" do
+ @backup.new_resource.should eql(@new_resource)
+ end
+
+ describe "for cases when we don't want to back anything up" do
+
+ before(:each) do
+ @backup.should_not_receive(:do_backup)
+ end
+
+ it "should not attempt to backup a file if :backup is false" do
+ @new_resource.should_receive(:backup).at_least(:once).and_return(false)
+ @backup.backup!
+ end
+
+ it "should not attempt to backup a file if :backup == 0" do
+ @new_resource.should_receive(:backup).at_least(:once).and_return(0)
+ @backup.backup!
+ end
+
+ it "should not attempt to backup a file if it does not exist" do
+ @new_resource.should_receive(:backup).at_least(:once).and_return(1)
+ File.should_receive(:exist?).with(tempfile.path).at_least(:once).and_return(false)
+ @backup.backup!
+ end
+
+ end
+
+ describe "for cases when we want to back things up" do
+ before(:each) do
+ @backup.should_receive(:do_backup)
+ end
+
+ describe "when the number of backups is specified as 1" do
+ before(:each) do
+ @new_resource.should_receive(:backup).at_least(:once).and_return(1)
+ end
+
+ it "should not delete anything if this is the only backup" do
+ @backup.should_receive(:sorted_backup_files).and_return(['a'])
+ @backup.should_not_receive(:delete_backup)
+ @backup.backup!
+ end
+
+ it "should keep only 1 backup copy" do
+ @backup.should_receive(:sorted_backup_files).and_return(['a', 'b', 'c'])
+ @backup.should_receive(:delete_backup).with('b')
+ @backup.should_receive(:delete_backup).with('c')
+ @backup.backup!
+ end
+ end
+
+ describe "when the number of backups is specified as 2" do
+ before(:each) do
+ @new_resource.should_receive(:backup).at_least(:once).and_return(2)
+ end
+
+ it "should not delete anything if we only have one other backup" do
+ @backup.should_receive(:sorted_backup_files).and_return(['a', 'b'])
+ @backup.should_not_receive(:delete_backup)
+ @backup.backup!
+ end
+
+ it "should keep only 2 backup copies" do
+ @backup.should_receive(:sorted_backup_files).and_return(['a', 'b', 'c', 'd'])
+ @backup.should_receive(:delete_backup).with('c')
+ @backup.should_receive(:delete_backup).with('d')
+ @backup.backup!
+ end
+ end
+ end
+
+ describe "backup_filename" do
+ it "should return a timestamped path" do
+ @backup.should_receive(:path).and_return('/a/b/c.txt')
+ @backup.send(:backup_filename).should =~ %r|^/a/b/c.txt.chef-\d{14}$|
+ end
+ it "should strip the drive letter off for windows" do
+ @backup.should_receive(:path).and_return('c:\a\b\c.txt')
+ @backup.send(:backup_filename).should =~ %r|^\\a\\b\\c.txt.chef-\d{14}$|
+ end
+ it "should strip the drive letter off for windows (with forwardslashes)" do
+ @backup.should_receive(:path).and_return('c:/a/b/c.txt')
+ @backup.send(:backup_filename).should =~ %r|^/a/b/c.txt.chef-\d{14}$|
+ end
+ end
+
+ describe "backup_path" do
+ it "uses the file's directory when Chef::Config[:file_backup_path] is nil" do
+ @backup.should_receive(:path).and_return('/a/b/c.txt')
+ Chef::Config[:file_backup_path] = nil
+ @backup.send(:backup_path).should =~ %r|^/a/b/c.txt.chef-\d{14}$|
+ end
+
+ it "uses the configured Chef::Config[:file_backup_path]" do
+ @backup.should_receive(:path).and_return('/a/b/c.txt')
+ Chef::Config[:file_backup_path] = '/backupdir'
+ @backup.send(:backup_path).should =~ %r|^/backupdir[\\/]+a/b/c.txt.chef-\d{14}$|
+ end
+
+ it "uses the configured Chef::Config[:file_backup_path] and strips the drive on windows" do
+ @backup.should_receive(:path).and_return('c:\\a\\b\\c.txt')
+ Chef::Config[:file_backup_path] = 'c:\backupdir'
+ @backup.send(:backup_path).should =~ %r|^c:\\backupdir[\\/]+a\\b\\c.txt.chef-\d{14}$|
+ end
+ end
+
+ # it "should keep the same ownership on backed up files" do (FIXME: functional test)
+
+end
diff --git a/spec/unit/util/diff_spec.rb b/spec/unit/util/diff_spec.rb
new file mode 100644
index 0000000000..ffb0cd6455
--- /dev/null
+++ b/spec/unit/util/diff_spec.rb
@@ -0,0 +1,254 @@
+#
+# Author:: Lamont Granquist (<lamont@opscode.com>)
+# Copyright:: Copyright (c) 2013 Opscode, Inc.
+# License:: Apache License, Version 2.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.
+#
+
+
+require 'spec_helper'
+require 'tmpdir'
+
+describe Chef::Util::Diff, :uses_diff => true do
+ before(:all) do
+ @original_config = Chef::Config.hash_dup
+ end
+
+ after(:all) do
+ Chef::Config.configuration = @original_config if @original_config
+ end
+
+ let!(:old_tempfile) { Tempfile.new("chef-util-diff-spec") }
+ let!(:new_tempfile) { Tempfile.new("chef-util-diff-spec") }
+ let!(:old_file) { old_tempfile.path }
+ let!(:new_file) { new_tempfile.path }
+
+ let(:differ) do # subject
+ differ = Chef::Util::Diff.new
+ differ.diff(old_file, new_file)
+ differ
+ end
+
+ it "should return a Chef::Util::Diff" do
+ expect(differ).to be_a_kind_of(Chef::Util::Diff)
+ end
+
+ it "should raise an exception if the old_file does not exist" do
+ old_tempfile.unlink
+ expect { differ.diff(old_file, new_file) }.to raise_error
+ end
+
+ it "should raise an exception if the new_file does not exist" do
+ new_tempfile.unlink
+ expect { differ.diff(old_file, new_file) }.to raise_error
+ end
+
+ describe "when the two files exist with no content" do
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql(["(no diff)"])
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+
+ describe "when diffs are disabled" do
+ before do
+ Chef::Config[:diff_disabled] = true
+ end
+
+ after do
+ Chef::Config[:diff_disabled] = false
+ end
+
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql( [ "(diff output suppressed by config)" ] )
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+
+ describe "when the old_file has binary content" do
+ before do
+ old_tempfile.write("\x01\xff")
+ old_tempfile.close
+ end
+
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql( [ "(current file is binary, diff output suppressed)" ] )
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+
+ describe "when the new_file has binary content" do
+ before do
+ new_tempfile.write("\x01\xff")
+ new_tempfile.close
+ end
+
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql( [ "(new content is binary, diff output suppressed)" ])
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+
+ describe "when testing the diff_filesize_threshold" do
+ before do
+ @diff_filesize_threshold_saved = Chef::Config[:diff_filesize_threshold]
+ Chef::Config[:diff_filesize_threshold] = 10
+ end
+
+ after do
+ Chef::Config[:diff_filesize_threshold] = @diff_filesize_threshold_saved
+ end
+
+ describe "when the old_file goes over the threshold" do
+ before do
+ old_tempfile.write("But thats what you get when Wu-Tang raised you")
+ old_tempfile.close
+ end
+
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql( [ "(file sizes exceed 10 bytes, diff output suppressed)" ])
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+
+ describe "when the new_file goes over the threshold" do
+ before do
+ new_tempfile.write("But thats what you get when Wu-Tang raised you")
+ new_tempfile.close
+ end
+
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql( [ "(file sizes exceed 10 bytes, diff output suppressed)" ])
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+ end
+
+ describe "when generating a valid diff" do
+ before do
+ old_tempfile.write("foo")
+ old_tempfile.close
+ new_tempfile.write("bar")
+ new_tempfile.close
+ end
+
+ it "calling for_output should return a unified diff" do
+ differ.for_output.size.should eql(5)
+ differ.for_output.join("\\n").should match(/^--- .*\\n\+\+\+ .*\\n@@ .* @@\\n-foo\\n\+bar$/)
+ end
+
+ it "calling for_reporting should return a unified diff" do
+ differ.for_reporting.should match(/^--- .*\\n\+\+\+ .*\\n@@ .* @@\\n-foo\\n\+bar$/)
+ end
+
+ describe "when the diff output is too long" do
+
+ before do
+ @diff_output_threshold_saved = Chef::Config[:diff_output_threshold]
+ Chef::Config[:diff_output_threshold] = 10
+ end
+
+ after do
+ Chef::Config[:diff_output_threshold] = @diff_output_threshold_saved
+ end
+
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql(["(long diff of over 10 characters, diff output suppressed)"])
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+ end
+
+ describe "when errors are thrown from shell_out" do
+ before do
+ differ.stub!(:shell_out).and_raise('boom')
+ differ.diff(old_file, new_file)
+ end
+
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql(["Could not determine diff. Error: boom"])
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+
+ describe "when shell_out returns stderr output" do
+ before do
+ @result = mock('result', :stdout => "", :stderr => "boom")
+ differ.stub!(:shell_out).and_return(@result)
+ differ.diff(old_file, new_file)
+ end
+
+ it "calling for_output should return the error message" do
+ expect(differ.for_output).to eql(["Could not determine diff. Error: boom"])
+ end
+
+ it "calling for_reporting should be nil" do
+ expect(differ.for_reporting).to be_nil
+ end
+ end
+
+ describe "when checking if files are binary or text" do
+
+ it "should identify zero-length files as text" do
+ Tempfile.new("chef-util-diff-spec") do |file|
+ differ.is_binary?(file.path).should be_false
+ end
+ end
+
+ it "should identify text files as text" do
+ Tempfile.new("chef-util-diff-spec") do |file|
+ file.write("This is a text file.")
+ file.write("With more than one line.")
+ file.write("And lets make sure that other printable chars work too: ~!@\#$%^&*()`:\"<>?{}|_+,./;'[]\\-=")
+ file.close
+ differ.is_binary?(file.path).should be_false
+ end
+ end
+
+ it "should identify a null-terminated string files as binary" do
+ Tempfile.new("chef-util-diff-spec") do |file|
+ file.write("This is a binary file.\0")
+ file.close
+ differ.is_binary?(file.path).should be_false
+ end
+ end
+
+ end
+
+end
+