diff options
Diffstat (limited to 'lib/chef/chef_fs')
26 files changed, 2534 insertions, 0 deletions
diff --git a/lib/chef/chef_fs/command_line.rb b/lib/chef/chef_fs/command_line.rb new file mode 100644 index 0000000000..a8362b962b --- /dev/null +++ b/lib/chef/chef_fs/command_line.rb @@ -0,0 +1,232 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system' + +class Chef + module ChefFS + module CommandLine + def self.diff(pattern, a_root, b_root, recurse_depth, output_mode) + found_result = false + Chef::ChefFS::FileSystem.list_pairs(pattern, a_root, b_root) do |a, b| + existed = diff_entries(a, b, recurse_depth, output_mode) do |diff| + yield diff + end + found_result = true if existed + end + if !found_result && pattern.exact_path + yield "#{pattern}: No such file or directory on remote or local" + end + end + + # Diff two known entries (could be files or dirs) + def self.diff_entries(old_entry, new_entry, recurse_depth, output_mode) + # If both are directories + if old_entry.dir? + if new_entry.dir? + if recurse_depth == 0 + if output_mode != :name_only && output_mode != :name_status + yield "Common subdirectories: #{old_entry.path}\n" + end + else + Chef::ChefFS::FileSystem.child_pairs(old_entry, new_entry).each do |old_child,new_child| + diff_entries(old_child, new_child, + recurse_depth ? recurse_depth - 1 : nil, output_mode) do |diff| + yield diff + end + end + end + + # If old is a directory and new is a file + elsif new_entry.exists? + if output_mode == :name_only + yield "#{new_entry.path_for_printing}\n" + elsif output_mode == :name_status + yield "T\t#{new_entry.path_for_printing}\n" + else + yield "File #{new_entry.path_for_printing} is a directory while file #{new_entry.path_for_printing} is a regular file\n" + end + + # If old is a directory and new does not exist + elsif new_entry.parent.can_have_child?(old_entry.name, old_entry.dir?) + if output_mode == :name_only + yield "#{new_entry.path_for_printing}\n" + elsif output_mode == :name_status + yield "D\t#{new_entry.path_for_printing}\n" + else + yield "Only in #{old_entry.parent.path_for_printing}: #{old_entry.name}\n" + end + end + + # If new is a directory and old is a file + elsif new_entry.dir? + if old_entry.exists? + if output_mode == :name_only + yield "#{new_entry.path_for_printing}\n" + elsif output_mode == :name_status + yield "T\t#{new_entry.path_for_printing}\n" + else + yield "File #{old_entry.path_for_printing} is a regular file while file #{old_entry.path_for_printing} is a directory\n" + end + + # If new is a directory and old does not exist + elsif old_entry.parent.can_have_child?(new_entry.name, new_entry.dir?) + if output_mode == :name_only + yield "#{new_entry.path_for_printing}\n" + elsif output_mode == :name_status + yield "A\t#{new_entry.path_for_printing}\n" + else + yield "Only in #{new_entry.parent.path_for_printing}: #{new_entry.name}\n" + end + end + + # Neither is a directory, so they are diffable with file diff + else + are_same, old_value, new_value = Chef::ChefFS::FileSystem.compare(old_entry, new_entry) + if are_same + return old_value != :none + else + if old_value == :none + old_exists = false + elsif old_value.nil? + old_exists = old_entry.exists? + else + old_exists = true + end + if new_value == :none + new_exists = false + elsif new_value.nil? + new_exists = new_entry.exists? + else + new_exists = true + end + + # If one of the files doesn't exist, we only want to print the diff if the + # other file *could be uploaded/downloaded*. + if !old_exists && !old_entry.parent.can_have_child?(new_entry.name, new_entry.dir?) + return true + end + if !new_exists && !new_entry.parent.can_have_child?(old_entry.name, old_entry.dir?) + return true + end + + if output_mode == :name_only + yield "#{new_entry.path_for_printing}\n" + elsif output_mode == :name_status + if old_value == :none || (old_value == nil && !old_entry.exists?) + yield "A\t#{new_entry.path_for_printing}\n" + elsif new_value == :none + yield "D\t#{new_entry.path_for_printing}\n" + else + yield "M\t#{new_entry.path_for_printing}\n" + end + else + # If we haven't read the values yet, get them now. + begin + old_value = old_entry.read if old_value.nil? + rescue Chef::ChefFS::FileSystem::NotFoundError + old_value = :none + end + begin + new_value = new_entry.read if new_value.nil? + rescue Chef::ChefFS::FileSystem::NotFoundError + new_value = :none + end + + old_path = old_entry.path_for_printing + new_path = new_entry.path_for_printing + result = '' + result << "diff --knife #{old_path} #{new_path}\n" + if old_value == :none + result << "new file\n" + old_path = "/dev/null" + old_value = '' + end + if new_value == :none + result << "deleted file\n" + new_path = "/dev/null" + new_value = '' + end + result << diff_text(old_path, new_path, old_value, new_value) + yield result + end + end + end + return true + end + + private + + def self.sort_keys(json_object) + if json_object.is_a?(Array) + json_object.map { |o| sort_keys(o) } + elsif json_object.is_a?(Hash) + new_hash = {} + json_object.keys.sort.each { |key| new_hash[key] = sort_keys(json_object[key]) } + new_hash + else + json_object + end + end + + def self.canonicalize_json(json_text) + parsed_json = JSON.parse(json_text, :create_additions => false) + sorted_json = sort_keys(parsed_json) + JSON.pretty_generate(sorted_json) + end + + def self.diff_text(old_path, new_path, old_value, new_value) + # Reformat JSON for a nicer diff. + if old_path =~ /\.json$/ + begin + reformatted_old_value = canonicalize_json(old_value) + reformatted_new_value = canonicalize_json(new_value) + old_value = reformatted_old_value + new_value = reformatted_new_value + rescue + # If JSON parsing fails, we just won't change any values and fall back + # to normal diff. + end + end + + # Copy to tempfiles before diffing + # TODO don't copy things that are already in files! Or find an in-memory diff algorithm + begin + new_tempfile = Tempfile.new("new") + new_tempfile.write(new_value) + new_tempfile.close + + begin + old_tempfile = Tempfile.new("old") + old_tempfile.write(old_value) + old_tempfile.close + + result = `diff -u #{old_tempfile.path} #{new_tempfile.path}` + result = result.gsub(/^--- #{old_tempfile.path}/, "--- #{old_path}") + result = result.gsub(/^\+\+\+ #{new_tempfile.path}/, "+++ #{new_path}") + result + ensure + old_tempfile.close! + end + ensure + new_tempfile.close! + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_pattern.rb b/lib/chef/chef_fs/file_pattern.rb new file mode 100644 index 0000000000..134d22cbd5 --- /dev/null +++ b/lib/chef/chef_fs/file_pattern.rb @@ -0,0 +1,312 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs' +require 'chef/chef_fs/path_utils' + +class Chef + module ChefFS + # + # Represents a glob pattern. This class is designed so that it can + # match arbitrary strings, and tell you about partial matches. + # + # Examples: + # * <tt>a*z</tt> + # - Matches <tt>abcz</tt> + # - Does not match <tt>ab/cd/ez</tt> + # - Does not match <tt>xabcz</tt> + # * <tt>a**z</tt> + # - Matches <tt>abcz</tt> + # - Matches <tt>ab/cd/ez</tt> + # + # Special characters supported: + # * <tt>/</tt> (and <tt>\\</tt> on Windows) - directory separators + # * <tt>\*</tt> - match zero or more characters (but not directory separators) + # * <tt>\*\*</tt> - match zero or more characters, including directory separators + # * <tt>?</tt> - match exactly one character (not a directory separator) + # Only on Unix: + # * <tt>[abc0-9]</tt> - match one of the included characters + # * <tt>\\<character></tt> - escape character: match the given character + # + class FilePattern + # Initialize a new FilePattern with the pattern string. + # + # Raises +ArgumentError+ if empty file pattern is specified + def initialize(pattern) + @pattern = pattern + end + + # The pattern string. + attr_reader :pattern + + # Reports whether this pattern could match children of <tt>path</tt>. + # If the pattern doesn't match the path up to this point or + # if it matches and doesn't allow further children, this will + # return <tt>false</tt>. + # + # ==== Attributes + # + # * +path+ - a path to check + # + # ==== Examples + # + # abc/def.could_match_children?('abc') == true + # abc.could_match_children?('abc') == false + # abc/def.could_match_children?('x') == false + # a**z.could_match_children?('ab/cd') == true + 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}/) + return false if is_absolute != argument_is_absolute + path = path[1,path.length-1] if argument_is_absolute + + path_parts = Chef::ChefFS::PathUtils::split(path) + # If the pattern is shorter than the path (or same size), children will be larger than the pattern, and will not match. + return false if regexp_parts.length <= path_parts.length && !has_double_star + # If the path doesn't match up to this point, children won't match either. + return false if path_parts.zip(regexp_parts).any? { |part,regexp| !regexp.nil? && !regexp.match(part) } + # Otherwise, it's possible we could match: the path matches to this point, and the pattern is longer than the path. + # TODO There is one edge case where the double star comes after some characters like abc**def--we could check whether the next + # bit of path starts with abc in that case. + return true + end + + # Returns the immediate child of a path that would be matched + # if this FilePattern was applied. If more than one child + # could match, this method returns nil. + # + # ==== Attributes + # + # * +path+ - The path to look for an exact child name under. + # + # ==== Returns + # + # The next directory in the pattern under the given path. + # If the directory part could match more than one child, it + # returns +nil+. + # + # ==== Examples + # + # abc/def.exact_child_name_under('abc') == 'def' + # abc/def/ghi.exact_child_name_under('abc') == 'def' + # abc/*/ghi.exact_child_name_under('abc') == nil + # abc/*/ghi.exact_child_name_under('abc/def') == 'ghi' + # abc/**/ghi.exact_child_name_under('abc/def') == nil + # + # 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}/) + dirs_in_path = Chef::ChefFS::PathUtils::split(path).length + return nil if exact_parts.length <= dirs_in_path + return exact_parts[dirs_in_path] + end + + # If this pattern represents an exact path, returns the exact path. + # + # abc/def.exact_path == 'abc/def' + # abc/*def.exact_path == 'abc/def' + # abc/x\\yz.exact_path == 'abc/xyz' + def exact_path + return nil if has_double_star || exact_parts.any? { |part| part.nil? } + result = Chef::ChefFS::PathUtils::join(*exact_parts) + is_absolute ? Chef::ChefFS::PathUtils::join('', result) : result + end + + # Returns the normalized version of the pattern, with / as the directory + # separator, and "." and ".." removed. + # + # This does not presently change things like \b to b, but in the future + # it might. + def normalized_pattern + calculate + @normalized_pattern + end + + # Tell whether this pattern matches absolute, or relative paths + def is_absolute + calculate + @is_absolute + end + + # Returns <tt>true+ if this pattern matches the path, <tt>false+ otherwise. + # + # 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}/) + return false if is_absolute != argument_is_absolute + path = path[1,path.length-1] if argument_is_absolute + !!regexp.match(path) + end + + # Returns the string pattern + def to_s + 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 + calculate + @regexp + end + + def regexp_parts + calculate + @regexp_parts + end + + def exact_parts + calculate + @exact_parts + end + + def has_double_star + calculate + @has_double_star + end + + def calculate + if !@regexp + @is_absolute = !!(@pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + + full_regexp_parts = [] + normalized_parts = [] + @regexp_parts = [] + @exact_parts = [] + @has_double_star = false + + Chef::ChefFS::PathUtils::split(pattern).each do |part| + regexp, exact, has_double_star = FilePattern::pattern_to_regexp(part) + if has_double_star + @has_double_star = true + end + + # Skip // and /./ (pretend it's not there) + if exact == '' || exact == '.' + next + end + + # Back up when you see .. (unless the prior part has ** in it, in which case .. must be preserved) + if exact == '..' + if @is_absolute && normalized_parts.length == 0 + # If we are at the root, just pretend the .. isn't there + next + elsif normalized_parts.length > 0 + regexp_prev, exact_prev, has_double_star_prev = FilePattern.pattern_to_regexp(normalized_parts[-1]) + if has_double_star_prev + raise ArgumentError, ".. overlapping a ** is unsupported" + end + full_regexp_parts.pop + normalized_parts.pop + if !@has_double_star + @regexp_parts.pop + @exact_parts.pop + end + next + end + end + + # Build up the regexp + full_regexp_parts << regexp + normalized_parts << part + if !@has_double_star + @regexp_parts << Regexp.new("^#{regexp}$") + @exact_parts << exact + end + end + + @regexp = Regexp.new("^#{full_regexp_parts.join(Chef::ChefFS::PathUtils::regexp_path_separator)}$") + @normalized_pattern = Chef::ChefFS::PathUtils.join(*normalized_parts) + @normalized_pattern = Chef::ChefFS::PathUtils.join('', @normalized_pattern) if @is_absolute + end + end + + def self.pattern_special_characters + if Chef::ChefFS::windows? + @pattern_special_characters ||= /(\*\*|\*|\?|[\*\?\.\|\(\)\[\]\{\}\+\\\\\^\$])/ + else + # Unix also supports character regexes and backslashes + @pattern_special_characters ||= /(\\.|\[[^\]]+\]|\*\*|\*|\?|[\*\?\.\|\(\)\[\]\{\}\+\\\\\^\$])/ + end + @pattern_special_characters + end + + def self.regexp_escape_characters + [ '[', '\\', '^', '$', '.', '|', '?', '*', '+', '(', ')', '{', '}' ] + end + + def self.pattern_to_regexp(pattern) + regexp = "" + exact = "" + has_double_star = false + pattern.split(pattern_special_characters).each_with_index do |part, index| + # Odd indexes from the split are symbols. Even are normal bits. + if index % 2 == 0 + exact << part if !exact.nil? + regexp << part + else + case part + # **, * and ? happen on both platforms. + when '**' + exact = nil + has_double_star = true + regexp << '.*' + when '*' + exact = nil + regexp << '[^\/]*' + when '?' + exact = nil + regexp << '.' + else + if part[0,1] == '\\' && part.length == 2 + # backslash escapes are only supported on Unix, and are handled here by leaving the escape on (it means the same thing in a regex) + exact << part[1,1] if !exact.nil? + if regexp_escape_characters.include?(part[1,1]) + regexp << part + else + regexp << part[1,1] + end + elsif part[0,1] == '[' && part.length > 1 + # [...] happens only on Unix, and is handled here by *not* backslashing (it means the same thing in and out of regex) + exact = nil + regexp << part + else + exact += part if !exact.nil? + regexp << "\\#{part}" + end + end + end + end + [regexp, exact, has_double_star] + end + end + end +end diff --git a/lib/chef/chef_fs/file_system.rb b/lib/chef/chef_fs/file_system.rb new file mode 100644 index 0000000000..1805869e32 --- /dev/null +++ b/lib/chef/chef_fs/file_system.rb @@ -0,0 +1,358 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/path_utils' + +class Chef + module ChefFS + module FileSystem + # Yields a list of all things under (and including) this entry that match the + # given pattern. + # + # ==== Attributes + # + # * +entry+ - Entry to start listing under + # * +pattern+ - Chef::ChefFS::FilePattern to match children under + # + def self.list(entry, pattern, &block) + # Include self in results if it matches + if pattern.match?(entry.path) + block.call(entry) + end + + if entry.dir? && pattern.could_match_children?(entry.path) + # If it's possible that our children could match, descend in and add matches. + exact_child_name = pattern.exact_child_name_under(entry.path) + + # If we've got an exact name, don't bother listing children; just grab the + # child with the given name. + if exact_child_name + exact_child = entry.child(exact_child_name) + if exact_child + list(exact_child, pattern, &block) + end + + # Otherwise, go through all children and find any matches + else + entry.children.each do |child| + list(child, pattern, &block) + end + end + end + end + + # Resolve the given path against the entry, returning + # the entry at the end of the path. + # + # ==== Attributes + # + # * +entry+ - the entry to start looking under. Relative + # paths will be resolved from here. + # * +path+ - the path to resolve. If it starts with +/+, + # the path will be resolved starting from +entry.root+. + # + # ==== Examples + # + # Chef::ChefFS::FileSystem.resolve_path(root_path, 'cookbooks/java/recipes/default.rb') + # + def self.resolve_path(entry, path) + return entry if path.length == 0 + return resolve_path(entry.root, path) if path[0,1] == "/" && entry.root != entry + if path[0,1] == "/" + path = path[1,path.length-1] + end + + result = entry + Chef::ChefFS::PathUtils::split(path).each do |part| + result = result.child(part) + end + result + end + + # Copy everything matching the given pattern from src to dest. + # + # After this method completes, everything in dest matching the + # given pattern will look identical to src. + # + # ==== Attributes + # + # * +pattern+ - Chef::ChefFS::FilePattern to match children under + # * +src_root+ - the root from which things will be copied + # * +dest_root+ - the root to which things will be copied + # * +recurse_depth+ - the maximum depth to copy things. +nil+ + # means infinite depth. 0 means no recursion. + # * +options+ - hash of options: + # - +purge+ - if +true+, items in +dest+ that are not in +src+ + # will be deleted from +dest+. If +false+, these items will + # be left alone. + # - +force+ - if +true+, matching files are always copied from + # +src+ to +dest+. If +false+, they will only be copied if + # actually different (which will take time to determine). + # - +dry_run+ - if +true+, action will not actually be taken; + # things will be printed out instead. + # + # ==== Examples + # + # Chef::ChefFS::FileSystem.copy_to(FilePattern.new('/cookbooks'), + # chef_fs, local_fs, nil, true) do |message| + # puts message + # end + # + def self.copy_to(pattern, src_root, dest_root, recurse_depth, options) + found_result = false + list_pairs(pattern, src_root, dest_root) do |src, dest| + found_result = true + new_dest_parent = get_or_create_parent(dest, options) + copy_entries(src, dest, new_dest_parent, recurse_depth, options) + end + if !found_result && pattern.exact_path + puts "#{pattern}: No such file or directory on remote or local" + end + end + + # Yield entries for children that are in either +a_root+ or +b_root+, with + # matching pairs matched up. + # + # ==== Yields + # + # Yields matching entries in pairs: + # + # [ a_entry, b_entry ] + # + # ==== Example + # + # Chef::ChefFS::FileSystem.list_pairs(FilePattern.new('**x.txt', a_root, b_root)) do |a, b| + # ... + # end + # + def self.list_pairs(pattern, a_root, b_root) + # Make sure everything on the server is also on the filesystem, and diff + found_paths = Set.new + Chef::ChefFS::FileSystem.list(a_root, pattern) do |a| + found_paths << a.path + b = Chef::ChefFS::FileSystem.resolve_path(b_root, a.path) + yield [ a, b ] + end + + # Check the outer regex pattern to see if it matches anything on the + # filesystem that isn't on the server + Chef::ChefFS::FileSystem.list(b_root, pattern) do |b| + if !found_paths.include?(b.path) + a = Chef::ChefFS::FileSystem.resolve_path(a_root, b.path) + yield [ a, b ] + end + end + end + + # Get entries for children of either a or b, with matching pairs matched up. + # + # ==== Returns + # + # An array of child pairs. + # + # [ [ a_child, b_child ], ... ] + # + # If a child is only in a or only in b, the other child entry will be + # retrieved by name (and will most likely be a "nonexistent child"). + # + # ==== Example + # + # Chef::ChefFS::FileSystem.child_pairs(a, b).length + # + def self.child_pairs(a, b) + # If both are directories, recurse into them and diff the children instead of returning ourselves. + result = [] + a_children_names = Set.new + a.children.each do |a_child| + a_children_names << a_child.name + result << [ a_child, b.child(a_child.name) ] + end + + # Check b for children that aren't in a + b.children.each do |b_child| + if !a_children_names.include?(b_child.name) + result << [ a.child(b_child.name), b_child ] + end + end + result + end + + def self.compare(a, b) + are_same, a_value, b_value = a.compare_to(b) + if are_same.nil? + are_same, b_value, a_value = b.compare_to(a) + end + if are_same.nil? + begin + a_value = a.read if a_value.nil? + rescue Chef::ChefFS::FileSystem::NotFoundError + a_value = :none + end + begin + b_value = b.read if b_value.nil? + rescue Chef::ChefFS::FileSystem::NotFoundError + b_value = :none + end + are_same = (a_value == b_value) + end + [ are_same, a_value, b_value ] + end + + private + + # Copy two entries (could be files or dirs) + def self.copy_entries(src_entry, dest_entry, new_dest_parent, recurse_depth, options) + # A NOTE about this algorithm: + # There are cases where this algorithm does too many network requests. + # knife upload with a specific filename will first check if the file + # exists (a "dir" in the parent) before deciding whether to POST or + # PUT it. If we just tried PUT (or POST) and then tried the other if + # the conflict failed, we wouldn't need to check existence. + # On the other hand, we may already have DONE the request, in which + # case we shouldn't waste time trying PUT if we know the file doesn't + # exist. + # Will need to decide how that works with checksums, though. + + if !src_entry.exists? + if options[:purge] + # If we would not have uploaded it, we will not purge it. + if src_entry.parent.can_have_child?(dest_entry.name, dest_entry.dir?) + if options[:dry_run] + puts "Would delete #{dest_entry.path_for_printing}" + else + dest_entry.delete(true) + puts "Deleted extra entry #{dest_entry.path_for_printing} (purge is on)" + end + else + Chef::Log.info("Not deleting extra entry #{dest_entry.path_for_printing} (purge is off)") + end + end + + elsif !dest_entry.exists? + if new_dest_parent.can_have_child?(src_entry.name, src_entry.dir?) + # If the entry can do a copy directly from filesystem, do that. + if new_dest_parent.respond_to?(:create_child_from) + if options[:dry_run] + puts "Would create #{dest_entry.path_for_printing}" + else + new_dest_parent.create_child_from(src_entry) + puts "Created #{dest_entry.path_for_printing}" + end + return + end + + if src_entry.dir? + if options[:dry_run] + puts "Would create #{dest_entry.path_for_printing}" + new_dest_dir = new_dest_parent.child(src_entry.name) + else + new_dest_dir = new_dest_parent.create_child(src_entry.name, nil) + puts "Created #{dest_entry.path_for_printing}/" + end + # Directory creation is recursive. + if recurse_depth != 0 + src_entry.children.each do |src_child| + new_dest_child = new_dest_dir.child(src_child.name) + copy_entries(src_child, new_dest_child, new_dest_dir, recurse_depth ? recurse_depth - 1 : recurse_depth, options) + end + end + else + if options[:dry_run] + puts "Would create #{dest_entry.path_for_printing}" + else + new_dest_parent.create_child(src_entry.name, src_entry.read) + puts "Created #{dest_entry.path_for_printing}" + end + end + end + + else + # Both exist. + + # If the entry can do a copy directly, do that. + if dest_entry.respond_to?(:copy_from) + if options[:force] || compare(src_entry, dest_entry)[0] == false + if options[:dry_run] + puts "Would update #{dest_entry.path_for_printing}" + else + dest_entry.copy_from(src_entry) + puts "Updated #{dest_entry.path_for_printing}" + end + end + return + end + + # If they are different types, log an error. + if src_entry.dir? + if dest_entry.dir? + # If both are directories, recurse into their children + if recurse_depth != 0 + child_pairs(src_entry, dest_entry).each do |src_child, dest_child| + copy_entries(src_child, dest_child, dest_entry, recurse_depth ? recurse_depth - 1 : recurse_depth, options) + end + end + else + # If they are different types. + Chef::Log.error("File #{dest_entry.path_for_printing} is a directory while file #{dest_entry.path_for_printing} is a regular file\n") + return + end + else + if dest_entry.dir? + Chef::Log.error("File #{dest_entry.path_for_printing} is a directory while file #{dest_entry.path_for_printing} is a regular file\n") + return + else + + # Both are files! Copy them unless we're sure they are the same. + if options[:force] + should_copy = true + src_value = nil + else + are_same, src_value, dest_value = compare(src_entry, dest_entry) + should_copy = !are_same + end + if should_copy + if options[:dry_run] + puts "Would update #{dest_entry.path_for_printing}" + else + src_value = src_entry.read if src_value.nil? + dest_entry.write(src_value) + puts "Updated #{dest_entry.path_for_printing}" + end + end + end + end + end + end + + def self.get_or_create_parent(entry, options) + parent = entry.parent + if parent && !parent.exists? + parent_parent = get_or_create_parent(entry.parent, options) + if options[:dry_run] + puts "Would create #{parent.path_for_printing}" + else + parent = parent_parent.create_child(parent.name, true) + puts "Created #{parent.path_for_printing}" + end + end + return parent + end + + end + end +end diff --git a/lib/chef/chef_fs/file_system/base_fs_dir.rb b/lib/chef/chef_fs/file_system/base_fs_dir.rb new file mode 100644 index 0000000000..74038f481b --- /dev/null +++ b/lib/chef/chef_fs/file_system/base_fs_dir.rb @@ -0,0 +1,47 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_object' +require 'chef/chef_fs/file_system/nonexistent_fs_object' + +class Chef + module ChefFS + module FileSystem + class BaseFSDir < BaseFSObject + def initialize(name, parent) + super + end + + def dir? + true + end + + # Override child(name) to provide a child object by name without the network read + def child(name) + children.select { |child| child.name == name }.first || NonexistentFSObject.new(name, self) + end + + def can_have_child?(name, is_dir) + true + end + + # Abstract: children + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/base_fs_object.rb b/lib/chef/chef_fs/file_system/base_fs_object.rb new file mode 100644 index 0000000000..855892fc89 --- /dev/null +++ b/lib/chef/chef_fs/file_system/base_fs_object.rb @@ -0,0 +1,121 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/path_utils' + +class Chef + module ChefFS + module FileSystem + class BaseFSObject + def initialize(name, parent) + @parent = parent + @name = name + if parent + @path = Chef::ChefFS::PathUtils::join(parent.path, name) + else + if name != '' + raise ArgumentError, "Name of root object must be empty string: was '#{name}' instead" + end + @path = '/' + end + end + + attr_reader :name + attr_reader :parent + attr_reader :path + + def root + parent ? parent.root : self + end + + def path_for_printing + if parent + parent_path = parent.path_for_printing + if parent_path == '.' + name + else + Chef::ChefFS::PathUtils::join(parent.path_for_printing, name) + end + else + name + end + end + + def dir? + false + end + + def exists? + true + end + + def child(name) + NonexistentFSObject.new(name, self) + end + + # Override can_have_child? to report whether a given file *could* be added + # to this directory. (Some directories can't have subdirs, some can only have .json + # files, etc.) + def can_have_child?(name, is_dir) + false + end + + # Override this if you have a special comparison algorithm that can tell + # you whether this entry is the same as another--either a quicker or a + # more reliable one. Callers will use this to decide whether to upload, + # download or diff an object. + # + # You should not override this if you're going to do the standard + # +self.read == other.read+. If you return +nil+, the caller will call + # +other.compare_to(you)+ instead. Give them a chance :) + # + # ==== Parameters + # + # * +other+ - the entry to compare to + # + # ==== Returns + # + # * +[ are_same, value, other_value ]+ + # +are_same+ may be +true+, +false+ or +nil+ (which means "don't know"). + # +value+ and +other_value+ must either be the text of +self+ or +other+, + # +:none+ (if the entry does not exist or has no value) or +nil+ if the + # value was not retrieved. + # * +nil+ if a definitive answer cannot be had and nothing was retrieved. + # + # ==== Example + # + # are_same, value, other_value = entry.compare_to(other) + # if are_same.nil? + # are_same, other_value, value = other.compare_to(entry) + # end + # if are_same.nil? + # value = entry.read if value.nil? + # other_value = entry.read if other_value.nil? + # are_same = (value == other_value) + # end + def compare_to(other) + return nil + end + + # Important directory attributes: name, parent, path, root + # Overridable attributes: dir?, child(name), path_for_printing + # Abstract: read, write, delete, children + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb new file mode 100644 index 0000000000..87d904e830 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb @@ -0,0 +1,109 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/file_system_entry' +require 'chef/cookbook/chefignore' +require 'chef/cookbook/cookbook_version_loader' +require 'chef/node' +require 'chef/role' +require 'chef/environment' +require 'chef/data_bag_item' +require 'chef/client' + +class Chef + module ChefFS + module FileSystem + # ChefRepositoryFileSystemEntry works just like FileSystemEntry, + # except it pretends files in /cookbooks/chefignore don't exist + # and it can inflate Chef objects + class ChefRepositoryFileSystemEntry < FileSystemEntry + def initialize(name, parent, file_path = nil) + super(name, parent, file_path) + # Load /cookbooks/chefignore + if name == "cookbooks" && path == "/cookbooks" # We check name first because it's a faster fail than path + @chefignore = Chef::Cookbook::Chefignore.new(self.file_path) + # If we are a cookbook or a cookbook subdirectory, empty directories + # underneath us are ignored (since they cannot be uploaded) + elsif parent && parent.name === "cookbooks" && parent.path == "/cookbooks" + @ignore_empty_directories = true + elsif parent && parent.ignore_empty_directories? + @ignore_empty_directories = true + end + end + + attr_reader :chefignore + + def ignore_empty_directories? + @ignore_empty_directories + end + + def chef_object + begin + if parent.path == "/cookbooks" + loader = Chef::Cookbook::CookbookVersionLoader.new(file_path, parent.chefignore) + loader.load_cookbooks + return loader.cookbook_version + end + + # Otherwise the information to inflate the object, is in the file (json_class). + return Chef::JSONCompat.from_json(read) + rescue + Chef::Log.error("Could not read #{path_for_printing} into a Chef object: #{$!}") + end + nil + end + + def children + @children ||= Dir.entries(file_path).select { |entry| entry != '.' && entry != '..' && !ignored?(entry) }. + map { |entry| ChefRepositoryFileSystemEntry.new(entry, self) } + end + + attr_reader :chefignore + + private + + def ignored?(child_name) + # empty directories inside a cookbook are ignored + if ignore_empty_directories? + child_path = PathUtils.join(file_path, child_name) + if File.directory?(child_path) && Dir.entries(child_path) == [ '.', '..' ] + return true + end + end + + ignorer = self + begin + if ignorer.chefignore + # Grab the path from entry to child + path_to_child = child_name + child = self + while child != ignorer + path_to_child = PathUtils.join(child.name, path_to_child) + child = child.parent + end + # Check whether that relative path is ignored + return ignorer.chefignore.ignored?(path_to_child) + end + ignorer = ignorer.parent + end while ignorer + end + + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb new file mode 100644 index 0000000000..fdad68003c --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb @@ -0,0 +1,31 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/chef_repository_file_system_entry' + +class Chef + module ChefFS + module FileSystem + class ChefRepositoryFileSystemRootDir < ChefRepositoryFileSystemEntry + def initialize(file_path) + super("", nil, file_path) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb new file mode 100644 index 0000000000..d3c217d11c --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb @@ -0,0 +1,84 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/rest_list_dir' +require 'chef/chef_fs/file_system/cookbooks_dir' +require 'chef/chef_fs/file_system/data_bags_dir' +require 'chef/chef_fs/file_system/nodes_dir' + +class Chef + module ChefFS + module FileSystem + class ChefServerRootDir < BaseFSDir + def initialize(root_name, chef_config, repo_mode) + super("", nil) + @chef_server_url = chef_config[:chef_server_url] + @chef_username = chef_config[:node_name] + @chef_private_key = chef_config[:client_key] + @environment = chef_config[:environment] + @repo_mode = repo_mode + @root_name = root_name + end + + attr_reader :chef_server_url + attr_reader :chef_username + attr_reader :chef_private_key + attr_reader :environment + attr_reader :repo_mode + + def rest + Chef::REST.new(chef_server_url, chef_username, chef_private_key) + end + + def api_path + "" + end + + def path_for_printing + "#{@root_name}/" + end + + def can_have_child?(name, is_dir) + is_dir && children.any? { |child| child.name == name } + end + + def children + @children ||= begin + result = [ + CookbooksDir.new(self), + DataBagsDir.new(self), + RestListDir.new("environments", self), + RestListDir.new("roles", self) + ] + if repo_mode == 'everything' + result += [ + RestListDir.new("clients", self), + NodesDir.new(self), + RestListDir.new("users", self) + ] + end + result.sort_by { |child| child.name } + end + end + + # Yeah, sorry, I'm not putting delete on this thing. + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/cookbook_dir.rb b/lib/chef/chef_fs/file_system/cookbook_dir.rb new file mode 100644 index 0000000000..e87d5dd49d --- /dev/null +++ b/lib/chef/chef_fs/file_system/cookbook_dir.rb @@ -0,0 +1,188 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/rest_list_dir' +require 'chef/chef_fs/file_system/cookbook_subdir' +require 'chef/chef_fs/file_system/cookbook_file' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/cookbook_version' +require 'chef/cookbook_uploader' + +class Chef + module ChefFS + module FileSystem + class CookbookDir < BaseFSDir + def initialize(name, parent, versions = nil) + super(name, parent) + @versions = versions + end + + attr_reader :versions + + COOKBOOK_SEGMENT_INFO = { + :attributes => { :ruby_only => true }, + :definitions => { :ruby_only => true }, + :recipes => { :ruby_only => true }, + :libraries => { :ruby_only => true }, + :templates => { :recursive => true }, + :files => { :recursive => true }, + :resources => { :ruby_only => true, :recursive => true }, + :providers => { :ruby_only => true, :recursive => true }, + :root_files => { } + } + + def add_child(child) + @children << child + end + + def api_path + "#{parent.api_path}/#{name}/_latest" + end + + def child(name) + # Since we're ignoring the rules and doing a network request here, + # we need to make sure we don't rethrow the exception. (child(name) + # is not supposed to fail.) + begin + result = children.select { |child| child.name == name }.first + return result if result + rescue Chef::ChefFS::FileSystem::NotFoundError + end + return NonexistentFSObject.new(name, self) + end + + def can_have_child?(name, is_dir) + # A cookbook's root may not have directories unless they are segment directories + if is_dir + return name != 'root_files' && + COOKBOOK_SEGMENT_INFO.keys.any? { |segment| segment.to_s == name } + end + true + end + + def children + if @children.nil? + @children = [] + manifest = chef_object.manifest + COOKBOOK_SEGMENT_INFO.each do |segment, segment_info| + next unless manifest.has_key?(segment) + + # Go through each file in the manifest for the segment, and + # add cookbook subdirs and files for it. + manifest[segment].each do |segment_file| + parts = segment_file[:path].split('/') + # Get or create the path to the file + container = self + parts[0,parts.length-1].each do |part| + old_container = container + container = old_container.children.select { |child| part == child.name }.first + if !container + container = CookbookSubdir.new(part, old_container, segment_info[:ruby_only], segment_info[:recursive]) + old_container.add_child(container) + end + end + # Create the file itself + container.add_child(CookbookFile.new(parts[parts.length-1], container, segment_file)) + end + end + end + @children + end + + def dir? + exists? + end + + def read + # This will only be called if dir? is false, which means exists? is false. + raise Chef::ChefFS::FileSystem::NotFoundError, path_for_printing + end + + def exists? + if !@versions + child = parent.children.select { |child| child.name == name }.first + @versions = child.versions if child + end + !!@versions + end + + def compare_to(other) + if !other.dir? + return [ !exists?, nil, nil ] + end + are_same = true + Chef::ChefFS::CommandLine::diff_entries(self, other, nil, :name_only) do + are_same = false + end + [ are_same, nil, nil ] + end + + def copy_from(other) + parent.upload_cookbook_from(other) + end + + def rest + parent.rest + end + + def chef_object + # We cheat and cache here, because it seems like a good idea to keep + # the cookbook view consistent with the directory structure. + return @chef_object if @chef_object + + # The negative (not found) response is cached + if @could_not_get_chef_object + raise Chef::ChefFS::FileSystem::NotFoundError.new(@could_not_get_chef_object), "#{path_for_printing} not found" + end + + begin + # We want to fail fast, for now, because of the 500 issue :/ + # This will make things worse for parallelism, a little, because + # Chef::Config is global and this could affect other requests while + # this request is going on. (We're not parallel yet, but we will be.) + # Chef bug http://tickets.opscode.com/browse/CHEF-3066 + old_retry_count = Chef::Config[:http_retry_count] + begin + Chef::Config[:http_retry_count] = 0 + @chef_object ||= rest.get_rest(api_path) + ensure + Chef::Config[:http_retry_count] = old_retry_count + end + rescue Net::HTTPServerException + if $!.response.code == "404" + @could_not_get_chef_object = $! + raise Chef::ChefFS::FileSystem::NotFoundError.new(@could_not_get_chef_object), "#{path_for_printing} not found" + else + raise + end + + # Chef bug http://tickets.opscode.com/browse/CHEF-3066 ... instead of 404 we get 500 right now. + # Remove this when that bug is fixed. + rescue Net::HTTPFatalError + if $!.response.code == "500" + @could_not_get_chef_object = $! + raise Chef::ChefFS::FileSystem::NotFoundError.new(@could_not_get_chef_object), "#{path_for_printing} not found" + else + raise + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/cookbook_file.rb b/lib/chef/chef_fs/file_system/cookbook_file.rb new file mode 100644 index 0000000000..baa71f5d9e --- /dev/null +++ b/lib/chef/chef_fs/file_system/cookbook_file.rb @@ -0,0 +1,78 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_object' +require 'digest/md5' + +class Chef + module ChefFS + module FileSystem + class CookbookFile < BaseFSObject + def initialize(name, parent, file) + super(name, parent) + @file = file + end + + attr_reader :file + + def checksum + file[:checksum] + end + + def read + old_sign_on_redirect = rest.sign_on_redirect + rest.sign_on_redirect = false + begin + rest.get_rest(file[:url]) + ensure + rest.sign_on_redirect = old_sign_on_redirect + end + end + + def rest + parent.rest + end + + def compare_to(other) + other_value = nil + if other.respond_to?(:checksum) + other_checksum = other.checksum + else + begin + other_value = other.read + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ false, nil, :none ] + end + other_checksum = calc_checksum(other_value) + end + [ checksum == other_checksum, nil, other_value ] + end + + private + + def calc_checksum(value) + begin + Digest::MD5.hexdigest(value) + rescue Chef::ChefFS::FileSystem::NotFoundError + nil + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/cookbook_subdir.rb b/lib/chef/chef_fs/file_system/cookbook_subdir.rb new file mode 100644 index 0000000000..73c709e01e --- /dev/null +++ b/lib/chef/chef_fs/file_system/cookbook_subdir.rb @@ -0,0 +1,54 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_dir' + +class Chef + module ChefFS + module FileSystem + class CookbookSubdir < BaseFSDir + def initialize(name, parent, ruby_only, recursive) + super(name, parent) + @children = [] + @ruby_only = ruby_only + @recursive = recursive + end + + attr_reader :versions + attr_reader :children + + def add_child(child) + @children << child + end + + def can_have_child?(name, is_dir) + if is_dir + return false if !@recursive + else + return false if @ruby_only && name !~ /\.rb$/ + end + true + end + + def rest + parent.rest + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/cookbooks_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_dir.rb new file mode 100644 index 0000000000..9249b42aaa --- /dev/null +++ b/lib/chef/chef_fs/file_system/cookbooks_dir.rb @@ -0,0 +1,68 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/rest_list_dir' +require 'chef/chef_fs/file_system/cookbook_dir' + +class Chef + module ChefFS + module FileSystem + class CookbooksDir < RestListDir + def initialize(parent) + super("cookbooks", parent) + end + + def child(name) + result = @children.select { |child| child.name == name }.first if @children + result || CookbookDir.new(name, self) + end + + def children + @children ||= rest.get_rest(api_path).map { |key, value| CookbookDir.new(key, self, value) } + end + + def create_child_from(other) + upload_cookbook_from(other) + end + + def upload_cookbook_from(other) + other_cookbook_version = other.chef_object + # TODO this only works on the file system. And it can't be broken into + # pieces. + begin + uploader = Chef::CookbookUploader.new(other_cookbook_version, other.parent.file_path) + uploader.upload_cookbooks + rescue Net::HTTPServerException => e + case e.response.code + when "409" + ui.error "Version #{other_cookbook_version.version} of cookbook #{other_cookbook_version.name} is frozen. Use --force to override." + Chef::Log.debug(e) + raise Exceptions::CookbookFrozen + else + raise + end + end + end + + def can_have_child?(name, is_dir) + is_dir + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/data_bag_dir.rb b/lib/chef/chef_fs/file_system/data_bag_dir.rb new file mode 100644 index 0000000000..41fb5dfc63 --- /dev/null +++ b/lib/chef/chef_fs/file_system/data_bag_dir.rb @@ -0,0 +1,78 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/rest_list_dir' +require 'chef/chef_fs/file_system/data_bag_item' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/must_delete_recursively_error' + +class Chef + module ChefFS + module FileSystem + class DataBagDir < RestListDir + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = nil + end + + def dir? + exists? + end + + def read + # This will only be called if dir? is false, which means exists? is false. + raise Chef::ChefFS::FileSystem::NotFoundError, "#{path_for_printing} not found" + end + + def exists? + if @exists.nil? + @exists = parent.children.any? { |child| child.name == name } + end + @exists + end + + def create_child(name, file_contents) + json = Chef::JSONCompat.from_json(file_contents).to_hash + id = name[0,name.length-5] + if json.include?('id') && json['id'] != id + raise "ID in #{path_for_printing}/#{name} must be '#{id}' (is '#{json['id']}')" + end + rest.post_rest(api_path, json) + _make_child_entry(name, true) + end + + def _make_child_entry(name, exists = nil) + DataBagItem.new(name, self, exists) + end + + def delete(recurse) + if !recurse + raise Chef::ChefFS::FileSystem::MustDeleteRecursivelyError.new, "#{path_for_printing} must be deleted recursively" + end + begin + rest.delete_rest(api_path) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/data_bag_item.rb b/lib/chef/chef_fs/file_system/data_bag_item.rb new file mode 100644 index 0000000000..2f6eb15232 --- /dev/null +++ b/lib/chef/chef_fs/file_system/data_bag_item.rb @@ -0,0 +1,59 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/rest_list_entry' + +class Chef + module ChefFS + module FileSystem + class DataBagItem < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent, exists) + end + + def write(file_contents) + # Write is just a little tiny bit different for data bags: + # you set raw_data in the JSON instead of putting the items + # in the top level. + json = Chef::JSONCompat.from_json(file_contents).to_hash + id = name[0,name.length-5] # Strip off the .json from the end + if json['id'] != id + raise "Id in #{path_for_printing}/#{name} must be '#{id}' (is '#{json['id']}')" + end + begin + data_bag = parent.name + json = { + "name" => "data_bag_item_#{data_bag}_#{id}", + "json_class" => "Chef::DataBagItem", + "chef_type" => "data_bag_item", + "data_bag" => data_bag, + "raw_data" => json + } + rest.put_rest(api_path, json) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/data_bags_dir.rb b/lib/chef/chef_fs/file_system/data_bags_dir.rb new file mode 100644 index 0000000000..6eca990545 --- /dev/null +++ b/lib/chef/chef_fs/file_system/data_bags_dir.rb @@ -0,0 +1,66 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/rest_list_dir' +require 'chef/chef_fs/file_system/data_bag_dir' + +class Chef + module ChefFS + module FileSystem + class DataBagsDir < RestListDir + def initialize(parent) + super("data_bags", parent, "data") + end + + def child(name) + result = @children.select { |child| child.name == name }.first if @children + result || DataBagDir.new(name, self) + end + + def children + begin + @children ||= rest.get_rest(api_path).keys.map do |entry| + DataBagDir.new(entry, self, true) + end + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + + def can_have_child?(name, is_dir) + is_dir + end + + def create_child(name, file_contents) + begin + rest.post_rest(api_path, { 'name' => name }) + rescue Net::HTTPServerException + if $!.response.code != "409" + raise + end + end + DataBagDir.new(name, self, true) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/file_system_entry.rb b/lib/chef/chef_fs/file_system/file_system_entry.rb new file mode 100644 index 0000000000..a86e0cb82a --- /dev/null +++ b/lib/chef/chef_fs/file_system/file_system_entry.rb @@ -0,0 +1,90 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/rest_list_dir' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/path_utils' +require 'fileutils' + +class Chef + module ChefFS + module FileSystem + class FileSystemEntry < BaseFSDir + def initialize(name, parent, file_path = nil) + super(name, parent) + @file_path = file_path || "#{parent.file_path}/#{name}" + end + + attr_reader :file_path + + def path_for_printing + Chef::ChefFS::PathUtils::relative_to(file_path, File.expand_path(Dir.pwd)) + end + + def children + begin + @children ||= Dir.entries(file_path).select { |entry| entry != '.' && entry != '..' }.map { |entry| FileSystemEntry.new(entry, self) } + rescue Errno::ENOENT + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{file_path} not found" + end + end + + def create_child(child_name, file_contents=nil) + result = FileSystemEntry.new(child_name, self) + if file_contents + result.write(file_contents) + else + Dir.mkdir(result.file_path) + end + result + end + + def dir? + File.directory?(file_path) + end + + def delete(recurse) + if dir? + if recurse + FileUtils.rm_rf(file_path) + else + File.rmdir(file_path) + end + else + File.delete(file_path) + end + end + + def read + begin + File.open(file_path, "rb") {|f| f.read} + rescue Errno::ENOENT + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{file_path} not found" + end + end + + def write(content) + File.open(file_path, 'wb') do |file| + file.write(content) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/file_system_error.rb b/lib/chef/chef_fs/file_system/file_system_error.rb new file mode 100644 index 0000000000..a461221108 --- /dev/null +++ b/lib/chef/chef_fs/file_system/file_system_error.rb @@ -0,0 +1,31 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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 + module ChefFS + module FileSystem + class FileSystemError < StandardError + def initialize(cause = nil) + @cause = cause + end + + attr_reader :cause + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/file_system_root_dir.rb b/lib/chef/chef_fs/file_system/file_system_root_dir.rb new file mode 100644 index 0000000000..afbf7b1901 --- /dev/null +++ b/lib/chef/chef_fs/file_system/file_system_root_dir.rb @@ -0,0 +1,31 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/file_system_entry' + +class Chef + module ChefFS + module FileSystem + class FileSystemRootDir < FileSystemEntry + def initialize(file_path) + super("", nil, file_path) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb b/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb new file mode 100644 index 0000000000..d247a5b4ed --- /dev/null +++ b/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb @@ -0,0 +1,31 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/file_system_error' + +class Chef + module ChefFS + module FileSystem + class MustDeleteRecursivelyError < FileSystemError + def initialize(cause = nil) + super(cause) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/nodes_dir.rb b/lib/chef/chef_fs/file_system/nodes_dir.rb new file mode 100644 index 0000000000..4dfbf6d850 --- /dev/null +++ b/lib/chef/chef_fs/file_system/nodes_dir.rb @@ -0,0 +1,47 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + class NodesDir < RestListDir + def initialize(parent) + super("nodes", parent) + end + + # Override children to respond to environment + def children + @children ||= begin + env_api_path = environment ? "environments/#{environment}/#{api_path}" : api_path + rest.get_rest(env_api_path).keys.map { |key| RestListEntry.new("#{key}.json", self, true) } + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb b/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb new file mode 100644 index 0000000000..dc82e83b0d --- /dev/null +++ b/lib/chef/chef_fs/file_system/nonexistent_fs_object.rb @@ -0,0 +1,40 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_object' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + class NonexistentFSObject < BaseFSObject + def initialize(name, parent) + super + end + + def exists? + false + end + + def read + raise Chef::ChefFS::FileSystem::NotFoundError, "Nonexistent #{path_for_printing}" + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/not_found_error.rb b/lib/chef/chef_fs/file_system/not_found_error.rb new file mode 100644 index 0000000000..0b608f1abf --- /dev/null +++ b/lib/chef/chef_fs/file_system/not_found_error.rb @@ -0,0 +1,31 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/file_system_error' + +class Chef + module ChefFS + module FileSystem + class NotFoundError < FileSystemError + def initialize(cause = nil) + super(cause) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/rest_list_dir.rb b/lib/chef/chef_fs/file_system/rest_list_dir.rb new file mode 100644 index 0000000000..0e8db4d7b9 --- /dev/null +++ b/lib/chef/chef_fs/file_system/rest_list_dir.rb @@ -0,0 +1,84 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + class RestListDir < BaseFSDir + def initialize(name, parent, api_path = nil) + super(name, parent) + @api_path = api_path || (parent.api_path == "" ? name : "#{parent.api_path}/#{name}") + end + + attr_reader :api_path + + def child(name) + result = @children.select { |child| child.name == name }.first if @children + result ||= can_have_child?(name, false) ? + _make_child_entry(name) : NonexistentFSObject.new(name, self) + end + + def can_have_child?(name, is_dir) + name =~ /\.json$/ && !is_dir + end + + def children + begin + @children ||= rest.get_rest(api_path).keys.map do |key| + _make_child_entry("#{key}.json", true) + end + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + + # NOTE if you change this significantly, you will likely need to change + # DataBagDir.create_child as well. + def create_child(name, file_contents) + json = Chef::JSONCompat.from_json(file_contents).to_hash + base_name = name[0,name.length-5] + if json.include?('name') && json['name'] != base_name + raise "Name in #{path_for_printing}/#{name} must be '#{base_name}' (is '#{json['name']}')" + end + rest.post_rest(api_path, json) + _make_child_entry(name, true) + end + + def environment + parent.environment + end + + def rest + parent.rest + end + + def _make_child_entry(name, exists = nil) + RestListEntry.new(name, self, exists) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/rest_list_entry.rb b/lib/chef/chef_fs/file_system/rest_list_entry.rb new file mode 100644 index 0000000000..dd504ef341 --- /dev/null +++ b/lib/chef/chef_fs/file_system/rest_list_entry.rb @@ -0,0 +1,123 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/base_fs_object' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/role' +require 'chef/node' + +class Chef + module ChefFS + module FileSystem + class RestListEntry < BaseFSObject + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def api_path + if name.length < 5 || name[-5,5] != ".json" + raise "Invalid name #{path}: must end in .json" + end + api_child_name = name[0,name.length-5] + "#{parent.api_path}/#{api_child_name}" + end + + def environment + parent.environment + end + + def exists? + if @exists.nil? + begin + @exists = parent.children.any? { |child| child.name == name } + rescue Chef::ChefFS::FileSystem::NotFoundError + @exists = false + end + end + @exists + end + + def delete(recurse) + begin + rest.delete_rest(api_path) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + + def read + Chef::JSONCompat.to_json_pretty(chef_object.to_hash) + end + + def chef_object + begin + # REST will inflate the Chef object using json_class + rest.get_rest(api_path) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + + def compare_to(other) + begin + other_value = other.read + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ nil, nil, :none ] + end + begin + value = chef_object.to_hash + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ false, :none, other_value ] + end + are_same = (value == Chef::JSONCompat.from_json(other_value, :create_additions => false)) + [ are_same, Chef::JSONCompat.to_json_pretty(value), other_value ] + end + + def rest + parent.rest + end + + def write(file_contents) + json = Chef::JSONCompat.from_json(file_contents).to_hash + base_name = name[0,name.length-5] + if json['name'] != base_name + raise "Name in #{path_for_printing}/#{name} must be '#{base_name}' (is '#{json['name']}')" + end + begin + rest.put_rest(api_path, json) + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new($!), "#{path_for_printing} not found" + else + raise + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/knife.rb b/lib/chef/chef_fs/knife.rb new file mode 100644 index 0000000000..8a116d980e --- /dev/null +++ b/lib/chef/chef_fs/knife.rb @@ -0,0 +1,77 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs/file_system/chef_server_root_dir' +require 'chef/chef_fs/file_system/chef_repository_file_system_root_dir' +require 'chef/chef_fs/file_pattern' +require 'chef/chef_fs/path_utils' +require 'chef/config' + +class Chef + module ChefFS + class Knife < Chef::Knife + def self.common_options + option :repo_mode, + :long => '--repo-mode MODE', + :default => "default", + :description => "Specifies the local repository layout. Values: default or full" + end + + def base_path + @base_path ||= begin + relative_to_base = Chef::ChefFS::PathUtils::relative_to(File.expand_path(Dir.pwd), chef_repo) + relative_to_base == '.' ? '/' : "/#{relative_to_base}" + end + end + + def chef_fs + @chef_fs ||= Chef::ChefFS::FileSystem::ChefServerRootDir.new("remote", Chef::Config, config[:repo_mode]) + end + + def chef_repo + @chef_repo ||= File.expand_path(File.join(Chef::Config.cookbook_path, "..")) + end + + def format_path(path) + if path[0,base_path.length] == base_path + if path == base_path + return "." + elsif path[base_path.length] == "/" + return path[base_path.length + 1, path.length - base_path.length - 1] + elsif base_path == "/" && path[0] == "/" + return path[1, path.length - 1] + end + end + path + end + + def local_fs + @local_fs ||= Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(chef_repo) + end + + def pattern_args + @pattern_args ||= pattern_args_from(name_args) + end + + def pattern_args_from(args) + args.map { |arg| Chef::ChefFS::FilePattern::relative_to(base_path, arg) }.to_a + end + + end + end +end diff --git a/lib/chef/chef_fs/path_utils.rb b/lib/chef/chef_fs/path_utils.rb new file mode 100644 index 0000000000..67c62a7545 --- /dev/null +++ b/lib/chef/chef_fs/path_utils.rb @@ -0,0 +1,64 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012 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/chef_fs' + +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] != source_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 + + 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(/^\/|\/$/, "") } + # Don't join empty bits + result = parts.select { |part| part != "" }.join("/") + # Put the / back on + absolute ? "/#{result}" : result + end + + def self.split(path) + path.split(Regexp.new(regexp_path_separator)) + end + + def self.regexp_path_separator + Chef::ChefFS::windows? ? '[/\\]' : '/' + end + + end + end +end |