summaryrefslogtreecommitdiff
path: root/lib/chef/chef_fs
diff options
context:
space:
mode:
authorKartik Null Cating-Subramanian <ksubramanian@chef.io>2015-06-16 19:04:40 -0400
committerKartik Null Cating-Subramanian <ksubramanian@chef.io>2015-06-30 12:22:37 -0400
commitf3250264d47455ab4031ff073fcc18596b872308 (patch)
treeebba4877a1b752076c7f4788e61b65f14d4a9d06 /lib/chef/chef_fs
parent27d8675ec1c80f1eb7ec57f7b6c854441bb395ee (diff)
downloadchef-f3250264d47455ab4031ff073fcc18596b872308.tar.gz
Use windows paths without case-sensitivity.ksubrama/path_space
Fixes #1684 Add tests for path manipulation in chef-fs. Clean up the handling of paths in chef-fs.
Diffstat (limited to 'lib/chef/chef_fs')
-rw-r--r--lib/chef/chef_fs/config.rb46
-rw-r--r--lib/chef/chef_fs/file_pattern.rb19
-rw-r--r--lib/chef/chef_fs/knife.rb42
-rw-r--r--lib/chef/chef_fs/path_utils.rb99
4 files changed, 126 insertions, 80 deletions
diff --git a/lib/chef/chef_fs/config.rb b/lib/chef/chef_fs/config.rb
index 6666a3deee..40cbb36530 100644
--- a/lib/chef/chef_fs/config.rb
+++ b/lib/chef/chef_fs/config.rb
@@ -111,7 +111,7 @@ class Chef
#
def initialize(chef_config = Chef::Config, cwd = Dir.pwd, options = {}, ui = nil)
@chef_config = chef_config
- @cwd = cwd
+ @cwd = File.expand_path(cwd)
@cookbook_version = options[:cookbook_version]
if @chef_config[:repo_mode] == 'everything' && is_hosted? && !ui.nil?
@@ -166,34 +166,37 @@ class Chef
# server_path('/home/jkeiser/chef_repo/cookbooks/blah') == '/cookbooks/blah'
# server_path('/home/*/chef_repo/cookbooks/blah') == nil
#
- # If there are multiple paths (cookbooks, roles, data bags, etc. can all
- # have separate paths), and cwd+the path reaches into one of them, we will
- # return a path relative to that. Otherwise we will return a path to
- # chef_repo.
+ # If there are multiple different, manually specified paths to object locations
+ # (cookbooks, roles, data bags, etc. can all have separate paths), and cwd+the
+ # path reaches into one of them, we will return a path relative to the first
+ # one to match it. Otherwise we expect the path provided to be to the chef
+ # repo path itself. Paths that are not available on the server are not supported.
#
# Globs are allowed as well, but globs outside server paths are NOT
# (presently) supported. See above examples. TODO support that.
#
# If the path does not reach into ANY specified directory, nil is returned.
def server_path(file_path)
- pwd = File.expand_path(Dir.pwd)
- absolute_pwd = Chef::ChefFS::PathUtils.realest_path(File.expand_path(file_path, pwd))
+ target_path = Chef::ChefFS::PathUtils.realest_path(file_path, @cwd)
# Check all object paths (cookbooks_dir, data_bags_dir, etc.)
+ # These are either manually specified by the user or autogenerated relative
+ # to chef_repo_path.
object_paths.each_pair do |name, paths|
paths.each do |path|
- realest_path = Chef::ChefFS::PathUtils.realest_path(path)
- if PathUtils.descendant_of?(absolute_pwd, realest_path)
- relative_path = Chef::ChefFS::PathUtils::relative_to(absolute_pwd, realest_path)
- return relative_path == '.' ? "/#{name}" : "/#{name}/#{relative_path}"
+ object_abs_path = Chef::ChefFS::PathUtils.realest_path(path, @cwd)
+ if relative_path = PathUtils.descendant_path(target_path, object_abs_path)
+ return Chef::ChefFS::PathUtils.join("/#{name}", relative_path)
end
end
end
# Check chef_repo_path
Array(@chef_config[:chef_repo_path]).flatten.each do |chef_repo_path|
- realest_chef_repo_path = Chef::ChefFS::PathUtils.realest_path(chef_repo_path)
- if absolute_pwd == realest_chef_repo_path
+ # We're using realest_path here but we really don't need to - we can just expand the
+ # path and use realpath because a repo_path if provided *must* exist.
+ realest_chef_repo_path = Chef::ChefFS::PathUtils.realest_path(chef_repo_path, @cwd)
+ if Chef::ChefFS::PathUtils.os_path_eq?(target_path, realest_chef_repo_path)
return '/'
end
end
@@ -201,15 +204,10 @@ class Chef
nil
end
- # The current directory, relative to server root
+ # The current directory, relative to server root. This is a case-sensitive server path.
+ # It only exists if the current directory is a child of one of the recognized object_paths below.
def base_path
- @base_path ||= begin
- if @chef_config[:chef_repo_path]
- server_path(File.expand_path(@cwd))
- else
- nil
- end
- end
+ @base_path ||= server_path(@cwd)
end
# Print the given server path, relative to the current directory
@@ -217,10 +215,10 @@ class Chef
server_path = entry.path
if base_path && server_path[0,base_path.length] == base_path
if server_path == base_path
- return "."
- elsif server_path[base_path.length,1] == "/"
+ return '.'
+ elsif server_path[base_path.length,1] == '/'
return server_path[base_path.length + 1, server_path.length - base_path.length - 1]
- elsif base_path == "/" && server_path[0,1] == "/"
+ elsif base_path == '/' && server_path[0,1] == '/'
return server_path[1, server_path.length - 1]
end
end
diff --git a/lib/chef/chef_fs/file_pattern.rb b/lib/chef/chef_fs/file_pattern.rb
index 134d22cbd5..b2351dac68 100644
--- a/lib/chef/chef_fs/file_pattern.rb
+++ b/lib/chef/chef_fs/file_pattern.rb
@@ -72,7 +72,7 @@ class Chef
def could_match_children?(path)
return false if path == '' # Empty string is not a path
- argument_is_absolute = !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/)
+ argument_is_absolute = Chef::ChefFS::PathUtils::is_absolute?(path)
return false if is_absolute != argument_is_absolute
path = path[1,path.length-1] if argument_is_absolute
@@ -111,7 +111,7 @@ class Chef
#
# This method assumes +could_match_children?(path)+ is +true+.
def exact_child_name_under(path)
- path = path[1,path.length-1] if !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/)
+ path = path[1,path.length-1] if Chef::ChefFS::PathUtils::is_absolute?(path)
dirs_in_path = Chef::ChefFS::PathUtils::split(path).length
return nil if exact_parts.length <= dirs_in_path
return exact_parts[dirs_in_path]
@@ -149,7 +149,7 @@ class Chef
# abc/*/def.match?('abc/foo/def') == true
# abc/*/def.match?('abc/foo') == false
def match?(path)
- argument_is_absolute = !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/)
+ argument_is_absolute = Chef::ChefFS::PathUtils::is_absolute?(path)
return false if is_absolute != argument_is_absolute
path = path[1,path.length-1] if argument_is_absolute
!!regexp.match(path)
@@ -160,17 +160,6 @@ class Chef
pattern
end
- # Given a relative file pattern and a directory, makes a new file pattern
- # starting with the directory.
- #
- # FilePattern.relative_to('/usr/local', 'bin/*grok') == FilePattern.new('/usr/local/bin/*grok')
- #
- # BUG: this does not support patterns starting with <tt>..</tt>
- def self.relative_to(dir, pattern)
- return FilePattern.new(pattern) if pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/
- FilePattern.new(Chef::ChefFS::PathUtils::join(dir, pattern))
- end
-
private
def regexp
@@ -195,7 +184,7 @@ class Chef
def calculate
if !@regexp
- @is_absolute = !!(@pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/)
+ @is_absolute = Chef::ChefFS::PathUtils::is_absolute?(@pattern)
full_regexp_parts = []
normalized_parts = []
diff --git a/lib/chef/chef_fs/knife.rb b/lib/chef/chef_fs/knife.rb
index 86872dab71..9101e455f8 100644
--- a/lib/chef/chef_fs/knife.rb
+++ b/lib/chef/chef_fs/knife.rb
@@ -17,6 +17,7 @@
#
require 'chef/knife'
+require 'pathname'
class Chef
module ChefFS
@@ -63,7 +64,7 @@ class Chef
# --chef-repo-path forcibly overrides all other paths
if config[:chef_repo_path]
Chef::Config[:chef_repo_path] = config[:chef_repo_path]
- %w(acl client cookbook container data_bag environment group node role user).each do |variable_name|
+ Chef::ChefFS::Config::INFLECTIONS.each_value do |variable_name|
Chef::Config.delete("#{variable_name}_path".to_sym)
end
end
@@ -98,14 +99,41 @@ class Chef
end
def pattern_arg_from(arg)
- # TODO support absolute file paths and not just patterns? Too much?
- # Could be super useful in a world with multiple repo paths
- if !@chef_fs_config.base_path && !Chef::ChefFS::PathUtils.is_absolute?(arg)
- # Check if chef repo path is specified to give a better error message
- ui.error("Attempt to use relative path '#{arg}' when current directory is outside the repository path")
+ inferred_path = nil
+ if Chef::ChefFS::PathUtils.is_absolute?(arg)
+ # We should be able to use this as-is - but the user might have incorrectly provided
+ # us with a path that is based off of the OS root path instead of the Chef-FS root.
+ # Do a quick and dirty sanity check.
+ if possible_server_path = @chef_fs_config.server_path(arg)
+ ui.warn("The absolute path provided is suspicious: #{arg}")
+ ui.warn("If you wish to refer to a file location, please provide a path that is rooted at the chef-repo.")
+ ui.warn("Consider writing '#{possible_server_path}' instead of '#{arg}'")
+ end
+ # Use the original path because we can't be sure.
+ inferred_path = arg
+ elsif arg[0,1] == '~'
+ # Let's be nice and fix it if possible - but warn the user.
+ ui.warn("A path relative to a user home directory has been provided: #{arg}")
+ ui.warn("Paths provided need to be rooted at the chef-repo being considered or be relative paths.")
+ inferred_path = @chef_fs_config.server_path(arg)
+ ui.warn("Using '#{inferred_path}' as the path instead of '#{arg}'.")
+ elsif Pathname.new(arg).absolute?
+ # It is definitely a system absolute path (such as C:\ or \\foo\bar) but it cannot be
+ # interpreted as a Chef-FS absolute path. Again attempt to be nice but warn the user.
+ ui.warn("An absolute file system path that isn't a server path was provided: #{arg}")
+ ui.warn("Paths provided need to be rooted at the chef-repo being considered or be relative paths.")
+ inferred_path = @chef_fs_config.server_path(arg)
+ ui.warn("Using '#{inferred_path}' as the path instead of '#{arg}'.")
+ elsif @chef_fs_config.base_path.nil?
+ # These are all relative paths. We can't resolve and root paths unless we are in the
+ # chef repo.
+ ui.error("Attempt to use relative path '#{arg}' when current directory is outside the repository path.")
+ ui.error("Current working directory is '#{@chef_fs_config.cwd}'.")
exit(1)
+ else
+ inferred_path = Chef::ChefFS::PathUtils::join(@chef_fs_config.base_path, arg)
end
- Chef::ChefFS::FilePattern.relative_to(@chef_fs_config.base_path, arg)
+ Chef::ChefFS::FilePattern.new(inferred_path)
end
def format_path(entry)
diff --git a/lib/chef/chef_fs/path_utils.rb b/lib/chef/chef_fs/path_utils.rb
index 9ef75ce2e5..595f966378 100644
--- a/lib/chef/chef_fs/path_utils.rb
+++ b/lib/chef/chef_fs/path_utils.rb
@@ -23,31 +23,31 @@ class Chef
module ChefFS
class PathUtils
- # If you are in 'source', this is what you would have to type to reach 'dest'
- # relative_to('/a/b/c/d/e', '/a/b/x/y') == '../../c/d/e'
- # relative_to('/a/b', '/a/b') == '.'
- def self.relative_to(dest, source)
- # Skip past the common parts
- source_parts = Chef::ChefFS::PathUtils.split(source)
- dest_parts = Chef::ChefFS::PathUtils.split(dest)
- i = 0
- until i >= source_parts.length || i >= dest_parts.length || source_parts[i] != dest_parts[i]
- i+=1
- end
- # dot-dot up from 'source' to the common ancestor, then
- # descend to 'dest' from the common ancestor
- result = Chef::ChefFS::PathUtils.join(*(['..']*(source_parts.length-i) + dest_parts[i,dest.length-i]))
- result == '' ? '.' : result
- end
+ # A Chef-FS path is a path in a chef-repository that can be used to address
+ # both files on a local file-system as well as objects on a chef server.
+ # These paths are stricter than file-system paths allowed on various OSes.
+ # Absolute Chef-FS paths begin with "/" (on windows, "\" is acceptable as well).
+ # "/" is used as the path element separator (on windows, "\" is acceptable as well).
+ # No directory/path element may contain a literal "\" character. Any such characters
+ # encountered are either dealt with as separators (on windows) or as escape
+ # characters (on POSIX systems). Relative Chef-FS paths may use ".." or "." but
+ # may never use these to back-out of the root of a Chef-FS path. Any such extraneous
+ # ".."s are ignored.
+ # Chef-FS paths are case sensitive (since the paths on the server are).
+ # On OSes with case insensitive paths, you may be unable to locally deal with two
+ # objects whose server paths only differ by case. OTOH, the case of path segments
+ # that are outside the Chef-FS root (such as when looking at a file-system absolute
+ # path to discover the Chef-FS root path) are handled in accordance to the rules
+ # of the local file-system and OS.
def self.join(*parts)
return "" if parts.length == 0
# Determine if it started with a slash
absolute = parts[0].length == 0 || parts[0].length > 0 && parts[0] =~ /^#{regexp_path_separator}/
# Remove leading and trailing slashes from each part so that the join will work (and the slash at the end will go away)
- parts = parts.map { |part| part.gsub(/^\/|\/$/, "") }
+ parts = parts.map { |part| part.gsub(/^#{regexp_path_separator}+|#{regexp_path_separator}+$/, '') }
# Don't join empty bits
- result = parts.select { |part| part != "" }.join("/")
+ result = parts.select { |part| part != '' }.join('/')
# Put the / back on
absolute ? "/#{result}" : result
end
@@ -60,36 +60,67 @@ class Chef
Chef::ChefFS::windows? ? '[\/\\\\]' : '/'
end
+ # Given a server path, determines if it is absolute.
+ def self.is_absolute?(path)
+ !!(path =~ /^#{regexp_path_separator}/)
+ end
# Given a path which may only be partly real (i.e. /x/y/z when only /x exists,
# or /x/y/*/blah when /x/y/z/blah exists), call File.realpath on the biggest
- # part that actually exists.
+ # part that actually exists. The paths operated on here are not Chef-FS paths.
+ # These are OS paths that may contain symlinks but may not also fully exist.
#
# If /x is a symlink to /blarghle, and has no subdirectories, then:
# PathUtils.realest_path('/x/y/z') == '/blarghle/y/z'
# PathUtils.realest_path('/x/*/z') == '/blarghle/*/z'
# PathUtils.realest_path('/*/y/z') == '/*/y/z'
- def self.realest_path(path)
- path = Pathname.new(path)
- begin
- path.realpath.to_s
- rescue Errno::ENOENT
- dirname = path.dirname
- if dirname
- PathUtils.join(realest_path(dirname), path.basename.to_s)
- else
- path.to_s
+ #
+ # TODO: Move this to wherever util/path_helper is these days.
+ def self.realest_path(path, cwd = Dir.pwd)
+ path = File.expand_path(path, cwd)
+ parent_path = File.dirname(path)
+ suffix = []
+
+ # File.dirname happens to return the path as its own dirname if you're
+ # at the root (such as at \\foo\bar, C:\ or /)
+ until parent_path == path do
+ # This can occur if a path such as "C:" is given. Ruby gives the parent as "C:."
+ # for reasons only it knows.
+ raise ArgumentError "Invalid path segment #{path}" if parent_path.length > path.length
+ begin
+ path = File.realpath(path)
+ break
+ rescue Errno::ENOENT
+ suffix << File.basename(path)
+ path = parent_path
+ parent_path = File.dirname(path)
end
end
+ File.join(path, *suffix.reverse)
end
- def self.descendant_of?(path, ancestor)
- path[0,ancestor.length] == ancestor &&
- (ancestor.length == path.length || path[ancestor.length,1] =~ /#{PathUtils.regexp_path_separator}/)
+ # Compares two path fragments according to the case-sentitivity of the host platform.
+ def self.os_path_eq?(left, right)
+ Chef::ChefFS::windows? ? left.casecmp(right) == 0 : left == right
end
- def self.is_absolute?(path)
- path =~ /^#{regexp_path_separator}/
+ # Given two general OS-dependent file paths, determines the relative path of the
+ # child with respect to the ancestor. Both child and ancestor must exist and be
+ # fully resolved - this is strictly a lexical comparison. No trailing slashes
+ # and other shenanigans are allowed.
+ #
+ # TODO: Move this to util/path_helper.
+ def self.descendant_path(path, ancestor)
+ candidate_fragment = path[0, ancestor.length]
+ return nil unless PathUtils.os_path_eq?(candidate_fragment, ancestor)
+ if ancestor.length == path.length
+ ''
+ elsif path[ancestor.length,1] =~ /#{PathUtils.regexp_path_separator}/
+ path[ancestor.length+1..-1]
+ else
+ nil
+ end
end
+
end
end
end