diff options
author | John Keiser <john@johnkeiser.com> | 2016-01-07 14:18:44 -0800 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2016-01-13 09:23:07 -0800 |
commit | 6d9c8b03466632b3cd11042a2b99a08ae5047309 (patch) | |
tree | 0bbff68a54b99e7f6f70819783a75bc9967093d1 /lib/chef | |
parent | 5e5cbe6df61ae69b9a5942ce51a7ad1f8bb6ea3a (diff) | |
download | chef-6d9c8b03466632b3cd11042a2b99a08ae5047309.tar.gz |
Move server and repository fs objects to their own directories
Diffstat (limited to 'lib/chef')
64 files changed, 2787 insertions, 2719 deletions
diff --git a/lib/chef/chef_fs/chef_fs_data_store.rb b/lib/chef/chef_fs/chef_fs_data_store.rb index f886cd8c9a..07cb3d2cd5 100644 --- a/lib/chef/chef_fs/chef_fs_data_store.rb +++ b/lib/chef/chef_fs/chef_fs_data_store.rb @@ -23,7 +23,7 @@ require 'chef_zero/data_store/data_not_found_error' require 'chef/chef_fs/file_pattern' require 'chef/chef_fs/file_system' require 'chef/chef_fs/file_system/not_found_error' -require 'chef/chef_fs/file_system/memory_root' +require 'chef/chef_fs/file_system/memory/memory_root' require 'fileutils' class Chef @@ -503,7 +503,7 @@ class Chef end # Create a little Chef::ChefFS memory filesystem with the data - cookbook_fs = Chef::ChefFS::FileSystem::MemoryRoot.new('uploading') + cookbook_fs = Chef::ChefFS::FileSystem::Memory::MemoryRoot.new('uploading') cookbook = Chef::JSONCompat.parse(data) cookbook.each_pair do |key, value| if value.is_a?(Array) diff --git a/lib/chef/chef_fs/config.rb b/lib/chef/chef_fs/config.rb index a666731b80..260882ae70 100644 --- a/lib/chef/chef_fs/config.rb +++ b/lib/chef/chef_fs/config.rb @@ -143,8 +143,8 @@ class Chef end def create_chef_fs - require 'chef/chef_fs/file_system/chef_server_root_dir' - Chef::ChefFS::FileSystem::ChefServerRootDir.new("remote", @chef_config, :cookbook_version => @cookbook_version) + require 'chef/chef_fs/file_system/chef_server/chef_server_root_dir' + Chef::ChefFS::FileSystem::ChefServer::ChefServerRootDir.new("remote", @chef_config, :cookbook_version => @cookbook_version) end def local_fs @@ -152,8 +152,8 @@ class Chef end def create_local_fs - require 'chef/chef_fs/file_system/chef_repository_file_system_root_dir' - Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(object_paths, Array(chef_config[:chef_repo_path]).flatten, @chef_config) + require 'chef/chef_fs/file_system/repository/chef_repository_file_system_root_dir' + Chef::ChefFS::FileSystem::Repository::ChefRepositoryFileSystemRootDir.new(object_paths, Array(chef_config[:chef_repo_path]).flatten, @chef_config) end # Returns the given real path's location relative to the server root. diff --git a/lib/chef/chef_fs/file_system/acl_dir.rb b/lib/chef/chef_fs/file_system/acl_dir.rb deleted file mode 100644 index 66cf794319..0000000000 --- a/lib/chef/chef_fs/file_system/acl_dir.rb +++ /dev/null @@ -1,63 +0,0 @@ -# -# Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef/chef_fs/file_system/base_fs_dir' -require 'chef/chef_fs/file_system/acl_entry' -require 'chef/chef_fs/file_system/operation_not_allowed_error' - -class Chef - module ChefFS - module FileSystem - class AclDir < BaseFSDir - def api_path - parent.parent.child(name).api_path - end - - def make_child_entry(name, exists = nil) - result = @children.select { |child| child.name == name }.first if @children - result || AclEntry.new(name, self, exists) - end - - def can_have_child?(name, is_dir) - name =~ /\.json$/ && !is_dir - end - - def children - if @children.nil? - # Grab the ACTUAL children (/nodes, /containers, etc.) and get their names - names = parent.parent.child(name).children.map { |child| child.dir? ? "#{child.name}.json" : child.name } - @children = names.map { |name| make_child_entry(name, true) } - end - @children - end - - def create_child(name, file_contents) - raise OperationNotAllowedError.new(:create_child, self, nil, "ACLs can only be updated, and can only be created when the corresponding object is created.") - end - - def data_handler - parent.data_handler - end - - def rest - parent.rest - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/acls_dir.rb b/lib/chef/chef_fs/file_system/acls_dir.rb deleted file mode 100644 index d8c9bf809c..0000000000 --- a/lib/chef/chef_fs/file_system/acls_dir.rb +++ /dev/null @@ -1,68 +0,0 @@ -# -# Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef/chef_fs/file_system/base_fs_dir' -require 'chef/chef_fs/file_system/acl_dir' -require 'chef/chef_fs/file_system/cookbooks_acl_dir' -require 'chef/chef_fs/file_system/acl_entry' -require 'chef/chef_fs/data_handler/acl_data_handler' - -class Chef - module ChefFS - module FileSystem - class AclsDir < BaseFSDir - ENTITY_TYPES = %w(clients containers cookbooks data_bags environments groups nodes roles) # we don't read sandboxes, so we don't read their acls - - def data_handler - @data_handler ||= Chef::ChefFS::DataHandler::AclDataHandler.new - end - - def api_path - parent.api_path - end - - def make_child_entry(name) - children.select { |child| child.name == name }.first - end - - def can_have_child?(name, is_dir) - is_dir ? ENTITY_TYPES.include?(name) : name == 'organization.json' - end - - def children - if @children.nil? - @children = ENTITY_TYPES.map do |entity_type| - case entity_type - when 'cookbooks' - CookbooksAclDir.new(entity_type, self) - else - AclDir.new(entity_type, self) - end - end - @children << AclEntry.new('organization.json', self, true) # the org acl is retrieved as GET /organizations/ORGNAME/ANYTHINGATALL/_acl - end - @children - end - - def rest - parent.rest - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb deleted file mode 100644 index ded390fa8d..0000000000 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb +++ /dev/null @@ -1,102 +0,0 @@ -# -# Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry' -require 'chef/chef_fs/file_system/cookbook_dir' -require 'chef/chef_fs/file_system/not_found_error' -require 'chef/cookbook/chefignore' -require 'chef/cookbook/cookbook_version_loader' - -class Chef - module ChefFS - module FileSystem - class ChefRepositoryFileSystemCookbookDir < ChefRepositoryFileSystemCookbookEntry - def initialize(name, parent, file_path = nil) - super(name, parent, file_path) - end - - def chef_object - begin - loader = Chef::Cookbook::CookbookVersionLoader.new(file_path, parent.chefignore) - # We need the canonical cookbook name if we are using versioned cookbooks, but we don't - # want to spend a lot of time adding code to the main Chef libraries - if root.versioned_cookbooks - canonical_name = canonical_cookbook_name(File.basename(file_path)) - raise "When versioned_cookbooks mode is on, cookbook #{file_path} must match format <cookbook_name>-x.y.z" unless canonical_name - - # KLUDGE: We shouldn't have to use instance_variable_set - loader.instance_variable_set(:@cookbook_name, canonical_name) - end - - loader.load_cookbooks - cb = loader.cookbook_version - if !cb - Chef::Log.error("Cookbook #{file_path} empty.") - raise "Cookbook #{file_path} empty." - end - cb - rescue => e - Chef::Log.error("Could not read #{path_for_printing} into a Chef object: #{e}") - Chef::Log.error(e.backtrace.join("\n")) - raise - end - end - - def children - super.select { |entry| !(entry.dir? && entry.children.size == 0 ) } - end - - def can_have_child?(name, is_dir) - if is_dir - # Only the given directories will be uploaded. - return CookbookDir::COOKBOOK_SEGMENT_INFO.keys.include?(name.to_sym) && name != 'root_files' - elsif name == Chef::Cookbook::CookbookVersionLoader::UPLOADED_COOKBOOK_VERSION_FILE - return false - end - super(name, is_dir) - end - - # Exposed as a class method so that it can be used elsewhere - def self.canonical_cookbook_name(entry_name) - name_match = Chef::ChefFS::FileSystem::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME.match(entry_name) - return nil if name_match.nil? - return name_match[1] - end - - def canonical_cookbook_name(entry_name) - self.class.canonical_cookbook_name(entry_name) - end - - def uploaded_cookbook_version_path - File.join(file_path, Chef::Cookbook::CookbookVersionLoader::UPLOADED_COOKBOOK_VERSION_FILE) - end - - def can_upload? - File.exists?(uploaded_cookbook_version_path) || children.size > 0 - end - - protected - - def make_child_entry(child_name) - segment_info = CookbookDir::COOKBOOK_SEGMENT_INFO[child_name.to_sym] || {} - ChefRepositoryFileSystemCookbookEntry.new(child_name, self, nil, segment_info[:ruby_only], segment_info[:recursive]) - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb deleted file mode 100644 index 914412f839..0000000000 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb +++ /dev/null @@ -1,80 +0,0 @@ -# -# Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef/chef_fs/file_system/chef_repository_file_system_entry' -require 'chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir' -require 'chef/chef_fs/file_system/not_found_error' - -class Chef - module ChefFS - module FileSystem - class ChefRepositoryFileSystemCookbookEntry < ChefRepositoryFileSystemEntry - def initialize(name, parent, file_path = nil, ruby_only = false, recursive = false) - super(name, parent, file_path) - @ruby_only = ruby_only - @recursive = recursive - end - - attr_reader :ruby_only - attr_reader :recursive - - def children - super.select { |entry| !(entry.dir? && entry.children.size == 0 ) } - end - - def can_have_child?(name, is_dir) - if is_dir - return recursive && name != '.' && name != '..' - elsif ruby_only - return false if name[-3..-1] != '.rb' - end - - # Check chefignore - ignorer = parent - loop do - if ignorer.is_a?(ChefRepositoryFileSystemCookbooksDir) - # Grab the path from entry to child - path_to_child = name - child = self - while child.parent != 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 || !ignorer.chefignore.ignored?(path_to_child) - end - ignorer = ignorer.parent - break unless ignorer - end - - true - end - - def write_pretty_json - false - end - - protected - - def make_child_entry(child_name) - ChefRepositoryFileSystemCookbookEntry.new(child_name, self, nil, ruby_only, recursive) - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb deleted file mode 100644 index 5b495666c3..0000000000 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb +++ /dev/null @@ -1,82 +0,0 @@ -# -# Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef/chef_fs/file_system/chef_repository_file_system_entry' -require 'chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir' -require 'chef/cookbook/chefignore' - -class Chef - module ChefFS - module FileSystem - class ChefRepositoryFileSystemCookbooksDir < ChefRepositoryFileSystemEntry - def initialize(name, parent, file_path) - super(name, parent, file_path) - begin - @chefignore = Chef::Cookbook::Chefignore.new(self.file_path) - rescue Errno::EISDIR - rescue Errno::EACCES - # Work around a bug in Chefignore when chefignore is a directory - end - end - - attr_reader :chefignore - - def children - super.select do |entry| - # empty cookbooks and cookbook directories are ignored - if !entry.can_upload? - Chef::Log.warn("Cookbook '#{entry.name}' is empty or entirely chefignored at #{entry.path_for_printing}") - false - else - true - end - end - end - - def can_have_child?(name, is_dir) - is_dir && !name.start_with?('.') - end - - def write_cookbook(cookbook_path, cookbook_version_json, from_fs) - cookbook_name = File.basename(cookbook_path) - child = make_child_entry(cookbook_name) - - # Use the copy/diff algorithm to copy it down so we don't destroy - # chefignored data. This is terribly un-thread-safe. - Chef::ChefFS::FileSystem.copy_to(Chef::ChefFS::FilePattern.new("/#{cookbook_path}"), from_fs, child, nil, {:purge => true}) - - # Write out .uploaded-cookbook-version.json - cookbook_file_path = File.join(file_path, cookbook_name) - if !File.exists?(cookbook_file_path) - FileUtils.mkdir_p(cookbook_file_path) - end - uploaded_cookbook_version_path = File.join(cookbook_file_path, Chef::Cookbook::CookbookVersionLoader::UPLOADED_COOKBOOK_VERSION_FILE) - File.open(uploaded_cookbook_version_path, 'w') do |file| - file.write(cookbook_version_json) - end - end - - protected - - def make_child_entry(child_name) - ChefRepositoryFileSystemCookbookDir.new(child_name, self) - end - 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 deleted file mode 100644 index 39172e7ab9..0000000000 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb +++ /dev/null @@ -1,81 +0,0 @@ -# -# Author:: John Keiser (<jkeiser@opscode.com>) -# Author:: Ho-Sheng Hsiao (<hosh@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/chef_fs/file_system/not_found_error' - -class Chef - module ChefFS - module FileSystem - # ChefRepositoryFileSystemEntry works just like FileSystemEntry, - # except can inflate Chef objects - class ChefRepositoryFileSystemEntry < FileSystemEntry - def initialize(name, parent, file_path = nil, data_handler = nil) - super(name, parent, file_path) - @data_handler = data_handler - end - - def write_pretty_json=(value) - @write_pretty_json = value - end - - def write_pretty_json - @write_pretty_json.nil? ? root.write_pretty_json : @write_pretty_json - end - - def data_handler - @data_handler || parent.data_handler - end - - def chef_object - begin - return data_handler.chef_object(Chef::JSONCompat.parse(read)) - rescue - Chef::Log.error("Could not read #{path_for_printing} into a Chef object: #{$!}") - end - nil - end - - def can_have_child?(name, is_dir) - !is_dir && name[-5..-1] == '.json' - end - - def write(file_contents) - if file_contents && write_pretty_json && name[-5..-1] == '.json' - file_contents = minimize(file_contents, self) - end - super(file_contents) - end - - def minimize(file_contents, entry) - object = Chef::JSONCompat.parse(file_contents) - object = data_handler.normalize(object, entry) - object = data_handler.minimize(object, entry) - Chef::JSONCompat.to_json_pretty(object) - end - - protected - - def make_child_entry(child_name) - ChefRepositoryFileSystemEntry.new(child_name, self) - 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 deleted file mode 100644 index b0bd08fc39..0000000000 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb +++ /dev/null @@ -1,199 +0,0 @@ -# -# Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2012-2016 Chef Software, 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/chef_repository_file_system_entry' -require 'chef/chef_fs/file_system/chef_repository_file_system_acls_dir' -require 'chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir' -require 'chef/chef_fs/file_system/chef_repository_file_system_data_bags_dir' -require 'chef/chef_fs/file_system/multiplexed_dir' -require 'chef/chef_fs/data_handler/client_data_handler' -require 'chef/chef_fs/data_handler/environment_data_handler' -require 'chef/chef_fs/data_handler/node_data_handler' -require 'chef/chef_fs/data_handler/policy_data_handler' -require 'chef/chef_fs/data_handler/policy_group_data_handler' -require 'chef/chef_fs/data_handler/role_data_handler' -require 'chef/chef_fs/data_handler/user_data_handler' -require 'chef/chef_fs/data_handler/group_data_handler' -require 'chef/chef_fs/data_handler/container_data_handler' - -class Chef - module ChefFS - module FileSystem - - # - # Represents the root of a local Chef repository, with directories for - # nodes, cookbooks, roles, etc. under it. - # - class ChefRepositoryFileSystemRootDir < BaseFSDir - # - # Create a new Chef Repository File System root. - # - # == Parameters - # [child_paths] - # A hash of child paths, e.g.: - # "nodes" => [ '/var/nodes', '/home/jkeiser/nodes' ], - # "roles" => [ '/var/roles' ], - # ... - # [root_paths] - # An array of paths representing the top level, where - # +org.json+, +members.json+, and +invites.json+ will be stored. - # [chef_config] - a hash of options that looks suspiciously like the ones - # stored in Chef::Config, containing at least these keys: - # :versioned_cookbooks:: whether to include versions in cookbook names - def initialize(child_paths, root_paths=[], chef_config=Chef::Config) - super("", nil) - @child_paths = child_paths - @root_paths = root_paths - @versioned_cookbooks = chef_config[:versioned_cookbooks] - end - - attr_accessor :write_pretty_json - - attr_reader :root_paths - attr_reader :child_paths - attr_reader :versioned_cookbooks - - CHILDREN = %w(org.json invitations.json members.json) - - def children - @children ||= begin - result = child_paths.keys.sort.map { |name| make_child_entry(name) } - result += CHILDREN.map { |name| make_child_entry(name) } - result.select { |c| c && c.exists? }.sort_by { |c| c.name } - end - end - - def can_have_child?(name, is_dir) - if is_dir - child_paths.has_key?(name) - elsif root_dir - CHILDREN.include?(name) - else - false - end - end - - def create_child(name, file_contents = nil) - if file_contents - child = root_dir.create_child(name, file_contents) - else - child_paths[name].each do |path| - begin - Dir.mkdir(path) - rescue Errno::EEXIST - end - end - child = make_child_entry(name) - end - @children = nil - child - end - - def json_class - nil - end - - # Used to print out a human-readable file system description - def fs_description - repo_paths = root_paths || [ File.dirname(child_paths['cookbooks'][0]) ] - result = "repository at #{repo_paths.join(', ')}\n" - if versioned_cookbooks - result << " Multiple versions per cookbook\n" - else - result << " One version per cookbook\n" - end - child_paths.each_pair do |name, paths| - if paths.any? { |path| !repo_paths.include?(File.dirname(path)) } - result << " #{name} at #{paths.join(', ')}\n" - end - end - result - end - - private - - # - # A FileSystemEntry representing the root path where invites.json, - # members.json and org.json may be found. - # - def root_dir - existing_paths = root_paths.select { |path| File.exists?(path) } - if existing_paths.size > 0 - MultiplexedDir.new(existing_paths.map do |path| - dir = ChefRepositoryFileSystemEntry.new(name, parent, path) - dir.write_pretty_json = !!write_pretty_json - dir - end) - end - end - - # - # Create a child entry of the appropriate type: - # cookbooks, data_bags, acls, etc. All will be multiplexed (i.e. if - # you have multiple paths for cookbooks, the multiplexed dir will grab - # cookbooks from all of them when you list or grab them). - # - def make_child_entry(name) - if CHILDREN.include?(name) - return nil if !root_dir - return root_dir.child(name) - end - - paths = (child_paths[name] || []).select { |path| File.exists?(path) } - if paths.size == 0 - return NonexistentFSObject.new(name, self) - end - case name - when 'cookbooks' - dirs = paths.map { |path| ChefRepositoryFileSystemCookbooksDir.new(name, self, path) } - when 'data_bags' - dirs = paths.map { |path| ChefRepositoryFileSystemDataBagsDir.new(name, self, path) } - when 'acls' - dirs = paths.map { |path| ChefRepositoryFileSystemAclsDir.new(name, self, path) } - else - data_handler = case name - when 'clients' - Chef::ChefFS::DataHandler::ClientDataHandler.new - when 'environments' - Chef::ChefFS::DataHandler::EnvironmentDataHandler.new - when 'nodes' - Chef::ChefFS::DataHandler::NodeDataHandler.new - when 'policies' - Chef::ChefFS::DataHandler::PolicyDataHandler.new - when 'policy_groups' - Chef::ChefFS::DataHandler::PolicyGroupDataHandler.new - when 'roles' - Chef::ChefFS::DataHandler::RoleDataHandler.new - when 'users' - Chef::ChefFS::DataHandler::UserDataHandler.new - when 'groups' - Chef::ChefFS::DataHandler::GroupDataHandler.new - when 'containers' - Chef::ChefFS::DataHandler::ContainerDataHandler.new - else - raise "Unknown top level path #{name}" - end - dirs = paths.map { |path| ChefRepositoryFileSystemEntry.new(name, self, path, data_handler) } - end - MultiplexedDir.new(dirs) - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/chef_server/acl_dir.rb b/lib/chef/chef_fs/file_system/chef_server/acl_dir.rb new file mode 100644 index 0000000000..fff47c986d --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/acl_dir.rb @@ -0,0 +1,65 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/acl_entry' +require 'chef/chef_fs/file_system/operation_not_allowed_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class AclDir < BaseFSDir + def api_path + parent.parent.child(name).api_path + end + + def make_child_entry(name, exists = nil) + result = @children.select { |child| child.name == name }.first if @children + result || AclEntry.new(name, self, exists) + end + + def can_have_child?(name, is_dir) + name =~ /\.json$/ && !is_dir + end + + def children + if @children.nil? + # Grab the ACTUAL children (/nodes, /containers, etc.) and get their names + names = parent.parent.child(name).children.map { |child| child.dir? ? "#{child.name}.json" : child.name } + @children = names.map { |name| make_child_entry(name, true) } + end + @children + end + + def create_child(name, file_contents) + raise OperationNotAllowedError.new(:create_child, self, nil, "ACLs can only be updated, and can only be created when the corresponding object is created.") + end + + def data_handler + parent.data_handler + end + + def rest + parent.rest + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/acl_entry.rb b/lib/chef/chef_fs/file_system/chef_server/acl_entry.rb new file mode 100644 index 0000000000..f87a7eaca9 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/acl_entry.rb @@ -0,0 +1,60 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/operation_not_allowed_error' +require 'chef/chef_fs/file_system/operation_failed_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class AclEntry < RestListEntry + PERMISSIONS = %w(create read update delete grant) + + def api_path + "#{super}/_acl" + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self, nil, "ACLs cannot be deleted") + end + + def write(file_contents) + # ACL writes are fun. + acls = data_handler.normalize(Chef::JSONCompat.parse(file_contents), self) + PERMISSIONS.each do |permission| + begin + rest.put("#{api_path}/#{permission}", { permission => acls[permission] }) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") + end + end + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/acls_dir.rb b/lib/chef/chef_fs/file_system/chef_server/acls_dir.rb new file mode 100644 index 0000000000..bc7168e31d --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/acls_dir.rb @@ -0,0 +1,70 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/acl_dir' +require 'chef/chef_fs/file_system/chef_server/cookbooks_acl_dir' +require 'chef/chef_fs/file_system/chef_server/acl_entry' +require 'chef/chef_fs/data_handler/acl_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class AclsDir < BaseFSDir + ENTITY_TYPES = %w(clients containers cookbooks data_bags environments groups nodes roles) # we don't read sandboxes, so we don't read their acls + + def data_handler + @data_handler ||= Chef::ChefFS::DataHandler::AclDataHandler.new + end + + def api_path + parent.api_path + end + + def make_child_entry(name) + children.select { |child| child.name == name }.first + end + + def can_have_child?(name, is_dir) + is_dir ? ENTITY_TYPES.include?(name) : name == 'organization.json' + end + + def children + if @children.nil? + @children = ENTITY_TYPES.map do |entity_type| + case entity_type + when 'cookbooks' + CookbooksAclDir.new(entity_type, self) + else + AclDir.new(entity_type, self) + end + end + @children << AclEntry.new('organization.json', self, true) # the org acl is retrieved as GET /organizations/ORGNAME/ANYTHINGATALL/_acl + end + @children + end + + def rest + parent.rest + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/chef_server_root_dir.rb b/lib/chef/chef_fs/file_system/chef_server/chef_server_root_dir.rb new file mode 100644 index 0000000000..1ac03166c2 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/chef_server_root_dir.rb @@ -0,0 +1,192 @@ +# +# 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/server_api' +require 'chef/chef_fs/file_system/chef_server/acls_dir' +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/cookbooks_dir' +require 'chef/chef_fs/file_system/chef_server/data_bags_dir' +require 'chef/chef_fs/file_system/chef_server/nodes_dir' +require 'chef/chef_fs/file_system/chef_server/org_entry' +require 'chef/chef_fs/file_system/chef_server/organization_invites_entry' +require 'chef/chef_fs/file_system/chef_server/organization_members_entry' +require 'chef/chef_fs/file_system/chef_server/policies_dir' +require 'chef/chef_fs/file_system/chef_server/policy_groups_dir' +require 'chef/chef_fs/file_system/chef_server/environments_dir' +require 'chef/chef_fs/data_handler/acl_data_handler' +require 'chef/chef_fs/data_handler/client_data_handler' +require 'chef/chef_fs/data_handler/environment_data_handler' +require 'chef/chef_fs/data_handler/node_data_handler' +require 'chef/chef_fs/data_handler/role_data_handler' +require 'chef/chef_fs/data_handler/user_data_handler' +require 'chef/chef_fs/data_handler/group_data_handler' +require 'chef/chef_fs/data_handler/container_data_handler' +require 'chef/chef_fs/data_handler/policy_group_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # + # Represents the root of a Chef server (or organization), under which + # nodes, roles, cookbooks, etc. can be found. + # + class ChefServerRootDir < BaseFSDir + # + # Create a new Chef server root. + # + # == Parameters + # + # [root_name] + # A friendly name for the root, for printing--like "remote" or "chef_central". + # [chef_config] + # A hash with options that look suspiciously like Chef::Config, including the + # following keys: + # :chef_server_url:: The URL to the Chef server or top of the organization + # :node_name:: The username to authenticate to the Chef server with + # :client_key:: The private key for the user for authentication + # :environment:: The environment in which you are presently working + # :repo_mode:: + # The repository mode, :hosted_everything, :everything or :static. + # This determines the set of subdirectories the Chef server will + # offer up. + # :versioned_cookbooks:: whether or not to include versions in cookbook names + # [options] + # Other options: + # :cookbook_version:: when cookbooks are retrieved, grab this version for them. + # :freeze:: freeze cookbooks on upload + # + def initialize(root_name, chef_config, options = {}) + 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 = chef_config[:repo_mode] + @versioned_cookbooks = chef_config[:versioned_cookbooks] + @root_name = root_name + @cookbook_version = options[:cookbook_version] # Used in knife diff and download for server cookbook version + end + + attr_reader :chef_server_url + attr_reader :chef_username + attr_reader :chef_private_key + attr_reader :environment + attr_reader :repo_mode + attr_reader :cookbook_version + attr_reader :versioned_cookbooks + + def fs_description + "Chef server at #{chef_server_url} (user #{chef_username}), repo_mode = #{repo_mode}" + end + + def rest + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :raw_output => true, :api_version => "0") + end + + def get_json(path) + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :api_version => "0").get(path) + end + + def chef_rest + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key) + end + + def api_path + "" + end + + def path_for_printing + "#{@root_name}/" + end + + def can_have_child?(name, is_dir) + result = children.select { |child| child.name == name }.first + result && !!result.dir? == !!is_dir + end + + def org + @org ||= begin + path = Pathname.new(URI.parse(chef_server_url).path).cleanpath + if File.dirname(path) == '/organizations' + File.basename(path) + else + # In Chef 12, everything is in an org. + 'chef' + end + end + end + + def make_child_entry(name) + children.select { |child| child.name == name }.first + end + + def children + @children ||= begin + result = [ + # /cookbooks + CookbooksDir.new("cookbooks", self), + # /data_bags + DataBagsDir.new("data_bags", self, "data"), + # /environments + EnvironmentsDir.new("environments", self, nil, Chef::ChefFS::DataHandler::EnvironmentDataHandler.new), + # /roles + RestListDir.new("roles", self, nil, Chef::ChefFS::DataHandler::RoleDataHandler.new) + ] + if repo_mode == 'hosted_everything' + result += [ + # /acls + AclsDir.new("acls", self), + # /clients + RestListDir.new("clients", self, nil, Chef::ChefFS::DataHandler::ClientDataHandler.new), + # /containers + RestListDir.new("containers", self, nil, Chef::ChefFS::DataHandler::ContainerDataHandler.new), + # /groups + RestListDir.new("groups", self, nil, Chef::ChefFS::DataHandler::GroupDataHandler.new), + # /nodes + NodesDir.new("nodes", self, nil, Chef::ChefFS::DataHandler::NodeDataHandler.new), + # /org.json + OrgEntry.new("org.json", self), + # /members.json + OrganizationMembersEntry.new("members.json", self), + # /invitations.json + OrganizationInvitesEntry.new("invitations.json", self), + # /policies + PoliciesDir.new("policies", self, nil, Chef::ChefFS::DataHandler::PolicyDataHandler.new), + # /policy_groups + PolicyGroupsDir.new("policy_groups", self, nil, Chef::ChefFS::DataHandler::PolicyGroupDataHandler.new), + ] + elsif repo_mode != 'static' + result += [ + # /clients + RestListDir.new("clients", self, nil, Chef::ChefFS::DataHandler::ClientDataHandler.new), + # /nodes + NodesDir.new("nodes", self, nil, Chef::ChefFS::DataHandler::NodeDataHandler.new), + # /users + RestListDir.new("users", self, nil, Chef::ChefFS::DataHandler::UserDataHandler.new) + ] + end + result.sort_by { |child| child.name } + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb b/lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb new file mode 100644 index 0000000000..51de0bfd86 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb @@ -0,0 +1,226 @@ +# +# 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/command_line' +require 'chef/chef_fs/file_system/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/cookbook_subdir' +require 'chef/chef_fs/file_system/chef_server/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 + module ChefServer + class CookbookDir < BaseFSDir + def initialize(name, parent, options = {}) + super(name, parent) + @exists = options[:exists] + # If the name is apache2-1.0.0 and versioned_cookbooks is on, we know + # the actual cookbook_name and version. + if root.versioned_cookbooks + if name =~ VALID_VERSIONED_COOKBOOK_NAME + @cookbook_name = $1 + @version = $2 + else + @exists = false + end + else + @cookbook_name = name + @version = root.cookbook_version # nil unless --cookbook-version specified in download/diff + end + end + + attr_reader :cookbook_name, :version + + 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 => { } + } + + # See Erchef code + # https://github.com/opscode/chef_objects/blob/968a63344d38fd507f6ace05f73d53e9cd7fb043/src/chef_regex.erl#L94 + VALID_VERSIONED_COOKBOOK_NAME = /^([.a-zA-Z0-9_-]+)-(\d+\.\d+\.\d+)$/ + + def add_child(child) + @children << child + end + + def api_path + "#{parent.api_path}/#{cookbook_name}/#{version || "_latest"}" + end + + def make_child_entry(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 + children.select { |child| child.name == name }.first + rescue Chef::ChefFS::FileSystem::NotFoundError + nil + end + end + + def can_have_child?(name, is_dir) + # A cookbook's root may not have directories unless they are segment directories + return name != 'root_files' && COOKBOOK_SEGMENT_INFO.keys.include?(name.to_sym) if is_dir + return 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 + @children = @children.sort_by { |c| c.name } + end + @children + end + + def dir? + exists? + end + + def delete(recurse) + if recurse + begin + rest.delete(api_path) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") + rescue Net::HTTPServerException + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "HTTP error deleting: #{e}") + end + end + else + raise NotFoundError.new(self) if !exists? + raise MustDeleteRecursivelyError.new(self, "#{path_for_printing} must be deleted recursively") + end + end + + # In versioned cookbook mode, actually check if the version exists + # Probably want to cache this. + def exists? + if @exists.nil? + @exists = parent.children.any? { |child| child.name == name } + end + @exists + 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).each do |type, old_entry, new_entry| + if [ :directory_to_file, :file_to_directory, :deleted, :added, :modified ].include?(type) + are_same = false + end + end + [ are_same, nil, nil ] + end + + def copy_from(other, options = {}) + parent.upload_cookbook_from(other, options) + 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(self, @could_not_get_chef_object) + 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 ||= Chef::CookbookVersion.json_create(root.get_json(api_path)) + ensure + Chef::Config[:http_retry_count] = old_retry_count + end + + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading: #{e}") + + rescue Net::HTTPServerException => e + if e.response.code == "404" + @could_not_get_chef_object = e + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, @could_not_get_chef_object) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") + 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 => e + if e.response.code == "500" + @could_not_get_chef_object = e + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, @could_not_get_chef_object) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") + end + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbook_file.rb b/lib/chef/chef_fs/file_system/chef_server/cookbook_file.rb new file mode 100644 index 0000000000..721c5092cf --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbook_file.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_object' +require 'chef/http/simple' +require 'openssl' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class CookbookFile < BaseFSObject + def initialize(name, parent, file) + super(name, parent) + @file = file + end + + attr_reader :file + + def checksum + file[:checksum] + end + + def read + begin + tmpfile = rest.streaming_request(file[:url]) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading #{file[:url]}: #{e}") + rescue Net::HTTPServerException => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "#{e.message} retrieving #{file[:url]}") + end + + begin + tmpfile.open + tmpfile.read + ensure + tmpfile.close! + 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) + OpenSSL::Digest::MD5.hexdigest(value) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbook_subdir.rb b/lib/chef/chef_fs/file_system/chef_server/cookbook_subdir.rb new file mode 100644 index 0000000000..55c0168077 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbook_subdir.rb @@ -0,0 +1,61 @@ +# +# 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 + module ChefServer + 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 make_child_entry(name) + result = @children.select { |child| child.name == name }.first if @children + result || NonexistentFSObject.new(name, self) + end + + def rest + parent.rest + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbooks_acl_dir.rb b/lib/chef/chef_fs/file_system/chef_server/cookbooks_acl_dir.rb new file mode 100644 index 0000000000..999fb58f79 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbooks_acl_dir.rb @@ -0,0 +1,43 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/chef_server/acl_dir' +require 'chef/chef_fs/file_system/chef_server/acl_entry' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class CookbooksAclDir < AclDir + # If versioned_cookbooks is on, the list of cookbooks will have versions + # in them. But all versions of a cookbook have the same acl, so even if + # we have cookbooks/apache2-1.0.0 and cookbooks/apache2-1.1.2, we will + # only have one acl: acls/cookbooks/apache2.json. Thus, the list of + # children of acls/cookbooks is a unique list of cookbook *names*. + def children + if @children.nil? + names = parent.parent.child(name).children.map { |child| "#{child.cookbook_name}.json" } + @children = names.uniq.map { |name| make_child_entry(name, true) } + end + @children + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/cookbooks_dir.rb b/lib/chef/chef_fs/file_system/chef_server/cookbooks_dir.rb new file mode 100644 index 0000000000..28d88ea330 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/cookbooks_dir.rb @@ -0,0 +1,154 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/cookbook_dir' +require 'chef/chef_fs/file_system/operation_failed_error' +require 'chef/chef_fs/file_system/cookbook_frozen_error' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_dir' +require 'chef/mixin/file_class' + +require 'tmpdir' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class CookbooksDir < RestListDir + + include Chef::Mixin::FileClass + + def make_child_entry(name) + result = @children.select { |child| child.name == name }.first if @children + result || CookbookDir.new(name, self) + end + + def children + @children ||= begin + if root.versioned_cookbooks + result = [] + root.get_json("#{api_path}/?num_versions=all").each_pair do |cookbook_name, cookbooks| + cookbooks['versions'].each do |cookbook_version| + result << CookbookDir.new("#{cookbook_name}-#{cookbook_version['version']}", self, :exists => true) + end + end + else + result = root.get_json(api_path).keys.map { |cookbook_name| CookbookDir.new(cookbook_name, self, :exists => true) } + end + result.sort_by(&:name) + end + end + + def create_child_from(other, options = {}) + @children = nil + upload_cookbook_from(other, options) + end + + def upload_cookbook_from(other, options = {}) + root.versioned_cookbooks ? upload_versioned_cookbook(other, options) : upload_unversioned_cookbook(other, options) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") + rescue Net::HTTPServerException => e + case e.response.code + when "409" + raise Chef::ChefFS::FileSystem::CookbookFrozenError.new(:write, self, e, "Cookbook #{other.name} is frozen") + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") + end + rescue Chef::Exceptions::CookbookFrozen => e + raise Chef::ChefFS::FileSystem::CookbookFrozenError.new(:write, self, e, "Cookbook #{other.name} is frozen") + end + + # Knife currently does not understand versioned cookbooks + # Cookbook Version uploader also requires a lot of refactoring + # to make this work. So instead, we make a temporary cookbook + # symlinking back to real cookbook, and upload the proxy. + def upload_versioned_cookbook(other, options) + cookbook_name = Chef::ChefFS::FileSystem::Repository::ChefRepositoryFileSystemCookbookDir.canonical_cookbook_name(other.name) + + Dir.mktmpdir do |temp_cookbooks_path| + proxy_cookbook_path = "#{temp_cookbooks_path}/#{cookbook_name}" + + # Make a symlink + file_class.symlink other.file_path, proxy_cookbook_path + + # Instantiate a proxy loader using the temporary symlink + proxy_loader = Chef::Cookbook::CookbookVersionLoader.new(proxy_cookbook_path, other.parent.chefignore) + proxy_loader.load_cookbooks + + cookbook_to_upload = proxy_loader.cookbook_version + cookbook_to_upload.freeze_version if options[:freeze] + + # Instantiate a new uploader based on the proxy loader + uploader = Chef::CookbookUploader.new(cookbook_to_upload, :force => options[:force], :rest => root.chef_rest) + + with_actual_cookbooks_dir(temp_cookbooks_path) do + upload_cookbook!(uploader) + end + + # + # When the temporary directory is being deleted on + # windows, the contents of the symlink under that + # directory is also deleted. So explicitly remove + # the symlink without removing the original contents if we + # are running on windows + # + if Chef::Platform.windows? + Dir.rmdir proxy_cookbook_path + end + end + end + + def upload_unversioned_cookbook(other, options) + cookbook_to_upload = other.chef_object + cookbook_to_upload.freeze_version if options[:freeze] + uploader = Chef::CookbookUploader.new(cookbook_to_upload, :force => options[:force], :rest => root.chef_rest) + + with_actual_cookbooks_dir(other.parent.file_path) do + upload_cookbook!(uploader) + end + end + + # Work around the fact that CookbookUploader doesn't understand chef_repo_path (yet) + def with_actual_cookbooks_dir(actual_cookbook_path) + old_cookbook_path = Chef::Config.cookbook_path + Chef::Config.cookbook_path = actual_cookbook_path if !Chef::Config.cookbook_path + + yield + ensure + Chef::Config.cookbook_path = old_cookbook_path + end + + def upload_cookbook!(uploader, options = {}) + if uploader.respond_to?(:upload_cookbook) + uploader.upload_cookbook + else + uploader.upload_cookbooks + end + end + + def can_have_child?(name, is_dir) + return false if !is_dir + return false if root.versioned_cookbooks && name !~ Chef::ChefFS::FileSystem::ChefServer::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME + return true + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/data_bag_dir.rb b/lib/chef/chef_fs/file_system/chef_server/data_bag_dir.rb new file mode 100644 index 0000000000..b657243d64 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/data_bag_dir.rb @@ -0,0 +1,71 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/must_delete_recursively_error' +require 'chef/chef_fs/data_handler/data_bag_item_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class DataBagDir < RestListDir + def initialize(name, parent, exists = nil) + super(name, parent, nil, Chef::ChefFS::DataHandler::DataBagItemDataHandler.new) + @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.new(self) + end + + def exists? + if @exists.nil? + @exists = parent.children.any? { |child| child.name == name } + end + @exists + end + + def delete(recurse) + if !recurse + raise NotFoundError.new(self) if !exists? + raise MustDeleteRecursivelyError.new(self, "#{path_for_printing} must be deleted recursively") + end + begin + rest.delete(api_path) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "HTTP error deleting: #{e}") + end + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/data_bags_dir.rb b/lib/chef/chef_fs/file_system/chef_server/data_bags_dir.rb new file mode 100644 index 0000000000..50952cfc1b --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/data_bags_dir.rb @@ -0,0 +1,69 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/data_bag_dir' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class DataBagsDir < RestListDir + def make_child_entry(name, exists = false) + result = @children.select { |child| child.name == name }.first if @children + result || DataBagDir.new(name, self, exists) + end + + def children + begin + @children ||= root.get_json(api_path).keys.sort.map { |entry| make_child_entry(entry, true) } + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout getting children: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error getting children: #{e}") + end + end + end + + def can_have_child?(name, is_dir) + is_dir + end + + def create_child(name, file_contents) + begin + rest.post(api_path, { 'name' => name }) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating child '#{name}': #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "409" + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Cannot create #{name} under #{path}: already exists") + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "HTTP error creating child '#{name}': #{e}") + end + end + @children = nil + DataBagDir.new(name, self, true) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/environments_dir.rb b/lib/chef/chef_fs/file_system/chef_server/environments_dir.rb index 984129bab3..721c8a38d4 100644 --- a/lib/chef/chef_fs/file_system/environments_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_server/environments_dir.rb @@ -17,36 +17,38 @@ # require 'chef/chef_fs/file_system/base_fs_dir' -require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' require 'chef/chef_fs/file_system/not_found_error' require 'chef/chef_fs/file_system/default_environment_cannot_be_modified_error' class Chef module ChefFS module FileSystem - class EnvironmentsDir < RestListDir - def make_child_entry(name, exists = nil) - if name == '_default.json' - DefaultEnvironmentEntry.new(name, self, exists) - else - super + module ChefServer + class EnvironmentsDir < RestListDir + def make_child_entry(name, exists = nil) + if name == '_default.json' + DefaultEnvironmentEntry.new(name, self, exists) + else + super + end end - end - class DefaultEnvironmentEntry < RestListEntry - def initialize(name, parent, exists = nil) - super(name, parent) - @exists = exists - end + class DefaultEnvironmentEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end - def delete(recurse) - raise NotFoundError.new(self) if !exists? - raise DefaultEnvironmentCannotBeModifiedError.new(:delete, self) - end + def delete(recurse) + raise NotFoundError.new(self) if !exists? + raise DefaultEnvironmentCannotBeModifiedError.new(:delete, self) + end - def write(file_contents) - raise NotFoundError.new(self) if !exists? - raise DefaultEnvironmentCannotBeModifiedError.new(:write, self) + def write(file_contents) + raise NotFoundError.new(self) if !exists? + raise DefaultEnvironmentCannotBeModifiedError.new(:write, self) + end end end end diff --git a/lib/chef/chef_fs/file_system/acl_entry.rb b/lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb index e739bd8f10..c0728fbe1f 100644 --- a/lib/chef/chef_fs/file_system/acl_entry.rb +++ b/lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb @@ -1,6 +1,6 @@ # # Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. +# Copyright:: Copyright (c) 2012 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,41 +16,36 @@ # limitations under the License. # -require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' require 'chef/chef_fs/file_system/not_found_error' -require 'chef/chef_fs/file_system/operation_not_allowed_error' -require 'chef/chef_fs/file_system/operation_failed_error' +require 'chef/chef_fs/data_handler/node_data_handler' class Chef module ChefFS module FileSystem - class AclEntry < RestListEntry - PERMISSIONS = %w(create read update delete grant) - - def api_path - "#{super}/_acl" - end - - def delete(recurse) - raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self, nil, "ACLs cannot be deleted") - end - - def write(file_contents) - # ACL writes are fun. - acls = data_handler.normalize(Chef::JSONCompat.parse(file_contents), self) - PERMISSIONS.each do |permission| + module ChefServer + class NodesDir < RestListDir + # Identical to RestListDir.children, except supports environments + def children begin - rest.put("#{api_path}/#{permission}", { permission => acls[permission] }) + @children ||= root.get_json(env_api_path).keys.sort.map do |key| + make_child_entry("#{key}.json", true) + end rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") rescue Net::HTTPServerException => e - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") end end end + + def env_api_path + environment ? "environments/#{environment}/#{api_path}" : api_path + end end end end diff --git a/lib/chef/chef_fs/file_system/chef_server/org_entry.rb b/lib/chef/chef_fs/file_system/chef_server/org_entry.rb new file mode 100644 index 0000000000..82c5764b42 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/org_entry.rb @@ -0,0 +1,31 @@ +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # /organizations/NAME/org.json + # Represents the actual data at /organizations/NAME (the full name, etc.) + class OrgEntry < RestListEntry + def data_handler + Chef::ChefFS::DataHandler::OrganizationDataHandler.new + end + + # /organizations/foo/org.json -> GET /organizations/foo + def api_path + parent.api_path + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/organization_invites_entry.rb b/lib/chef/chef_fs/file_system/chef_server/organization_invites_entry.rb new file mode 100644 index 0000000000..cf276c6466 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/organization_invites_entry.rb @@ -0,0 +1,61 @@ +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_invites_data_handler' +require 'chef/json_compat' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # /organizations/NAME/invitations.json + # read data from: + # - GET /organizations/NAME/association_requests + # write data to: + # - remove from list: DELETE /organizations/NAME/association_requests/id + # - add to list: POST /organizations/NAME/association_requests + class OrganizationInvitesEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + Chef::ChefFS::DataHandler::OrganizationInvitesDataHandler.new + end + + # /organizations/foo/invites.json -> /organizations/foo/association_requests + def api_path + File.join(parent.api_path, 'association_requests') + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + + def write(contents) + desired_invites = minimize_value(Chef::JSONCompat.parse(contents, :create_additions => false)) + actual_invites = _read_json.inject({}) { |h,val| h[val['username']] = val['id']; h } + invites = actual_invites.keys + (desired_invites - invites).each do |invite| + begin + rest.post(api_path, { 'user' => invite }) + rescue Net::HTTPServerException => e + if e.response.code == '409' + Chef::Log.warn("Could not invite #{invite} to organization #{org}: #{api_error_text(e.response)}") + else + raise + end + end + end + (invites - desired_invites).each do |invite| + rest.delete(File.join(api_path, actual_invites[invite])) + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/organization_members_entry.rb b/lib/chef/chef_fs/file_system/chef_server/organization_members_entry.rb new file mode 100644 index 0000000000..1c1c231643 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/organization_members_entry.rb @@ -0,0 +1,60 @@ +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_members_data_handler' +require 'chef/json_compat' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # /organizations/NAME/members.json + # reads data from: + # - GET /organizations/NAME/users + # writes data to: + # - remove from list: DELETE /organizations/NAME/users/name + # - add to list: POST /organizations/NAME/users/name + class OrganizationMembersEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + Chef::ChefFS::DataHandler::OrganizationMembersDataHandler.new + end + + # /organizations/foo/members.json -> /organizations/foo/users + def api_path + File.join(parent.api_path, 'users') + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + + def write(contents) + desired_members = minimize_value(Chef::JSONCompat.parse(contents, :create_additions => false)) + members = minimize_value(_read_json) + (desired_members - members).each do |member| + begin + rest.post(api_path, 'username' => member) + rescue Net::HTTPServerException => e + if %w(404 405).include?(e.response.code) + raise "Chef server at #{api_path} does not allow you to directly add members. Please either upgrade your Chef server or move the users you want into invitations.json instead of members.json." + else + raise + end + end + end + (members - desired_members).each do |member| + rest.delete(File.join(api_path, member)) + end + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/policies_dir.rb b/lib/chef/chef_fs/file_system/chef_server/policies_dir.rb new file mode 100644 index 0000000000..b6c34cfee7 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/policies_dir.rb @@ -0,0 +1,160 @@ +# +# 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/rest_list_dir' +require 'chef/chef_fs/file_system/chef_server/policy_revision_entry' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # + # Server API: + # /policies - list of policies by name + # - /policies/NAME - represents a policy with all revisions + # - /policies/NAME/revisions - list of revisions for that policy + # - /policies/NAME/revisions/REVISION - actual policy-revision document + # + # Local Repository and ChefFS: + # /policies - PoliciesDir - maps to server API /policies + # - /policies/NAME-REVISION.json - PolicyRevision - maps to /policies/NAME/revisions/REVISION + # + class PoliciesDir < RestListDir + # Children: NAME-REVISION.json for all revisions of all policies + # + # /nodes: { + # "node1": "https://api.opscode.com/organizations/myorg/nodes/node1", + # "node2": "https://api.opscode.com/organizations/myorg/nodes/node2", + # } + # + # /policies: { + # "foo": {} + # } + + def make_child_entry(name, exists = nil) + @children.select { |child| child.name == name }.first if @children + PolicyRevisionEntry.new(name, self, exists) + end + + # Children come from /policies in this format: + # { + # "foo": { + # "uri": "https://api.opscode.com/organizations/essentials/policies/foo", + # "revisions": { + # "1.0.0": { + # + # }, + # "1.0.1": { + # + # } + # } + # } + # } + def children + begin + # Grab the names of the children, append json, and make child entries + @children ||= begin + result = [] + data = root.get_json(api_path) + data.keys.sort.each do |policy_name| + data[policy_name]["revisions"].keys.each do |policy_revision| + filename = "#{policy_name}-#{policy_revision}.json" + result << make_child_entry(filename, true) + end + end + result + end + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if $!.response.code == "404" + # GET /organizations/ORG/policies returned 404, but that just might be because + # we are talking to an older version of the server that doesn't support policies. + # Do GET /orgqanizations/ORG to find out if the org exists at all. + # TODO use server API version instead of a second network request. + begin + root.get_json(parent.api_path) + # Return empty list if the organization exists but /policies didn't work + [] + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + end + end + + # + # Does POST <api_path> with file_contents + # + def create_child(name, file_contents) + # Parse the contents to ensure they are valid JSON + begin + object = Chef::JSONCompat.parse(file_contents) + rescue Chef::Exceptions::JSON::ParseError => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") + end + + # Create the child entry that will be returned + result = make_child_entry(name, true) + + # Normalize the file_contents before post (add defaults, etc.) + if data_handler + object = data_handler.normalize_for_post(object, result) + data_handler.verify_integrity(object, result) do |error| + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") + end + end + + # POST /api_path with the normalized file_contents + begin + policy_name, policy_revision = data_handler.name_and_revision(name) + rest.post("#{api_path}/#{policy_name}/revisions", object) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + # 409 = AlreadyExistsError + elsif $!.response.code == "409" + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") + end + end + + # Clear the cache of children so that if someone asks for children + # again, we will get it again + @children = nil + + result + end + + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/policy_group_entry.rb b/lib/chef/chef_fs/file_system/chef_server/policy_group_entry.rb new file mode 100644 index 0000000000..76ececbd5b --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/policy_group_entry.rb @@ -0,0 +1,137 @@ +# +# 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/already_exists_error' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/file_system/operation_failed_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # Represents an entire policy group. + # Server path: /organizations/ORG/policy_groups/GROUP + # Repository path: policy_groups\GROUP.json + # Format: + # { + # "policies": { + # "a": { "revision_id": "1.0.0" } + # } + # } + class PolicyGroupEntry < RestListEntry + # delete is handled normally: + # DELETE /organizations/ORG/policy_groups/GROUP + + # read is handled normally: + # GET /organizations/ORG/policy_groups/GROUP + + # write is different. + # For each policy: + # - PUT /organizations/ORG/policy_groups/GROUP/policies/POLICY + # For each policy on the server but not the client: + # - DELETE /organizations/ORG/policy_groups/GROUP/policies/POLICY + # If the server has associations for a, b and c, + # And the client wants associations for a, x and y, + # We must PUT a, x and y + # And DELETE b and c + def write(file_contents) + # Parse the contents to ensure they are valid JSON + begin + object = Chef::JSONCompat.parse(file_contents) + rescue Chef::Exceptions::JSON::ParseError => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") + end + + if data_handler + object = data_handler.normalize_for_put(object, self) + data_handler.verify_integrity(object, self) do |error| + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") + end + end + + begin + + # this should all get carted to PolicyGroupEntry#write. + + # the server demands the full policy data, but we want users' local policy_group documents to just + # have the data you'd get from GET /policy_groups/POLICY_GROUP. so we try to fetch that. + + # ordinarily this would be POST to the normal URL, but we do PUT to + # /organizations/{organization}/policy_groups/{policy_group}/policies/{policy_name} with the full + # policy data, for each individual policy. + policy_datas = {} + + object["policies"].each do |policy_name, policy_data| + policy_path = "/policies/#{policy_name}/revisions/#{policy_data["revision_id"]}" + + get_data = begin + rest.get(policy_path) + rescue Net::HTTPServerException => e + raise "Could not find policy '#{policy_name}'' with revision '#{policy_data["revision_id"]}'' on the server" + end + + # GET policy data + server_policy_data = Chef::JSONCompat.parse(get_data) + + # if it comes back 404, raise an Exception with "Policy file X does not exist with revision Y on the server" + + # otherwise, add it to the list of policyfile datas. + policy_datas[policy_name] = server_policy_data + end + + begin + existing_group = Chef::JSONCompat.parse(self.read) + rescue NotFoundError + # It's OK if the group doesn't already exist, just means no existing policies + end + + # now we have the fullpolicy data for each policies, which is what the PUT endpoint demands. + policy_datas.each do |policy_name, policy_data| + # PUT /organizations/ORG/policy_groups/GROUP/policies/NAME + rest.put("#{api_path}/policies/#{policy_name}", policy_data) + end + + # Now we need to remove any policies that are *not* in our current group. + if existing_group && existing_group['policies'] + (existing_group['policies'].keys - policy_datas.keys).each do |policy_name| + rest.delete("#{api_path}/policies/#{policy_name}") + end + end + + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + # 409 = AlreadyExistsError + elsif $!.response.code == "409" + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") + end + end + + self + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/policy_groups_dir.rb b/lib/chef/chef_fs/file_system/chef_server/policy_groups_dir.rb index c98ae50b97..0c061ebdb2 100644 --- a/lib/chef/chef_fs/file_system/policy_groups_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_server/policy_groups_dir.rb @@ -17,23 +17,25 @@ # require 'chef/chef_fs/file_system/base_fs_dir' -require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' require 'chef/chef_fs/file_system/not_found_error' -require 'chef/chef_fs/file_system/policy_group_entry' +require 'chef/chef_fs/file_system/chef_server/policy_group_entry' class Chef module ChefFS module FileSystem - class PolicyGroupsDir < RestListDir - def make_child_entry(name, exists = nil) - PolicyGroupEntry.new(name, self, exists) - end + module ChefServer + class PolicyGroupsDir < RestListDir + def make_child_entry(name, exists = nil) + PolicyGroupEntry.new(name, self, exists) + end - def create_child(name, file_contents) - entry = make_child_entry(name, true) - entry.write(file_contents) - @children = nil - entry + def create_child(name, file_contents) + entry = make_child_entry(name, true) + entry.write(file_contents) + @children = nil + entry + end end end end diff --git a/lib/chef/chef_fs/file_system/chef_server/policy_revision_entry.rb b/lib/chef/chef_fs/file_system/chef_server/policy_revision_entry.rb new file mode 100644 index 0000000000..7ea93278d0 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/policy_revision_entry.rb @@ -0,0 +1,25 @@ +require 'chef/chef_fs/file_system/chef_server/rest_list_entry' +require 'chef/chef_fs/data_handler/policy_data_handler' + +class Chef + module ChefFS + module FileSystem + module ChefServer + # /policies/NAME-REVISION.json + # Represents the actual data at /organizations/ORG/policies/NAME/revisions/REVISION + class PolicyRevisionEntry < RestListEntry + + # /policies/foo-1.0.0.json -> /policies/foo/revisions/1.0.0 + def api_path(options={}) + policy_name, revision_id = data_handler.name_and_revision(name) + "#{parent.api_path}/#{policy_name}/revisions/#{revision_id}" + end + + def write(file_contents) + raise OperationNotAllowedError.new(:write, self, nil, "cannot be updated: policy revisions are immutable once uploaded. If you want to change the policy, create a new revision with your changes") + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/rest_list_dir.rb b/lib/chef/chef_fs/file_system/chef_server/rest_list_dir.rb new file mode 100644 index 0000000000..84790190d1 --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/rest_list_dir.rb @@ -0,0 +1,179 @@ +# +# 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/chef_server/rest_list_entry' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class RestListDir < BaseFSDir + def initialize(name, parent, api_path = nil, data_handler = nil) + super(name, parent) + @api_path = api_path || (parent.api_path == "" ? name : "#{parent.api_path}/#{name}") + @data_handler = data_handler + end + + attr_reader :api_path + attr_reader :data_handler + + def can_have_child?(name, is_dir) + name =~ /\.json$/ && !is_dir + end + + # + # When talking to a modern (12.0+) Chef server + # knife list / + # -> /nodes + # -> /policies + # -> /policy_groups + # -> /roles + # + # 12.0 or 12.1 will fail when you do this: + # knife list / --recursive + # Because it thinks /policies exists, and when it tries to list its children + # it gets a 404 (indicating it actually doesn't exist). + # + # With this change, knife list / --recursive will list /policies as a real, empty directory. + # + # Alternately, we could have done some sort of detection when we listed the top level + # and determined which endpoints the server would support, and returned only those. + # So you wouldn't see /policies in that case at all. + # The issue with that is there's no efficient way to do it because we can't find out + # the server version directly, and can't ask the server for a list of the endpoints it supports. + # + + + # + # Does GET /<api_path>, assumes the result is of the format: + # + # { + # "foo": "<api_path>/foo", + # "bar": "<api_path>/bar", + # } + # + # Children are foo.json and bar.json in this case. + # + def children + begin + # Grab the names of the children, append json, and make child entries + @children ||= root.get_json(api_path).keys.sort.map do |key| + make_child_entry("#{key}.json", true) + end + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if $!.response.code == "404" + + if parent.is_a?(ChefServerRootDir) + # GET /organizations/ORG/<container> returned 404, but that just might be because + # we are talking to an older version of the server that doesn't support policies. + # Do GET /orgqanizations/ORG to find out if the org exists at all. + # TODO use server API version instead of a second network request. + begin + root.get_json(parent.api_path) + # Return empty list if the organization exists but /policies didn't work + [] + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + else + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") + end + end + end + + # + # Does POST <api_path> with file_contents + # + def create_child(name, file_contents) + # Parse the contents to ensure they are valid JSON + begin + object = Chef::JSONCompat.parse(file_contents) + rescue Chef::Exceptions::JSON::ParseError => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") + end + + # Create the child entry that will be returned + result = make_child_entry(name, true) + + # Normalize the file_contents before post (add defaults, etc.) + if data_handler + object = data_handler.normalize_for_post(object, result) + data_handler.verify_integrity(object, result) do |error| + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") + end + end + + # POST /api_path with the normalized file_contents + begin + rest.post(api_path, object) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") + rescue Net::HTTPServerException => e + # 404 = NotFoundError + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + # 409 = AlreadyExistsError + elsif $!.response.code == "409" + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") + # Anything else is unexpected (OperationFailedError) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") + end + end + + # Clear the cache of children so that if someone asks for children + # again, we will get it again + @children = nil + + result + end + + def org + parent.org + end + + def environment + parent.environment + end + + def rest + parent.rest + end + + def make_child_entry(name, exists = nil) + @children.select { |child| child.name == name }.first if @children + RestListEntry.new(name, self, exists) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb b/lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb new file mode 100644 index 0000000000..4ceab42c1b --- /dev/null +++ b/lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb @@ -0,0 +1,187 @@ +# +# 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/chef_fs/file_system/operation_failed_error' +require 'chef/role' +require 'chef/node' +require 'chef/json_compat' + +class Chef + module ChefFS + module FileSystem + module ChefServer + class RestListEntry < BaseFSObject + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + parent.data_handler + end + + def api_child_name + if name.length < 5 || name[-5,5] != ".json" + raise "Invalid name #{path}: must end in .json" + end + name[0,name.length-5] + end + + def api_path + "#{parent.api_path}/#{api_child_name}" + end + + def org + parent.org + 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(api_path) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") + end + end + end + + def read + Chef::JSONCompat.to_json_pretty(minimize_value(_read_json)) + end + + def _read_json + begin + # Minimize the value (get rid of defaults) so the results don't look terrible + root.get_json(api_path) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading: #{e}") + rescue Net::HTTPServerException => e + if $!.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") + end + end + end + + def chef_object + # REST will inflate the Chef object using json_class + data_handler.json_class.json_create(read) + end + + def minimize_value(value) + data_handler.minimize(data_handler.normalize(value, self), self) + end + + def compare_to(other) + # TODO this pair of reads can be parallelized + + # Grab the other value + begin + other_value_json = other.read + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ nil, nil, :none ] + end + + # Grab this value + begin + value = _read_json + rescue Chef::ChefFS::FileSystem::NotFoundError + return [ false, :none, other_value_json ] + end + + # Minimize (and normalize) both values for easy and beautiful diffs + value = minimize_value(value) + value_json = Chef::JSONCompat.to_json_pretty(value) + begin + other_value = Chef::JSONCompat.parse(other_value_json) + rescue Chef::Exceptions::JSON::ParseError => e + Chef::Log.warn("Parse error reading #{other.path_for_printing} as JSON: #{e}") + return [ nil, value_json, other_value_json ] + end + other_value = minimize_value(other_value) + other_value_json = Chef::JSONCompat.to_json_pretty(other_value) + + [ value == other_value, value_json, other_value_json ] + end + + def rest + parent.rest + end + + def write(file_contents) + begin + object = Chef::JSONCompat.parse(file_contents) + rescue Chef::Exceptions::JSON::ParseError => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Parse error reading JSON: #{e}") + end + + if data_handler + object = data_handler.normalize_for_put(object, self) + data_handler.verify_integrity(object, self) do |error| + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, nil, "#{error}") + end + end + + begin + rest.put(api_path, object) + rescue Timeout::Error => e + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) + else + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") + end + end + end + + def api_error_text(response) + begin + Chef::JSONCompat.parse(response.body)['error'].join("\n") + rescue + response.body + end + end + 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 deleted file mode 100644 index badb70ce50..0000000000 --- a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb +++ /dev/null @@ -1,190 +0,0 @@ -# -# 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/server_api' -require 'chef/chef_fs/file_system/acls_dir' -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' -require 'chef/chef_fs/file_system/org_entry' -require 'chef/chef_fs/file_system/organization_invites_entry' -require 'chef/chef_fs/file_system/organization_members_entry' -require 'chef/chef_fs/file_system/policies_dir' -require 'chef/chef_fs/file_system/policy_groups_dir' -require 'chef/chef_fs/file_system/environments_dir' -require 'chef/chef_fs/data_handler/acl_data_handler' -require 'chef/chef_fs/data_handler/client_data_handler' -require 'chef/chef_fs/data_handler/environment_data_handler' -require 'chef/chef_fs/data_handler/node_data_handler' -require 'chef/chef_fs/data_handler/role_data_handler' -require 'chef/chef_fs/data_handler/user_data_handler' -require 'chef/chef_fs/data_handler/group_data_handler' -require 'chef/chef_fs/data_handler/container_data_handler' -require 'chef/chef_fs/data_handler/policy_group_data_handler' - -class Chef - module ChefFS - module FileSystem - # - # Represents the root of a Chef server (or organization), under which - # nodes, roles, cookbooks, etc. can be found. - # - class ChefServerRootDir < BaseFSDir - # - # Create a new Chef server root. - # - # == Parameters - # - # [root_name] - # A friendly name for the root, for printing--like "remote" or "chef_central". - # [chef_config] - # A hash with options that look suspiciously like Chef::Config, including the - # following keys: - # :chef_server_url:: The URL to the Chef server or top of the organization - # :node_name:: The username to authenticate to the Chef server with - # :client_key:: The private key for the user for authentication - # :environment:: The environment in which you are presently working - # :repo_mode:: - # The repository mode, :hosted_everything, :everything or :static. - # This determines the set of subdirectories the Chef server will - # offer up. - # :versioned_cookbooks:: whether or not to include versions in cookbook names - # [options] - # Other options: - # :cookbook_version:: when cookbooks are retrieved, grab this version for them. - # :freeze:: freeze cookbooks on upload - # - def initialize(root_name, chef_config, options = {}) - 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 = chef_config[:repo_mode] - @versioned_cookbooks = chef_config[:versioned_cookbooks] - @root_name = root_name - @cookbook_version = options[:cookbook_version] # Used in knife diff and download for server cookbook version - end - - attr_reader :chef_server_url - attr_reader :chef_username - attr_reader :chef_private_key - attr_reader :environment - attr_reader :repo_mode - attr_reader :cookbook_version - attr_reader :versioned_cookbooks - - def fs_description - "Chef server at #{chef_server_url} (user #{chef_username}), repo_mode = #{repo_mode}" - end - - def rest - Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :raw_output => true, :api_version => "0") - end - - def get_json(path) - Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :api_version => "0").get(path) - end - - def chef_rest - Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key) - end - - def api_path - "" - end - - def path_for_printing - "#{@root_name}/" - end - - def can_have_child?(name, is_dir) - result = children.select { |child| child.name == name }.first - result && !!result.dir? == !!is_dir - end - - def org - @org ||= begin - path = Pathname.new(URI.parse(chef_server_url).path).cleanpath - if File.dirname(path) == '/organizations' - File.basename(path) - else - # In Chef 12, everything is in an org. - 'chef' - end - end - end - - def make_child_entry(name) - children.select { |child| child.name == name }.first - end - - def children - @children ||= begin - result = [ - # /cookbooks - CookbooksDir.new("cookbooks", self), - # /data_bags - DataBagsDir.new("data_bags", self, "data"), - # /environments - EnvironmentsDir.new("environments", self, nil, Chef::ChefFS::DataHandler::EnvironmentDataHandler.new), - # /roles - RestListDir.new("roles", self, nil, Chef::ChefFS::DataHandler::RoleDataHandler.new) - ] - if repo_mode == 'hosted_everything' - result += [ - # /acls - AclsDir.new("acls", self), - # /clients - RestListDir.new("clients", self, nil, Chef::ChefFS::DataHandler::ClientDataHandler.new), - # /containers - RestListDir.new("containers", self, nil, Chef::ChefFS::DataHandler::ContainerDataHandler.new), - # /groups - RestListDir.new("groups", self, nil, Chef::ChefFS::DataHandler::GroupDataHandler.new), - # /nodes - NodesDir.new("nodes", self, nil, Chef::ChefFS::DataHandler::NodeDataHandler.new), - # /org.json - OrgEntry.new("org.json", self), - # /members.json - OrganizationMembersEntry.new("members.json", self), - # /invitations.json - OrganizationInvitesEntry.new("invitations.json", self), - # /policies - PoliciesDir.new("policies", self, nil, Chef::ChefFS::DataHandler::PolicyDataHandler.new), - # /policy_groups - PolicyGroupsDir.new("policy_groups", self, nil, Chef::ChefFS::DataHandler::PolicyGroupDataHandler.new), - ] - elsif repo_mode != 'static' - result += [ - # /clients - RestListDir.new("clients", self, nil, Chef::ChefFS::DataHandler::ClientDataHandler.new), - # /nodes - NodesDir.new("nodes", self, nil, Chef::ChefFS::DataHandler::NodeDataHandler.new), - # /users - RestListDir.new("users", self, nil, Chef::ChefFS::DataHandler::UserDataHandler.new) - ] - end - result.sort_by { |child| child.name } - end - end - 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 deleted file mode 100644 index 42408c0869..0000000000 --- a/lib/chef/chef_fs/file_system/cookbook_dir.rb +++ /dev/null @@ -1,224 +0,0 @@ -# -# 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/command_line' -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, options = {}) - super(name, parent) - @exists = options[:exists] - # If the name is apache2-1.0.0 and versioned_cookbooks is on, we know - # the actual cookbook_name and version. - if root.versioned_cookbooks - if name =~ VALID_VERSIONED_COOKBOOK_NAME - @cookbook_name = $1 - @version = $2 - else - @exists = false - end - else - @cookbook_name = name - @version = root.cookbook_version # nil unless --cookbook-version specified in download/diff - end - end - - attr_reader :cookbook_name, :version - - 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 => { } - } - - # See Erchef code - # https://github.com/opscode/chef_objects/blob/968a63344d38fd507f6ace05f73d53e9cd7fb043/src/chef_regex.erl#L94 - VALID_VERSIONED_COOKBOOK_NAME = /^([.a-zA-Z0-9_-]+)-(\d+\.\d+\.\d+)$/ - - def add_child(child) - @children << child - end - - def api_path - "#{parent.api_path}/#{cookbook_name}/#{version || "_latest"}" - end - - def make_child_entry(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 - children.select { |child| child.name == name }.first - rescue Chef::ChefFS::FileSystem::NotFoundError - nil - end - end - - def can_have_child?(name, is_dir) - # A cookbook's root may not have directories unless they are segment directories - return name != 'root_files' && COOKBOOK_SEGMENT_INFO.keys.include?(name.to_sym) if is_dir - return 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 - @children = @children.sort_by { |c| c.name } - end - @children - end - - def dir? - exists? - end - - def delete(recurse) - if recurse - begin - rest.delete(api_path) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") - rescue Net::HTTPServerException - if $!.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "HTTP error deleting: #{e}") - end - end - else - raise NotFoundError.new(self) if !exists? - raise MustDeleteRecursivelyError.new(self, "#{path_for_printing} must be deleted recursively") - end - end - - # In versioned cookbook mode, actually check if the version exists - # Probably want to cache this. - def exists? - if @exists.nil? - @exists = parent.children.any? { |child| child.name == name } - end - @exists - 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).each do |type, old_entry, new_entry| - if [ :directory_to_file, :file_to_directory, :deleted, :added, :modified ].include?(type) - are_same = false - end - end - [ are_same, nil, nil ] - end - - def copy_from(other, options = {}) - parent.upload_cookbook_from(other, options) - 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(self, @could_not_get_chef_object) - 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 ||= Chef::CookbookVersion.json_create(root.get_json(api_path)) - ensure - Chef::Config[:http_retry_count] = old_retry_count - end - - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading: #{e}") - - rescue Net::HTTPServerException => e - if e.response.code == "404" - @could_not_get_chef_object = e - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, @could_not_get_chef_object) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") - 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 => e - if e.response.code == "500" - @could_not_get_chef_object = e - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, @could_not_get_chef_object) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") - 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 deleted file mode 100644 index f755174183..0000000000 --- a/lib/chef/chef_fs/file_system/cookbook_file.rb +++ /dev/null @@ -1,82 +0,0 @@ -# -# 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/http/simple' -require 'openssl' - -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 - begin - tmpfile = rest.streaming_request(file[:url]) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading #{file[:url]}: #{e}") - rescue Net::HTTPServerException => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "#{e.message} retrieving #{file[:url]}") - end - - begin - tmpfile.open - tmpfile.read - ensure - tmpfile.close! - 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) - OpenSSL::Digest::MD5.hexdigest(value) - 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 deleted file mode 100644 index e7a6d3bccd..0000000000 --- a/lib/chef/chef_fs/file_system/cookbook_subdir.rb +++ /dev/null @@ -1,59 +0,0 @@ -# -# 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 make_child_entry(name) - result = @children.select { |child| child.name == name }.first if @children - result || NonexistentFSObject.new(name, self) - end - - def rest - parent.rest - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb deleted file mode 100644 index 560ceb4886..0000000000 --- a/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb +++ /dev/null @@ -1,41 +0,0 @@ -# -# Author:: John Keiser (<jkeiser@opscode.com>) -# Copyright:: Copyright (c) 2013 Opscode, Inc. -# License:: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -require 'chef/chef_fs/file_system/acl_dir' -require 'chef/chef_fs/file_system/acl_entry' - -class Chef - module ChefFS - module FileSystem - class CookbooksAclDir < AclDir - # If versioned_cookbooks is on, the list of cookbooks will have versions - # in them. But all versions of a cookbook have the same acl, so even if - # we have cookbooks/apache2-1.0.0 and cookbooks/apache2-1.1.2, we will - # only have one acl: acls/cookbooks/apache2.json. Thus, the list of - # children of acls/cookbooks is a unique list of cookbook *names*. - def children - if @children.nil? - names = parent.parent.child(name).children.map { |child| "#{child.cookbook_name}.json" } - @children = names.uniq.map { |name| make_child_entry(name, true) } - end - @children - 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 deleted file mode 100644 index 8cbc06931b..0000000000 --- a/lib/chef/chef_fs/file_system/cookbooks_dir.rb +++ /dev/null @@ -1,152 +0,0 @@ -# -# 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' -require 'chef/chef_fs/file_system/operation_failed_error' -require 'chef/chef_fs/file_system/cookbook_frozen_error' -require 'chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir' -require 'chef/mixin/file_class' - -require 'tmpdir' - -class Chef - module ChefFS - module FileSystem - class CookbooksDir < RestListDir - - include Chef::Mixin::FileClass - - def make_child_entry(name) - result = @children.select { |child| child.name == name }.first if @children - result || CookbookDir.new(name, self) - end - - def children - @children ||= begin - if root.versioned_cookbooks - result = [] - root.get_json("#{api_path}/?num_versions=all").each_pair do |cookbook_name, cookbooks| - cookbooks['versions'].each do |cookbook_version| - result << CookbookDir.new("#{cookbook_name}-#{cookbook_version['version']}", self, :exists => true) - end - end - else - result = root.get_json(api_path).keys.map { |cookbook_name| CookbookDir.new(cookbook_name, self, :exists => true) } - end - result.sort_by(&:name) - end - end - - def create_child_from(other, options = {}) - @children = nil - upload_cookbook_from(other, options) - end - - def upload_cookbook_from(other, options = {}) - root.versioned_cookbooks ? upload_versioned_cookbook(other, options) : upload_unversioned_cookbook(other, options) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") - rescue Net::HTTPServerException => e - case e.response.code - when "409" - raise Chef::ChefFS::FileSystem::CookbookFrozenError.new(:write, self, e, "Cookbook #{other.name} is frozen") - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") - end - rescue Chef::Exceptions::CookbookFrozen => e - raise Chef::ChefFS::FileSystem::CookbookFrozenError.new(:write, self, e, "Cookbook #{other.name} is frozen") - end - - # Knife currently does not understand versioned cookbooks - # Cookbook Version uploader also requires a lot of refactoring - # to make this work. So instead, we make a temporary cookbook - # symlinking back to real cookbook, and upload the proxy. - def upload_versioned_cookbook(other, options) - cookbook_name = Chef::ChefFS::FileSystem::ChefRepositoryFileSystemCookbookDir.canonical_cookbook_name(other.name) - - Dir.mktmpdir do |temp_cookbooks_path| - proxy_cookbook_path = "#{temp_cookbooks_path}/#{cookbook_name}" - - # Make a symlink - file_class.symlink other.file_path, proxy_cookbook_path - - # Instantiate a proxy loader using the temporary symlink - proxy_loader = Chef::Cookbook::CookbookVersionLoader.new(proxy_cookbook_path, other.parent.chefignore) - proxy_loader.load_cookbooks - - cookbook_to_upload = proxy_loader.cookbook_version - cookbook_to_upload.freeze_version if options[:freeze] - - # Instantiate a new uploader based on the proxy loader - uploader = Chef::CookbookUploader.new(cookbook_to_upload, :force => options[:force], :rest => root.chef_rest) - - with_actual_cookbooks_dir(temp_cookbooks_path) do - upload_cookbook!(uploader) - end - - # - # When the temporary directory is being deleted on - # windows, the contents of the symlink under that - # directory is also deleted. So explicitly remove - # the symlink without removing the original contents if we - # are running on windows - # - if Chef::Platform.windows? - Dir.rmdir proxy_cookbook_path - end - end - end - - def upload_unversioned_cookbook(other, options) - cookbook_to_upload = other.chef_object - cookbook_to_upload.freeze_version if options[:freeze] - uploader = Chef::CookbookUploader.new(cookbook_to_upload, :force => options[:force], :rest => root.chef_rest) - - with_actual_cookbooks_dir(other.parent.file_path) do - upload_cookbook!(uploader) - end - end - - # Work around the fact that CookbookUploader doesn't understand chef_repo_path (yet) - def with_actual_cookbooks_dir(actual_cookbook_path) - old_cookbook_path = Chef::Config.cookbook_path - Chef::Config.cookbook_path = actual_cookbook_path if !Chef::Config.cookbook_path - - yield - ensure - Chef::Config.cookbook_path = old_cookbook_path - end - - def upload_cookbook!(uploader, options = {}) - if uploader.respond_to?(:upload_cookbook) - uploader.upload_cookbook - else - uploader.upload_cookbooks - end - end - - def can_have_child?(name, is_dir) - return false if !is_dir - return false if root.versioned_cookbooks && name !~ Chef::ChefFS::FileSystem::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME - return true - 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 deleted file mode 100644 index 7a4463e69d..0000000000 --- a/lib/chef/chef_fs/file_system/data_bag_dir.rb +++ /dev/null @@ -1,69 +0,0 @@ -# -# 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/not_found_error' -require 'chef/chef_fs/file_system/must_delete_recursively_error' -require 'chef/chef_fs/data_handler/data_bag_item_data_handler' - -class Chef - module ChefFS - module FileSystem - class DataBagDir < RestListDir - def initialize(name, parent, exists = nil) - super(name, parent, nil, Chef::ChefFS::DataHandler::DataBagItemDataHandler.new) - @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.new(self) - end - - def exists? - if @exists.nil? - @exists = parent.children.any? { |child| child.name == name } - end - @exists - end - - def delete(recurse) - if !recurse - raise NotFoundError.new(self) if !exists? - raise MustDeleteRecursivelyError.new(self, "#{path_for_printing} must be deleted recursively") - end - begin - rest.delete(api_path) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") - rescue Net::HTTPServerException => e - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "HTTP error deleting: #{e}") - 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 deleted file mode 100644 index 0645a6c16f..0000000000 --- a/lib/chef/chef_fs/file_system/data_bags_dir.rb +++ /dev/null @@ -1,67 +0,0 @@ -# -# 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 make_child_entry(name, exists = false) - result = @children.select { |child| child.name == name }.first if @children - result || DataBagDir.new(name, self, exists) - end - - def children - begin - @children ||= root.get_json(api_path).keys.sort.map { |entry| make_child_entry(entry, true) } - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout getting children: #{e}") - rescue Net::HTTPServerException => e - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error getting children: #{e}") - end - end - end - - def can_have_child?(name, is_dir) - is_dir - end - - def create_child(name, file_contents) - begin - rest.post(api_path, { 'name' => name }) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating child '#{name}': #{e}") - rescue Net::HTTPServerException => e - if e.response.code == "409" - raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Cannot create #{name} under #{path}: already exists") - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "HTTP error creating child '#{name}': #{e}") - end - end - @children = nil - 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 deleted file mode 100644 index 5ce8b3320a..0000000000 --- a/lib/chef/chef_fs/file_system/file_system_entry.rb +++ /dev/null @@ -1,115 +0,0 @@ -# -# 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/already_exists_error' -require 'chef/chef_fs/file_system/must_delete_recursively_error' -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 - file_path - end - - def children - # Except cookbooks and data bag dirs, all things must be json files - begin - Dir.entries(file_path).sort. - map { |child_name| make_child_entry(child_name) }. - select { |child| child && can_have_child?(child.name, child.dir?) } - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end - end - - def create_child(child_name, file_contents=nil) - child = make_child_entry(child_name) - if child.exists? - raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, child) - end - if file_contents - child.write(file_contents) - else - begin - Dir.mkdir(child.file_path) - rescue Errno::EEXIST - raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, child) - end - end - child - end - - def dir? - File.directory?(file_path) - end - - def delete(recurse) - begin - if dir? - if !recurse - raise MustDeleteRecursivelyError.new(self, $!) - end - FileUtils.rm_r(file_path) - else - File.delete(file_path) - end - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end - end - - def exists? - File.exists?(file_path) && (parent.nil? || parent.can_have_child?(name, dir?)) - end - - def read - begin - File.open(file_path, "rb") {|f| f.read} - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end - end - - def write(content) - File.open(file_path, 'wb') do |file| - file.write(content) - end - end - - protected - - def make_child_entry(child_name) - FileSystemEntry.new(child_name, self) - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/memory/memory_dir.rb b/lib/chef/chef_fs/file_system/memory/memory_dir.rb new file mode 100644 index 0000000000..b571a6f469 --- /dev/null +++ b/lib/chef/chef_fs/file_system/memory/memory_dir.rb @@ -0,0 +1,53 @@ +require 'chef/chef_fs/file_system/base_fs_dir' +require 'chef/chef_fs/file_system/memory/memory_file' + +class Chef + module ChefFS + module FileSystem + module Memory + class MemoryDir < Chef::ChefFS::FileSystem::BaseFSDir + def initialize(name, parent) + super(name, parent) + @children = [] + end + + attr_reader :children + + def make_child_entry(name) + @children.select { |child| child.name == name }.first + end + + def add_child(child) + @children.push(child) + end + + def can_have_child?(name, is_dir) + root.cannot_be_in_regex ? (name !~ root.cannot_be_in_regex) : true + end + + def add_file(path, value) + path_parts = path.split('/') + dir = add_dir(path_parts[0..-2].join('/')) + file = MemoryFile.new(path_parts[-1], dir, value) + dir.add_child(file) + file + end + + def add_dir(path) + path_parts = path.split('/') + dir = self + path_parts.each do |path_part| + subdir = dir.child(path_part) + if !subdir.exists? + subdir = MemoryDir.new(path_part, dir) + dir.add_child(subdir) + end + dir = subdir + end + dir + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/memory/memory_file.rb b/lib/chef/chef_fs/file_system/memory/memory_file.rb new file mode 100644 index 0000000000..8b280eeb7d --- /dev/null +++ b/lib/chef/chef_fs/file_system/memory/memory_file.rb @@ -0,0 +1,19 @@ +require 'chef/chef_fs/file_system/base_fs_object' + +class Chef + module ChefFS + module FileSystem + module Memory + class MemoryFile < Chef::ChefFS::FileSystem::BaseFSObject + def initialize(name, parent, value) + super(name, parent) + @value = value + end + def read + return @value + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/memory/memory_root.rb b/lib/chef/chef_fs/file_system/memory/memory_root.rb new file mode 100644 index 0000000000..f4a7b5208b --- /dev/null +++ b/lib/chef/chef_fs/file_system/memory/memory_root.rb @@ -0,0 +1,23 @@ +require 'chef/chef_fs/file_system/memory/memory_dir' + +class Chef + module ChefFS + module FileSystem + module Memory + class MemoryRoot < MemoryDir + def initialize(pretty_name, cannot_be_in_regex = nil) + super('', nil) + @pretty_name = pretty_name + @cannot_be_in_regex = cannot_be_in_regex + end + + attr_reader :cannot_be_in_regex + + def path_for_printing + @pretty_name + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/memory_dir.rb b/lib/chef/chef_fs/file_system/memory_dir.rb deleted file mode 100644 index 260a91693c..0000000000 --- a/lib/chef/chef_fs/file_system/memory_dir.rb +++ /dev/null @@ -1,51 +0,0 @@ -require 'chef/chef_fs/file_system/base_fs_dir' -require 'chef/chef_fs/file_system/memory_file' - -class Chef - module ChefFS - module FileSystem - class MemoryDir < Chef::ChefFS::FileSystem::BaseFSDir - def initialize(name, parent) - super(name, parent) - @children = [] - end - - attr_reader :children - - def make_child_entry(name) - @children.select { |child| child.name == name }.first - end - - def add_child(child) - @children.push(child) - end - - def can_have_child?(name, is_dir) - root.cannot_be_in_regex ? (name !~ root.cannot_be_in_regex) : true - end - - def add_file(path, value) - path_parts = path.split('/') - dir = add_dir(path_parts[0..-2].join('/')) - file = MemoryFile.new(path_parts[-1], dir, value) - dir.add_child(file) - file - end - - def add_dir(path) - path_parts = path.split('/') - dir = self - path_parts.each do |path_part| - subdir = dir.child(path_part) - if !subdir.exists? - subdir = MemoryDir.new(path_part, dir) - dir.add_child(subdir) - end - dir = subdir - end - dir - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/memory_file.rb b/lib/chef/chef_fs/file_system/memory_file.rb deleted file mode 100644 index 0c44e703f1..0000000000 --- a/lib/chef/chef_fs/file_system/memory_file.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'chef/chef_fs/file_system/base_fs_object' - -class Chef - module ChefFS - module FileSystem - class MemoryFile < Chef::ChefFS::FileSystem::BaseFSObject - def initialize(name, parent, value) - super(name, parent) - @value = value - end - def read - return @value - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/memory_root.rb b/lib/chef/chef_fs/file_system/memory_root.rb deleted file mode 100644 index 4a83830946..0000000000 --- a/lib/chef/chef_fs/file_system/memory_root.rb +++ /dev/null @@ -1,21 +0,0 @@ -require 'chef/chef_fs/file_system/memory_dir' - -class Chef - module ChefFS - module FileSystem - class MemoryRoot < MemoryDir - def initialize(pretty_name, cannot_be_in_regex = nil) - super('', nil) - @pretty_name = pretty_name - @cannot_be_in_regex = cannot_be_in_regex - end - - attr_reader :cannot_be_in_regex - - def path_for_printing - @pretty_name - 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 deleted file mode 100644 index 12bc1c06c8..0000000000 --- a/lib/chef/chef_fs/file_system/nodes_dir.rb +++ /dev/null @@ -1,51 +0,0 @@ -# -# 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' -require 'chef/chef_fs/data_handler/node_data_handler' - -class Chef - module ChefFS - module FileSystem - class NodesDir < RestListDir - # Identical to RestListDir.children, except supports environments - def children - begin - @children ||= root.get_json(env_api_path).keys.sort.map do |key| - make_child_entry("#{key}.json", true) - end - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") - rescue Net::HTTPServerException => e - if $!.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") - end - end - end - - def env_api_path - environment ? "environments/#{environment}/#{api_path}" : api_path - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/org_entry.rb b/lib/chef/chef_fs/file_system/org_entry.rb deleted file mode 100644 index df3acba528..0000000000 --- a/lib/chef/chef_fs/file_system/org_entry.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'chef/chef_fs/file_system/rest_list_entry' -require 'chef/chef_fs/data_handler/organization_data_handler' - -class Chef - module ChefFS - module FileSystem - # /organizations/NAME/org.json - # Represents the actual data at /organizations/NAME (the full name, etc.) - class OrgEntry < RestListEntry - def data_handler - Chef::ChefFS::DataHandler::OrganizationDataHandler.new - end - - # /organizations/foo/org.json -> GET /organizations/foo - def api_path - parent.api_path - end - - def exists? - parent.exists? - end - - def delete(recurse) - raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/organization_invites_entry.rb b/lib/chef/chef_fs/file_system/organization_invites_entry.rb deleted file mode 100644 index 5df37085cb..0000000000 --- a/lib/chef/chef_fs/file_system/organization_invites_entry.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'chef/chef_fs/file_system/rest_list_entry' -require 'chef/chef_fs/data_handler/organization_invites_data_handler' -require 'chef/json_compat' - -class Chef - module ChefFS - module FileSystem - # /organizations/NAME/invitations.json - # read data from: - # - GET /organizations/NAME/association_requests - # write data to: - # - remove from list: DELETE /organizations/NAME/association_requests/id - # - add to list: POST /organizations/NAME/association_requests - class OrganizationInvitesEntry < RestListEntry - def initialize(name, parent, exists = nil) - super(name, parent) - @exists = exists - end - - def data_handler - Chef::ChefFS::DataHandler::OrganizationInvitesDataHandler.new - end - - # /organizations/foo/invites.json -> /organizations/foo/association_requests - def api_path - File.join(parent.api_path, 'association_requests') - end - - def exists? - parent.exists? - end - - def delete(recurse) - raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) - end - - def write(contents) - desired_invites = minimize_value(Chef::JSONCompat.parse(contents, :create_additions => false)) - actual_invites = _read_json.inject({}) { |h,val| h[val['username']] = val['id']; h } - invites = actual_invites.keys - (desired_invites - invites).each do |invite| - begin - rest.post(api_path, { 'user' => invite }) - rescue Net::HTTPServerException => e - if e.response.code == '409' - Chef::Log.warn("Could not invite #{invite} to organization #{org}: #{api_error_text(e.response)}") - else - raise - end - end - end - (invites - desired_invites).each do |invite| - rest.delete(File.join(api_path, actual_invites[invite])) - end - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/organization_members_entry.rb b/lib/chef/chef_fs/file_system/organization_members_entry.rb deleted file mode 100644 index 40042a9cbc..0000000000 --- a/lib/chef/chef_fs/file_system/organization_members_entry.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'chef/chef_fs/file_system/rest_list_entry' -require 'chef/chef_fs/data_handler/organization_members_data_handler' -require 'chef/json_compat' - -class Chef - module ChefFS - module FileSystem - # /organizations/NAME/members.json - # reads data from: - # - GET /organizations/NAME/users - # writes data to: - # - remove from list: DELETE /organizations/NAME/users/name - # - add to list: POST /organizations/NAME/users/name - class OrganizationMembersEntry < RestListEntry - def initialize(name, parent, exists = nil) - super(name, parent) - @exists = exists - end - - def data_handler - Chef::ChefFS::DataHandler::OrganizationMembersDataHandler.new - end - - # /organizations/foo/members.json -> /organizations/foo/users - def api_path - File.join(parent.api_path, 'users') - end - - def exists? - parent.exists? - end - - def delete(recurse) - raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) - end - - def write(contents) - desired_members = minimize_value(Chef::JSONCompat.parse(contents, :create_additions => false)) - members = minimize_value(_read_json) - (desired_members - members).each do |member| - begin - rest.post(api_path, 'username' => member) - rescue Net::HTTPServerException => e - if %w(404 405).include?(e.response.code) - raise "Chef server at #{api_path} does not allow you to directly add members. Please either upgrade your Chef server or move the users you want into invitations.json instead of members.json." - else - raise - end - end - end - (members - desired_members).each do |member| - rest.delete(File.join(api_path, member)) - end - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/policies_dir.rb b/lib/chef/chef_fs/file_system/policies_dir.rb deleted file mode 100644 index a999ca0218..0000000000 --- a/lib/chef/chef_fs/file_system/policies_dir.rb +++ /dev/null @@ -1,158 +0,0 @@ -# -# 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/policy_revision_entry' - -class Chef - module ChefFS - module FileSystem - # - # Server API: - # /policies - list of policies by name - # - /policies/NAME - represents a policy with all revisions - # - /policies/NAME/revisions - list of revisions for that policy - # - /policies/NAME/revisions/REVISION - actual policy-revision document - # - # Local Repository and ChefFS: - # /policies - PoliciesDir - maps to server API /policies - # - /policies/NAME-REVISION.json - PolicyRevision - maps to /policies/NAME/revisions/REVISION - # - class PoliciesDir < RestListDir - # Children: NAME-REVISION.json for all revisions of all policies - # - # /nodes: { - # "node1": "https://api.opscode.com/organizations/myorg/nodes/node1", - # "node2": "https://api.opscode.com/organizations/myorg/nodes/node2", - # } - # - # /policies: { - # "foo": {} - # } - - def make_child_entry(name, exists = nil) - @children.select { |child| child.name == name }.first if @children - PolicyRevisionEntry.new(name, self, exists) - end - - # Children come from /policies in this format: - # { - # "foo": { - # "uri": "https://api.opscode.com/organizations/essentials/policies/foo", - # "revisions": { - # "1.0.0": { - # - # }, - # "1.0.1": { - # - # } - # } - # } - # } - def children - begin - # Grab the names of the children, append json, and make child entries - @children ||= begin - result = [] - data = root.get_json(api_path) - data.keys.sort.each do |policy_name| - data[policy_name]["revisions"].keys.each do |policy_revision| - filename = "#{policy_name}-#{policy_revision}.json" - result << make_child_entry(filename, true) - end - end - result - end - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") - rescue Net::HTTPServerException => e - # 404 = NotFoundError - if $!.response.code == "404" - # GET /organizations/ORG/policies returned 404, but that just might be because - # we are talking to an older version of the server that doesn't support policies. - # Do GET /orgqanizations/ORG to find out if the org exists at all. - # TODO use server API version instead of a second network request. - begin - root.get_json(parent.api_path) - # Return empty list if the organization exists but /policies didn't work - [] - rescue Net::HTTPServerException => e - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") - end - # Anything else is unexpected (OperationFailedError) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") - end - end - end - - # - # Does POST <api_path> with file_contents - # - def create_child(name, file_contents) - # Parse the contents to ensure they are valid JSON - begin - object = Chef::JSONCompat.parse(file_contents) - rescue Chef::Exceptions::JSON::ParseError => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") - end - - # Create the child entry that will be returned - result = make_child_entry(name, true) - - # Normalize the file_contents before post (add defaults, etc.) - if data_handler - object = data_handler.normalize_for_post(object, result) - data_handler.verify_integrity(object, result) do |error| - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") - end - end - - # POST /api_path with the normalized file_contents - begin - policy_name, policy_revision = data_handler.name_and_revision(name) - rest.post("#{api_path}/#{policy_name}/revisions", object) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") - rescue Net::HTTPServerException => e - # 404 = NotFoundError - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) - # 409 = AlreadyExistsError - elsif $!.response.code == "409" - raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") - # Anything else is unexpected (OperationFailedError) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") - end - end - - # Clear the cache of children so that if someone asks for children - # again, we will get it again - @children = nil - - result - end - - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/policy_group_entry.rb b/lib/chef/chef_fs/file_system/policy_group_entry.rb deleted file mode 100644 index 9885310cc7..0000000000 --- a/lib/chef/chef_fs/file_system/policy_group_entry.rb +++ /dev/null @@ -1,135 +0,0 @@ -# -# 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/already_exists_error' -require 'chef/chef_fs/file_system/not_found_error' -require 'chef/chef_fs/file_system/operation_failed_error' - -class Chef - module ChefFS - module FileSystem - # Represents an entire policy group. - # Server path: /organizations/ORG/policy_groups/GROUP - # Repository path: policy_groups\GROUP.json - # Format: - # { - # "policies": { - # "a": { "revision_id": "1.0.0" } - # } - # } - class PolicyGroupEntry < RestListEntry - # delete is handled normally: - # DELETE /organizations/ORG/policy_groups/GROUP - - # read is handled normally: - # GET /organizations/ORG/policy_groups/GROUP - - # write is different. - # For each policy: - # - PUT /organizations/ORG/policy_groups/GROUP/policies/POLICY - # For each policy on the server but not the client: - # - DELETE /organizations/ORG/policy_groups/GROUP/policies/POLICY - # If the server has associations for a, b and c, - # And the client wants associations for a, x and y, - # We must PUT a, x and y - # And DELETE b and c - def write(file_contents) - # Parse the contents to ensure they are valid JSON - begin - object = Chef::JSONCompat.parse(file_contents) - rescue Chef::Exceptions::JSON::ParseError => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") - end - - if data_handler - object = data_handler.normalize_for_put(object, self) - data_handler.verify_integrity(object, self) do |error| - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") - end - end - - begin - - # this should all get carted to PolicyGroupEntry#write. - - # the server demands the full policy data, but we want users' local policy_group documents to just - # have the data you'd get from GET /policy_groups/POLICY_GROUP. so we try to fetch that. - - # ordinarily this would be POST to the normal URL, but we do PUT to - # /organizations/{organization}/policy_groups/{policy_group}/policies/{policy_name} with the full - # policy data, for each individual policy. - policy_datas = {} - - object["policies"].each do |policy_name, policy_data| - policy_path = "/policies/#{policy_name}/revisions/#{policy_data["revision_id"]}" - - get_data = begin - rest.get(policy_path) - rescue Net::HTTPServerException => e - raise "Could not find policy '#{policy_name}'' with revision '#{policy_data["revision_id"]}'' on the server" - end - - # GET policy data - server_policy_data = Chef::JSONCompat.parse(get_data) - - # if it comes back 404, raise an Exception with "Policy file X does not exist with revision Y on the server" - - # otherwise, add it to the list of policyfile datas. - policy_datas[policy_name] = server_policy_data - end - - begin - existing_group = Chef::JSONCompat.parse(self.read) - rescue NotFoundError - # It's OK if the group doesn't already exist, just means no existing policies - end - - # now we have the fullpolicy data for each policies, which is what the PUT endpoint demands. - policy_datas.each do |policy_name, policy_data| - # PUT /organizations/ORG/policy_groups/GROUP/policies/NAME - rest.put("#{api_path}/policies/#{policy_name}", policy_data) - end - - # Now we need to remove any policies that are *not* in our current group. - if existing_group && existing_group['policies'] - (existing_group['policies'].keys - policy_datas.keys).each do |policy_name| - rest.delete("#{api_path}/policies/#{policy_name}") - end - end - - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") - rescue Net::HTTPServerException => e - # 404 = NotFoundError - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) - # 409 = AlreadyExistsError - elsif $!.response.code == "409" - raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") - # Anything else is unexpected (OperationFailedError) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") - end - end - - self - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/policy_revision_entry.rb b/lib/chef/chef_fs/file_system/policy_revision_entry.rb deleted file mode 100644 index 896586c54f..0000000000 --- a/lib/chef/chef_fs/file_system/policy_revision_entry.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'chef/chef_fs/file_system/rest_list_entry' -require 'chef/chef_fs/data_handler/policy_data_handler' - -class Chef - module ChefFS - module FileSystem - # /policies/NAME-REVISION.json - # Represents the actual data at /organizations/ORG/policies/NAME/revisions/REVISION - class PolicyRevisionEntry < RestListEntry - - # /policies/foo-1.0.0.json -> /policies/foo/revisions/1.0.0 - def api_path(options={}) - policy_name, revision_id = data_handler.name_and_revision(name) - "#{parent.api_path}/#{policy_name}/revisions/#{revision_id}" - end - - def write(file_contents) - raise OperationNotAllowedError.new(:write, self, nil, "cannot be updated: policy revisions are immutable once uploaded. If you want to change the policy, create a new revision with your changes") - end - end - end - end -end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_acls_dir.rb b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_acls_dir.rb index 7d2a930633..7d647c0fc3 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_acls_dir.rb +++ b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_acls_dir.rb @@ -16,22 +16,24 @@ # limitations under the License. # -require 'chef/chef_fs/file_system/chef_repository_file_system_entry' -require 'chef/chef_fs/file_system/acls_dir' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_entry' +require 'chef/chef_fs/file_system/chef_server/acls_dir' require 'chef/chef_fs/data_handler/acl_data_handler' class Chef module ChefFS module FileSystem - class ChefRepositoryFileSystemAclsDir < ChefRepositoryFileSystemEntry - def initialize(name, parent, path = nil) - super(name, parent, path, Chef::ChefFS::DataHandler::AclDataHandler.new) - end + module Repository + class ChefRepositoryFileSystemAclsDir < ChefRepositoryFileSystemEntry + def initialize(name, parent, path = nil) + super(name, parent, path, Chef::ChefFS::DataHandler::AclDataHandler.new) + end - def can_have_child?(name, is_dir) - is_dir ? Chef::ChefFS::FileSystem::AclsDir::ENTITY_TYPES.include?(name) : name == 'organization.json' + def can_have_child?(name, is_dir) + is_dir ? Chef::ChefFS::FileSystem::ChefServer::AclsDir::ENTITY_TYPES.include?(name) : name == 'organization.json' + end end end end end -end
\ No newline at end of file +end diff --git a/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_dir.rb b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_dir.rb new file mode 100644 index 0000000000..91b8652de3 --- /dev/null +++ b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_dir.rb @@ -0,0 +1,104 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_entry' +require 'chef/chef_fs/file_system/chef_server/cookbook_dir' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/cookbook/chefignore' +require 'chef/cookbook/cookbook_version_loader' + +class Chef + module ChefFS + module FileSystem + module Repository + class ChefRepositoryFileSystemCookbookDir < ChefRepositoryFileSystemCookbookEntry + def initialize(name, parent, file_path = nil) + super(name, parent, file_path) + end + + def chef_object + begin + loader = Chef::Cookbook::CookbookVersionLoader.new(file_path, parent.chefignore) + # We need the canonical cookbook name if we are using versioned cookbooks, but we don't + # want to spend a lot of time adding code to the main Chef libraries + if root.versioned_cookbooks + canonical_name = canonical_cookbook_name(File.basename(file_path)) + raise "When versioned_cookbooks mode is on, cookbook #{file_path} must match format <cookbook_name>-x.y.z" unless canonical_name + + # KLUDGE: We shouldn't have to use instance_variable_set + loader.instance_variable_set(:@cookbook_name, canonical_name) + end + + loader.load_cookbooks + cb = loader.cookbook_version + if !cb + Chef::Log.error("Cookbook #{file_path} empty.") + raise "Cookbook #{file_path} empty." + end + cb + rescue => e + Chef::Log.error("Could not read #{path_for_printing} into a Chef object: #{e}") + Chef::Log.error(e.backtrace.join("\n")) + raise + end + end + + def children + super.select { |entry| !(entry.dir? && entry.children.size == 0 ) } + end + + def can_have_child?(name, is_dir) + if is_dir + # Only the given directories will be uploaded. + return Chef::ChefFS::FileSystem::ChefServer::CookbookDir::COOKBOOK_SEGMENT_INFO.keys.include?(name.to_sym) && name != 'root_files' + elsif name == Chef::Cookbook::CookbookVersionLoader::UPLOADED_COOKBOOK_VERSION_FILE + return false + end + super(name, is_dir) + end + + # Exposed as a class method so that it can be used elsewhere + def self.canonical_cookbook_name(entry_name) + name_match = Chef::ChefFS::FileSystem::ChefServer::CookbookDir::VALID_VERSIONED_COOKBOOK_NAME.match(entry_name) + return nil if name_match.nil? + return name_match[1] + end + + def canonical_cookbook_name(entry_name) + self.class.canonical_cookbook_name(entry_name) + end + + def uploaded_cookbook_version_path + File.join(file_path, Chef::Cookbook::CookbookVersionLoader::UPLOADED_COOKBOOK_VERSION_FILE) + end + + def can_upload? + File.exists?(uploaded_cookbook_version_path) || children.size > 0 + end + + protected + + def make_child_entry(child_name) + segment_info = Chef::ChefFS::FileSystem::ChefServer::CookbookDir::COOKBOOK_SEGMENT_INFO[child_name.to_sym] || {} + ChefRepositoryFileSystemCookbookEntry.new(child_name, self, nil, segment_info[:ruby_only], segment_info[:recursive]) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_entry.rb b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_entry.rb new file mode 100644 index 0000000000..574ed44828 --- /dev/null +++ b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_entry.rb @@ -0,0 +1,82 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_entry' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_cookbooks_dir' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + module Repository + class ChefRepositoryFileSystemCookbookEntry < ChefRepositoryFileSystemEntry + def initialize(name, parent, file_path = nil, ruby_only = false, recursive = false) + super(name, parent, file_path) + @ruby_only = ruby_only + @recursive = recursive + end + + attr_reader :ruby_only + attr_reader :recursive + + def children + super.select { |entry| !(entry.dir? && entry.children.size == 0 ) } + end + + def can_have_child?(name, is_dir) + if is_dir + return recursive && name != '.' && name != '..' + elsif ruby_only + return false if name[-3..-1] != '.rb' + end + + # Check chefignore + ignorer = parent + loop do + if ignorer.is_a?(ChefRepositoryFileSystemCookbooksDir) + # Grab the path from entry to child + path_to_child = name + child = self + while child.parent != 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 || !ignorer.chefignore.ignored?(path_to_child) + end + ignorer = ignorer.parent + break unless ignorer + end + + true + end + + def write_pretty_json + false + end + + protected + + def make_child_entry(child_name) + ChefRepositoryFileSystemCookbookEntry.new(child_name, self, nil, ruby_only, recursive) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbooks_dir.rb b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbooks_dir.rb new file mode 100644 index 0000000000..5c88aa2aa1 --- /dev/null +++ b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_cookbooks_dir.rb @@ -0,0 +1,84 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2013 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_entry' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_cookbook_dir' +require 'chef/cookbook/chefignore' + +class Chef + module ChefFS + module FileSystem + module Repository + class ChefRepositoryFileSystemCookbooksDir < ChefRepositoryFileSystemEntry + def initialize(name, parent, file_path) + super(name, parent, file_path) + begin + @chefignore = Chef::Cookbook::Chefignore.new(self.file_path) + rescue Errno::EISDIR + rescue Errno::EACCES + # Work around a bug in Chefignore when chefignore is a directory + end + end + + attr_reader :chefignore + + def children + super.select do |entry| + # empty cookbooks and cookbook directories are ignored + if !entry.can_upload? + Chef::Log.warn("Cookbook '#{entry.name}' is empty or entirely chefignored at #{entry.path_for_printing}") + false + else + true + end + end + end + + def can_have_child?(name, is_dir) + is_dir && !name.start_with?('.') + end + + def write_cookbook(cookbook_path, cookbook_version_json, from_fs) + cookbook_name = File.basename(cookbook_path) + child = make_child_entry(cookbook_name) + + # Use the copy/diff algorithm to copy it down so we don't destroy + # chefignored data. This is terribly un-thread-safe. + Chef::ChefFS::FileSystem.copy_to(Chef::ChefFS::FilePattern.new("/#{cookbook_path}"), from_fs, child, nil, {:purge => true}) + + # Write out .uploaded-cookbook-version.json + cookbook_file_path = File.join(file_path, cookbook_name) + if !File.exists?(cookbook_file_path) + FileUtils.mkdir_p(cookbook_file_path) + end + uploaded_cookbook_version_path = File.join(cookbook_file_path, Chef::Cookbook::CookbookVersionLoader::UPLOADED_COOKBOOK_VERSION_FILE) + File.open(uploaded_cookbook_version_path, 'w') do |file| + file.write(cookbook_version_json) + end + end + + protected + + def make_child_entry(child_name) + ChefRepositoryFileSystemCookbookDir.new(child_name, self) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_data_bags_dir.rb b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_data_bags_dir.rb index 73556b2c0b..a0d564704a 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_data_bags_dir.rb +++ b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_data_bags_dir.rb @@ -16,19 +16,21 @@ # limitations under the License. # -require 'chef/chef_fs/file_system/chef_repository_file_system_entry' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_entry' require 'chef/chef_fs/data_handler/data_bag_item_data_handler' class Chef module ChefFS module FileSystem - class ChefRepositoryFileSystemDataBagsDir < ChefRepositoryFileSystemEntry - def initialize(name, parent, path = nil) - super(name, parent, path, Chef::ChefFS::DataHandler::DataBagItemDataHandler.new) - end + module Repository + class ChefRepositoryFileSystemDataBagsDir < ChefRepositoryFileSystemEntry + def initialize(name, parent, path = nil) + super(name, parent, path, Chef::ChefFS::DataHandler::DataBagItemDataHandler.new) + end - def can_have_child?(name, is_dir) - is_dir && !name.start_with?('.') + def can_have_child?(name, is_dir) + is_dir && !name.start_with?('.') + end end end end diff --git a/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_entry.rb new file mode 100644 index 0000000000..92658c07fa --- /dev/null +++ b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_entry.rb @@ -0,0 +1,83 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Author:: Ho-Sheng Hsiao (<hosh@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/repository/file_system_entry' +require 'chef/chef_fs/file_system/not_found_error' + +class Chef + module ChefFS + module FileSystem + module Repository + # ChefRepositoryFileSystemEntry works just like FileSystemEntry, + # except can inflate Chef objects + class ChefRepositoryFileSystemEntry < FileSystemEntry + def initialize(name, parent, file_path = nil, data_handler = nil) + super(name, parent, file_path) + @data_handler = data_handler + end + + def write_pretty_json=(value) + @write_pretty_json = value + end + + def write_pretty_json + @write_pretty_json.nil? ? root.write_pretty_json : @write_pretty_json + end + + def data_handler + @data_handler || parent.data_handler + end + + def chef_object + begin + return data_handler.chef_object(Chef::JSONCompat.parse(read)) + rescue + Chef::Log.error("Could not read #{path_for_printing} into a Chef object: #{$!}") + end + nil + end + + def can_have_child?(name, is_dir) + !is_dir && name[-5..-1] == '.json' + end + + def write(file_contents) + if file_contents && write_pretty_json && name[-5..-1] == '.json' + file_contents = minimize(file_contents, self) + end + super(file_contents) + end + + def minimize(file_contents, entry) + object = Chef::JSONCompat.parse(file_contents) + object = data_handler.normalize(object, entry) + object = data_handler.minimize(object, entry) + Chef::JSONCompat.to_json_pretty(object) + end + + protected + + def make_child_entry(child_name) + ChefRepositoryFileSystemEntry.new(child_name, self) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_root_dir.rb b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_root_dir.rb new file mode 100644 index 0000000000..c189323c9a --- /dev/null +++ b/lib/chef/chef_fs/file_system/repository/chef_repository_file_system_root_dir.rb @@ -0,0 +1,201 @@ +# +# Author:: John Keiser (<jkeiser@opscode.com>) +# Copyright:: Copyright (c) 2012-2016 Chef Software, 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/repository/chef_repository_file_system_entry' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_acls_dir' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_cookbooks_dir' +require 'chef/chef_fs/file_system/repository/chef_repository_file_system_data_bags_dir' +require 'chef/chef_fs/file_system/multiplexed_dir' +require 'chef/chef_fs/data_handler/client_data_handler' +require 'chef/chef_fs/data_handler/environment_data_handler' +require 'chef/chef_fs/data_handler/node_data_handler' +require 'chef/chef_fs/data_handler/policy_data_handler' +require 'chef/chef_fs/data_handler/policy_group_data_handler' +require 'chef/chef_fs/data_handler/role_data_handler' +require 'chef/chef_fs/data_handler/user_data_handler' +require 'chef/chef_fs/data_handler/group_data_handler' +require 'chef/chef_fs/data_handler/container_data_handler' + +class Chef + module ChefFS + module FileSystem + module Repository + + # + # Represents the root of a local Chef repository, with directories for + # nodes, cookbooks, roles, etc. under it. + # + class ChefRepositoryFileSystemRootDir < BaseFSDir + # + # Create a new Chef Repository File System root. + # + # == Parameters + # [child_paths] + # A hash of child paths, e.g.: + # "nodes" => [ '/var/nodes', '/home/jkeiser/nodes' ], + # "roles" => [ '/var/roles' ], + # ... + # [root_paths] + # An array of paths representing the top level, where + # +org.json+, +members.json+, and +invites.json+ will be stored. + # [chef_config] - a hash of options that looks suspiciously like the ones + # stored in Chef::Config, containing at least these keys: + # :versioned_cookbooks:: whether to include versions in cookbook names + def initialize(child_paths, root_paths=[], chef_config=Chef::Config) + super("", nil) + @child_paths = child_paths + @root_paths = root_paths + @versioned_cookbooks = chef_config[:versioned_cookbooks] + end + + attr_accessor :write_pretty_json + + attr_reader :root_paths + attr_reader :child_paths + attr_reader :versioned_cookbooks + + CHILDREN = %w(org.json invitations.json members.json) + + def children + @children ||= begin + result = child_paths.keys.sort.map { |name| make_child_entry(name) } + result += CHILDREN.map { |name| make_child_entry(name) } + result.select { |c| c && c.exists? }.sort_by { |c| c.name } + end + end + + def can_have_child?(name, is_dir) + if is_dir + child_paths.has_key?(name) + elsif root_dir + CHILDREN.include?(name) + else + false + end + end + + def create_child(name, file_contents = nil) + if file_contents + child = root_dir.create_child(name, file_contents) + else + child_paths[name].each do |path| + begin + Dir.mkdir(path) + rescue Errno::EEXIST + end + end + child = make_child_entry(name) + end + @children = nil + child + end + + def json_class + nil + end + + # Used to print out a human-readable file system description + def fs_description + repo_paths = root_paths || [ File.dirname(child_paths['cookbooks'][0]) ] + result = "repository at #{repo_paths.join(', ')}\n" + if versioned_cookbooks + result << " Multiple versions per cookbook\n" + else + result << " One version per cookbook\n" + end + child_paths.each_pair do |name, paths| + if paths.any? { |path| !repo_paths.include?(File.dirname(path)) } + result << " #{name} at #{paths.join(', ')}\n" + end + end + result + end + + private + + # + # A FileSystemEntry representing the root path where invites.json, + # members.json and org.json may be found. + # + def root_dir + existing_paths = root_paths.select { |path| File.exists?(path) } + if existing_paths.size > 0 + MultiplexedDir.new(existing_paths.map do |path| + dir = ChefRepositoryFileSystemEntry.new(name, parent, path) + dir.write_pretty_json = !!write_pretty_json + dir + end) + end + end + + # + # Create a child entry of the appropriate type: + # cookbooks, data_bags, acls, etc. All will be multiplexed (i.e. if + # you have multiple paths for cookbooks, the multiplexed dir will grab + # cookbooks from all of them when you list or grab them). + # + def make_child_entry(name) + if CHILDREN.include?(name) + return nil if !root_dir + return root_dir.child(name) + end + + paths = (child_paths[name] || []).select { |path| File.exists?(path) } + if paths.size == 0 + return NonexistentFSObject.new(name, self) + end + case name + when 'cookbooks' + dirs = paths.map { |path| ChefRepositoryFileSystemCookbooksDir.new(name, self, path) } + when 'data_bags' + dirs = paths.map { |path| ChefRepositoryFileSystemDataBagsDir.new(name, self, path) } + when 'acls' + dirs = paths.map { |path| ChefRepositoryFileSystemAclsDir.new(name, self, path) } + else + data_handler = case name + when 'clients' + Chef::ChefFS::DataHandler::ClientDataHandler.new + when 'environments' + Chef::ChefFS::DataHandler::EnvironmentDataHandler.new + when 'nodes' + Chef::ChefFS::DataHandler::NodeDataHandler.new + when 'policies' + Chef::ChefFS::DataHandler::PolicyDataHandler.new + when 'policy_groups' + Chef::ChefFS::DataHandler::PolicyGroupDataHandler.new + when 'roles' + Chef::ChefFS::DataHandler::RoleDataHandler.new + when 'users' + Chef::ChefFS::DataHandler::UserDataHandler.new + when 'groups' + Chef::ChefFS::DataHandler::GroupDataHandler.new + when 'containers' + Chef::ChefFS::DataHandler::ContainerDataHandler.new + else + raise "Unknown top level path #{name}" + end + dirs = paths.map { |path| ChefRepositoryFileSystemEntry.new(name, self, path, data_handler) } + end + MultiplexedDir.new(dirs) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/repository/file_system_entry.rb b/lib/chef/chef_fs/file_system/repository/file_system_entry.rb new file mode 100644 index 0000000000..f8c8d490d2 --- /dev/null +++ b/lib/chef/chef_fs/file_system/repository/file_system_entry.rb @@ -0,0 +1,117 @@ +# +# 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/chef_server/rest_list_dir' +require 'chef/chef_fs/file_system/already_exists_error' +require 'chef/chef_fs/file_system/must_delete_recursively_error' +require 'chef/chef_fs/file_system/not_found_error' +require 'chef/chef_fs/path_utils' +require 'fileutils' + +class Chef + module ChefFS + module FileSystem + module Repository + 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 + file_path + end + + def children + # Except cookbooks and data bag dirs, all things must be json files + begin + Dir.entries(file_path).sort. + map { |child_name| make_child_entry(child_name) }. + select { |child| child && can_have_child?(child.name, child.dir?) } + rescue Errno::ENOENT + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + end + + def create_child(child_name, file_contents=nil) + child = make_child_entry(child_name) + if child.exists? + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, child) + end + if file_contents + child.write(file_contents) + else + begin + Dir.mkdir(child.file_path) + rescue Errno::EEXIST + raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, child) + end + end + child + end + + def dir? + File.directory?(file_path) + end + + def delete(recurse) + begin + if dir? + if !recurse + raise MustDeleteRecursivelyError.new(self, $!) + end + FileUtils.rm_r(file_path) + else + File.delete(file_path) + end + rescue Errno::ENOENT + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + end + + def exists? + File.exists?(file_path) && (parent.nil? || parent.can_have_child?(name, dir?)) + end + + def read + begin + File.open(file_path, "rb") {|f| f.read} + rescue Errno::ENOENT + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + end + end + + def write(content) + File.open(file_path, 'wb') do |file| + file.write(content) + end + end + + protected + + def make_child_entry(child_name) + FileSystemEntry.new(child_name, self) + end + end + 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/repository/file_system_root_dir.rb index afbf7b1901..e4c9a455e3 100644 --- a/lib/chef/chef_fs/file_system/file_system_root_dir.rb +++ b/lib/chef/chef_fs/file_system/repository/file_system_root_dir.rb @@ -16,14 +16,16 @@ # limitations under the License. # -require 'chef/chef_fs/file_system/file_system_entry' +require 'chef/chef_fs/file_system/repository/file_system_entry' class Chef module ChefFS module FileSystem - class FileSystemRootDir < FileSystemEntry - def initialize(file_path) - super("", nil, file_path) + module Repository + class FileSystemRootDir < FileSystemEntry + def initialize(file_path) + super("", nil, file_path) + 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 deleted file mode 100644 index 1190069035..0000000000 --- a/lib/chef/chef_fs/file_system/rest_list_dir.rb +++ /dev/null @@ -1,177 +0,0 @@ -# -# 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, data_handler = nil) - super(name, parent) - @api_path = api_path || (parent.api_path == "" ? name : "#{parent.api_path}/#{name}") - @data_handler = data_handler - end - - attr_reader :api_path - attr_reader :data_handler - - def can_have_child?(name, is_dir) - name =~ /\.json$/ && !is_dir - end - - # - # When talking to a modern (12.0+) Chef server - # knife list / - # -> /nodes - # -> /policies - # -> /policy_groups - # -> /roles - # - # 12.0 or 12.1 will fail when you do this: - # knife list / --recursive - # Because it thinks /policies exists, and when it tries to list its children - # it gets a 404 (indicating it actually doesn't exist). - # - # With this change, knife list / --recursive will list /policies as a real, empty directory. - # - # Alternately, we could have done some sort of detection when we listed the top level - # and determined which endpoints the server would support, and returned only those. - # So you wouldn't see /policies in that case at all. - # The issue with that is there's no efficient way to do it because we can't find out - # the server version directly, and can't ask the server for a list of the endpoints it supports. - # - - - # - # Does GET /<api_path>, assumes the result is of the format: - # - # { - # "foo": "<api_path>/foo", - # "bar": "<api_path>/bar", - # } - # - # Children are foo.json and bar.json in this case. - # - def children - begin - # Grab the names of the children, append json, and make child entries - @children ||= root.get_json(api_path).keys.sort.map do |key| - make_child_entry("#{key}.json", true) - end - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") - rescue Net::HTTPServerException => e - # 404 = NotFoundError - if $!.response.code == "404" - - if parent.is_a?(ChefServerRootDir) - # GET /organizations/ORG/<container> returned 404, but that just might be because - # we are talking to an older version of the server that doesn't support policies. - # Do GET /orgqanizations/ORG to find out if the org exists at all. - # TODO use server API version instead of a second network request. - begin - root.get_json(parent.api_path) - # Return empty list if the organization exists but /policies didn't work - [] - rescue Net::HTTPServerException => e - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") - end - else - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end - - # Anything else is unexpected (OperationFailedError) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") - end - end - end - - # - # Does POST <api_path> with file_contents - # - def create_child(name, file_contents) - # Parse the contents to ensure they are valid JSON - begin - object = Chef::JSONCompat.parse(file_contents) - rescue Chef::Exceptions::JSON::ParseError => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Parse error reading JSON creating child '#{name}': #{e}") - end - - # Create the child entry that will be returned - result = make_child_entry(name, true) - - # Normalize the file_contents before post (add defaults, etc.) - if data_handler - object = data_handler.normalize_for_post(object, result) - data_handler.verify_integrity(object, result) do |error| - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, nil, "Error creating '#{name}': #{error}") - end - end - - # POST /api_path with the normalized file_contents - begin - rest.post(api_path, object) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Timeout creating '#{name}': #{e}") - rescue Net::HTTPServerException => e - # 404 = NotFoundError - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) - # 409 = AlreadyExistsError - elsif $!.response.code == "409" - raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, self, e, "Failure creating '#{name}': #{path}/#{name} already exists") - # Anything else is unexpected (OperationFailedError) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "Failure creating '#{name}': #{e.message}") - end - end - - # Clear the cache of children so that if someone asks for children - # again, we will get it again - @children = nil - - result - end - - def org - parent.org - end - - def environment - parent.environment - end - - def rest - parent.rest - end - - def make_child_entry(name, exists = nil) - @children.select { |child| child.name == name }.first if @children - 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 deleted file mode 100644 index 49c6cc9e67..0000000000 --- a/lib/chef/chef_fs/file_system/rest_list_entry.rb +++ /dev/null @@ -1,185 +0,0 @@ -# -# 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/chef_fs/file_system/operation_failed_error' -require 'chef/role' -require 'chef/node' -require 'chef/json_compat' - -class Chef - module ChefFS - module FileSystem - class RestListEntry < BaseFSObject - def initialize(name, parent, exists = nil) - super(name, parent) - @exists = exists - end - - def data_handler - parent.data_handler - end - - def api_child_name - if name.length < 5 || name[-5,5] != ".json" - raise "Invalid name #{path}: must end in .json" - end - name[0,name.length-5] - end - - def api_path - "#{parent.api_path}/#{api_child_name}" - end - - def org - parent.org - 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(api_path) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") - rescue Net::HTTPServerException => e - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") - end - end - end - - def read - Chef::JSONCompat.to_json_pretty(minimize_value(_read_json)) - end - - def _read_json - begin - # Minimize the value (get rid of defaults) so the results don't look terrible - root.get_json(api_path) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "Timeout reading: #{e}") - rescue Net::HTTPServerException => e - if $!.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") - end - end - end - - def chef_object - # REST will inflate the Chef object using json_class - data_handler.json_class.json_create(read) - end - - def minimize_value(value) - data_handler.minimize(data_handler.normalize(value, self), self) - end - - def compare_to(other) - # TODO this pair of reads can be parallelized - - # Grab the other value - begin - other_value_json = other.read - rescue Chef::ChefFS::FileSystem::NotFoundError - return [ nil, nil, :none ] - end - - # Grab this value - begin - value = _read_json - rescue Chef::ChefFS::FileSystem::NotFoundError - return [ false, :none, other_value_json ] - end - - # Minimize (and normalize) both values for easy and beautiful diffs - value = minimize_value(value) - value_json = Chef::JSONCompat.to_json_pretty(value) - begin - other_value = Chef::JSONCompat.parse(other_value_json) - rescue Chef::Exceptions::JSON::ParseError => e - Chef::Log.warn("Parse error reading #{other.path_for_printing} as JSON: #{e}") - return [ nil, value_json, other_value_json ] - end - other_value = minimize_value(other_value) - other_value_json = Chef::JSONCompat.to_json_pretty(other_value) - - [ value == other_value, value_json, other_value_json ] - end - - def rest - parent.rest - end - - def write(file_contents) - begin - object = Chef::JSONCompat.parse(file_contents) - rescue Chef::Exceptions::JSON::ParseError => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Parse error reading JSON: #{e}") - end - - if data_handler - object = data_handler.normalize_for_put(object, self) - data_handler.verify_integrity(object, self) do |error| - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, nil, "#{error}") - end - end - - begin - rest.put(api_path, object) - rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "Timeout writing: #{e}") - rescue Net::HTTPServerException => e - if e.response.code == "404" - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, e) - else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") - end - end - end - - def api_error_text(response) - begin - Chef::JSONCompat.parse(response.body)['error'].join("\n") - rescue - response.body - end - end - end - - end - end -end |