diff options
author | Kartik Null Cating-Subramanian <ksubramanian@chef.io> | 2015-06-16 19:04:40 -0400 |
---|---|---|
committer | Kartik Null Cating-Subramanian <ksubramanian@chef.io> | 2015-06-30 12:22:37 -0400 |
commit | f3250264d47455ab4031ff073fcc18596b872308 (patch) | |
tree | ebba4877a1b752076c7f4788e61b65f14d4a9d06 /lib/chef/chef_fs | |
parent | 27d8675ec1c80f1eb7ec57f7b6c854441bb395ee (diff) | |
download | chef-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.rb | 46 | ||||
-rw-r--r-- | lib/chef/chef_fs/file_pattern.rb | 19 | ||||
-rw-r--r-- | lib/chef/chef_fs/knife.rb | 42 | ||||
-rw-r--r-- | lib/chef/chef_fs/path_utils.rb | 99 |
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 |