summaryrefslogtreecommitdiff
path: root/lib/chef/chef_fs/file_system/chef_server
diff options
context:
space:
mode:
authorJohn Keiser <john@johnkeiser.com>2016-01-07 14:18:44 -0800
committerJohn Keiser <john@johnkeiser.com>2016-01-13 09:23:07 -0800
commit6d9c8b03466632b3cd11042a2b99a08ae5047309 (patch)
tree0bbff68a54b99e7f6f70819783a75bc9967093d1 /lib/chef/chef_fs/file_system/chef_server
parent5e5cbe6df61ae69b9a5942ce51a7ad1f8bb6ea3a (diff)
downloadchef-6d9c8b03466632b3cd11042a2b99a08ae5047309.tar.gz
Move server and repository fs objects to their own directories
Diffstat (limited to 'lib/chef/chef_fs/file_system/chef_server')
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/acl_dir.rb65
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/acl_entry.rb60
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/acls_dir.rb70
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/chef_server_root_dir.rb192
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/cookbook_dir.rb226
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/cookbook_file.rb84
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/cookbook_subdir.rb61
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/cookbooks_acl_dir.rb43
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/cookbooks_dir.rb154
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/data_bag_dir.rb71
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/data_bags_dir.rb69
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/environments_dir.rb57
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb53
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/org_entry.rb31
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/organization_invites_entry.rb61
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/organization_members_entry.rb60
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/policies_dir.rb160
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/policy_group_entry.rb137
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/policy_groups_dir.rb43
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/policy_revision_entry.rb25
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/rest_list_dir.rb179
-rw-r--r--lib/chef/chef_fs/file_system/chef_server/rest_list_entry.rb187
22 files changed, 2088 insertions, 0 deletions
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/chef_server/environments_dir.rb b/lib/chef/chef_fs/file_system/chef_server/environments_dir.rb
new file mode 100644
index 0000000000..721c8a38d4
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/chef_server/environments_dir.rb
@@ -0,0 +1,57 @@
+#
+# 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'
+require 'chef/chef_fs/file_system/default_environment_cannot_be_modified_error'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ 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
+
+ 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 write(file_contents)
+ raise NotFoundError.new(self) if !exists?
+ raise DefaultEnvironmentCannotBeModifiedError.new(:write, self)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb b/lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb
new file mode 100644
index 0000000000..c0728fbe1f
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/chef_server/nodes_dir.rb
@@ -0,0 +1,53 @@
+#
+# 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'
+require 'chef/chef_fs/data_handler/node_data_handler'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ module ChefServer
+ 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
+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/chef_server/policy_groups_dir.rb b/lib/chef/chef_fs/file_system/chef_server/policy_groups_dir.rb
new file mode 100644
index 0000000000..0c061ebdb2
--- /dev/null
+++ b/lib/chef/chef_fs/file_system/chef_server/policy_groups_dir.rb
@@ -0,0 +1,43 @@
+#
+# 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'
+require 'chef/chef_fs/file_system/chef_server/policy_group_entry'
+
+class Chef
+ module ChefFS
+ module FileSystem
+ 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
+ end
+ end
+ 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