diff options
author | John Keiser <john@johnkeiser.com> | 2015-11-17 14:40:33 -0800 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2015-12-15 13:01:32 -0800 |
commit | 9dbe7491927ecf73d5757179dc6e26c1946f46c0 (patch) | |
tree | 231566c67d2a72831126468398b37b4b7b20116c | |
parent | b743fcdbd172862386d172b42bbbc31cfe2f4239 (diff) | |
download | chef-9dbe7491927ecf73d5757179dc6e26c1946f46c0.tar.gz |
Add knife/ChefFS support for policyfiles and policy_groups.
42 files changed, 1032 insertions, 191 deletions
diff --git a/.gitignore b/.gitignore index b5fa1a24d7..cdb29d0679 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ pkg tags */tags *~ +.chef # you should check in your Gemfile.lock in applications, and not in gems external_tests/*.lock @@ -5,6 +5,8 @@ gem "activesupport", "< 4.0.0", :group => :compat_testing, :platform => "ruby" gem 'chef-config', path: "chef-config" if File.exists?(__FILE__ + '../chef-config') +gem 'chef-zero', github: 'chef/chef-zero' + group(:docgen) do gem "yard" end diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb index d8cc4bff37..1ad6266d14 100644 --- a/chef-config/lib/chef-config/config.rb +++ b/chef-config/lib/chef-config/config.rb @@ -185,6 +185,14 @@ module ChefConfig # Defaults to <chef_repo_path>/nodes. default(:node_path) { derive_path_from_chef_repo_path('nodes') } + # Location of policies on disk. String or array of strings. + # Defaults to <chef_repo_path>/policies. + default(:policy_path) { derive_path_from_chef_repo_path('policies') } + + # Location of policy_groups on disk. String or array of strings. + # Defaults to <chef_repo_path>/policy_groups. + default(:policy_group_path) { derive_path_from_chef_repo_path('policy_groups') } + # Location of roles on disk. String or array of strings. # Defaults to <chef_repo_path>/roles. default(:role_path) { derive_path_from_chef_repo_path('roles') } diff --git a/lib/chef/chef_fs.rb b/lib/chef/chef_fs.rb index bc445e53ad..13cf39c9ec 100644 --- a/lib/chef/chef_fs.rb +++ b/lib/chef/chef_fs.rb @@ -1,5 +1,55 @@ require 'chef/platform' +# +# ChefFS was designed to be a near-1:1 translation between Chef server endpoints +# and local data, so that it could be used for: +# +# 1. User editing, diffing and viewing of server content locally +# 2. knife download, upload and diff (supporting the above scenario) +# 3. chef-client -z (serving user repository directly) +# +# This is the translation between chef-zero data stores (which correspond +# closely to server endpoints) and the ChefFS repository format. +# +# |-----------------------------------|-----------------------------------| +# | chef-zero DataStore | ChefFS (repository) | +# |-----------------------------------|-----------------------------------| +# | <root> | org.json | +# | association_requests/NAME | invitations.json | +# | clients/NAME | clients/NAME.json | +# | cookbooks/NAME/VERSION | cookbooks/NAME/metadata.rb | +# | containers/NAME | containers/NAME.json | +# | data/BAG/ITEM | data_bags/BAG/ITEM.json | +# | environments/NAME | environments/NAME.json | +# | groups/NAME | groups/NAME.json | +# | nodes/NAME | nodes/NAME.json | +# | policies/NAME/REVISION | policies/NAME-REVISION.json | +# | policy_groups/NAME/policies/PNAME | policy_groups/NAME.json | +# | roles/NAME | roles/NAME.json | +# | sandboxes/ID | <not stored on disk, just memory> | +# | users/NAME | members.json | +# | file_store/COOKBOOK/VERSION/PATH | cookbooks/COOKBOOK/PATH | +# | **/_acl | acls/**.json | +# |-----------------------------------|-----------------------------------| +# +# +# ## The Code +# +# There are two main entry points to ChefFS: +# +# - ChefServerRootDir represents the chef server (under an org) and surfaces a +# filesystem-like interface (FSBaseObject / FSBaseDir) that maps the REST API +# to the same format as you would have on disk. +# - ChefRepositoryFileSystemRootDir represents the local repository where you +# put your cookbooks, roles, policies, etc. +# +# Because these two map to a common directory structure, diff, upload, download, +# and other filesystem operations, can easily be done in a generic manner. +# +# These are instantiated by Chef::ChefFS::Config's `chef_fs` and `local_fs` +# methods. +# + class Chef module ChefFS def self.windows? diff --git a/lib/chef/chef_fs/chef_fs_data_store.rb b/lib/chef/chef_fs/chef_fs_data_store.rb index 4d07135a0a..fef8b1df77 100644 --- a/lib/chef/chef_fs/chef_fs_data_store.rb +++ b/lib/chef/chef_fs/chef_fs_data_store.rb @@ -241,6 +241,22 @@ class Chef raise ChefZero::DataStore::DataNotFoundError.new(to_zero_path(e.entry), e) end + # /policy_groups/NAME/policies/POLICYNAME: return the revision of the given policy + elsif path[0] == 'policy_groups' && path[2] == 'policies' && path.length == 4 + with_entry(path[0..1]) do |entry| + policy_group = Chef::JSONCompat.parse(entry) + if !policy_group['policies'] || !policy_group['policies'][path[3]] || !policy_group['policies'][path[3]] + raise ChefZero::DataStore::DataNotFoundError.new(path, entry) + end + # The policy group looks like: + # { + # "policies": { + # "x": { "revision_id": "10" } + # } + # } + Chef::JSONCompat.to_json_pretty(policy_group['policies'][path[3]]["revision_id"]) + end + # GET [/organizations/ORG]/users/NAME -> /users/NAME # Manipulates members.json elsif is_org? && path[0] == 'users' && path.length == 2 @@ -382,6 +398,17 @@ class Chef if use_memory_store?(path) @memory_store.list(path) + elsif path[0] == 'policy_groups' && path.length == 2 + with_entry(path) do |entry| + [ 'policies' ] + end + + elsif path[0] == 'policy_groups' && path[2] == 'policies' && path.length == 3 + with_entry([ 'policy_groups', path[1] ]) do |entry| + policy_group = Chef::JSONCompat.parse(entry.read) + (policy_group['policies'] || {}).keys + end + elsif path[0] == 'cookbooks' && path.length == 1 with_entry(path) do |entry| begin @@ -444,6 +471,12 @@ class Chef @memory_store.exists_dir?(path) elsif path[0] == 'cookbooks' && path.length == 2 list([ path[0] ]).include?(path[1]) + # /policy_groups/NAME/policies + elsif path[0] == 'policy_groups' && path[2] == 'policies' && path.length == 3 + exists_dir?(path[0..1]) + # /policy_groups/NAME/policies/POLICYNAME + elsif path[0] == 'policy_groups' && path[2] == 'policies' && path.length == 4 + exists_dir?(path[0..1]) && list(path[0..2]).include?(path[3]) else Chef::ChefFS::FileSystem.resolve_path(chef_fs, to_chef_fs_path(path)).exists? end diff --git a/lib/chef/chef_fs/config.rb b/lib/chef/chef_fs/config.rb index 40cbb36530..a666731b80 100644 --- a/lib/chef/chef_fs/config.rb +++ b/lib/chef/chef_fs/config.rb @@ -40,7 +40,8 @@ class Chef "nodes" => "node", "roles" => "role", "users" => "user", - "policies" => "policy" + "policies" => "policy", + "policy_groups" => "policy_group" } INFLECTIONS.each { |k,v| k.freeze; v.freeze } INFLECTIONS.freeze @@ -232,11 +233,11 @@ class Chef result = {} case @chef_config[:repo_mode] when 'static' - object_names = %w(cookbooks data_bags environments roles policies) + object_names = %w(cookbooks data_bags environments roles) when 'hosted_everything' - object_names = %w(acls clients cookbooks containers data_bags environments groups nodes roles policies) + object_names = %w(acls clients cookbooks containers data_bags environments groups nodes roles policies policy_groups) else - object_names = %w(clients cookbooks data_bags environments nodes roles users policies) + object_names = %w(clients cookbooks data_bags environments nodes roles users) end object_names.each do |object_name| # cookbooks -> cookbook_path diff --git a/lib/chef/chef_fs/data_handler/policy_data_handler.rb b/lib/chef/chef_fs/data_handler/policy_data_handler.rb index 769c13c364..9f4d7479fc 100644 --- a/lib/chef/chef_fs/data_handler/policy_data_handler.rb +++ b/lib/chef/chef_fs/data_handler/policy_data_handler.rb @@ -4,12 +4,39 @@ class Chef module ChefFS module DataHandler class PolicyDataHandler < DataHandlerBase + def name_and_revision(name) + # foo-1.0.0 = foo, 1.0.0 + name = remove_dot_json(name) + if name =~ /^(.*)-([^-]*)$/ + name, revision_id = $1, $2 + end + revision_id ||= '0.0.0' + [ name, revision_id ] + end def normalize(policy, entry) - policy + # foo-1.0.0 = foo, 1.0.0 + name, revision_id = name_and_revision(entry.name) + defaults = { + 'name' => name, + 'revision_id' => revision_id, + 'run_list' => [], + 'cookbook_locks' => {} + } + normalize_hash(policy, defaults) + end + + def verify_integrity(object_data, entry, &on_error) + name, revision = name_and_revision(entry.name) + if object_data['name'] != name + on_error.call("Object name '#{object_data['name']}' doesn't match entry '#{entry.name}'.") + end + + if object_data['revision_id'] != revision + on_error.call("Object revision ID '#{object_data['revision']}' doesn't match entry '#{entry.name}'.") + end end end end end end - diff --git a/lib/chef/chef_fs/data_handler/policy_group_data_handler.rb b/lib/chef/chef_fs/data_handler/policy_group_data_handler.rb new file mode 100644 index 0000000000..6ada606e49 --- /dev/null +++ b/lib/chef/chef_fs/data_handler/policy_group_data_handler.rb @@ -0,0 +1,27 @@ +require 'chef/chef_fs/data_handler/data_handler_base' + +class Chef + module ChefFS + module DataHandler + class PolicyGroupDataHandler < DataHandlerBase + + def normalize(policy_group, entry) + defaults = { + "name" => remove_dot_json(entry.name), + "policies" => {} + } + result = normalize_hash(policy_group, defaults) + result.delete("uri") # not useful data + result + end + + def verify_integrity(object_data, entry, &on_error) + if object_data["policies"].empty? + on_error.call("Policy group #{object_data["name"]} does not have any policies in it.") + end + end + + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/acl_dir.rb b/lib/chef/chef_fs/file_system/acl_dir.rb index 9f68d7cda7..66cf794319 100644 --- a/lib/chef/chef_fs/file_system/acl_dir.rb +++ b/lib/chef/chef_fs/file_system/acl_dir.rb @@ -47,7 +47,7 @@ class Chef end def create_child(name, file_contents) - raise OperationNotAllowedError.new(:create_child, self), "ACLs can only be updated, and can only be created when the corresponding object is created." + 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 diff --git a/lib/chef/chef_fs/file_system/acl_entry.rb b/lib/chef/chef_fs/file_system/acl_entry.rb index b2545af5ae..e739bd8f10 100644 --- a/lib/chef/chef_fs/file_system/acl_entry.rb +++ b/lib/chef/chef_fs/file_system/acl_entry.rb @@ -32,7 +32,7 @@ class Chef end def delete(recurse) - raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self), "ACLs cannot be deleted." + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self, nil, "ACLs cannot be deleted") end def write(file_contents) @@ -42,12 +42,12 @@ class Chef 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}" + 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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") 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 index a8c63726b7..d8c9bf809c 100644 --- a/lib/chef/chef_fs/file_system/acls_dir.rb +++ b/lib/chef/chef_fs/file_system/acls_dir.rb @@ -28,10 +28,6 @@ class Chef 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 initialize(parent) - super('acls', parent) - end - def data_handler @data_handler ||= Chef::ChefFS::DataHandler::AclDataHandler.new end diff --git a/lib/chef/chef_fs/file_system/already_exists_error.rb b/lib/chef/chef_fs/file_system/already_exists_error.rb index bf8994fdf3..e6d4a8fa2c 100644 --- a/lib/chef/chef_fs/file_system/already_exists_error.rb +++ b/lib/chef/chef_fs/file_system/already_exists_error.rb @@ -22,9 +22,6 @@ class Chef module ChefFS module FileSystem class AlreadyExistsError < OperationFailedError - def initialize(operation, entry, cause = nil) - super(operation, entry, cause) - 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 index 267fe30456..6a59ccff85 100644 --- 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 @@ -21,11 +21,12 @@ 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/chef_repository_file_system_policies_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' @@ -163,8 +164,6 @@ class Chef dirs = paths.map { |path| ChefRepositoryFileSystemCookbooksDir.new(name, self, path) } when 'data_bags' dirs = paths.map { |path| ChefRepositoryFileSystemDataBagsDir.new(name, self, path) } - when 'policies' - dirs = paths.map { |path| ChefRepositoryFileSystemPoliciesDir.new(name, self, path) } when 'acls' dirs = paths.map { |path| ChefRepositoryFileSystemAclsDir.new(name, self, path) } else @@ -175,6 +174,10 @@ class Chef 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' 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 index a243e0ae6b..09181ac4b4 100644 --- a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb @@ -26,12 +26,18 @@ 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 @@ -133,26 +139,45 @@ class Chef def children @children ||= begin result = [ - CookbooksDir.new(self), - DataBagsDir.new(self), - EnvironmentsDir.new(self), + # /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 += [ - AclsDir.new(self), + # /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), - NodesDir.new(self), + # /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), - OrganizationInvitesEntry.new("invitations.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), - NodesDir.new(self), + # /nodes + NodesDir.new("nodes", self, nil, Chef::ChefFS::DataHandler::NodeDataHandler.new), + # /users RestListDir.new("users", self, nil, Chef::ChefFS::DataHandler::UserDataHandler.new) ] end diff --git a/lib/chef/chef_fs/file_system/cookbook_dir.rb b/lib/chef/chef_fs/file_system/cookbook_dir.rb index c0f0390e98..42408c0869 100644 --- a/lib/chef/chef_fs/file_system/cookbook_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbook_dir.rb @@ -128,17 +128,17 @@ class Chef begin rest.delete(api_path) rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e), "Timeout deleting: #{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}" + 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" + raise MustDeleteRecursivelyError.new(self, "#{path_for_printing} must be deleted recursively") end end @@ -197,14 +197,14 @@ class Chef end rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e), "Timeout reading: #{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}" + 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. @@ -214,7 +214,7 @@ class Chef @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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") 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 index 16203b727c..f755174183 100644 --- a/lib/chef/chef_fs/file_system/cookbook_file.rb +++ b/lib/chef/chef_fs/file_system/cookbook_file.rb @@ -39,9 +39,9 @@ class Chef 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}" + 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]}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "#{e.message} retrieving #{file[:url]}") end begin diff --git a/lib/chef/chef_fs/file_system/cookbook_frozen_error.rb b/lib/chef/chef_fs/file_system/cookbook_frozen_error.rb index 705673384d..cbb25cd03a 100644 --- a/lib/chef/chef_fs/file_system/cookbook_frozen_error.rb +++ b/lib/chef/chef_fs/file_system/cookbook_frozen_error.rb @@ -22,9 +22,6 @@ class Chef module ChefFS module FileSystem class CookbookFrozenError < AlreadyExistsError - def initialize(operation, entry, cause = nil) - super(operation, entry, cause) - 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 index 6f49c28996..8cbc06931b 100644 --- a/lib/chef/chef_fs/file_system/cookbooks_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbooks_dir.rb @@ -32,10 +32,6 @@ class Chef include Chef::Mixin::FileClass - def initialize(parent) - super("cookbooks", parent) - end - def make_child_entry(name) result = @children.select { |child| child.name == name }.first if @children result || CookbookDir.new(name, self) @@ -65,16 +61,16 @@ class Chef 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}" + 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" + 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}" + 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" + raise Chef::ChefFS::FileSystem::CookbookFrozenError.new(:write, self, e, "Cookbook #{other.name} is frozen") end # Knife currently does not understand versioned cookbooks diff --git a/lib/chef/chef_fs/file_system/data_bag_dir.rb b/lib/chef/chef_fs/file_system/data_bag_dir.rb index 212f76fdb9..7a4463e69d 100644 --- a/lib/chef/chef_fs/file_system/data_bag_dir.rb +++ b/lib/chef/chef_fs/file_system/data_bag_dir.rb @@ -49,17 +49,17 @@ class Chef def delete(recurse) if !recurse raise NotFoundError.new(self) if !exists? - raise MustDeleteRecursivelyError.new(self), "#{path_for_printing} must be deleted recursively" + 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}" + 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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "HTTP error deleting: #{e}") 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 index 1cb61bbd1a..0645a6c16f 100644 --- a/lib/chef/chef_fs/file_system/data_bags_dir.rb +++ b/lib/chef/chef_fs/file_system/data_bags_dir.rb @@ -23,10 +23,6 @@ class Chef module ChefFS module FileSystem class DataBagsDir < RestListDir - def initialize(parent) - super("data_bags", parent, "data") - end - def make_child_entry(name, exists = false) result = @children.select { |child| child.name == name }.first if @children result || DataBagDir.new(name, self, exists) @@ -36,12 +32,12 @@ class Chef 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}" + 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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error getting children: #{e}") end end end @@ -54,12 +50,12 @@ class Chef 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}" + 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" + 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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e, "HTTP error creating child '#{name}': #{e}") end end @children = nil diff --git a/lib/chef/chef_fs/file_system/default_environment_cannot_be_modified_error.rb b/lib/chef/chef_fs/file_system/default_environment_cannot_be_modified_error.rb index 8ca3b917ca..a077474da8 100644 --- a/lib/chef/chef_fs/file_system/default_environment_cannot_be_modified_error.rb +++ b/lib/chef/chef_fs/file_system/default_environment_cannot_be_modified_error.rb @@ -22,10 +22,6 @@ class Chef module ChefFS module FileSystem class DefaultEnvironmentCannotBeModifiedError < OperationNotAllowedError - def initialize(operation, entry, cause = nil) - super(operation, entry, cause) - end - def reason result = super result + " (default environment cannot be modified)" diff --git a/lib/chef/chef_fs/file_system/environments_dir.rb b/lib/chef/chef_fs/file_system/environments_dir.rb index 3aee3ee5af..984129bab3 100644 --- a/lib/chef/chef_fs/file_system/environments_dir.rb +++ b/lib/chef/chef_fs/file_system/environments_dir.rb @@ -20,16 +20,11 @@ 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/file_system/default_environment_cannot_be_modified_error' -require 'chef/chef_fs/data_handler/environment_data_handler' class Chef module ChefFS module FileSystem class EnvironmentsDir < RestListDir - def initialize(parent) - super("environments", parent, nil, Chef::ChefFS::DataHandler::EnvironmentDataHandler.new) - end - def make_child_entry(name, exists = nil) if name == '_default.json' DefaultEnvironmentEntry.new(name, self, exists) @@ -46,12 +41,12 @@ class Chef def delete(recurse) raise NotFoundError.new(self) if !exists? - raise DefaultEnvironmentCannotBeModifiedError.new(:delete, self), "#{path_for_printing} cannot be deleted." + raise DefaultEnvironmentCannotBeModifiedError.new(:delete, self) end def write(file_contents) raise NotFoundError.new(self) if !exists? - raise DefaultEnvironmentCannotBeModifiedError.new(:write, self), "#{path_for_printing} cannot be updated." + raise DefaultEnvironmentCannotBeModifiedError.new(:write, self) end end end diff --git a/lib/chef/chef_fs/file_system/file_system_error.rb b/lib/chef/chef_fs/file_system/file_system_error.rb index 80aff35893..6a5cedec34 100644 --- a/lib/chef/chef_fs/file_system/file_system_error.rb +++ b/lib/chef/chef_fs/file_system/file_system_error.rb @@ -20,13 +20,24 @@ class Chef module ChefFS module FileSystem class FileSystemError < StandardError - def initialize(entry, cause = nil) + # @param entry The entry which had an issue. + # @param cause The wrapped exception (if any). + # @param reason A string describing why this exception happened. + def initialize(entry, cause = nil, reason = nil) + super(reason) @entry = entry @cause = cause + @reason = reason end + # The entry which had an issue. attr_reader :entry + + # The wrapped exception (if any). attr_reader :cause + + # A string describing why this exception happened. + attr_reader :reason end end end diff --git a/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb b/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb index bfa8ba28ce..11b26e514e 100644 --- a/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb +++ b/lib/chef/chef_fs/file_system/must_delete_recursively_error.rb @@ -22,9 +22,6 @@ class Chef module ChefFS module FileSystem class MustDeleteRecursivelyError < FileSystemError - def initialize(entry, cause = nil) - super(entry, cause) - 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 index 2610b06a82..12bc1c06c8 100644 --- a/lib/chef/chef_fs/file_system/nodes_dir.rb +++ b/lib/chef/chef_fs/file_system/nodes_dir.rb @@ -25,10 +25,6 @@ class Chef module ChefFS module FileSystem class NodesDir < RestListDir - def initialize(parent) - super("nodes", parent, nil, Chef::ChefFS::DataHandler::NodeDataHandler.new) - end - # Identical to RestListDir.children, except supports environments def children begin @@ -36,12 +32,12 @@ class Chef make_child_entry("#{key}.json", true) end rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e), "Timeout retrieving children: #{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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "HTTP error retrieving children: #{e}") end end end diff --git a/lib/chef/chef_fs/file_system/not_found_error.rb b/lib/chef/chef_fs/file_system/not_found_error.rb index 9eab3d6131..088c38cc64 100644 --- a/lib/chef/chef_fs/file_system/not_found_error.rb +++ b/lib/chef/chef_fs/file_system/not_found_error.rb @@ -22,9 +22,6 @@ class Chef module ChefFS module FileSystem class NotFoundError < FileSystemError - def initialize(entry, cause = nil) - super(entry, cause) - end end end end diff --git a/lib/chef/chef_fs/file_system/operation_failed_error.rb b/lib/chef/chef_fs/file_system/operation_failed_error.rb index 28d170d628..70461e082e 100644 --- a/lib/chef/chef_fs/file_system/operation_failed_error.rb +++ b/lib/chef/chef_fs/file_system/operation_failed_error.rb @@ -22,8 +22,8 @@ class Chef module ChefFS module FileSystem class OperationFailedError < FileSystemError - def initialize(operation, entry, cause = nil) - super(entry, cause) + def initialize(operation, entry, cause = nil, reason = nil) + super(entry, cause, reason) @operation = operation end diff --git a/lib/chef/chef_fs/file_system/operation_not_allowed_error.rb b/lib/chef/chef_fs/file_system/operation_not_allowed_error.rb index 4b4f9742a8..e756cc76a3 100644 --- a/lib/chef/chef_fs/file_system/operation_not_allowed_error.rb +++ b/lib/chef/chef_fs/file_system/operation_not_allowed_error.rb @@ -22,26 +22,24 @@ class Chef module ChefFS module FileSystem class OperationNotAllowedError < FileSystemError - def initialize(operation, entry, cause = nil) - super(entry, cause) + def initialize(operation, entry, cause = nil, reason = nil) + reason ||= + case operation + when :delete + "cannot be deleted" + when :write + "cannot be updated" + when :create_child + "cannot have a child created under it" + when :read + "cannot be read" + end + super(entry, cause, reason) @operation = operation end attr_reader :operation attr_reader :entry - - def reason - case operation - when :delete - "cannot be deleted" - when :write - "cannot be updated" - when :create_child - "cannot have a child created under it" - when :read - "cannot be read" - 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 index 852956e1e5..df3acba528 100644 --- a/lib/chef/chef_fs/file_system/org_entry.rb +++ b/lib/chef/chef_fs/file_system/org_entry.rb @@ -7,11 +7,6 @@ class Chef # /organizations/NAME/org.json # Represents the actual data at /organizations/NAME (the full name, etc.) class OrgEntry < RestListEntry - def initialize(name, parent, exists = nil) - super(name, parent) - @exists = exists - end - def data_handler Chef::ChefFS::DataHandler::OrganizationDataHandler.new end diff --git a/lib/chef/chef_fs/file_system/policies_dir.rb b/lib/chef/chef_fs/file_system/policies_dir.rb new file mode 100644 index 0000000000..9edaf7cf0a --- /dev/null +++ b/lib/chef/chef_fs/file_system/policies_dir.rb @@ -0,0 +1,145 @@ +# +# 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" + raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + # 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 new file mode 100644 index 0000000000..9885310cc7 --- /dev/null +++ b/lib/chef/chef_fs/file_system/policy_group_entry.rb @@ -0,0 +1,135 @@ +# +# 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/chef_repository_file_system_policies_dir.rb b/lib/chef/chef_fs/file_system/policy_groups_dir.rb index 42768f10b7..c98ae50b97 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_policies_dir.rb +++ b/lib/chef/chef_fs/file_system/policy_groups_dir.rb @@ -16,23 +16,26 @@ # limitations under the License. # -require 'chef/chef_fs/file_system/chef_repository_file_system_entry' -require 'chef/chef_fs/data_handler/policy_data_handler' +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/file_system/policy_group_entry' class Chef module ChefFS module FileSystem - - class ChefRepositoryFileSystemPoliciesDir < ChefRepositoryFileSystemEntry - def initialize(name, parent, path = nil) - super(name, parent, path, Chef::ChefFS::DataHandler::PolicyDataHandler.new) + class PolicyGroupsDir < RestListDir + def make_child_entry(name, exists = nil) + PolicyGroupEntry.new(name, self, exists) end - def can_have_child?(name, is_dir) - is_dir && !name.start_with?('.') + def create_child(name, file_contents) + entry = make_child_entry(name, true) + entry.write(file_contents) + @children = nil + entry 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 new file mode 100644 index 0000000000..896586c54f --- /dev/null +++ b/lib/chef/chef_fs/file_system/policy_revision_entry.rb @@ -0,0 +1,23 @@ +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/rest_list_dir.rb b/lib/chef/chef_fs/file_system/rest_list_dir.rb index 0ac735a2c4..4c520aa71a 100644 --- a/lib/chef/chef_fs/file_system/rest_list_dir.rb +++ b/lib/chef/chef_fs/file_system/rest_list_dir.rb @@ -37,52 +37,77 @@ class Chef name =~ /\.json$/ && !is_dir end + # + # 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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e, "Timeout retrieving children: #{e}") rescue Net::HTTPServerException => e + # 404 = NotFoundError if $!.response.code == "404" raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + # Anything else is unexpected (OperationFailedError) else - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e), "HTTP error retrieving children: #{e}" + 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}" + 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), "Error creating '#{name}': #{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}" + 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" + 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}" + 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 diff --git a/lib/chef/chef_fs/file_system/rest_list_entry.rb b/lib/chef/chef_fs/file_system/rest_list_entry.rb index f68794cb0d..49c6cc9e67 100644 --- a/lib/chef/chef_fs/file_system/rest_list_entry.rb +++ b/lib/chef/chef_fs/file_system/rest_list_entry.rb @@ -70,12 +70,12 @@ class Chef begin rest.delete(api_path) rescue Timeout::Error => e - raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e), "Timeout deleting: #{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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:delete, self, e, "Timeout deleting: #{e}") end end end @@ -89,12 +89,12 @@ class Chef # 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}" + 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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:read, self, e, "HTTP error reading: #{e}") end end end @@ -148,25 +148,25 @@ class Chef 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}" + 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), "#{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}" + 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}" + raise Chef::ChefFS::FileSystem::OperationFailedError.new(:write, self, e, "HTTP error writing: #{e}") end end end diff --git a/lib/chef/chef_fs/parallelizer/parallel_enumerable.rb b/lib/chef/chef_fs/parallelizer/parallel_enumerable.rb index 8845b6926a..26dd30592c 100644 --- a/lib/chef/chef_fs/parallelizer/parallel_enumerable.rb +++ b/lib/chef/chef_fs/parallelizer/parallel_enumerable.rb @@ -266,7 +266,7 @@ class Chef begin output = @block.call(input) @unconsumed_output.push([ output, index, input, :result ]) - rescue + rescue StandardError, ScriptError if @options[:stop_on_exception] @unconsumed_input.clear end diff --git a/lib/chef/knife/list.rb b/lib/chef/knife/list.rb index 137d61f3a5..263b0e7431 100644 --- a/lib/chef/knife/list.rb +++ b/lib/chef/knife/list.rb @@ -47,6 +47,7 @@ class Chef args = pattern_args_from(patterns) all_results = parallelize(pattern_args_from(patterns)) do |pattern| pattern_results = Chef::ChefFS::FileSystem.list(config[:local] ? local_fs : chef_fs, pattern).to_a + if pattern_results.first && !pattern_results.first.exists? && pattern.exact_path ui.error "#{format_path(pattern_results.first)}: No such file or directory" self.exit_code = 1 diff --git a/spec/integration/knife/delete_spec.rb b/spec/integration/knife/delete_spec.rb index 733a7ef72b..384f06cf83 100644 --- a/spec/integration/knife/delete_spec.rb +++ b/spec/integration/knife/delete_spec.rb @@ -967,6 +967,8 @@ EOM organization 'foo' do container 'x', {} group 'x', {} + policy 'x', '1.2.3', {} + policy_group 'x', { 'policies' => { 'x' => { 'revision_id' => '1.2.3' } } } end end @@ -975,7 +977,7 @@ EOM end it 'knife delete /acls/containers/environments.json fails with a reasonable error' do - knife('delete /acls/containers/environments.json').should_fail "ERROR: /acls/containers/environments.json (remote) cannot be deleted.\n" + knife('delete /acls/containers/environments.json').should_fail "ERROR: /acls/containers/environments.json (remote) ACLs cannot be deleted.\n" end it 'knife delete /containers/x.json succeeds' do @@ -988,6 +990,18 @@ EOM knife('raw /groups/x.json').should_fail(/404/) end + it 'knife delete /policies/x-1.2.3.json succeeds' do + knife('raw /policies/x/revisions/1.2.3').should_succeed "{\n \"name\": \"x\",\n \"revision_id\": \"1.2.3\",\n \"run_list\": [\n\n ],\n \"cookbook_locks\": {\n\n }\n}\n" + knife('delete /policies/x-1.2.3.json').should_succeed "Deleted /policies/x-1.2.3.json\n" + knife('raw /policies/x/revisions/1.2.3').should_fail(/404/) + end + + it 'knife delete /policy_groups/x.json succeeds' do + knife('raw /policy_groups/x').should_succeed "{\n \"uri\": \"http://127.0.0.1:8900/organizations/foo/policy_groups/x\",\n \"policies\": {\n \"x\": {\n \"revision_id\": \"1.2.3\"\n }\n }\n}\n" + knife('delete /policy_groups/x.json').should_succeed "Deleted /policy_groups/x.json\n" + knife('raw /policy_groups/x').should_fail(/404/) + end + it 'knife delete /org.json fails with a reasonable error' do knife('delete /org.json').should_fail "ERROR: /org.json (remote) cannot be deleted.\n" end diff --git a/spec/integration/knife/download_spec.rb b/spec/integration/knife/download_spec.rb index b8a19061b7..0c334fc65c 100644 --- a/spec/integration/knife/download_spec.rb +++ b/spec/integration/knife/download_spec.rb @@ -1094,27 +1094,18 @@ EOM when_the_chef_server "is in Enterprise mode", :osc_compat => false, :single_org => false do before do - organization 'foo' do - container 'x', {} - group 'x', {} - end + user 'foo', {} + user 'bar', {} + user 'foobar', {} + organization 'foo', { 'full_name' => 'Something' } end before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo') end - when_the_repository 'has existing top level files' do - before do - file 'invitations.json', {} - end - it "can still download top level files" do - knife('download /invitations.json').should_succeed - end - end - - when_the_repository 'is empty' do - it 'knife download / downloads everything' do + when_the_repository "has all the default stuff" do + before do knife('download /').should_succeed <<EOM Created /acls Created /acls/clients @@ -1131,7 +1122,6 @@ Created /acls/containers/nodes.json Created /acls/containers/policies.json Created /acls/containers/roles.json Created /acls/containers/sandboxes.json -Created /acls/containers/x.json Created /acls/cookbooks Created /acls/data_bags Created /acls/environments @@ -1141,7 +1131,6 @@ Created /acls/groups/admins.json Created /acls/groups/billing-admins.json Created /acls/groups/clients.json Created /acls/groups/users.json -Created /acls/groups/x.json Created /acls/nodes Created /acls/roles Created /acls/organization.json @@ -1159,7 +1148,6 @@ Created /containers/nodes.json Created /containers/policies.json Created /containers/roles.json Created /containers/sandboxes.json -Created /containers/x.json Created /cookbooks Created /data_bags Created /environments @@ -1169,14 +1157,141 @@ Created /groups/admins.json Created /groups/billing-admins.json Created /groups/clients.json Created /groups/users.json -Created /groups/x.json Created /invitations.json Created /members.json Created /nodes Created /org.json +Created /policies +Created /policy_groups Created /roles EOM - knife('diff --name-status /').should_succeed '' + end + + context 'and the server has one of each thing' do + before do + # acl_for %w(organizations foo groups blah) + client 'x', {} + cookbook 'x', '1.0.0' + container 'x', {} + data_bag 'x', { 'y' => {} } + environment 'x', {} + group 'x', {} + org_invite 'foo' + org_member 'bar' + node 'x', {} + policy 'x', '1.0.0', {} + policy 'blah', '1.0.0', {} + policy_group 'x', { + 'policies' => { + 'x' => { 'revision_id' => '1.0.0' }, + 'blah' => { 'revision_id' => '1.0.0' } + } + } + role 'x', {} + end + + before do + knife('download /acls /groups/clients.json /groups/users.json').should_succeed <<-EOM +Created /acls/clients/x.json +Created /acls/containers/x.json +Created /acls/cookbooks/x.json +Created /acls/data_bags/x.json +Created /acls/environments/x.json +Created /acls/groups/x.json +Created /acls/nodes/x.json +Created /acls/roles/x.json +Updated /groups/clients.json +Updated /groups/users.json +EOM + end + + it 'knife download / downloads everything' do + knife('download /').should_succeed <<EOM +Created /clients/x.json +Created /containers/x.json +Created /cookbooks/x +Created /cookbooks/x/metadata.rb +Created /data_bags/x +Created /data_bags/x/y.json +Created /environments/x.json +Created /groups/x.json +Updated /invitations.json +Updated /members.json +Created /nodes/x.json +Created /policies/blah-1.0.0.json +Created /policies/x-1.0.0.json +Created /policy_groups/x.json +Created /roles/x.json +EOM + knife('diff --name-status /').should_succeed '' + end + + context "and the repository has an identical copy of each thing" do + before do + # TODO We have to upload acls for an existing group due to a lack of + # dependency detection during upload. Fix that! + file 'clients/x.json', { 'public_key' => ChefZero::PUBLIC_KEY } + file 'containers/x.json', {} + file 'cookbooks/x/metadata.rb', cb_metadata("x", "1.0.0") + file 'data_bags/x/y.json', {} + file 'environments/x.json', {} + file 'groups/x.json', {} + file 'invitations.json', [ 'foo' ] + file 'members.json', [ 'bar' ] + file 'nodes/x.json', {} + file 'org.json', { 'full_name' => 'Something' } + file 'policies/x-1.0.0.json', { } + file 'policies/blah-1.0.0.json', { } + file 'policy_groups/x.json', { 'policies' => { 'x' => { 'revision_id' => '1.0.0' }, 'blah' => { 'revision_id' => '1.0.0' } } } + file 'roles/x.json', {} + end + + it 'knife download makes no changes' do + knife('download /').should_succeed '' + end + end + + context "and the repository has a slightly different copy of each thing" do + before do + # acl_for %w(organizations foo groups blah) + file 'clients/x.json', { 'validator' => true } + file 'containers/x.json', {} + file 'cookbooks/x/metadata.rb', cb_metadata("x", "1.0.1") + file 'data_bags/x/y.json', { 'a' => 'b' } + file 'environments/x.json', { 'description' => 'foo' } + file 'groups/x.json', { 'description' => 'foo' } + file 'groups/x.json', { 'groups' => [ 'admin' ] } + file 'nodes/x.json', { 'run_list' => [ 'blah' ] } + file 'org.json', { 'full_name' => 'Something Else ' } + file 'policies/x-1.0.0.json', { 'run_list' => [ 'blah' ] } + file 'policy_groups/x.json', { + 'policies' => { + 'x' => { 'revision_id' => '1.0.1' }, + 'y' => { 'revision_id' => '1.0.0' } + } + } + file 'roles/x.json', { 'run_list' => [ 'blah' ] } + end + + it 'knife download updates everything' do + knife('download /').should_succeed <<EOM +Updated /clients/x.json +Updated /cookbooks/x/metadata.rb +Updated /data_bags/x/y.json +Updated /environments/x.json +Updated /groups/x.json +Updated /invitations.json +Updated /members.json +Updated /nodes/x.json +Updated /org.json +Created /policies/blah-1.0.0.json +Updated /policies/x-1.0.0.json +Updated /policy_groups/x.json +Updated /roles/x.json +EOM + knife('diff --name-status /').should_succeed '' + end + end end end end diff --git a/spec/integration/knife/list_spec.rb b/spec/integration/knife/list_spec.rb index b289642c7d..eb2ed279e5 100644 --- a/spec/integration/knife/list_spec.rb +++ b/spec/integration/knife/list_spec.rb @@ -27,7 +27,7 @@ describe 'knife list', :workstation do when_the_chef_server "is empty" do it "knife list / returns all top level directories" do - knife('list /').should_succeed <<EOM + knife('list /').should_succeed <<-EOM /clients /cookbooks /data_bags @@ -39,7 +39,7 @@ EOM end it "knife list -R / returns everything" do - knife('list -R /').should_succeed <<EOM + knife('list -R /').should_succeed <<-EOM /: clients cookbooks @@ -82,6 +82,9 @@ EOM environment 'environment2', {} node 'node1', {} node 'node2', {} + policy 'policy1', '1.2.3', {} + policy 'policy2', '1.2.3', {} + policy 'policy2', '1.3.5', {} role 'role1', {} role 'role2', {} user 'user1', {} @@ -89,7 +92,7 @@ EOM end it "knife list / returns all top level directories" do - knife('list /').should_succeed <<EOM + knife('list /').should_succeed <<-EOM /clients /cookbooks /data_bags @@ -101,7 +104,7 @@ EOM end it "knife list -R / returns everything" do - knife('list -R /').should_succeed <<EOM + knife('list -R /').should_succeed <<-EOM /: clients cookbooks @@ -164,7 +167,7 @@ EOM end it "knife list -R --flat / returns everything" do - knife('list -R --flat /').should_succeed <<EOM + knife('list -R --flat /').should_succeed <<-EOM /clients /clients/chef-validator.json /clients/chef-webui.json @@ -202,7 +205,7 @@ EOM end it "knife list -Rfp / returns everything" do - knife('list -Rfp /').should_succeed <<EOM + knife('list -Rfp /').should_succeed <<-EOM /clients/ /clients/chef-validator.json /clients/chef-webui.json @@ -240,7 +243,7 @@ EOM end it "knife list /cookbooks returns the list of cookbooks" do - knife('list /cookbooks').should_succeed <<EOM + knife('list /cookbooks').should_succeed <<-EOM /cookbooks/cookbook1 /cookbooks/cookbook2 EOM @@ -251,7 +254,7 @@ EOM end it "knife list /**.rb returns all ruby files" do - knife('list /**.rb').should_succeed <<EOM + knife('list /**.rb').should_succeed <<-EOM /cookbooks/cookbook1/metadata.rb /cookbooks/cookbook2/metadata.rb /cookbooks/cookbook2/recipes/default.rb @@ -259,7 +262,7 @@ EOM end it "knife list /cookbooks/**.rb returns all ruby files" do - knife('list /cookbooks/**.rb').should_succeed <<EOM + knife('list /cookbooks/**.rb').should_succeed <<-EOM /cookbooks/cookbook1/metadata.rb /cookbooks/cookbook2/metadata.rb /cookbooks/cookbook2/recipes/default.rb @@ -267,7 +270,7 @@ EOM end it "knife list /**.json returns all json files" do - knife('list /**.json').should_succeed <<EOM + knife('list /**.json').should_succeed <<-EOM /clients/chef-validator.json /clients/chef-webui.json /clients/client1.json @@ -290,7 +293,7 @@ EOM end it "knife list /data**.json returns all data bag json files" do - knife('list /data**.json').should_succeed <<EOM + knife('list /data**.json').should_succeed <<-EOM /data_bags/bag1/item1.json /data_bags/bag1/item2.json /data_bags/bag2/item1.json @@ -322,7 +325,7 @@ EOM before { cwd '.' } it "knife list -Rfp returns everything" do - knife('list -Rfp').should_succeed <<EOM + knife('list -Rfp').should_succeed <<-EOM clients/ clients/chef-validator.json clients/chef-webui.json @@ -367,7 +370,7 @@ EOM before { cwd 'cookbooks' } it "knife list -Rfp / returns everything" do - knife('list -Rfp /').should_succeed <<EOM + knife('list -Rfp /').should_succeed <<-EOM /clients/ /clients/chef-validator.json /clients/chef-webui.json @@ -405,7 +408,7 @@ EOM end it "knife list -Rfp .. returns everything" do - knife('list -Rfp ..').should_succeed <<EOM + knife('list -Rfp ..').should_succeed <<-EOM /clients/ /clients/chef-validator.json /clients/chef-webui.json @@ -443,7 +446,7 @@ EOM end it "knife list -Rfp returns cookbooks" do - knife('list -Rfp').should_succeed <<EOM + knife('list -Rfp').should_succeed <<-EOM cookbook1/ cookbook1/metadata.rb cookbook2/ @@ -462,7 +465,7 @@ EOM before { cwd 'cookbooks/cookbook2' } it "knife list -Rfp returns cookbooks" do - knife('list -Rfp').should_succeed <<EOM + knife('list -Rfp').should_succeed <<-EOM metadata.rb recipes/ recipes/default.rb @@ -481,7 +484,7 @@ EOM before { cwd 'cookbooks' } it "knife list -Rfp returns cookbooks" do - knife('list -Rfp').should_succeed <<EOM + knife('list -Rfp').should_succeed <<-EOM cookbook1/ cookbook1/metadata.rb cookbook2/ @@ -496,7 +499,7 @@ EOM before { cwd 'symlinked' } it "knife list -Rfp returns cookbooks" do - knife('list -Rfp').should_succeed <<EOM + knife('list -Rfp').should_succeed <<-EOM cookbook1/ cookbook1/metadata.rb cookbook2/ @@ -518,7 +521,7 @@ EOM before { cwd 'real_cookbooks' } it "knife list -Rfp returns cookbooks" do - knife('list -Rfp').should_succeed <<EOM + knife('list -Rfp').should_succeed <<-EOM cookbook1/ cookbook1/metadata.rb cookbook2/ @@ -533,7 +536,7 @@ EOM before { cwd 'cookbooks' } it "knife list -Rfp returns cookbooks" do - knife('list -Rfp').should_succeed <<EOM + knife('list -Rfp').should_succeed <<-EOM cookbook1/ cookbook1/metadata.rb cookbook2/ @@ -586,6 +589,7 @@ EOM file 'environments/environment2.json', {} file 'nodes/node1.json', {} file 'nodes/node2.json', {} + file 'roles/role1.json', {} file 'roles/role2.json', {} file 'users/user1.json', {} @@ -593,7 +597,7 @@ EOM end it "knife list -Rfp / returns everything" do - knife('list -Rp --local --flat /').should_succeed <<EOM + knife('list -Rp --local --flat /').should_succeed <<-EOM /clients/ /clients/client1.json /clients/client2.json @@ -653,7 +657,7 @@ EOM context 'and is empty' do it "knife list / returns all top level directories" do - knife('list /').should_succeed <<EOM + knife('list /').should_succeed <<-EOM /acls /clients /containers @@ -665,12 +669,14 @@ EOM /members.json /nodes /org.json +/policies +/policy_groups /roles EOM end it "knife list -R / returns everything" do - knife('list -R /').should_succeed <<EOM + knife('list -R /').should_succeed <<-EOM /: acls clients @@ -683,6 +689,8 @@ invitations.json members.json nodes org.json +policies +policy_groups roles /acls: @@ -760,23 +768,17 @@ users.json /nodes: +/policies: + +/policy_groups: + /roles: EOM end end - end - - when_the_chef_server "is in Enterprise mode", :osc_compat => false, :single_org => false do - before do - organization 'foo' - end - - before :each do - Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo') - end it 'knife list -R / returns everything' do - knife('list -R /').should_succeed <<EOM + knife('list -R /').should_succeed <<-EOM /: acls clients @@ -789,6 +791,8 @@ invitations.json members.json nodes org.json +policies +policy_groups roles /acls: @@ -866,8 +870,149 @@ users.json /nodes: +/policies: + +/policy_groups: + /roles: EOM end + + context "has plenty of stuff in it" do + before do + client 'client1', {} + client 'client2', {} + container 'container1', {} + container 'container2', {} + cookbook 'cookbook1', '1.0.0' + cookbook 'cookbook2', '1.0.1', { 'recipes' => { 'default.rb' => '' } } + data_bag 'bag1', { 'item1' => {}, 'item2' => {} } + data_bag 'bag2', { 'item1' => {}, 'item2' => {} } + environment 'environment1', {} + environment 'environment2', {} + group 'group1', {} + group 'group2', {} + node 'node1', {} + node 'node2', {} + org_invite 'user1' + org_member 'user2' + policy 'policy1', '1.2.3', {} + policy 'policy2', '1.2.3', {} + policy 'policy2', '1.3.5', {} + policy_group 'policy_group1', { 'policies' => { 'policy1' => { 'revision_id' => '1.2.3' } } } + policy_group 'policy_group2', { 'policies' => { 'policy2' => { 'revision_id' => '1.3.5' } } } + role 'role1', {} + role 'role2', {} + user 'user1', {} + user 'user2', {} + end + + it "knife list -Rfp / returns everything" do + knife('list -Rfp /').should_succeed <<-EOM +/acls/ +/acls/clients/ +/acls/clients/client1.json +/acls/clients/client2.json +/acls/clients/foo-validator.json +/acls/containers/ +/acls/containers/clients.json +/acls/containers/container1.json +/acls/containers/container2.json +/acls/containers/containers.json +/acls/containers/cookbook_artifacts.json +/acls/containers/cookbooks.json +/acls/containers/data.json +/acls/containers/environments.json +/acls/containers/groups.json +/acls/containers/nodes.json +/acls/containers/policies.json +/acls/containers/roles.json +/acls/containers/sandboxes.json +/acls/cookbooks/ +/acls/cookbooks/cookbook1.json +/acls/cookbooks/cookbook2.json +/acls/data_bags/ +/acls/data_bags/bag1.json +/acls/data_bags/bag2.json +/acls/environments/ +/acls/environments/_default.json +/acls/environments/environment1.json +/acls/environments/environment2.json +/acls/groups/ +/acls/groups/admins.json +/acls/groups/billing-admins.json +/acls/groups/clients.json +/acls/groups/group1.json +/acls/groups/group2.json +/acls/groups/users.json +/acls/nodes/ +/acls/nodes/node1.json +/acls/nodes/node2.json +/acls/organization.json +/acls/roles/ +/acls/roles/role1.json +/acls/roles/role2.json +/clients/ +/clients/client1.json +/clients/client2.json +/clients/foo-validator.json +/containers/ +/containers/clients.json +/containers/container1.json +/containers/container2.json +/containers/containers.json +/containers/cookbook_artifacts.json +/containers/cookbooks.json +/containers/data.json +/containers/environments.json +/containers/groups.json +/containers/nodes.json +/containers/policies.json +/containers/roles.json +/containers/sandboxes.json +/cookbooks/ +/cookbooks/cookbook1/ +/cookbooks/cookbook1/metadata.rb +/cookbooks/cookbook2/ +/cookbooks/cookbook2/metadata.rb +/cookbooks/cookbook2/recipes/ +/cookbooks/cookbook2/recipes/default.rb +/data_bags/ +/data_bags/bag1/ +/data_bags/bag1/item1.json +/data_bags/bag1/item2.json +/data_bags/bag2/ +/data_bags/bag2/item1.json +/data_bags/bag2/item2.json +/environments/ +/environments/_default.json +/environments/environment1.json +/environments/environment2.json +/groups/ +/groups/admins.json +/groups/billing-admins.json +/groups/clients.json +/groups/group1.json +/groups/group2.json +/groups/users.json +/invitations.json +/members.json +/nodes/ +/nodes/node1.json +/nodes/node2.json +/org.json +/policies/ +/policies/policy1-1.2.3.json +/policies/policy2-1.2.3.json +/policies/policy2-1.3.5.json +/policy_groups/ +/policy_groups/policy_group1.json +/policy_groups/policy_group2.json +/roles/ +/roles/role1.json +/roles/role2.json +EOM + end + end end end diff --git a/spec/integration/knife/upload_spec.rb b/spec/integration/knife/upload_spec.rb index d72cbb82f5..f7d4fb3cd4 100644 --- a/spec/integration/knife/upload_spec.rb +++ b/spec/integration/knife/upload_spec.rb @@ -1248,6 +1248,7 @@ EOM end end # with versioned cookbooks + when_the_chef_server 'has a user' do before do user 'x', {} @@ -1268,7 +1269,7 @@ EOM user 'foo', {} user 'bar', {} user 'foobar', {} - organization 'foo', { 'full_name' => 'Something'} + organization 'foo', { 'full_name' => 'Something' } end before :each do @@ -1278,7 +1279,7 @@ EOM context 'and has nothing but a single group named blah' do group 'blah', {} - when_the_repository 'has one of each thing' do + when_the_repository 'has at least one of each thing' do before do # TODO We have to upload acls for an existing group due to a lack of @@ -1292,8 +1293,11 @@ EOM file 'groups/x.json', {} file 'invitations.json', [ 'foo' ] file 'members.json', [ 'bar' ] - file 'nodes/x.json', {} file 'org.json', { 'full_name' => 'wootles' } + file 'nodes/x.json', {} + file 'policies/x-1.0.0.json', { } + file 'policies/blah-1.0.0.json', { } + file 'policy_groups/x.json', { 'policies' => { 'x' => { 'revision_id' => '1.0.0' }, 'blah' => { 'revision_id' => '1.0.0' } } } file 'roles/x.json', {} end @@ -1311,11 +1315,102 @@ Updated /invitations.json Updated /members.json Created /nodes/x.json Updated /org.json +Created /policies/blah-1.0.0.json +Created /policies/x-1.0.0.json +Created /policy_groups/x.json Created /roles/x.json EOM expect(api.get('association_requests').map { |a| a['username'] }).to eq([ 'foo' ]) expect(api.get('users').map { |a| a['user']['username'] }).to eq([ 'bar' ]) end + + context "When the chef server has an identical copy of each thing" do + before do + file 'invitations.json', [ 'foo' ] + file 'members.json', [ 'bar' ] + file 'org.json', { 'full_name' => 'Something' } + + # acl_for %w(organizations foo groups blah) + client 'x', {} + cookbook 'x', '1.0.0' + container 'x', {} + data_bag 'x', { 'y' => {} } + environment 'x', {} + group 'x', {} + org_invite 'foo' + org_member 'bar' + node 'x', {} + policy 'x', '1.0.0', {} + policy 'blah', '1.0.0', {} + policy_group 'x', { + 'policies' => { + 'x' => { 'revision_id' => '1.0.0' }, + 'blah' => { 'revision_id' => '1.0.0' } + } + } + role 'x', {} + end + + it 'knife upload makes no changes' do + knife('upload /').should_succeed <<EOM +Updated /acls/groups/blah.json +EOM + end + end + + context "When the chef server has a slightly different copy of the policy revision" do + before do + policy 'x', '1.0.0', { 'run_list' => [ 'blah' ] } + end + + it "should fail because policies are not updateable" do + knife("upload /policies/x-1.0.0.json").should_fail <<EOM +ERROR: /policies/x-1.0.0.json cannot be updated: policy revisions are immutable once uploaded. If you want to change the policy, create a new revision with your changes. +EOM + end + end + + context "When the chef server has a slightly different copy of each thing (except policy revisions)" do + before do + # acl_for %w(organizations foo groups blah) + client 'x', { 'validator' => true } + container 'x', {} + cookbook 'x', '1.0.0', { 'recipes' => { 'default.rb' => '' } } + data_bag 'x', { 'y' => { 'a' => 'b' } } + environment 'x', { 'description' => 'foo' } + group 'x', { 'groups' => [ 'admin' ] } + node 'x', { 'run_list' => [ 'blah' ] } + policy 'x', '1.0.0', { } + policy 'x', '1.0.1', { } + policy 'y', '1.0.0', { } + policy_group 'x', { + 'policies' => { + 'x' => { 'revision_id' => '1.0.1' }, + 'y' => { 'revision_id' => '1.0.0' } + } + } + role 'x', { 'run_list' => [ 'blah' ] } + end + + it 'knife upload updates everything' do + knife('upload /').should_succeed <<EOM +Updated /acls/groups/blah.json +Updated /clients/x.json +Updated /cookbooks/x +Updated /data_bags/x/y.json +Updated /environments/x.json +Updated /groups/x.json +Updated /invitations.json +Updated /members.json +Updated /nodes/x.json +Updated /org.json +Created /policies/blah-1.0.0.json +Updated /policy_groups/x.json +Updated /roles/x.json +EOM + knife('diff --name-status --diff-filter=AMT /').should_succeed '' + end + end end when_the_repository 'has an org.json that does not change full_name' do diff --git a/spec/unit/chef_fs/config_spec.rb b/spec/unit/chef_fs/config_spec.rb index c7c47ad8ab..c9cf756dc0 100644 --- a/spec/unit/chef_fs/config_spec.rb +++ b/spec/unit/chef_fs/config_spec.rb @@ -102,9 +102,5 @@ describe Chef::ChefFS::Config do it "sets the correct user path on the local FS object" do expect(local_fs.child_paths["users"]).to eq([platform_path("/base_path/users")]) end - - it "sets the correct policy path on the local FS object" do - expect(local_fs.child_paths["policies"]).to eq([platform_path("/base_path/policies")]) - end end end |