summaryrefslogtreecommitdiff
path: root/lib/chef/chef_fs
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/chef_fs')
-rw-r--r--lib/chef/chef_fs/command_line.rb232
-rw-r--r--lib/chef/chef_fs/file_pattern.rb312
-rw-r--r--lib/chef/chef_fs/file_system.rb358
-rw-r--r--lib/chef/chef_fs/file_system/base_fs_dir.rb47
-rw-r--r--lib/chef/chef_fs/file_system/base_fs_object.rb121
-rw-r--r--lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb109
-rw-r--r--lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb31
-rw-r--r--lib/chef/chef_fs/file_system/chef_server_root_dir.rb84
-rw-r--r--lib/chef/chef_fs/file_system/cookbook_dir.rb188
-rw-r--r--lib/chef/chef_fs/file_system/cookbook_file.rb78
-rw-r--r--lib/chef/chef_fs/file_system/cookbook_subdir.rb54
-rw-r--r--lib/chef/chef_fs/file_system/cookbooks_dir.rb68
-rw-r--r--lib/chef/chef_fs/file_system/data_bag_dir.rb78
-rw-r--r--lib/chef/chef_fs/file_system/data_bag_item.rb59
-rw-r--r--lib/chef/chef_fs/file_system/data_bags_dir.rb66
-rw-r--r--lib/chef/chef_fs/file_system/file_system_entry.rb90
-rw-r--r--lib/chef/chef_fs/file_system/file_system_error.rb31
-rw-r--r--lib/chef/chef_fs/file_system/file_system_root_dir.rb31
-rw-r--r--lib/chef/chef_fs/file_system/must_delete_recursively_error.rb31
-rw-r--r--lib/chef/chef_fs/file_system/nodes_dir.rb47
-rw-r--r--lib/chef/chef_fs/file_system/nonexistent_fs_object.rb40
-rw-r--r--lib/chef/chef_fs/file_system/not_found_error.rb31
-rw-r--r--lib/chef/chef_fs/file_system/rest_list_dir.rb84
-rw-r--r--lib/chef/chef_fs/file_system/rest_list_entry.rb123
-rw-r--r--lib/chef/chef_fs/knife.rb77
-rw-r--r--lib/chef/chef_fs/path_utils.rb64
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