diff options
18 files changed, 653 insertions, 31 deletions
diff --git a/lib/chef/chef_fs/config.rb b/lib/chef/chef_fs/config.rb index 536409a109..c9f187a3bd 100644 --- a/lib/chef/chef_fs/config.rb +++ b/lib/chef/chef_fs/config.rb @@ -68,7 +68,7 @@ class Chef def create_local_fs require 'chef/chef_fs/file_system/chef_repository_file_system_root_dir' - Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(object_paths) + Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(object_paths, Array(chef_config[:chef_repo_path]).flatten) end # Returns the given real path's location relative to the server root. diff --git a/lib/chef/chef_fs/data_handler/organization_data_handler.rb b/lib/chef/chef_fs/data_handler/organization_data_handler.rb new file mode 100644 index 0000000000..1f2f9ffaf5 --- /dev/null +++ b/lib/chef/chef_fs/data_handler/organization_data_handler.rb @@ -0,0 +1,30 @@ +require 'chef/chef_fs/data_handler/data_handler_base' + +class Chef + module ChefFS + module DataHandler + class OrganizationDataHandler < DataHandlerBase + def normalize(organization, entry) + result = normalize_hash(organization, { + 'name' => entry.org, + 'full_name' => entry.org, + 'org_type' => 'Business', + 'clientname' => "#{entry.org}-validator", + 'billing_plan' => 'platform-free', + }) + result + end + + def preserve_key(key) + return key == 'name' + end + + def verify_integrity(object, entry, &on_error) + if entry.org != object['name'] + on_error.call("Name must be '#{entry.org}' (is '#{object['name']}')") + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/data_handler/organization_invites_data_handler.rb b/lib/chef/chef_fs/data_handler/organization_invites_data_handler.rb new file mode 100644 index 0000000000..db56ecc504 --- /dev/null +++ b/lib/chef/chef_fs/data_handler/organization_invites_data_handler.rb @@ -0,0 +1,17 @@ +require 'chef/chef_fs/data_handler/data_handler_base' + +class Chef + module ChefFS + module DataHandler + class OrganizationInvitesDataHandler < DataHandlerBase + def normalize(invites, entry) + invites.map { |invite| invite.is_a?(Hash) ? invite['username'] : invite }.sort.uniq + end + + def minimize(invites, entry) + invites + end + end + end + end +end diff --git a/lib/chef/chef_fs/data_handler/organization_members_data_handler.rb b/lib/chef/chef_fs/data_handler/organization_members_data_handler.rb new file mode 100644 index 0000000000..afa331775c --- /dev/null +++ b/lib/chef/chef_fs/data_handler/organization_members_data_handler.rb @@ -0,0 +1,17 @@ +require 'chef/chef_fs/data_handler/data_handler_base' + +class Chef + module ChefFS + module DataHandler + class OrganizationMembersDataHandler < DataHandlerBase + def normalize(members, entry) + members.map { |member| member.is_a?(Hash) ? member['user']['username'] : member }.sort.uniq + end + + def minimize(members, entry) + members + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system.rb b/lib/chef/chef_fs/file_system.rb index 4d15d7af33..730fa0e5cc 100644 --- a/lib/chef/chef_fs/file_system.rb +++ b/lib/chef/chef_fs/file_system.rb @@ -273,7 +273,6 @@ class Chef # case we shouldn't waste time trying PUT if we know the file doesn't # exist. # Will need to decide how that works with checksums, though. - error = false begin dest_path = format_path.call(dest_entry) if ui diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb index 6ccdc2cf5f..9acfe4b936 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb @@ -31,8 +31,12 @@ class Chef @data_handler = data_handler end + def write_pretty_json=(value) + @write_pretty_json = value + end + def write_pretty_json - root.write_pretty_json + @write_pretty_json.nil? ? root.write_pretty_json : @write_pretty_json end def data_handler 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 d615e0f415..14f3e8413b 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 @@ -34,31 +34,50 @@ class Chef module ChefFS module FileSystem class ChefRepositoryFileSystemRootDir < BaseFSDir - def initialize(child_paths) + def initialize(child_paths, root_paths=nil) super("", nil) @child_paths = child_paths + @root_paths = root_paths end attr_accessor :write_pretty_json + attr_reader :root_paths attr_reader :child_paths + CHILDREN = %w(invitations.json members.json org.json) + def children - @children ||= child_paths.keys.sort.map { |name| make_child_entry(name) }.select { |child| !child.nil? } + @children ||= begin + result = child_paths.keys.sort.map { |name| make_child_entry(name) }.select { |child| !child.nil? } + result += root_dir.children.select { |c| CHILDREN.include?(c.name) } if root_dir + result.sort_by { |c| c.name } + end end def can_have_child?(name, is_dir) - child_paths.has_key?(name) && is_dir + if is_dir + child_paths.has_key?(name) + elsif root_paths + CHILDREN.include?(name) + else + false + end end def create_child(name, file_contents = nil) - child_paths[name].each do |path| - begin - Dir.mkdir(path) - rescue Errno::EEXIST + if file_contents + d = root_dir + child = root_dir.create_child(name, file_contents) + else + child_paths[name].each do |path| + begin + Dir.mkdir(path) + rescue Errno::EEXIST + end end + child = make_child_entry(name) end - child = make_child_entry(name) @children = nil child end @@ -69,15 +88,15 @@ class Chef # Used to print out the filesystem def fs_description - repo_path = File.dirname(child_paths['cookbooks'][0]) - result = "repository at #{repo_path}\n" + repo_paths = root_paths || [ File.dirname(child_paths['cookbooks'][0]) ] + result = "repository at #{repo_paths.join(', ')}\n" if Chef::Config[:versioned_cookbooks] result << " Multiple versions per cookbook\n" else result << " One version per cookbook\n" end child_paths.each_pair do |name, paths| - if paths.any? { |path| File.dirname(path) != repo_path } + if paths.any? { |path| !repo_paths.include?(File.dirname(path)) } result << " #{name} at #{paths.join(', ')}\n" end end @@ -86,6 +105,14 @@ class Chef private + def root_dir + MultiplexedDir.new(root_paths.select { |path| File.exists?(path) }.map do |path| + dir = ChefRepositoryFileSystemEntry.new(name, parent, path) + dir.write_pretty_json = !!write_pretty_json + dir + end) + end + def make_child_entry(name) paths = child_paths[name].select do |path| File.exists?(path) 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 0083ee4cfa..069a2e29fa 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 @@ -23,6 +23,9 @@ require 'chef/chef_fs/file_system/rest_list_dir' require 'chef/chef_fs/file_system/cookbooks_dir' require 'chef/chef_fs/file_system/data_bags_dir' require 'chef/chef_fs/file_system/nodes_dir' +require 'chef/chef_fs/file_system/org_entry' +require 'chef/chef_fs/file_system/organization_invites_entry' +require 'chef/chef_fs/file_system/organization_members_entry' require 'chef/chef_fs/file_system/environments_dir' require 'chef/chef_fs/data_handler/client_data_handler' require 'chef/chef_fs/data_handler/role_data_handler' @@ -81,10 +84,13 @@ class Chef end def org - @org ||= if URI.parse(chef_server_url).path =~ /^\/+organizations\/+([^\/]+)$/ - $1 - else - nil + @org ||= begin + path = Pathname.new(URI.parse(chef_server_url).path).cleanpath + if File.dirname(path) == '/organizations' + File.basename(path) + else + nil + end end end @@ -102,7 +108,10 @@ class Chef RestListDir.new("clients", self, nil, Chef::ChefFS::DataHandler::ClientDataHandler.new), RestListDir.new("containers", self, nil, Chef::ChefFS::DataHandler::ContainerDataHandler.new), RestListDir.new("groups", self, nil, Chef::ChefFS::DataHandler::GroupDataHandler.new), - NodesDir.new(self) + NodesDir.new(self), + OrgEntry.new("org.json", self), + OrganizationMembersEntry.new("members.json", self), + OrganizationInvitesEntry.new("invitations.json", self) ] elsif repo_mode != 'static' result += [ diff --git a/lib/chef/chef_fs/file_system/org_entry.rb b/lib/chef/chef_fs/file_system/org_entry.rb new file mode 100644 index 0000000000..852956e1e5 --- /dev/null +++ b/lib/chef/chef_fs/file_system/org_entry.rb @@ -0,0 +1,34 @@ +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_data_handler' + +class Chef + module ChefFS + module FileSystem + # /organizations/NAME/org.json + # Represents the actual data at /organizations/NAME (the full name, etc.) + class OrgEntry < RestListEntry + def initialize(name, parent, exists = nil) + super(name, parent) + @exists = exists + end + + def data_handler + Chef::ChefFS::DataHandler::OrganizationDataHandler.new + end + + # /organizations/foo/org.json -> GET /organizations/foo + def api_path + parent.api_path + end + + def exists? + parent.exists? + end + + def delete(recurse) + raise Chef::ChefFS::FileSystem::OperationNotAllowedError.new(:delete, self) + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/organization_invites_entry.rb b/lib/chef/chef_fs/file_system/organization_invites_entry.rb new file mode 100644 index 0000000000..a83e058b77 --- /dev/null +++ b/lib/chef/chef_fs/file_system/organization_invites_entry.rb @@ -0,0 +1,59 @@ +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_invites_data_handler' + +class Chef + module ChefFS + module FileSystem + # /organizations/NAME/invitations.json + # Represents the actual data at + # read: + # - GET /organizations/NAME/association_requests + # write: + # - 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(JSON.parse(contents, :create_additions => false)) + actual_invites = _read_json.inject({}) { |h,val| h[val['username']] = val['id']; h } + invites = actual_invites.keys + (desired_invites - invites).each do |invite| + begin + rest.post(api_path, { 'user' => invite }) + rescue Net::HTTPServerException => e + if e.response.code == '409' + Chef::Log.warn("Could not invite #{invite} to organization #{org}: #{api_error_text(e.response)}") + else + raise + end + end + end + (invites - desired_invites).each do |invite| + rest.delete(File.join(api_path, actual_invites[invite])) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/organization_members_entry.rb b/lib/chef/chef_fs/file_system/organization_members_entry.rb new file mode 100644 index 0000000000..c1151413f5 --- /dev/null +++ b/lib/chef/chef_fs/file_system/organization_members_entry.rb @@ -0,0 +1,58 @@ +require 'chef/chef_fs/file_system/rest_list_entry' +require 'chef/chef_fs/data_handler/organization_members_data_handler' + +class Chef + module ChefFS + module FileSystem + # /organizations/NAME/members.json + # Represents the actual data at + # read: + # - GET /organizations/NAME/users + # write: + # - 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(JSON.parse(contents, :create_additions => false)) + members = minimize_value(_read_json) + (desired_members - members).each do |member| + begin + rest.post(File.join(api_path, member), {}) + rescue Net::HTTPServerException => e + if e.response.code == '404' + raise "Chef server at #{api_path} does not allow you to directly add members. Please either upgrade your Chef server or move the users you want into invitations.json instead of members.json." + else + raise + end + end + end + (members - desired_members).each do |member| + rest.delete(File.join(api_path, member)) + end + end + end + end + end +end diff --git a/lib/chef/chef_fs/file_system/rest_list_entry.rb b/lib/chef/chef_fs/file_system/rest_list_entry.rb index 67252a6f2f..ac47ff4f25 100644 --- a/lib/chef/chef_fs/file_system/rest_list_entry.rb +++ b/lib/chef/chef_fs/file_system/rest_list_entry.rb @@ -80,13 +80,13 @@ class Chef end def read - Chef::JSONCompat.to_json_pretty(_read_hash) + Chef::JSONCompat.to_json_pretty(minimize_value(_read_json)) end - def _read_hash + def _read_json begin # Minimize the value (get rid of defaults) so the results don't look terrible - minimize_value(root.get_json(api_path)) + 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 @@ -119,7 +119,7 @@ class Chef # Grab this value begin - value = _read_hash + value = _read_json rescue Chef::ChefFS::FileSystem::NotFoundError return [ false, :none, other_value_json ] end @@ -169,7 +169,16 @@ class Chef end end end + + def api_error_text(response) + begin + JSON.parse(response.body)['error'].join("\n") + rescue + response.body + end + end end + end end end diff --git a/lib/chef/chef_fs/parallelizer.rb b/lib/chef/chef_fs/parallelizer.rb index 116a626869..f29a2deae7 100644 --- a/lib/chef/chef_fs/parallelizer.rb +++ b/lib/chef/chef_fs/parallelizer.rb @@ -53,6 +53,7 @@ class Chef end def resize(to_threads, wait = true, timeout = nil) + to_threads = 0 if to_threads < num_threads threads_to_stop = @threads[to_threads..num_threads-1] @threads = @threads.slice(0, to_threads) @@ -89,6 +90,7 @@ class Chef begin while !@stop_thread[Thread.current] begin + puts "Got a task!" task = @tasks.pop task.call rescue diff --git a/spec/integration/knife/delete_spec.rb b/spec/integration/knife/delete_spec.rb index dc52cc71a4..a9ba9dc3e1 100644 --- a/spec/integration/knife/delete_spec.rb +++ b/spec/integration/knife/delete_spec.rb @@ -962,9 +962,12 @@ EOM end end - when_the_chef_server "is in Enterprise mode", :osc_compat => false, :single_org => false do + when_the_chef_server "is in Enterprise mode", :focus, :osc_compat => false, :single_org => false do before do - organization 'foo' + organization 'foo' do + container 'x', {} + group 'x', {} + end end before :each do @@ -974,5 +977,27 @@ EOM 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" end + + it 'knife delete /containers/x.json succeeds' do + knife('delete /containers/x.json').should_succeed "Deleted /containers/x.json\n" + knife('raw /containers/x.json').should_fail(/404/) + end + + it 'knife delete /groups/x.json succeeds' do + knife('delete /groups/x.json').should_succeed "Deleted /groups/x.json\n" + knife('raw /groups/x.json').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 + + it 'knife delete /invitations.json fails with a reasonable error' do + knife('delete /invitations.json').should_fail "ERROR: /invitations.json (remote) cannot be deleted.\n" + end + + it 'knife delete /members.json fails with a reasonable error' do + knife('delete /members.json').should_fail "ERROR: /members.json (remote) cannot be deleted.\n" + end end end diff --git a/spec/integration/knife/download_spec.rb b/spec/integration/knife/download_spec.rb index 4126acc9fc..17779d1f47 100644 --- a/spec/integration/knife/download_spec.rb +++ b/spec/integration/knife/download_spec.rb @@ -1091,4 +1091,80 @@ EOM end end end + + 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 + end + + before :each do + Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo') + end + + when_the_repository 'is empty' do + it 'knife download / downloads everything' do + knife('download /').should_succeed <<EOM +Created /acls +Created /acls/clients +Created /acls/clients/foo-validator.json +Created /acls/containers +Created /acls/containers/clients.json +Created /acls/containers/containers.json +Created /acls/containers/cookbooks.json +Created /acls/containers/data.json +Created /acls/containers/environments.json +Created /acls/containers/groups.json +Created /acls/containers/nodes.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 +Created /acls/environments/_default.json +Created /acls/groups +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 +Created /clients +Created /clients/foo-validator.json +Created /containers +Created /containers/clients.json +Created /containers/containers.json +Created /containers/cookbooks.json +Created /containers/data.json +Created /containers/environments.json +Created /containers/groups.json +Created /containers/nodes.json +Created /containers/roles.json +Created /containers/sandboxes.json +Created /containers/x.json +Created /cookbooks +Created /data_bags +Created /environments +Created /environments/_default.json +Created /groups +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 /roles +EOM + knife('diff --name-status /').should_succeed '' + end + end + end end diff --git a/spec/integration/knife/list_spec.rb b/spec/integration/knife/list_spec.rb index 1f70d3bc3d..48e85f43bb 100644 --- a/spec/integration/knife/list_spec.rb +++ b/spec/integration/knife/list_spec.rb @@ -651,7 +651,7 @@ EOM Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo') end - context 'and has plenty of stuff in it' do + context 'and is empty' do it "knife list / returns all top level directories" do knife('list /').should_succeed <<EOM /acls @@ -755,4 +755,105 @@ 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 +/: +acls +clients +containers +cookbooks +data_bags +environments +groups +invitations.json +members.json +nodes +org.json +roles + +/acls: +clients +containers +cookbooks +data_bags +environments +groups +nodes +organization.json +roles + +/acls/clients: +foo-validator.json + +/acls/containers: +clients.json +containers.json +cookbooks.json +data.json +environments.json +groups.json +nodes.json +roles.json +sandboxes.json + +/acls/cookbooks: + +/acls/data_bags: + +/acls/environments: +_default.json + +/acls/groups: +admins.json +billing-admins.json +clients.json +users.json + +/acls/nodes: + +/acls/roles: + +/clients: +foo-validator.json + +/containers: +clients.json +containers.json +cookbooks.json +data.json +environments.json +groups.json +nodes.json +roles.json +sandboxes.json + +/cookbooks: + +/data_bags: + +/environments: +_default.json + +/groups: +admins.json +billing-admins.json +clients.json +users.json + +/nodes: + +/roles: +EOM + end + end end diff --git a/spec/integration/knife/upload_spec.rb b/spec/integration/knife/upload_spec.rb index 4e447589f3..05d33d1a17 100644 --- a/spec/integration/knife/upload_spec.rb +++ b/spec/integration/knife/upload_spec.rb @@ -1216,4 +1216,157 @@ EOM end end end + + when_the_chef_server "is in Enterprise mode", :osc_compat => false, :single_org => false do + before do + 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 + + context 'and has nothing but a single group named blah' do + group 'blah', {} + + when_the_repository 'has one 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 'acls/groups/blah.json', {} + 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' => 'wootles' } + file 'roles/x.json', {} + end + + it 'knife upload / uploads everything' do + knife('upload /').should_succeed <<EOM +Updated /acls/groups/blah.json +Created /clients/x.json +Created /containers/x.json +Created /cookbooks/x +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 +Updated /org.json +Created /roles/x.json +EOM + api.get('association_requests').map { |a| a['username'] }.should == [ 'foo' ] + api.get('users').map { |a| a['user']['username'] }.should == [ 'bar' ] + end + end + + when_the_repository 'has an org.json that does not change full_name' do + before do + file 'org.json', { 'full_name' => 'Something' } + end + + it 'knife upload / emits a warning for bar and adds foo and foobar' do + knife('upload /').should_succeed '' + api.get('/')['full_name'].should == 'Something' + end + end + + when_the_repository 'has an org.json that changes full_name' do + before do + file 'org.json', { 'full_name' => 'Something Else'} + end + + it 'knife upload / emits a warning for bar and adds foo and foobar' do + knife('upload /').should_succeed "Updated /org.json\n" + api.get('/')['full_name'].should == 'Something Else' + end + end + + context 'and has invited foo and bar is already a member' do + org_invite 'foo' + org_member 'bar' + + when_the_repository 'wants to invite foo, bar and foobar' do + before do + file 'invitations.json', [ 'foo', 'bar', 'foobar' ] + end + + it 'knife upload / emits a warning for bar and invites foobar' do + knife('upload /').should_succeed "Updated /invitations.json\n", :stderr => "WARN: Could not invite bar to organization foo: User bar is already in organization foo\n" + api.get('association_requests').map { |a| a['username'] }.should == [ 'foo', 'foobar' ] + api.get('users').map { |a| a['user']['username'] }.should == [ 'bar' ] + end + end + + when_the_repository 'wants to make foo, bar and foobar members' do + before do + file 'members.json', [ 'foo', 'bar', 'foobar' ] + end + + it 'knife upload / emits a warning for bar and adds foo and foobar' do + knife('upload /').should_succeed "Updated /members.json\n" + api.get('association_requests').map { |a| a['username'] }.should == [ ] + api.get('users').map { |a| a['user']['username'] }.should == [ 'bar', 'foo', 'foobar' ] + end + end + + when_the_repository 'wants to invite foo and have bar as a member' do + before do + file 'invitations.json', [ 'foo' ] + file 'members.json', [ 'bar' ] + end + + it 'knife upload / does nothing' do + knife('upload /').should_succeed '' + api.get('association_requests').map { |a| a['username'] }.should == [ 'foo' ] + api.get('users').map { |a| a['user']['username'] }.should == [ 'bar' ] + end + end + end + + context 'and has invited bar and foo' do + org_invite 'bar', 'foo' + + when_the_repository 'wants to invite foo and bar (different order)' do + before do + file 'invitations.json', [ 'foo', 'bar' ] + end + + it 'knife upload / does nothing' do + knife('upload /').should_succeed '' + api.get('association_requests').map { |a| a['username'] }.should == [ 'bar', 'foo' ] + api.get('users').map { |a| a['user']['username'] }.should == [ ] + end + end + end + + context 'and has already added bar and foo as members of the org' do + org_member 'bar', 'foo' + + when_the_repository 'wants to add foo and bar (different order)' do + before do + file 'members.json', [ 'foo', 'bar' ] + end + + it 'knife upload / does nothing' do + knife('upload /').should_succeed '' + api.get('association_requests').map { |a| a['username'] }.should == [ ] + api.get('users').map { |a| a['user']['username'] }.should == [ 'bar', 'foo' ] + end + end + end + end + end end diff --git a/spec/support/shared/integration/integration_helper.rb b/spec/support/shared/integration/integration_helper.rb index a6595e778d..465633b9e0 100644 --- a/spec/support/shared/integration/integration_helper.rb +++ b/spec/support/shared/integration/integration_helper.rb @@ -20,9 +20,9 @@ require 'tmpdir' require 'fileutils' require 'chef/config' -require 'chef_zero/rspec' - require 'chef/json_compat' +require 'chef/server_api' +require 'chef_zero/rspec' require 'support/shared/integration/knife_support' require 'support/shared/integration/app_server_support' require 'spec_helper' @@ -53,6 +53,10 @@ module IntegrationSupport includer_class.extend(ClassMethods) end + def api + Chef::ServerAPI.new + end + def directory(relative_path, &block) old_parent_path = @parent_path @parent_path = path_to(relative_path) @@ -67,10 +71,8 @@ module IntegrationSupport FileUtils.mkdir_p(dir) unless dir == '.' File.open(filename, 'w') do |file| raw = case contents - when Hash + when Hash, Array JSON.pretty_generate(contents) - when Array - contents.join("\n") else contents end |