diff --git a/lib/chef/provider/chef_acl.rb b/lib/chef/provider/chef_acl.rb deleted file mode 100644 index 4451a3b..0000000 --- a/lib/chef/provider/chef_acl.rb +++ /dev/null @@ -1,446 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_acl' -require 'chef/chef_fs/data_handler/acl_data_handler' -require 'chef/chef_fs/parallelizer' -require 'uri' - -class Chef - class Provider - class ChefAcl < Cheffish::ChefProviderBase - provides :chef_acl - - def whyrun_supported? - true - end - - action :create do - if new_resource.remove_rights && new_resource.complete - Chef::Log.warn("'remove_rights' is redundant when 'complete' is specified: all rights not specified in a 'rights' declaration will be removed.") - end - # Verify that we're not destroying all hope of ACL recovery here - if new_resource.complete && (!new_resource.rights || !new_resource.rights.any? { |r| r[:permissions].include?(:all) || r[:permissions].include?(:grant) }) - # NOTE: if superusers exist, this should turn into a warning. - raise "'complete' specified on chef_acl resource, but no GRANT permissions were granted. I'm sorry Dave, I can't let you remove all access to an object with no hope of recovery." - end - - # Find all matching paths so we can update them (resolve * and **) - paths = match_paths(new_resource.path) - if paths.size == 0 && !new_resource.path.split('/').any? { |p| p == '*' } - raise "Path #{new_resource.path} cannot have an ACL set on it!" - end - - # Go through the matches and update the ACLs for them - paths.each do |path| - create_acl(path) - end - end - - # Update the ACL if necessary. - def create_acl(path) - changed = false - # There may not be an ACL path for some valid paths (/ and /organizations, - # for example). We want to recurse into these, but we don't want to try to - # update nonexistent ACLs for them. - acl = acl_path(path) - if acl - # It's possible to make a custom container - current_json = current_acl(acl) - if current_json - - # Compare the desired and current json for the ACL, and update if different. - modify = {} - desired_acl(acl).each do |permission, desired_json| - differences = json_differences(sort_values(current_json[permission]), sort_values(desired_json)) - - if differences.size > 0 - # Verify we aren't trying to destroy grant permissions - if permission == 'grant' && desired_json['actors'] == [] && desired_json['groups'] == [] - # NOTE: if superusers exist, this should turn into a warning. - raise "chef_acl attempted to remove all actors from GRANT! I'm sorry Dave, I can't let you remove access to an object with no hope of recovery." - end - modify[differences] ||= {} - modify[differences][permission] = desired_json - end - end - - if modify.size > 0 - changed = true - description = [ "update acl #{path} at #{rest_url(path)}" ] + modify.map do |diffs, permissions| - diffs.map { |diff| " #{permissions.keys.join(', ')}:#{diff}" } - end.flatten(1) - converge_by description do - modify.values.each do |permissions| - permissions.each do |permission, desired_json| - rest.put(rest_url("#{acl}/#{permission}"), { permission => desired_json }) - end - end - end - end - end - end - - # If we have been asked to recurse, do so. - # If recurse is on_change, then we will recurse if there is no ACL, or if - # the ACL has changed. - if new_resource.recursive == true || (new_resource.recursive == :on_change && (!acl || changed)) - children, error = list(path, '*') - Chef::ChefFS::Parallelizer.parallel_do(children) do |child| - next if child.split('/')[-1] == 'containers' - create_acl(child) - end - # containers mess up our descent, so we do them last - Chef::ChefFS::Parallelizer.parallel_do(children) do |child| - next if child.split('/')[-1] != 'containers' - create_acl(child) - end - - end - end - - # Get the current ACL for the given path - def current_acl(acl_path) - @current_acls ||= {} - if !@current_acls.has_key?(acl_path) - @current_acls[acl_path] = begin - rest.get(rest_url(acl_path)) - rescue Net::HTTPServerException => e - unless e.response.code == '404' && new_resource.path.split('/').any? { |p| p == '*' } - raise - end - end - end - @current_acls[acl_path] - end - - # Get the desired acl for the given acl path - def desired_acl(acl_path) - result = new_resource.raw_json ? new_resource.raw_json.dup : {} - - # Calculate the JSON based on rights - add_rights(acl_path, result) - - if new_resource.complete - result = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(result, nil) - else - # If resource is incomplete, use current json to fill any holes - current_acl(acl_path).each do |permission, perm_hash| - if !result[permission] - result[permission] = perm_hash.dup - else - result[permission] = result[permission].dup - perm_hash.each do |type, actors| - if !result[permission][type] - result[permission][type] = actors - else - result[permission][type] = result[permission][type].dup - result[permission][type] |= actors - end - end - end - end - - remove_rights(result) - end - result - end - - def sort_values(json) - json.each do |key, value| - json[key] = value.sort if value.is_a?(Array) - end - json - end - - def add_rights(acl_path, json) - if new_resource.rights - new_resource.rights.each do |rights| - if rights[:permissions].delete(:all) - rights[:permissions] |= current_acl(acl_path).keys - end - - Array(rights[:permissions]).each do |permission| - ace = json[permission.to_s] ||= {} - # WTF, no distinction between users and clients? The Chef API doesn't - # let us distinguish, so we have no choice :/ This means that: - # 1. If you specify :users => 'foo', and client 'foo' exists, it will - # pick that (whether user 'foo' exists or not) - # 2. If you specify :clients => 'foo', and user 'foo' exists but - # client 'foo' does not, it will pick user 'foo' and put it in the - # ACL - # 3. If an existing item has user 'foo' on it and you specify :clients - # => 'foo' instead, idempotence will not notice that anything needs - # to be updated and nothing will happen. - if rights[:users] - ace['actors'] ||= [] - ace['actors'] |= Array(rights[:users]) - end - if rights[:clients] - ace['actors'] ||= [] - ace['actors'] |= Array(rights[:clients]) - end - if rights[:groups] - ace['groups'] ||= [] - ace['groups'] |= Array(rights[:groups]) - end - end - end - end - end - - def remove_rights(json) - if new_resource.remove_rights - new_resource.remove_rights.each do |rights| - rights[:permissions].each do |permission| - if permission == :all - json.each_key do |key| - ace = json[key] = json[key.dup] - ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors'] - ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors'] - ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups'] - end - else - ace = json[permission.to_s] = json[permission.to_s].dup - if ace - ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors'] - ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors'] - ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups'] - end - end - end - end - end - end - - def load_current_resource - end - - # - # Matches chef_acl paths like nodes, nodes/*. - # - # == Examples - # match_paths('nodes'): [ 'nodes' ] - # match_paths('nodes/*'): [ 'nodes/x', 'nodes/y', 'nodes/z' ] - # match_paths('*'): [ 'clients', 'environments', 'nodes', 'roles', ... ] - # match_paths('/'): [ '/' ] - # match_paths(''): [ '' ] - # match_paths('/*'): [ '/organizations', '/users' ] - # match_paths('/organizations/*/*'): [ '/organizations/foo/clients', '/organizations/foo/environments', ..., '/organizations/bar/clients', '/organizations/bar/environments', ... ] - # - def match_paths(path) - # Turn multiple slashes into one - # nodes//x -> nodes/x - path = path.gsub(/[\/]+/, '/') - # If it's absolute, start the matching with /. If it's relative, start with '' (relative root). - if path[0] == '/' - matches = [ '/' ] - else - matches = [ '' ] - end - - # Split the path, and get rid of the empty path at the beginning and end - # (/a/b/c/ -> [ 'a', 'b', 'c' ]) - parts = path.split('/').select { |x| x != '' }.to_a - - # Descend until we find the matches: - # path = 'a/b/c' - # parts = [ 'a', 'b', 'c' ] - # Starting matches = [ '' ] - parts.each_with_index do |part, index| - # For each match, list / and set matches to that. - # - # Example: /*/foo - # 1. To start, - # matches = [ '/' ], part = '*'. - # list('/', '*') = [ '/organizations, '/users' ] - # 2. matches = [ '/organizations', '/users' ], part = 'foo' - # list('/organizations', 'foo') = [ '/organizations/foo' ] - # list('/users', 'foo') = [ '/users/foo' ] - # - # Result: /*/foo = [ '/organizations/foo', '/users/foo' ] - # - matches = Chef::ChefFS::Parallelizer.parallelize(matches) do |path| - found, error = list(path, part) - if error - if parts[0..index-1].all? { |p| p != '*' } - raise error - end - [] - else - found - end - end.flatten(1).to_a - end - - matches - end - - # - # Takes a normal path and finds the Chef path to get / set its ACL. - # - # nodes/x -> nodes/x/_acl - # nodes -> containers/nodes/_acl - # '' -> organizations/_acl (the org acl) - # /organizations/foo -> /organizations/foo/organizations/_acl - # /users/foo -> /users/foo/_acl - # /organizations/foo/nodes/x -> /organizations/foo/nodes/x/_acl - # - def acl_path(path) - parts = path.split('/').select { |x| x != '' }.to_a - prefix = (path[0] == '/') ? '/' : '' - - case parts.size - when 0 - # /, empty (relative root) - # The root of the server has no publicly visible ACLs. Only nodes/*, etc. - if prefix == '' - ::File.join('organizations', '_acl') - end - - when 1 - # nodes, roles, etc. - # The top level organizations and users containers have no publicly - # visible ACLs. Only nodes/*, etc. - if prefix == '' - ::File.join('containers', path, '_acl') - end - - when 2 - # /organizations/NAME, /users/NAME, nodes/NAME, roles/NAME, etc. - if prefix == '/' && parts[0] == 'organizations' - ::File.join(path, 'organizations', '_acl') - else - ::File.join(path, '_acl') - end - - when 3 - # /organizations/NAME/nodes, cookbooks/NAME/VERSION, etc. - if prefix == '/' - ::File.join('/', parts[0], parts[1], 'containers', parts[2], '_acl') - else - ::File.join(parts[0], parts[1], '_acl') - end - - when 4 - # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION/BLAH - # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION, etc. - if prefix == '/' - ::File.join(path, '_acl') - else - ::File.join(parts[0], parts[1], '_acl') - end - - else - # /organizations/NAME/cookbooks/NAME/VERSION/..., cookbooks/NAME/VERSION/A/B/... - if prefix == '/' - ::File.join('/', parts[0], parts[1], parts[2], parts[3], '_acl') - else - ::File.join(parts[0], parts[1], '_acl') - end - end - end - - # - # Lists the securable children under a path (the ones that either have ACLs - # or have children with ACLs). - # - # list('nodes', 'x') -> [ 'nodes/x' ] - # list('nodes', '*') -> [ 'nodes/x', 'nodes/y', 'nodes/z' ] - # list('', '*') -> [ 'clients', 'environments', 'nodes', 'roles', ... ] - # list('/', '*') -> [ '/organizations'] - # list('cookbooks', 'x') -> [ 'cookbooks/x' ] - # list('cookbooks/x', '*') -> [ ] # Individual cookbook versions do not have their own ACLs - # list('/organizations/foo/nodes', '*') -> [ '/organizations/foo/nodes/x', '/organizations/foo/nodes/y' ] - # - # The list of children of an organization is == the list of containers. If new - # containers are added, the list of children will grow. This allows the system - # to extend to new types of objects and allow cheffish to work with them. - # - def list(path, child) - # TODO make ChefFS understand top level organizations and stop doing this altogether. - parts = path.split('/').select { |x| x != '' }.to_a - absolute = (path[0] == '/') - if absolute && parts[0] == 'organizations' - return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 3 - else - return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 1 - end - - error = nil - - if child == '*' - case parts.size - when 0 - # /*, * - if absolute - results = [ "/organizations", "/users" ] - else - results, error = rest_list("containers") - end - - when 1 - # /organizations/*, /users/*, roles/*, nodes/*, etc. - results, error = rest_list(path) - if !error - results = results.map { |result| ::File.join(path, result) } - end - - when 2 - # /organizations/NAME/* - results, error = rest_list(::File.join(path, 'containers')) - if !error - results = results.map { |result| ::File.join(path, result) } - end - - when 3 - # /organizations/NAME/TYPE/* - results, error = rest_list(path) - if !error - results = results.map { |result| ::File.join(path, result) } - end - end - - else - if child == 'data_bags' && - (parts.size == 0 || (parts.size == 2 && parts[0] == 'organizations')) - child = 'data' - end - - if absolute - # /, /users/, /organizations/, /organizations/foo/, /organizations/foo/nodes/ ... - results = [ ::File.join('/', parts[0..2], child) ] - elsif parts.size == 0 - # (nodes, roles, etc.) - results = [ child ] - else - # nodes/, roles/, etc. - results = [ ::File.join(parts[0], child) ] - end - end - - [ results, error ] - end - - def rest_url(path) - path[0] == '/' ? URI.join(rest.url, path) : path - end - - def rest_list(path) - begin - # All our rest lists are hashes where the keys are the names - [ rest.get(rest_url(path)).keys, nil ] - rescue Net::HTTPServerException => e - if e.response.code == '405' || e.response.code == '404' - parts = path.split('/').select { |p| p != '' }.to_a - - # We KNOW we expect these to exist. Other containers may or may not. - unless (parts.size == 1 || (parts.size == 3 && parts[0] == 'organizations')) && - %w(clients containers cookbooks data environments groups nodes roles).include?(parts[-1]) - return [ [], "Cannot get list of #{path}: HTTP response code #{e.response.code}" ] - end - end - raise - end - end - end - end -end diff --git a/lib/chef/provider/chef_client.rb b/lib/chef/provider/chef_client.rb deleted file mode 100644 index b287adb..0000000 --- a/lib/chef/provider/chef_client.rb +++ /dev/null @@ -1,53 +0,0 @@ -require 'cheffish/actor_provider_base' -require 'chef/resource/chef_client' -require 'chef/chef_fs/data_handler/client_data_handler' - -class Chef - class Provider - class ChefClient < Cheffish::ActorProviderBase - provides :chef_client - - def whyrun_supported? - true - end - - def actor_type - 'client' - end - - def actor_path - 'clients' - end - - action :create do - create_actor - end - - action :delete do - delete_actor - end - - # - # Helpers - # - - def resource_class - Chef::Resource::ChefClient - end - - def data_handler - Chef::ChefFS::DataHandler::ClientDataHandler.new - end - - def keys - { - 'name' => :name, - 'admin' => :admin, - 'validator' => :validator, - 'public_key' => :source_key - } - end - - end - end -end diff --git a/lib/chef/provider/chef_container.rb b/lib/chef/provider/chef_container.rb deleted file mode 100644 index d3dd27b..0000000 --- a/lib/chef/provider/chef_container.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_container' -require 'chef/chef_fs/data_handler/container_data_handler' - -class Chef - class Provider - class ChefContainer < Cheffish::ChefProviderBase - provides :chef_container - - def whyrun_supported? - true - end - - action :create do - if !@current_exists - converge_by "create container #{new_resource.name} at #{rest.url}" do - rest.post("containers", normalize_for_post(new_json)) - end - end - end - - action :delete do - if @current_exists - converge_by "delete container #{new_resource.name} at #{rest.url}" do - rest.delete("containers/#{new_resource.name}") - end - end - end - - def load_current_resource - begin - @current_exists = rest.get("containers/#{new_resource.name}") - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_exists = false - else - raise - end - end - end - - def new_json - {} - end - - def data_handler - Chef::ChefFS::DataHandler::ContainerDataHandler.new - end - - def keys - { 'containername' => :name, 'containerpath' => :name } - end - end - end -end diff --git a/lib/chef/provider/chef_data_bag.rb b/lib/chef/provider/chef_data_bag.rb deleted file mode 100644 index 287e590..0000000 --- a/lib/chef/provider/chef_data_bag.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_data_bag' - -class Chef - class Provider - class ChefDataBag < Cheffish::ChefProviderBase - provides :chef_data_bag - - def whyrun_supported? - true - end - - action :create do - if !current_resource_exists? - converge_by "create data bag #{new_resource.name} at #{rest.url}" do - rest.post("data", { 'name' => new_resource.name }) - end - end - end - - action :delete do - if current_resource_exists? - converge_by "delete data bag #{new_resource.name} at #{rest.url}" do - rest.delete("data/#{new_resource.name}") - end - end - end - - def load_current_resource - begin - @current_resource = json_to_resource(rest.get("data/#{new_resource.name}")) - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_resource = not_found_resource - else - raise - end - end - end - - # - # Helpers - # - # Gives us new_json, current_json, not_found_json, etc. - - def resource_class - Chef::Resource::ChefDataBag - end - - def json_to_resource(json) - Chef::Resource::ChefDataBag.new(json['name'], run_context) - end - end - end -end diff --git a/lib/chef/provider/chef_data_bag_item.rb b/lib/chef/provider/chef_data_bag_item.rb deleted file mode 100644 index 9f83b41..0000000 --- a/lib/chef/provider/chef_data_bag_item.rb +++ /dev/null @@ -1,278 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_data_bag_item' -require 'chef/chef_fs/data_handler/data_bag_item_data_handler' -require 'chef/encrypted_data_bag_item' - -class Chef - class Provider - class ChefDataBagItem < Cheffish::ChefProviderBase - provides :chef_data_bag_item - - def whyrun_supported? - true - end - - action :create do - differences = calculate_differences - - if current_resource_exists? - if differences.size > 0 - description = [ "update data bag item #{new_resource.id} at #{rest.url}" ] + differences - converge_by description do - rest.put("data/#{new_resource.data_bag}/#{new_resource.id}", normalize_for_put(new_json)) - end - end - else - description = [ "create data bag item #{new_resource.id} at #{rest.url}" ] + differences - converge_by description do - rest.post("data/#{new_resource.data_bag}", normalize_for_post(new_json)) - end - end - end - - action :delete do - if current_resource_exists? - converge_by "delete data bag item #{new_resource.id} at #{rest.url}" do - rest.delete("data/#{new_resource.data_bag}/#{new_resource.id}") - end - end - end - - def load_current_resource - begin - json = rest.get("data/#{new_resource.data_bag}/#{new_resource.id}") - resource = Chef::Resource::ChefDataBagItem.new(new_resource.name, run_context) - resource.raw_data json - @current_resource = resource - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_resource = not_found_resource - else - raise - end - end - - # Determine if data bag is encrypted and if so, what its version is - first_real_key, first_real_value = (current_resource.raw_data || {}).select { |key, value| key != 'id' && !value.nil? }.first - if first_real_value - if first_real_value.is_a?(Hash) && - first_real_value['version'].is_a?(Integer) && - first_real_value['version'] > 0 && - first_real_value.has_key?('encrypted_data') - - current_resource.encrypt true - current_resource.encryption_version first_real_value['version'] - - decrypt_error = nil - - # Check if the desired secret is the one (which it generally should be) - - if new_resource.secret || new_resource.secret_path - begin - Chef::EncryptedDataBagItem::Decryptor.for(first_real_value, new_secret).for_decrypted_item - current_resource.secret new_secret - rescue Chef::EncryptedDataBagItem::DecryptionFailure - decrypt_error = $! - end - end - - # If the current secret doesn't work, look through the specified old secrets - - if !current_resource.secret - old_secrets = [] - if new_resource.old_secret - old_secrets += Array(new_resource.old_secret) - end - if new_resource.old_secret_path - old_secrets += Array(new_resource.old_secret_path).map do |secret_path| - Chef::EncryptedDataBagItem.load_secret(new_resource.old_secret_file) - end - end - old_secrets.each do |secret| - begin - Chef::EncryptedDataBagItem::Decryptor.for(first_real_value, secret).for_decrypted_item - current_resource.secret secret - rescue Chef::EncryptedDataBagItem::DecryptionFailure - decrypt_error = $! - end - end - - # If we couldn't figure out the secret, emit a warning (this isn't a fatal flaw unless we - # need to reuse one of the values from the data bag) - if !current_resource.secret - if decrypt_error - Chef::Log.warn "Existing data bag is encrypted, but could not decrypt: #{decrypt_error.message}." - else - Chef::Log.warn "Existing data bag is encrypted, but no secret was specified." - end - end - end - end - else - - # There are no encryptable values, so pretend encryption is the same as desired - - current_resource.encrypt new_resource.encrypt - current_resource.encryption_version new_resource.encryption_version - if new_resource.secret || new_resource.secret_path - current_resource.secret new_secret - end - end - end - - def new_json - @new_json ||= begin - if new_encrypt - # Encrypt new stuff - result = encrypt(new_decrypted, new_secret, new_resource.encryption_version) - else - result = new_decrypted - end - result - end - end - - def new_encrypt - new_resource.encrypt.nil? ? current_resource.encrypt : new_resource.encrypt - end - - def new_secret - @new_secret ||= begin - if new_resource.secret - new_resource.secret - elsif new_resource.secret_path - Chef::EncryptedDataBagItem.load_secret(new_resource.secret_path) - elsif new_resource.encrypt.nil? - current_resource.secret - else - raise "Data bag item #{new_resource.name} has encryption on but no secret or secret_path is specified" - end - end - end - - def decrypt(json, secret) - Chef::EncryptedDataBagItem.new(json, secret).to_hash - end - - def encrypt(json, secret, version) - old_version = run_context.config[:data_bag_encrypt_version] - run_context.config[:data_bag_encrypt_version] = version - begin - Chef::EncryptedDataBagItem.encrypt_data_bag_item(json, secret) - ensure - run_context.config[:data_bag_encrypt_version] = old_version - end - end - - # Get the desired (new) json pre-encryption, for comparison purposes - def new_decrypted - @new_decrypted ||= begin - if new_resource.complete - result = new_resource.raw_data || {} - else - result = current_decrypted.merge(new_resource.raw_data || {}) - end - result['id'] = new_resource.id - result = apply_modifiers(new_resource.raw_data_modifiers, result) - end - end - - # Get the current json decrypted, for comparison purposes - def current_decrypted - @current_decrypted ||= begin - if current_resource.secret - decrypt(current_resource.raw_data || { 'id' => new_resource.id }, current_resource.secret) - elsif current_resource.encrypt - raise "Could not decrypt current data bag item #{current_resource.name}" - else - current_resource.raw_data || { 'id' => new_resource.id } - end - end - end - - # Figure out the differences between new and current - def calculate_differences - if new_encrypt - if current_resource.encrypt - # Both are encrypted, check if the encryption type is the same - description = '' - if new_secret != current_resource.secret - description << ' with new secret' - end - if new_resource.encryption_version != current_resource.encryption_version - description << " from v#{current_resource.encryption_version} to v#{new_resource.encryption_version} encryption" - end - - if description != '' - # Encryption is different, we're reencrypting - differences = [ "re-encrypt#{description}"] - else - # Encryption is the same, we're just updating - differences = [] - end - else - # New stuff should be encrypted, old is not. Encrypting. - differences = [ "encrypt with v#{new_resource.encryption_version} encryption" ] - end - - # Get differences in the actual json - if current_resource.secret - json_differences(current_decrypted, new_decrypted, false, '', differences) - elsif current_resource.encrypt - # Encryption is different and we can't read the old values. Only allow the change - # if we're overwriting the data bag item - if !new_resource.complete - raise "Cannot encrypt #{new_resource.name} due to failure to decrypt existing resource. Set 'complete true' to overwrite or add the old secret as old_secret / old_secret_path." - end - differences = [ "overwrite data bag item (cannot decrypt old data bag item)"] - differences = (new_resource.raw_data.keys & current_resource.raw_data.keys).map { |key| "overwrite #{key}"} - differences += (new_resource.raw_data.keys - current_resource.raw_data.keys).map { |key| "add #{key}"} - differences += (current_resource.raw_data.keys - new_resource.raw_data.keys).map { |key| "remove #{key}" } - else - json_differences(current_decrypted, new_decrypted, false, '', differences) - end - else - if current_resource.encrypt - # New stuff should not be encrypted, old is. Decrypting. - differences = [ "decrypt data bag item to plaintext" ] - else - differences = [] - end - json_differences(current_decrypted, new_decrypted, true, '', differences) - end - differences - end - - # - # Helpers - # - - def resource_class - Chef::Resource::ChefDataBagItem - end - - def data_handler - Chef::ChefFS::DataHandler::DataBagItemDataHandler.new - end - - def keys - { - 'id' => :id, - 'data_bag' => :data_bag, - 'raw_data' => :raw_data - } - end - - def not_found_resource - resource = super - resource.data_bag new_resource.data_bag - resource - end - - def fake_entry - FakeEntry.new("#{new_resource.id}.json", FakeEntry.new(new_resource.data_bag)) - end - - end - end -end diff --git a/lib/chef/provider/chef_environment.rb b/lib/chef/provider/chef_environment.rb deleted file mode 100644 index 372b8fd..0000000 --- a/lib/chef/provider/chef_environment.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_environment' -require 'chef/chef_fs/data_handler/environment_data_handler' - -class Chef - class Provider - class ChefEnvironment < Cheffish::ChefProviderBase - provides :chef_environment - - def whyrun_supported? - true - end - - action :create do - differences = json_differences(current_json, new_json) - - if current_resource_exists? - if differences.size > 0 - description = [ "update environment #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.put("environments/#{new_resource.name}", normalize_for_put(new_json)) - end - end - else - description = [ "create environment #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.post("environments", normalize_for_post(new_json)) - end - end - end - - action :delete do - if current_resource_exists? - converge_by "delete environment #{new_resource.name} at #{rest.url}" do - rest.delete("environments/#{new_resource.name}") - end - end - end - - def load_current_resource - begin - @current_resource = json_to_resource(rest.get("environments/#{new_resource.name}")) - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_resource = not_found_resource - else - raise - end - end - end - - def augment_new_json(json) - # Apply modifiers - json['default_attributes'] = apply_modifiers(new_resource.default_attribute_modifiers, json['default_attributes']) - json['override_attributes'] = apply_modifiers(new_resource.override_attribute_modifiers, json['override_attributes']) - json - end - - # - # Helpers - # - - def resource_class - Chef::Resource::ChefEnvironment - end - - def data_handler - Chef::ChefFS::DataHandler::EnvironmentDataHandler.new - end - - def keys - { - 'name' => :name, - 'description' => :description, - 'cookbook_versions' => :cookbook_versions, - 'default_attributes' => :default_attributes, - 'override_attributes' => :override_attributes - } - end - - end - end -end diff --git a/lib/chef/provider/chef_group.rb b/lib/chef/provider/chef_group.rb deleted file mode 100644 index 687a7e3..0000000 --- a/lib/chef/provider/chef_group.rb +++ /dev/null @@ -1,83 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_group' -require 'chef/chef_fs/data_handler/group_data_handler' - -class Chef - class Provider - class ChefGroup < Cheffish::ChefProviderBase - provides :chef_group - - def whyrun_supported? - true - end - - action :create do - differences = json_differences(current_json, new_json) - - if current_resource_exists? - if differences.size > 0 - description = [ "update group #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.put("groups/#{new_resource.name}", normalize_for_put(new_json)) - end - end - else - description = [ "create group #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.post("groups", normalize_for_post(new_json)) - end - end - end - - action :delete do - if current_resource_exists? - converge_by "delete group #{new_resource.name} at #{rest.url}" do - rest.delete("groups/#{new_resource.name}") - end - end - end - - def load_current_resource - begin - @current_resource = json_to_resource(rest.get("groups/#{new_resource.name}")) - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_resource = not_found_resource - else - raise - end - end - end - - def augment_new_json(json) - # Apply modifiers - json['users'] |= new_resource.users - json['clients'] |= new_resource.clients - json['groups'] |= new_resource.groups - json['users'] -= new_resource.remove_users - json['clients'] -= new_resource.remove_clients - json['groups'] -= new_resource.remove_groups - json - end - - # - # Helpers - # - - def resource_class - Chef::Resource::ChefGroup - end - - def data_handler - Chef::ChefFS::DataHandler::GroupDataHandler.new - end - - def keys - { - 'name' => :name, - 'groupname' => :name - } - end - end - end -end diff --git a/lib/chef/provider/chef_mirror.rb b/lib/chef/provider/chef_mirror.rb deleted file mode 100644 index af4e4e6..0000000 --- a/lib/chef/provider/chef_mirror.rb +++ /dev/null @@ -1,169 +0,0 @@ -require 'chef/provider/lwrp_base' -require 'chef/chef_fs/file_pattern' -require 'chef/chef_fs/file_system' -require 'chef/chef_fs/parallelizer' -require 'chef/chef_fs/file_system/chef_server_root_dir' -require 'chef/chef_fs/file_system/chef_repository_file_system_root_dir' - -class Chef - class Provider - class ChefMirror < Chef::Provider::LWRPBase - provides :chef_mirror - - def whyrun_supported? - true - end - - action :upload do - with_modified_config do - copy_to(local_fs, remote_fs) - end - end - - action :download do - with_modified_config do - copy_to(remote_fs, local_fs) - end - end - - def with_modified_config - # pre-Chef-12 ChefFS reads versioned_cookbooks out of Chef::Config instead of - # taking it as an input, so we need to modify it for the duration of copy_to - @old_versioned_cookbooks = Chef::Config.versioned_cookbooks - # If versioned_cookbooks is explicitly set, set it. - if !new_resource.versioned_cookbooks.nil? - Chef::Config.versioned_cookbooks = new_resource.versioned_cookbooks - - # If new_resource.chef_repo_path is set, versioned_cookbooks defaults to true. - # Otherwise, it stays at its current Chef::Config value. - elsif new_resource.chef_repo_path - Chef::Config.versioned_cookbooks = true - end - - begin - yield - ensure - Chef::Config.versioned_cookbooks = @old_versioned_cookbooks - end - end - - def copy_to(src_root, dest_root) - if new_resource.concurrency && new_resource.concurrency <= 0 - raise "chef_mirror.concurrency must be above 0! Was set to #{new_resource.concurrency}" - end - # Honor concurrency - Chef::ChefFS::Parallelizer.threads = (new_resource.concurrency || 10) - 1 - - # We don't let the user pass absolute paths; we want to reserve those for - # multi-org support (/organizations/foo). - if new_resource.path[0] == '/' - raise "Absolute paths in chef_mirror not yet supported." - end - # Copy! - path = Chef::ChefFS::FilePattern.new("/#{new_resource.path}") - ui = CopyListener.new(self) - error = Chef::ChefFS::FileSystem.copy_to(path, src_root, dest_root, nil, options, ui, proc { |p| p.path }) - - if error - raise "Errors while copying:#{ui.errors.map { |e| "#{e}\n" }.join('')}" - end - end - - def local_fs - # If chef_repo_path is set to a string, put it in the form it usually is in - # chef config (:chef_repo_path, :node_path, etc.) - path_config = new_resource.chef_repo_path - if path_config.is_a?(Hash) - chef_repo_path = path_config.delete(:chef_repo_path) - elsif path_config - chef_repo_path = path_config - path_config = {} - else - chef_repo_path = Chef::Config.chef_repo_path - path_config = Chef::Config - end - chef_repo_path = Array(chef_repo_path).flatten - - # Go through the expected object paths and figure out the local paths for each. - case repo_mode - when 'hosted_everything' - object_names = %w(acls clients cookbooks containers data_bags environments groups nodes roles) - else - object_names = %w(clients cookbooks data_bags environments nodes roles users) - end - - object_paths = {} - object_names.each do |object_name| - variable_name = "#{object_name[0..-2]}_path" # cookbooks -> cookbook_path - if path_config[variable_name.to_sym] - paths = Array(path_config[variable_name.to_sym]).flatten - else - paths = chef_repo_path.map { |path| ::File.join(path, object_name) } - end - object_paths[object_name] = paths.map { |path| ::File.expand_path(path) } - end - - # Set up the root dir - Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(object_paths) - end - - def remote_fs - config = { - :chef_server_url => new_resource.chef_server[:chef_server_url], - :node_name => new_resource.chef_server[:options][:client_name], - :client_key => new_resource.chef_server[:options][:signing_key_filename], - :repo_mode => repo_mode, - :versioned_cookbooks => Chef::Config.versioned_cookbooks - } - Chef::ChefFS::FileSystem::ChefServerRootDir.new("remote", config) - end - - def repo_mode - new_resource.chef_server[:chef_server_url] =~ /\/organizations\// ? 'hosted_everything' : 'everything' - end - - def options - result = { - :purge => new_resource.purge, - :freeze => new_resource.freeze, - :diff => new_resource.no_diff, - :dry_run => whyrun_mode? - } - result[:diff] = !result[:diff] - result[:repo_mode] = repo_mode - result[:concurrency] = new_resource.concurrency if new_resource.concurrency - result - end - - def load_current_resource - end - - class CopyListener - def initialize(mirror) - @mirror = mirror - @errors = [] - end - - attr_reader :mirror - attr_reader :errors - - # TODO output is not *always* indicative of a change. We may want to give - # ChefFS the ability to tell us that info. For now though, assuming any output - # means change is pretty damn close to the truth. - def output(str) - mirror.converge_by str do - end - end - def warn(str) - mirror.converge_by "WARNING: #{str}" do - end - end - def error(str) - mirror.converge_by "ERROR: #{str}" do - end - @errors << str - end - end - end - end -end diff --git a/lib/chef/provider/chef_node.rb b/lib/chef/provider/chef_node.rb deleted file mode 100644 index dfab017..0000000 --- a/lib/chef/provider/chef_node.rb +++ /dev/null @@ -1,87 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_node' -require 'chef/chef_fs/data_handler/node_data_handler' - -class Chef - class Provider - class ChefNode < Cheffish::ChefProviderBase - provides :chef_node - - def whyrun_supported? - true - end - - action :create do - differences = json_differences(current_json, new_json) - - if current_resource_exists? - if differences.size > 0 - description = [ "update node #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.put("nodes/#{new_resource.name}", normalize_for_put(new_json)) - end - end - else - description = [ "create node #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.post("nodes", normalize_for_post(new_json)) - end - end - end - - action :delete do - if current_resource_exists? - converge_by "delete node #{new_resource.name} at #{rest.url}" do - rest.delete("nodes/#{new_resource.name}") - end - end - end - - def load_current_resource - begin - @current_resource = json_to_resource(rest.get("nodes/#{new_resource.name}")) - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_resource = not_found_resource - else - raise - end - end - end - - def augment_new_json(json) - # Preserve tags even if "attributes" was overwritten directly - json['normal']['tags'] = current_json['normal']['tags'] unless json['normal']['tags'] - # Apply modifiers - json['run_list'] = apply_run_list_modifiers(new_resource.run_list_modifiers, new_resource.run_list_removers, json['run_list']) - json['normal'] = apply_modifiers(new_resource.attribute_modifiers, json['normal']) - # Preserve default/override/automatic even when "complete true" - json['default'] = current_json['default'] - json['override'] = current_json['override'] - json['automatic'] = current_json['automatic'] - json - end - - # - # Helpers - # - - def resource_class - Chef::Resource::ChefNode - end - - def data_handler - Chef::ChefFS::DataHandler::NodeDataHandler.new - end - - def keys - { - 'name' => :name, - 'chef_environment' => :chef_environment, - 'run_list' => :run_list, - 'normal' => :attributes - } - end - end - end -end diff --git a/lib/chef/provider/chef_organization.rb b/lib/chef/provider/chef_organization.rb deleted file mode 100644 index 1d59123..0000000 --- a/lib/chef/provider/chef_organization.rb +++ /dev/null @@ -1,155 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_organization' -require 'chef/chef_fs/data_handler/data_handler_base' - -class Chef - class Provider - class ChefOrganization < Cheffish::ChefProviderBase - provides :chef_organization - - def whyrun_supported? - true - end - - action :create do - differences = json_differences(current_json, new_json) - - if current_resource_exists? - if differences.size > 0 - description = [ "update organization #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.put("#{rest.root_url}/organizations/#{new_resource.name}", normalize_for_put(new_json)) - end - end - else - description = [ "create organization #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.post("#{rest.root_url}/organizations", normalize_for_post(new_json)) - end - end - - # Revoke invites and memberships when asked - invites_to_remove.each do |user| - if outstanding_invites.has_key?(user) - converge_by "revoke #{user}'s invitation to organization #{new_resource.name}" do - rest.delete("#{rest.root_url}/organizations/#{new_resource.name}/association_requests/#{outstanding_invites[user]}") - end - end - end - members_to_remove.each do |user| - if existing_members.include?(user) - converge_by "remove #{user} from organization #{new_resource.name}" do - rest.delete("#{rest.root_url}/organizations/#{new_resource.name}/users/#{user}") - end - end - end - - # Invite and add members when asked - new_resource.invites.each do |user| - if !existing_members.include?(user) && !outstanding_invites.has_key?(user) - converge_by "invite #{user} to organization #{new_resource.name}" do - rest.post("#{rest.root_url}/organizations/#{new_resource.name}/association_requests", { 'user' => user }) - end - end - end - new_resource.members.each do |user| - if !existing_members.include?(user) - converge_by "Add #{user} to organization #{new_resource.name}" do - rest.post("#{rest.root_url}/organizations/#{new_resource.name}/users/", { 'username' => user }) - end - end - end - end - - def existing_members - @existing_members ||= rest.get("#{rest.root_url}/organizations/#{new_resource.name}/users").map { |u| u['user']['username'] } - end - - def outstanding_invites - @outstanding_invites ||= begin - invites = {} - rest.get("#{rest.root_url}/organizations/#{new_resource.name}/association_requests").each do |r| - invites[r['username']] = r['id'] - end - invites - end - end - - def invites_to_remove - if new_resource.complete - if new_resource.invites_specified? || new_resource.members_specified? - outstanding_invites.keys - (new_resource.invites | new_resource.members) - else - [] - end - else - new_resource.remove_members - end - end - - def members_to_remove - if new_resource.complete - if new_resource.members_specified? - existing_members - (new_resource.invites | new_resource.members) - else - [] - end - else - new_resource.remove_members - end - end - - action :delete do - if current_resource_exists? - converge_by "delete organization #{new_resource.name} at #{rest.url}" do - rest.delete("#{rest.root_url}/organizations/#{new_resource.name}") - end - end - end - - def load_current_resource - begin - @current_resource = json_to_resource(rest.get("#{rest.root_url}/organizations/#{new_resource.name}")) - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_resource = not_found_resource - else - raise - end - end - end - - # - # Helpers - # - - def resource_class - Chef::Resource::ChefOrganization - end - - def data_handler - OrganizationDataHandler.new - end - - def keys - { - 'name' => :name, - 'full_name' => :full_name - } - end - - class OrganizationDataHandler < Chef::ChefFS::DataHandler::DataHandlerBase - def normalize(organization, entry) - # Normalize the order of the keys for easier reading - normalize_hash(organization, { - 'name' => remove_dot_json(entry.name), - 'full_name' => remove_dot_json(entry.name), - 'org_type' => 'Business', - 'clientname' => "#{remove_dot_json(entry.name)}-validator", - 'billing_plan' => 'platform-free' - }) - end - end - end - end -end diff --git a/lib/chef/provider/chef_resolved_cookbooks.rb b/lib/chef/provider/chef_resolved_cookbooks.rb deleted file mode 100644 index 63cb39b..0000000 --- a/lib/chef/provider/chef_resolved_cookbooks.rb +++ /dev/null @@ -1,46 +0,0 @@ -require 'chef/provider/lwrp_base' -require 'chef_zero' - -class Chef - class Provider - class ChefResolvedCookbooks < Chef::Provider::LWRPBase - provides :chef_resolved_cookbooks - - action :resolve do - new_resource.cookbooks_from.each do |path| - ::Dir.entries(path).each do |name| - if ::File.directory?(::File.join(path, name)) && name != '.' && name != '..' - new_resource.berksfile.cookbook name, :path => ::File.join(path, name) - end - end - end - - new_resource.berksfile.install - - # Ridley really really wants a key :/ - if new_resource.chef_server[:options][:signing_key_filename] - new_resource.berksfile.upload( - :server_url => new_resource.chef_server[:chef_server_url], - :client_name => new_resource.chef_server[:options][:client_name], - :client_key => new_resource.chef_server[:options][:signing_key_filename]) - else - file = Tempfile.new('privatekey') - begin - file.write(ChefZero::PRIVATE_KEY) - file.close - - new_resource.berksfile.upload( - :server_url => new_resource.chef_server[:chef_server_url], - :client_name => new_resource.chef_server[:options][:client_name] || 'me', - :client_key => file.path) - - ensure - file.close - file.unlink - end - end - end - - end - end -end diff --git a/lib/chef/provider/chef_role.rb b/lib/chef/provider/chef_role.rb deleted file mode 100644 index da7b593..0000000 --- a/lib/chef/provider/chef_role.rb +++ /dev/null @@ -1,84 +0,0 @@ -require 'cheffish/chef_provider_base' -require 'chef/resource/chef_role' -require 'chef/chef_fs/data_handler/role_data_handler' - -class Chef - class Provider - class ChefRole < Cheffish::ChefProviderBase - provides :chef_role - - def whyrun_supported? - true - end - - action :create do - differences = json_differences(current_json, new_json) - - if current_resource_exists? - if differences.size > 0 - description = [ "update role #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.put("roles/#{new_resource.name}", normalize_for_put(new_json)) - end - end - else - description = [ "create role #{new_resource.name} at #{rest.url}" ] + differences - converge_by description do - rest.post("roles", normalize_for_post(new_json)) - end - end - end - - action :delete do - if current_resource_exists? - converge_by "delete role #{new_resource.name} at #{rest.url}" do - rest.delete("roles/#{new_resource.name}") - end - end - end - - def load_current_resource - begin - @current_resource = json_to_resource(rest.get("roles/#{new_resource.name}")) - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_resource = not_found_resource - else - raise - end - end - end - - def augment_new_json(json) - # Apply modifiers - json['run_list'] = apply_run_list_modifiers(new_resource.run_list_modifiers, new_resource.run_list_removers, json['run_list']) - json['default_attributes'] = apply_modifiers(new_resource.default_attribute_modifiers, json['default_attributes']) - json['override_attributes'] = apply_modifiers(new_resource.override_attribute_modifiers, json['override_attributes']) - json - end - - # - # Helpers - # - - def resource_class - Chef::Resource::ChefRole - end - - def data_handler - Chef::ChefFS::DataHandler::RoleDataHandler.new - end - - def keys - { - 'name' => :name, - 'description' => :description, - 'run_list' => :run_list, - 'env_run_lists' => :env_run_lists, - 'default_attributes' => :default_attributes, - 'override_attributes' => :override_attributes - } - end - end - end -end diff --git a/lib/chef/provider/chef_user.rb b/lib/chef/provider/chef_user.rb deleted file mode 100644 index 287a8db..0000000 --- a/lib/chef/provider/chef_user.rb +++ /dev/null @@ -1,59 +0,0 @@ -require 'cheffish/actor_provider_base' -require 'chef/resource/chef_user' -require 'chef/chef_fs/data_handler/user_data_handler' - -class Chef - class Provider - class ChefUser < Cheffish::ActorProviderBase - provides :chef_user - - def whyrun_supported? - true - end - - action :create do - create_actor - end - - action :delete do - delete_actor - end - - # - # Helpers - # - # Gives us new_json, current_json, not_found_json, etc. - - def actor_type - 'user' - end - - def actor_path - "#{rest.root_url}/users" - end - - def resource_class - Chef::Resource::ChefUser - end - - def data_handler - Chef::ChefFS::DataHandler::UserDataHandler.new - end - - def keys - { - 'name' => :name, - 'username' => :name, - 'display_name' => :display_name, - 'admin' => :admin, - 'email' => :email, - 'password' => :password, - 'external_authentication_uid' => :external_authentication_uid, - 'recovery_authentication_enabled' => :recovery_authentication_enabled, - 'public_key' => :source_key - } - end - - end - end -end diff --git a/lib/chef/provider/private_key.rb b/lib/chef/provider/private_key.rb deleted file mode 100644 index 9b0be90..0000000 --- a/lib/chef/provider/private_key.rb +++ /dev/null @@ -1,225 +0,0 @@ -require 'chef/provider/lwrp_base' -require 'openssl' -require 'cheffish/key_formatter' - -class Chef - class Provider - class PrivateKey < Chef::Provider::LWRPBase - provides :private_key - - action :create do - create_key(false, :create) - end - - action :regenerate do - create_key(true, :regenerate) - end - - action :delete do - if current_resource.path - converge_by "delete private key #{new_path}" do - ::File.unlink(new_path) - end - end - end - - use_inline_resources - - def whyrun_supported? - true - end - - def create_key(regenerate, action) - if @should_create_directory - Cheffish.inline_resource(self, action) do - directory run_context.config[:private_key_write_path] - end - end - - final_private_key = nil - if new_source_key - # - # Create private key from source - # - desired_output = encode_private_key(new_source_key) - if current_resource.path == :none || desired_output != IO.read(new_path) - converge_by "reformat key at #{new_resource.source_key_path} to #{new_resource.format} private key #{new_path} (#{new_resource.pass_phrase ? ", #{new_resource.cipher} password" : ""})" do - IO.write(new_path, desired_output) - end - end - - final_private_key = new_source_key - - else - # - # Generate a new key - # - if current_resource.action == [ :delete ] || regenerate || - (new_resource.regenerate_if_different && - (!current_private_key || - current_resource.size != new_resource.size || - current_resource.type != new_resource.type)) - - case new_resource.type - when :rsa - if new_resource.exponent - final_private_key = OpenSSL::PKey::RSA.generate(new_resource.size, new_resource.exponent) - else - final_private_key = OpenSSL::PKey::RSA.generate(new_resource.size) - end - when :dsa - final_private_key = OpenSSL::PKey::DSA.generate(new_resource.size) - end - - generated_key = true - elsif !current_private_key - raise "Could not read private key from #{current_resource.path}: missing pass phrase?" - else - final_private_key = current_private_key - generated_key = false - end - - if generated_key - generated_description = " (#{new_resource.size} bits#{new_resource.pass_phrase ? ", #{new_resource.cipher} password" : ""})" - - if new_path != :none - action = current_resource.path == :none ? 'create' : 'overwrite' - converge_by "#{action} #{new_resource.type} private key #{new_path}#{generated_description}" do - write_private_key(final_private_key) - end - else - converge_by "generate private key#{generated_description}" do - end - end - else - # Warn if existing key has different characteristics than expected - if current_resource.size != new_resource.size - Chef::Log.warn("Mismatched key size! #{current_resource.path} is #{current_resource.size} bytes, desired is #{new_resource.size} bytes. Use action :regenerate to force key regeneration.") - elsif current_resource.type != new_resource.type - Chef::Log.warn("Mismatched key type! #{current_resource.path} is #{current_resource.type}, desired is #{new_resource.type} bytes. Use action :regenerate to force key regeneration.") - end - - if current_resource.format != new_resource.format - converge_by "change format of #{new_resource.type} private key #{new_path} from #{current_resource.format} to #{new_resource.format}" do - write_private_key(current_private_key) - end - elsif (@current_file_mode & 0077) != 0 - new_mode = @current_file_mode & 07700 - converge_by "change mode of private key #{new_path} to #{new_mode.to_s(8)}" do - ::File.chmod(new_mode, new_path) - end - end - end - end - - if new_resource.public_key_path - public_key_path = new_resource.public_key_path - public_key_format = new_resource.public_key_format - Cheffish.inline_resource(self, action) do - public_key public_key_path do - source_key final_private_key - format public_key_format - end - end - end - - if new_resource.after - new_resource.after.call(new_resource, final_private_key) - end - end - - def encode_private_key(key) - key_format = {} - key_format[:format] = new_resource.format if new_resource.format - key_format[:pass_phrase] = new_resource.pass_phrase if new_resource.pass_phrase - key_format[:cipher] = new_resource.cipher if new_resource.cipher - Cheffish::KeyFormatter.encode(key, key_format) - end - - def write_private_key(key) - ::File.open(new_path, 'w') do |file| - file.chmod(0600) - file.write(encode_private_key(key)) - end - end - - def new_source_key - @new_source_key ||= begin - if new_resource.source_key.is_a?(String) - source_key, source_key_format = Cheffish::KeyFormatter.decode(new_resource.source_key, new_resource.source_key_pass_phrase) - source_key - elsif new_resource.source_key - new_resource.source_key - elsif new_resource.source_key_path - source_key, source_key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.source_key_path), new_resource.source_key_pass_phrase, new_resource.source_key_path) - source_key - else - nil - end - end - end - - attr_reader :current_private_key - - def new_path - new_key_with_path[1] - end - - def new_key_with_path - path = new_resource.path - if path.is_a?(Symbol) - return [ nil, path ] - elsif Pathname.new(path).relative? - private_key, private_key_path = Cheffish.get_private_key_with_path(path, run_context.config) - if private_key - return [ private_key, (private_key_path || :none) ] - elsif run_context.config[:private_key_write_path] - @should_create_directory = true - path = ::File.join(run_context.config[:private_key_write_path], path) - return [ nil, path ] - else - raise "Could not find key #{path} and Chef::Config.private_key_write_path is not set." - end - elsif ::File.exist?(path) - return [ IO.read(path), path ] - else - return [ nil, path ] - end - end - - def load_current_resource - resource = Chef::Resource::PrivateKey.new(new_resource.name, run_context) - - new_key, new_path = new_key_with_path - if new_path != :none && ::File.exist?(new_path) - resource.path new_path - @current_file_mode = ::File.stat(new_path).mode - else - resource.path :none - end - - if new_key - begin - key, key_format = Cheffish::KeyFormatter.decode(new_key, new_resource.pass_phrase, new_path) - if key - @current_private_key = key - resource.format key_format[:format] - resource.type key_format[:type] - resource.size key_format[:size] - resource.exponent key_format[:exponent] - resource.pass_phrase key_format[:pass_phrase] - resource.cipher key_format[:cipher] - end - rescue - # If there's an error reading, we assume format and type are wrong and don't futz with them - Chef::Log.warn("Error reading #{new_path}: #{$!}") - end - else - resource.action :delete - end - - @current_resource = resource - end - end - end -end diff --git a/lib/chef/provider/public_key.rb b/lib/chef/provider/public_key.rb deleted file mode 100644 index ce916c7..0000000 --- a/lib/chef/provider/public_key.rb +++ /dev/null @@ -1,88 +0,0 @@ -require 'chef/provider/lwrp_base' -require 'openssl' -require 'cheffish/key_formatter' - -class Chef - class Provider - class PublicKey < Chef::Provider::LWRPBase - provides :public_key - - action :create do - if !new_source_key - raise "No source key specified" - end - desired_output = encode_public_key(new_source_key) - if Array(current_resource.action) == [ :delete ] || desired_output != IO.read(new_resource.path) - converge_by "write #{new_resource.format} public key #{new_resource.path} from #{new_source_key_publicity} key #{new_resource.source_key_path}" do - IO.write(new_resource.path, desired_output) - # TODO permissions on file? - end - end - end - - action :delete do - if Array(current_resource.action) == [ :create ] - converge_by "delete public key #{new_resource.path}" do - ::File.unlink(new_resource.path) - end - end - end - - def whyrun_supported? - true - end - - def encode_public_key(key) - key_format = {} - key_format[:format] = new_resource.format if new_resource.format - Cheffish::KeyFormatter.encode(key, key_format) - end - - attr_reader :current_public_key - attr_reader :new_source_key_publicity - - def new_source_key - @new_source_key ||= begin - if new_resource.source_key.is_a?(String) - source_key, source_key_format = Cheffish::KeyFormatter.decode(new_resource.source_key, new_resource.source_key_pass_phrase) - elsif new_resource.source_key - source_key = new_resource.source_key - elsif new_resource.source_key_path - source_key, source_key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.source_key_path), new_resource.source_key_pass_phrase, new_resource.source_key_path) - else - return nil - end - - if source_key.private? - @new_source_key_publicity = 'private' - source_key.public_key - else - @new_source_key_publicity = 'public' - source_key - end - end - end - - def load_current_resource - if ::File.exist?(new_resource.path) - resource = Chef::Resource::PublicKey.new(new_resource.path, run_context) - begin - key, key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.path), nil, new_resource.path) - if key - @current_public_key = key - resource.format key_format[:format] - end - rescue - # If there is an error reading we assume format and such is broken - end - - @current_resource = resource - else - not_found_resource = Chef::Resource::PublicKey.new(new_resource.path, run_context) - not_found_resource.action :delete - @current_resource = not_found_resource - end - end - end - end -end diff --git a/lib/chef/resource/chef_acl.rb b/lib/chef/resource/chef_acl.rb index 102bd38..20b30a0 100644 --- a/lib/chef/resource/chef_acl.rb +++ b/lib/chef/resource/chef_acl.rb @@ -1,14 +1,14 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' +require 'chef/chef_fs/data_handler/acl_data_handler' +require 'chef/chef_fs/parallelizer' +require 'uri' class Chef class Resource - class ChefAcl < ChefCompat::Resource + class ChefAcl < Cheffish::BaseResource resource_name :chef_acl - allowed_actions :create, :nothing - default_action :create - def initialize(*args) super chef_server run_context.cheffish.current_chef_server @@ -64,6 +64,438 @@ def remove_rights(*values) @remove_rights << args end end + + action :create do + if new_resource.remove_rights && new_resource.complete + Chef::Log.warn("'remove_rights' is redundant when 'complete' is specified: all rights not specified in a 'rights' declaration will be removed.") + end + # Verify that we're not destroying all hope of ACL recovery here + if new_resource.complete && (!new_resource.rights || !new_resource.rights.any? { |r| r[:permissions].include?(:all) || r[:permissions].include?(:grant) }) + # NOTE: if superusers exist, this should turn into a warning. + raise "'complete' specified on chef_acl resource, but no GRANT permissions were granted. I'm sorry Dave, I can't let you remove all access to an object with no hope of recovery." + end + + # Find all matching paths so we can update them (resolve * and **) + paths = match_paths(new_resource.path) + if paths.size == 0 && !new_resource.path.split('/').any? { |p| p == '*' } + raise "Path #{new_resource.path} cannot have an ACL set on it!" + end + + # Go through the matches and update the ACLs for them + paths.each do |path| + create_acl(path) + end + end + + action_class.class_eval do + # Update the ACL if necessary. + def create_acl(path) + changed = false + # There may not be an ACL path for some valid paths (/ and /organizations, + # for example). We want to recurse into these, but we don't want to try to + # update nonexistent ACLs for them. + acl = acl_path(path) + if acl + # It's possible to make a custom container + current_json = current_acl(acl) + if current_json + + # Compare the desired and current json for the ACL, and update if different. + modify = {} + desired_acl(acl).each do |permission, desired_json| + differences = json_differences(sort_values(current_json[permission]), sort_values(desired_json)) + + if differences.size > 0 + # Verify we aren't trying to destroy grant permissions + if permission == 'grant' && desired_json['actors'] == [] && desired_json['groups'] == [] + # NOTE: if superusers exist, this should turn into a warning. + raise "chef_acl attempted to remove all actors from GRANT! I'm sorry Dave, I can't let you remove access to an object with no hope of recovery." + end + modify[differences] ||= {} + modify[differences][permission] = desired_json + end + end + + if modify.size > 0 + changed = true + description = [ "update acl #{path} at #{rest_url(path)}" ] + modify.map do |diffs, permissions| + diffs.map { |diff| " #{permissions.keys.join(', ')}:#{diff}" } + end.flatten(1) + converge_by description do + modify.values.each do |permissions| + permissions.each do |permission, desired_json| + rest.put(rest_url("#{acl}/#{permission}"), { permission => desired_json }) + end + end + end + end + end + end + + # If we have been asked to recurse, do so. + # If recurse is on_change, then we will recurse if there is no ACL, or if + # the ACL has changed. + if new_resource.recursive == true || (new_resource.recursive == :on_change && (!acl || changed)) + children, error = list(path, '*') + Chef::ChefFS::Parallelizer.parallel_do(children) do |child| + next if child.split('/')[-1] == 'containers' + create_acl(child) + end + # containers mess up our descent, so we do them last + Chef::ChefFS::Parallelizer.parallel_do(children) do |child| + next if child.split('/')[-1] != 'containers' + create_acl(child) + end + + end + end + + # Get the current ACL for the given path + def current_acl(acl_path) + @current_acls ||= {} + if !@current_acls.has_key?(acl_path) + @current_acls[acl_path] = begin + rest.get(rest_url(acl_path)) + rescue Net::HTTPServerException => e + unless e.response.code == '404' && new_resource.path.split('/').any? { |p| p == '*' } + raise + end + end + end + @current_acls[acl_path] + end + + # Get the desired acl for the given acl path + def desired_acl(acl_path) + result = new_resource.raw_json ? new_resource.raw_json.dup : {} + + # Calculate the JSON based on rights + add_rights(acl_path, result) + + if new_resource.complete + result = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(result, nil) + else + # If resource is incomplete, use current json to fill any holes + current_acl(acl_path).each do |permission, perm_hash| + if !result[permission] + result[permission] = perm_hash.dup + else + result[permission] = result[permission].dup + perm_hash.each do |type, actors| + if !result[permission][type] + result[permission][type] = actors + else + result[permission][type] = result[permission][type].dup + result[permission][type] |= actors + end + end + end + end + + remove_rights(result) + end + result + end + + def sort_values(json) + json.each do |key, value| + json[key] = value.sort if value.is_a?(Array) + end + json + end + + def add_rights(acl_path, json) + if new_resource.rights + new_resource.rights.each do |rights| + if rights[:permissions].delete(:all) + rights[:permissions] |= current_acl(acl_path).keys + end + + Array(rights[:permissions]).each do |permission| + ace = json[permission.to_s] ||= {} + # WTF, no distinction between users and clients? The Chef API doesn't + # let us distinguish, so we have no choice :/ This means that: + # 1. If you specify :users => 'foo', and client 'foo' exists, it will + # pick that (whether user 'foo' exists or not) + # 2. If you specify :clients => 'foo', and user 'foo' exists but + # client 'foo' does not, it will pick user 'foo' and put it in the + # ACL + # 3. If an existing item has user 'foo' on it and you specify :clients + # => 'foo' instead, idempotence will not notice that anything needs + # to be updated and nothing will happen. + if rights[:users] + ace['actors'] ||= [] + ace['actors'] |= Array(rights[:users]) + end + if rights[:clients] + ace['actors'] ||= [] + ace['actors'] |= Array(rights[:clients]) + end + if rights[:groups] + ace['groups'] ||= [] + ace['groups'] |= Array(rights[:groups]) + end + end + end + end + end + + def remove_rights(json) + if new_resource.remove_rights + new_resource.remove_rights.each do |rights| + rights[:permissions].each do |permission| + if permission == :all + json.each_key do |key| + ace = json[key] = json[key.dup] + ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors'] + ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors'] + ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups'] + end + else + ace = json[permission.to_s] = json[permission.to_s].dup + if ace + ace['actors'] = ace['actors'] - Array(rights[:users]) if rights[:users] && ace['actors'] + ace['actors'] = ace['actors'] - Array(rights[:clients]) if rights[:clients] && ace['actors'] + ace['groups'] = ace['groups'] - Array(rights[:groups]) if rights[:groups] && ace['groups'] + end + end + end + end + end + end + + def load_current_resource + end + + # + # Matches chef_acl paths like nodes, nodes/*. + # + # == Examples + # match_paths('nodes'): [ 'nodes' ] + # match_paths('nodes/*'): [ 'nodes/x', 'nodes/y', 'nodes/z' ] + # match_paths('*'): [ 'clients', 'environments', 'nodes', 'roles', ... ] + # match_paths('/'): [ '/' ] + # match_paths(''): [ '' ] + # match_paths('/*'): [ '/organizations', '/users' ] + # match_paths('/organizations/*/*'): [ '/organizations/foo/clients', '/organizations/foo/environments', ..., '/organizations/bar/clients', '/organizations/bar/environments', ... ] + # + def match_paths(path) + # Turn multiple slashes into one + # nodes//x -> nodes/x + path = path.gsub(/[\/]+/, '/') + # If it's absolute, start the matching with /. If it's relative, start with '' (relative root). + if path[0] == '/' + matches = [ '/' ] + else + matches = [ '' ] + end + + # Split the path, and get rid of the empty path at the beginning and end + # (/a/b/c/ -> [ 'a', 'b', 'c' ]) + parts = path.split('/').select { |x| x != '' }.to_a + + # Descend until we find the matches: + # path = 'a/b/c' + # parts = [ 'a', 'b', 'c' ] + # Starting matches = [ '' ] + parts.each_with_index do |part, index| + # For each match, list / and set matches to that. + # + # Example: /*/foo + # 1. To start, + # matches = [ '/' ], part = '*'. + # list('/', '*') = [ '/organizations, '/users' ] + # 2. matches = [ '/organizations', '/users' ], part = 'foo' + # list('/organizations', 'foo') = [ '/organizations/foo' ] + # list('/users', 'foo') = [ '/users/foo' ] + # + # Result: /*/foo = [ '/organizations/foo', '/users/foo' ] + # + matches = Chef::ChefFS::Parallelizer.parallelize(matches) do |path| + found, error = list(path, part) + if error + if parts[0..index-1].all? { |p| p != '*' } + raise error + end + [] + else + found + end + end.flatten(1).to_a + end + + matches + end + + # + # Takes a normal path and finds the Chef path to get / set its ACL. + # + # nodes/x -> nodes/x/_acl + # nodes -> containers/nodes/_acl + # '' -> organizations/_acl (the org acl) + # /organizations/foo -> /organizations/foo/organizations/_acl + # /users/foo -> /users/foo/_acl + # /organizations/foo/nodes/x -> /organizations/foo/nodes/x/_acl + # + def acl_path(path) + parts = path.split('/').select { |x| x != '' }.to_a + prefix = (path[0] == '/') ? '/' : '' + + case parts.size + when 0 + # /, empty (relative root) + # The root of the server has no publicly visible ACLs. Only nodes/*, etc. + if prefix == '' + ::File.join('organizations', '_acl') + end + + when 1 + # nodes, roles, etc. + # The top level organizations and users containers have no publicly + # visible ACLs. Only nodes/*, etc. + if prefix == '' + ::File.join('containers', path, '_acl') + end + + when 2 + # /organizations/NAME, /users/NAME, nodes/NAME, roles/NAME, etc. + if prefix == '/' && parts[0] == 'organizations' + ::File.join(path, 'organizations', '_acl') + else + ::File.join(path, '_acl') + end + + when 3 + # /organizations/NAME/nodes, cookbooks/NAME/VERSION, etc. + if prefix == '/' + ::File.join('/', parts[0], parts[1], 'containers', parts[2], '_acl') + else + ::File.join(parts[0], parts[1], '_acl') + end + + when 4 + # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION/BLAH + # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION, etc. + if prefix == '/' + ::File.join(path, '_acl') + else + ::File.join(parts[0], parts[1], '_acl') + end + + else + # /organizations/NAME/cookbooks/NAME/VERSION/..., cookbooks/NAME/VERSION/A/B/... + if prefix == '/' + ::File.join('/', parts[0], parts[1], parts[2], parts[3], '_acl') + else + ::File.join(parts[0], parts[1], '_acl') + end + end + end + + # + # Lists the securable children under a path (the ones that either have ACLs + # or have children with ACLs). + # + # list('nodes', 'x') -> [ 'nodes/x' ] + # list('nodes', '*') -> [ 'nodes/x', 'nodes/y', 'nodes/z' ] + # list('', '*') -> [ 'clients', 'environments', 'nodes', 'roles', ... ] + # list('/', '*') -> [ '/organizations'] + # list('cookbooks', 'x') -> [ 'cookbooks/x' ] + # list('cookbooks/x', '*') -> [ ] # Individual cookbook versions do not have their own ACLs + # list('/organizations/foo/nodes', '*') -> [ '/organizations/foo/nodes/x', '/organizations/foo/nodes/y' ] + # + # The list of children of an organization is == the list of containers. If new + # containers are added, the list of children will grow. This allows the system + # to extend to new types of objects and allow cheffish to work with them. + # + def list(path, child) + # TODO make ChefFS understand top level organizations and stop doing this altogether. + parts = path.split('/').select { |x| x != '' }.to_a + absolute = (path[0] == '/') + if absolute && parts[0] == 'organizations' + return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 3 + else + return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 1 + end + + error = nil + + if child == '*' + case parts.size + when 0 + # /*, * + if absolute + results = [ "/organizations", "/users" ] + else + results, error = rest_list("containers") + end + + when 1 + # /organizations/*, /users/*, roles/*, nodes/*, etc. + results, error = rest_list(path) + if !error + results = results.map { |result| ::File.join(path, result) } + end + + when 2 + # /organizations/NAME/* + results, error = rest_list(::File.join(path, 'containers')) + if !error + results = results.map { |result| ::File.join(path, result) } + end + + when 3 + # /organizations/NAME/TYPE/* + results, error = rest_list(path) + if !error + results = results.map { |result| ::File.join(path, result) } + end + end + + else + if child == 'data_bags' && + (parts.size == 0 || (parts.size == 2 && parts[0] == 'organizations')) + child = 'data' + end + + if absolute + # /, /users/, /organizations/, /organizations/foo/, /organizations/foo/nodes/ ... + results = [ ::File.join('/', parts[0..2], child) ] + elsif parts.size == 0 + # (nodes, roles, etc.) + results = [ child ] + else + # nodes/, roles/, etc. + results = [ ::File.join(parts[0], child) ] + end + end + + [ results, error ] + end + + def rest_url(path) + path[0] == '/' ? URI.join(rest.url, path) : path + end + + def rest_list(path) + begin + # All our rest lists are hashes where the keys are the names + [ rest.get(rest_url(path)).keys, nil ] + rescue Net::HTTPServerException => e + if e.response.code == '405' || e.response.code == '404' + parts = path.split('/').select { |p| p != '' }.to_a + + # We KNOW we expect these to exist. Other containers may or may not. + unless (parts.size == 1 || (parts.size == 3 && parts[0] == 'organizations')) && + %w(clients containers cookbooks data environments groups nodes roles).include?(parts[-1]) + return [ [], "Cannot get list of #{path}: HTTP response code #{e.response.code}" ] + end + end + raise + end + end + end + end end end diff --git a/lib/chef/resource/chef_client.rb b/lib/chef/resource/chef_client.rb index 3c4cd8e..ed9b98b 100644 --- a/lib/chef/resource/chef_client.rb +++ b/lib/chef/resource/chef_client.rb @@ -1,14 +1,11 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/chef_actor_base' class Chef class Resource - class ChefClient < ChefCompat::Resource + class ChefClient < Cheffish::ChefActorBase resource_name :chef_client - allowed_actions :create, :delete, :regenerate_keys, :nothing - default_action :create - def initialize(*args) super chef_server run_context.cheffish.current_chef_server @@ -43,6 +40,45 @@ def before(&block) def after(&block) block ? @after = block : @after end + + action :create do + create_actor + end + + action :delete do + delete_actor + end + + action_class.class_eval do + def actor_type + 'client' + end + + def actor_path + 'clients' + end + + # + # Helpers + # + + def resource_class + Chef::Resource::ChefClient + end + + def data_handler + Chef::ChefFS::DataHandler::ClientDataHandler.new + end + + def keys + { + 'name' => :name, + 'admin' => :admin, + 'validator' => :validator, + 'public_key' => :source_key + } + end + end end end end diff --git a/lib/chef/resource/chef_container.rb b/lib/chef/resource/chef_container.rb index 4c88da2..c613bbe 100644 --- a/lib/chef/resource/chef_container.rb +++ b/lib/chef/resource/chef_container.rb @@ -1,14 +1,12 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' +require 'chef/chef_fs/data_handler/container_data_handler' class Chef class Resource - class ChefContainer < ChefCompat::Resource + class ChefContainer < Cheffish::BaseResource resource_name :chef_container - allowed_actions :create, :delete, :nothing - default_action :create - # Grab environment from with_environment def initialize(*args) super @@ -17,6 +15,49 @@ def initialize(*args) property :name, :kind_of => String, :regex => Cheffish::NAME_REGEX, :name_attribute => true property :chef_server, :kind_of => Hash + + + action :create do + if !@current_exists + converge_by "create container #{new_resource.name} at #{rest.url}" do + rest.post("containers", normalize_for_post(new_json)) + end + end + end + + action :delete do + if @current_exists + converge_by "delete container #{new_resource.name} at #{rest.url}" do + rest.delete("containers/#{new_resource.name}") + end + end + end + + action_class.class_eval do + def load_current_resource + begin + @current_exists = rest.get("containers/#{new_resource.name}") + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_exists = false + else + raise + end + end + end + + def new_json + {} + end + + def data_handler + Chef::ChefFS::DataHandler::ContainerDataHandler.new + end + + def keys + { 'containername' => :name, 'containerpath' => :name } + end + end end end end diff --git a/lib/chef/resource/chef_data_bag.rb b/lib/chef/resource/chef_data_bag.rb index e8d4ae8..b8eb50d 100644 --- a/lib/chef/resource/chef_data_bag.rb +++ b/lib/chef/resource/chef_data_bag.rb @@ -1,14 +1,11 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' class Chef class Resource - class ChefDataBag < ChefCompat::Resource + class ChefDataBag < Cheffish::BaseResource resource_name :chef_data_bag - allowed_actions :create, :delete, :nothing - default_action :create - def initialize(*args) super chef_server run_context.cheffish.current_chef_server @@ -17,6 +14,50 @@ def initialize(*args) property :name, :kind_of => String, :regex => Cheffish::NAME_REGEX, :name_attribute => true property :chef_server, :kind_of => Hash + + + action :create do + if !current_resource_exists? + converge_by "create data bag #{new_resource.name} at #{rest.url}" do + rest.post("data", { 'name' => new_resource.name }) + end + end + end + + action :delete do + if current_resource_exists? + converge_by "delete data bag #{new_resource.name} at #{rest.url}" do + rest.delete("data/#{new_resource.name}") + end + end + end + + action_class.class_eval do + def load_current_resource + begin + @current_resource = json_to_resource(rest.get("data/#{new_resource.name}")) + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_resource = not_found_resource + else + raise + end + end + end + + # + # Helpers + # + # Gives us new_json, current_json, not_found_json, etc. + + def resource_class + Chef::Resource::ChefDataBag + end + + def json_to_resource(json) + Chef::Resource::ChefDataBag.new(json['name'], run_context) + end + end end end end diff --git a/lib/chef/resource/chef_data_bag_item.rb b/lib/chef/resource/chef_data_bag_item.rb index 3e53abd..414074b 100644 --- a/lib/chef/resource/chef_data_bag_item.rb +++ b/lib/chef/resource/chef_data_bag_item.rb @@ -1,15 +1,14 @@ require 'cheffish' require 'chef/config' -require 'chef_compat/resource' +require 'cheffish/base_resource' +require 'chef/chef_fs/data_handler/data_bag_item_data_handler' +require 'chef/encrypted_data_bag_item' class Chef class Resource - class ChefDataBagItem < ChefCompat::Resource + class ChefDataBagItem < Cheffish::BaseResource resource_name :chef_data_bag_item - allowed_actions :create, :delete, :nothing - default_action :create - def initialize(*args) super name @name @@ -116,6 +115,269 @@ def value(raw_data_path, value=NOT_PASSED, &block) raise "value requires either a value or a block" end end + + action :create do + differences = calculate_differences + + if current_resource_exists? + if differences.size > 0 + description = [ "update data bag item #{new_resource.id} at #{rest.url}" ] + differences + converge_by description do + rest.put("data/#{new_resource.data_bag}/#{new_resource.id}", normalize_for_put(new_json)) + end + end + else + description = [ "create data bag item #{new_resource.id} at #{rest.url}" ] + differences + converge_by description do + rest.post("data/#{new_resource.data_bag}", normalize_for_post(new_json)) + end + end + end + + action :delete do + if current_resource_exists? + converge_by "delete data bag item #{new_resource.id} at #{rest.url}" do + rest.delete("data/#{new_resource.data_bag}/#{new_resource.id}") + end + end + end + + action_class.class_eval do + def load_current_resource + begin + json = rest.get("data/#{new_resource.data_bag}/#{new_resource.id}") + resource = Chef::Resource::ChefDataBagItem.new(new_resource.name, run_context) + resource.raw_data json + @current_resource = resource + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_resource = not_found_resource + else + raise + end + end + + # Determine if data bag is encrypted and if so, what its version is + first_real_key, first_real_value = (current_resource.raw_data || {}).select { |key, value| key != 'id' && !value.nil? }.first + if first_real_value + if first_real_value.is_a?(Hash) && + first_real_value['version'].is_a?(Integer) && + first_real_value['version'] > 0 && + first_real_value.has_key?('encrypted_data') + + current_resource.encrypt true + current_resource.encryption_version first_real_value['version'] + + decrypt_error = nil + + # Check if the desired secret is the one (which it generally should be) + + if new_resource.secret || new_resource.secret_path + begin + Chef::EncryptedDataBagItem::Decryptor.for(first_real_value, new_secret).for_decrypted_item + current_resource.secret new_secret + rescue Chef::EncryptedDataBagItem::DecryptionFailure + decrypt_error = $! + end + end + + # If the current secret doesn't work, look through the specified old secrets + + if !current_resource.secret + old_secrets = [] + if new_resource.old_secret + old_secrets += Array(new_resource.old_secret) + end + if new_resource.old_secret_path + old_secrets += Array(new_resource.old_secret_path).map do |secret_path| + Chef::EncryptedDataBagItem.load_secret(new_resource.old_secret_file) + end + end + old_secrets.each do |secret| + begin + Chef::EncryptedDataBagItem::Decryptor.for(first_real_value, secret).for_decrypted_item + current_resource.secret secret + rescue Chef::EncryptedDataBagItem::DecryptionFailure + decrypt_error = $! + end + end + + # If we couldn't figure out the secret, emit a warning (this isn't a fatal flaw unless we + # need to reuse one of the values from the data bag) + if !current_resource.secret + if decrypt_error + Chef::Log.warn "Existing data bag is encrypted, but could not decrypt: #{decrypt_error.message}." + else + Chef::Log.warn "Existing data bag is encrypted, but no secret was specified." + end + end + end + end + else + + # There are no encryptable values, so pretend encryption is the same as desired + + current_resource.encrypt new_resource.encrypt + current_resource.encryption_version new_resource.encryption_version + if new_resource.secret || new_resource.secret_path + current_resource.secret new_secret + end + end + end + + def new_json + @new_json ||= begin + if new_encrypt + # Encrypt new stuff + result = encrypt(new_decrypted, new_secret, new_resource.encryption_version) + else + result = new_decrypted + end + result + end + end + + def new_encrypt + new_resource.encrypt.nil? ? current_resource.encrypt : new_resource.encrypt + end + + def new_secret + @new_secret ||= begin + if new_resource.secret + new_resource.secret + elsif new_resource.secret_path + Chef::EncryptedDataBagItem.load_secret(new_resource.secret_path) + elsif new_resource.encrypt.nil? + current_resource.secret + else + raise "Data bag item #{new_resource.name} has encryption on but no secret or secret_path is specified" + end + end + end + + def decrypt(json, secret) + Chef::EncryptedDataBagItem.new(json, secret).to_hash + end + + def encrypt(json, secret, version) + old_version = run_context.config[:data_bag_encrypt_version] + run_context.config[:data_bag_encrypt_version] = version + begin + Chef::EncryptedDataBagItem.encrypt_data_bag_item(json, secret) + ensure + run_context.config[:data_bag_encrypt_version] = old_version + end + end + + # Get the desired (new) json pre-encryption, for comparison purposes + def new_decrypted + @new_decrypted ||= begin + if new_resource.complete + result = new_resource.raw_data || {} + else + result = current_decrypted.merge(new_resource.raw_data || {}) + end + result['id'] = new_resource.id + result = apply_modifiers(new_resource.raw_data_modifiers, result) + end + end + + # Get the current json decrypted, for comparison purposes + def current_decrypted + @current_decrypted ||= begin + if current_resource.secret + decrypt(current_resource.raw_data || { 'id' => new_resource.id }, current_resource.secret) + elsif current_resource.encrypt + raise "Could not decrypt current data bag item #{current_resource.name}" + else + current_resource.raw_data || { 'id' => new_resource.id } + end + end + end + + # Figure out the differences between new and current + def calculate_differences + if new_encrypt + if current_resource.encrypt + # Both are encrypted, check if the encryption type is the same + description = '' + if new_secret != current_resource.secret + description << ' with new secret' + end + if new_resource.encryption_version != current_resource.encryption_version + description << " from v#{current_resource.encryption_version} to v#{new_resource.encryption_version} encryption" + end + + if description != '' + # Encryption is different, we're reencrypting + differences = [ "re-encrypt#{description}"] + else + # Encryption is the same, we're just updating + differences = [] + end + else + # New stuff should be encrypted, old is not. Encrypting. + differences = [ "encrypt with v#{new_resource.encryption_version} encryption" ] + end + + # Get differences in the actual json + if current_resource.secret + json_differences(current_decrypted, new_decrypted, false, '', differences) + elsif current_resource.encrypt + # Encryption is different and we can't read the old values. Only allow the change + # if we're overwriting the data bag item + if !new_resource.complete + raise "Cannot encrypt #{new_resource.name} due to failure to decrypt existing resource. Set 'complete true' to overwrite or add the old secret as old_secret / old_secret_path." + end + differences = [ "overwrite data bag item (cannot decrypt old data bag item)"] + differences = (new_resource.raw_data.keys & current_resource.raw_data.keys).map { |key| "overwrite #{key}"} + differences += (new_resource.raw_data.keys - current_resource.raw_data.keys).map { |key| "add #{key}"} + differences += (current_resource.raw_data.keys - new_resource.raw_data.keys).map { |key| "remove #{key}" } + else + json_differences(current_decrypted, new_decrypted, false, '', differences) + end + else + if current_resource.encrypt + # New stuff should not be encrypted, old is. Decrypting. + differences = [ "decrypt data bag item to plaintext" ] + else + differences = [] + end + json_differences(current_decrypted, new_decrypted, true, '', differences) + end + differences + end + + # + # Helpers + # + + def resource_class + Chef::Resource::ChefDataBagItem + end + + def data_handler + Chef::ChefFS::DataHandler::DataBagItemDataHandler.new + end + + def keys + { + 'id' => :id, + 'data_bag' => :data_bag, + 'raw_data' => :raw_data + } + end + + def not_found_resource + resource = super + resource.data_bag new_resource.data_bag + resource + end + + def fake_entry + FakeEntry.new("#{new_resource.id}.json", FakeEntry.new(new_resource.data_bag)) + end + end end end end diff --git a/lib/chef/resource/chef_environment.rb b/lib/chef/resource/chef_environment.rb index 851d126..e9ba99c 100644 --- a/lib/chef/resource/chef_environment.rb +++ b/lib/chef/resource/chef_environment.rb @@ -1,15 +1,13 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' require 'chef/environment' +require 'chef/chef_fs/data_handler/environment_data_handler' class Chef class Resource - class ChefEnvironment < ChefCompat::Resource + class ChefEnvironment < Cheffish::BaseResource resource_name :chef_environment - allowed_actions :create, :delete, :nothing - default_action :create - def initialize(*args) super chef_server run_context.cheffish.current_chef_server @@ -72,6 +70,76 @@ def override(attribute_path, value=NOT_PASSED, &block) alias :attributes :default_attributes alias :property :default + + + action :create do + differences = json_differences(current_json, new_json) + + if current_resource_exists? + if differences.size > 0 + description = [ "update environment #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.put("environments/#{new_resource.name}", normalize_for_put(new_json)) + end + end + else + description = [ "create environment #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.post("environments", normalize_for_post(new_json)) + end + end + end + + action :delete do + if current_resource_exists? + converge_by "delete environment #{new_resource.name} at #{rest.url}" do + rest.delete("environments/#{new_resource.name}") + end + end + end + + action_class.class_eval do + def load_current_resource + begin + @current_resource = json_to_resource(rest.get("environments/#{new_resource.name}")) + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_resource = not_found_resource + else + raise + end + end + end + + def augment_new_json(json) + # Apply modifiers + json['default_attributes'] = apply_modifiers(new_resource.default_attribute_modifiers, json['default_attributes']) + json['override_attributes'] = apply_modifiers(new_resource.override_attribute_modifiers, json['override_attributes']) + json + end + + # + # Helpers + # + + def resource_class + Chef::Resource::ChefEnvironment + end + + def data_handler + Chef::ChefFS::DataHandler::EnvironmentDataHandler.new + end + + def keys + { + 'name' => :name, + 'description' => :description, + 'cookbook_versions' => :cookbook_versions, + 'default_attributes' => :default_attributes, + 'override_attributes' => :override_attributes + } + end + end end end end diff --git a/lib/chef/resource/chef_group.rb b/lib/chef/resource/chef_group.rb index d2cc5ac..cd82637 100644 --- a/lib/chef/resource/chef_group.rb +++ b/lib/chef/resource/chef_group.rb @@ -1,15 +1,13 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' require 'chef/run_list/run_list_item' +require 'chef/chef_fs/data_handler/group_data_handler' class Chef class Resource - class ChefGroup < ChefCompat::Resource + class ChefGroup < Cheffish::BaseResource resource_name :chef_group - allowed_actions :create, :delete, :nothing - default_action :create - # Grab environment from with_environment def initialize(*args) super @@ -48,6 +46,77 @@ def remove_groups(*remove_groups) property :raw_json, :kind_of => Hash property :chef_server, :kind_of => Hash + + + action :create do + differences = json_differences(current_json, new_json) + + if current_resource_exists? + if differences.size > 0 + description = [ "update group #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.put("groups/#{new_resource.name}", normalize_for_put(new_json)) + end + end + else + description = [ "create group #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.post("groups", normalize_for_post(new_json)) + end + end + end + + action :delete do + if current_resource_exists? + converge_by "delete group #{new_resource.name} at #{rest.url}" do + rest.delete("groups/#{new_resource.name}") + end + end + end + + action_class.class_eval do + def load_current_resource + begin + @current_resource = json_to_resource(rest.get("groups/#{new_resource.name}")) + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_resource = not_found_resource + else + raise + end + end + end + + def augment_new_json(json) + # Apply modifiers + json['users'] |= new_resource.users + json['clients'] |= new_resource.clients + json['groups'] |= new_resource.groups + json['users'] -= new_resource.remove_users + json['clients'] -= new_resource.remove_clients + json['groups'] -= new_resource.remove_groups + json + end + + # + # Helpers + # + + def resource_class + Chef::Resource::ChefGroup + end + + def data_handler + Chef::ChefFS::DataHandler::GroupDataHandler.new + end + + def keys + { + 'name' => :name, + 'groupname' => :name + } + end + end end end end diff --git a/lib/chef/resource/chef_mirror.rb b/lib/chef/resource/chef_mirror.rb index 440221b..6557e3d 100644 --- a/lib/chef/resource/chef_mirror.rb +++ b/lib/chef/resource/chef_mirror.rb @@ -1,14 +1,16 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' +require 'chef/chef_fs/file_pattern' +require 'chef/chef_fs/file_system' +require 'chef/chef_fs/parallelizer' +require 'chef/chef_fs/file_system/chef_server_root_dir' +require 'chef/chef_fs/file_system/chef_repository_file_system_root_dir' class Chef class Resource - class ChefMirror < ChefCompat::Resource + class ChefMirror < Cheffish::BaseResource resource_name :chef_mirror - allowed_actions :upload, :download, :nothing - default_action :nothing - def initialize(*args) super chef_server run_context.cheffish.current_chef_server @@ -47,6 +49,161 @@ def initialize(*args) # Number of parallel threads to list/upload/download with. Defaults to 10. property :concurrency, :kind_of => Integer + + + action :upload do + with_modified_config do + copy_to(local_fs, remote_fs) + end + end + + action :download do + with_modified_config do + copy_to(remote_fs, local_fs) + end + end + + action_class.class_eval do + + def with_modified_config + # pre-Chef-12 ChefFS reads versioned_cookbooks out of Chef::Config instead of + # taking it as an input, so we need to modify it for the duration of copy_to + @old_versioned_cookbooks = Chef::Config.versioned_cookbooks + # If versioned_cookbooks is explicitly set, set it. + if !new_resource.versioned_cookbooks.nil? + Chef::Config.versioned_cookbooks = new_resource.versioned_cookbooks + + # If new_resource.chef_repo_path is set, versioned_cookbooks defaults to true. + # Otherwise, it stays at its current Chef::Config value. + elsif new_resource.chef_repo_path + Chef::Config.versioned_cookbooks = true + end + + begin + yield + ensure + Chef::Config.versioned_cookbooks = @old_versioned_cookbooks + end + end + + def copy_to(src_root, dest_root) + if new_resource.concurrency && new_resource.concurrency <= 0 + raise "chef_mirror.concurrency must be above 0! Was set to #{new_resource.concurrency}" + end + # Honor concurrency + Chef::ChefFS::Parallelizer.threads = (new_resource.concurrency || 10) - 1 + + # We don't let the user pass absolute paths; we want to reserve those for + # multi-org support (/organizations/foo). + if new_resource.path[0] == '/' + raise "Absolute paths in chef_mirror not yet supported." + end + # Copy! + path = Chef::ChefFS::FilePattern.new("/#{new_resource.path}") + ui = CopyListener.new(self) + error = Chef::ChefFS::FileSystem.copy_to(path, src_root, dest_root, nil, options, ui, proc { |p| p.path }) + + if error + raise "Errors while copying:#{ui.errors.map { |e| "#{e}\n" }.join('')}" + end + end + + def local_fs + # If chef_repo_path is set to a string, put it in the form it usually is in + # chef config (:chef_repo_path, :node_path, etc.) + path_config = new_resource.chef_repo_path + if path_config.is_a?(Hash) + chef_repo_path = path_config.delete(:chef_repo_path) + elsif path_config + chef_repo_path = path_config + path_config = {} + else + chef_repo_path = Chef::Config.chef_repo_path + path_config = Chef::Config + end + chef_repo_path = Array(chef_repo_path).flatten + + # Go through the expected object paths and figure out the local paths for each. + case repo_mode + when 'hosted_everything' + object_names = %w(acls clients cookbooks containers data_bags environments groups nodes roles) + else + object_names = %w(clients cookbooks data_bags environments nodes roles users) + end + + object_paths = {} + object_names.each do |object_name| + variable_name = "#{object_name[0..-2]}_path" # cookbooks -> cookbook_path + if path_config[variable_name.to_sym] + paths = Array(path_config[variable_name.to_sym]).flatten + else + paths = chef_repo_path.map { |path| ::File.join(path, object_name) } + end + object_paths[object_name] = paths.map { |path| ::File.expand_path(path) } + end + + # Set up the root dir + Chef::ChefFS::FileSystem::ChefRepositoryFileSystemRootDir.new(object_paths) + end + + def remote_fs + config = { + :chef_server_url => new_resource.chef_server[:chef_server_url], + :node_name => new_resource.chef_server[:options][:client_name], + :client_key => new_resource.chef_server[:options][:signing_key_filename], + :repo_mode => repo_mode, + :versioned_cookbooks => Chef::Config.versioned_cookbooks + } + Chef::ChefFS::FileSystem::ChefServerRootDir.new("remote", config) + end + + def repo_mode + new_resource.chef_server[:chef_server_url] =~ /\/organizations\// ? 'hosted_everything' : 'everything' + end + + def options + result = { + :purge => new_resource.purge, + :freeze => new_resource.freeze, + :diff => new_resource.no_diff, + :dry_run => whyrun_mode? + } + result[:diff] = !result[:diff] + result[:repo_mode] = repo_mode + result[:concurrency] = new_resource.concurrency if new_resource.concurrency + result + end + + def load_current_resource + end + + class CopyListener + def initialize(mirror) + @mirror = mirror + @errors = [] + end + + attr_reader :mirror + attr_reader :errors + + # TODO output is not *always* indicative of a change. We may want to give + # ChefFS the ability to tell us that info. For now though, assuming any output + # means change is pretty damn close to the truth. + def output(str) + mirror.converge_by str do + end + end + def warn(str) + mirror.converge_by "WARNING: #{str}" do + end + end + def error(str) + mirror.converge_by "ERROR: #{str}" do + end + @errors << str + end + end + end end end end diff --git a/lib/chef/resource/chef_node.rb b/lib/chef/resource/chef_node.rb index 28c921c..e504b95 100644 --- a/lib/chef/resource/chef_node.rb +++ b/lib/chef/resource/chef_node.rb @@ -1,14 +1,12 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' +require 'chef/chef_fs/data_handler/node_data_handler' class Chef class Resource - class ChefNode < ChefCompat::Resource + class ChefNode < Cheffish::BaseResource resource_name :chef_node - allowed_actions :create, :delete, :nothing - default_action :create - # Grab environment from with_environment def initialize(*args) super @@ -17,6 +15,80 @@ def initialize(*args) end Cheffish.node_attributes(self) + + action :create do + differences = json_differences(current_json, new_json) + + if current_resource_exists? + if differences.size > 0 + description = [ "update node #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.put("nodes/#{new_resource.name}", normalize_for_put(new_json)) + end + end + else + description = [ "create node #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.post("nodes", normalize_for_post(new_json)) + end + end + end + + action :delete do + if current_resource_exists? + converge_by "delete node #{new_resource.name} at #{rest.url}" do + rest.delete("nodes/#{new_resource.name}") + end + end + end + + action_class.class_eval do + def load_current_resource + begin + @current_resource = json_to_resource(rest.get("nodes/#{new_resource.name}")) + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_resource = not_found_resource + else + raise + end + end + end + + def augment_new_json(json) + # Preserve tags even if "attributes" was overwritten directly + json['normal']['tags'] = current_json['normal']['tags'] unless json['normal']['tags'] + # Apply modifiers + json['run_list'] = apply_run_list_modifiers(new_resource.run_list_modifiers, new_resource.run_list_removers, json['run_list']) + json['normal'] = apply_modifiers(new_resource.attribute_modifiers, json['normal']) + # Preserve default/override/automatic even when "complete true" + json['default'] = current_json['default'] + json['override'] = current_json['override'] + json['automatic'] = current_json['automatic'] + json + end + + # + # Helpers + # + + def resource_class + Chef::Resource::ChefNode + end + + def data_handler + Chef::ChefFS::DataHandler::NodeDataHandler.new + end + + def keys + { + 'name' => :name, + 'chef_environment' => :chef_environment, + 'run_list' => :run_list, + 'normal' => :attributes + } + end + end end end end diff --git a/lib/chef/resource/chef_organization.rb b/lib/chef/resource/chef_organization.rb index b48677d..74419c1 100644 --- a/lib/chef/resource/chef_organization.rb +++ b/lib/chef/resource/chef_organization.rb @@ -1,15 +1,13 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' require 'chef/run_list/run_list_item' +require 'chef/chef_fs/data_handler/data_handler_base' class Chef class Resource - class ChefOrganization < ChefCompat::Resource + class ChefOrganization < Cheffish::BaseResource resource_name :chef_organization - allowed_actions :create, :delete, :nothing - default_action :create - # Grab environment from with_environment def initialize(*args) super @@ -64,6 +62,152 @@ def remove_members(*users) property :complete, :kind_of => [ TrueClass, FalseClass ] property :raw_json, :kind_of => Hash property :chef_server, :kind_of => Hash + + + action :create do + differences = json_differences(current_json, new_json) + + if current_resource_exists? + if differences.size > 0 + description = [ "update organization #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.put("#{rest.root_url}/organizations/#{new_resource.name}", normalize_for_put(new_json)) + end + end + else + description = [ "create organization #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.post("#{rest.root_url}/organizations", normalize_for_post(new_json)) + end + end + + # Revoke invites and memberships when asked + invites_to_remove.each do |user| + if outstanding_invites.has_key?(user) + converge_by "revoke #{user}'s invitation to organization #{new_resource.name}" do + rest.delete("#{rest.root_url}/organizations/#{new_resource.name}/association_requests/#{outstanding_invites[user]}") + end + end + end + members_to_remove.each do |user| + if existing_members.include?(user) + converge_by "remove #{user} from organization #{new_resource.name}" do + rest.delete("#{rest.root_url}/organizations/#{new_resource.name}/users/#{user}") + end + end + end + + # Invite and add members when asked + new_resource.invites.each do |user| + if !existing_members.include?(user) && !outstanding_invites.has_key?(user) + converge_by "invite #{user} to organization #{new_resource.name}" do + rest.post("#{rest.root_url}/organizations/#{new_resource.name}/association_requests", { 'user' => user }) + end + end + end + new_resource.members.each do |user| + if !existing_members.include?(user) + converge_by "Add #{user} to organization #{new_resource.name}" do + rest.post("#{rest.root_url}/organizations/#{new_resource.name}/users/", { 'username' => user }) + end + end + end + end + + action_class.class_eval do + def existing_members + @existing_members ||= rest.get("#{rest.root_url}/organizations/#{new_resource.name}/users").map { |u| u['user']['username'] } + end + + def outstanding_invites + @outstanding_invites ||= begin + invites = {} + rest.get("#{rest.root_url}/organizations/#{new_resource.name}/association_requests").each do |r| + invites[r['username']] = r['id'] + end + invites + end + end + + def invites_to_remove + if new_resource.complete + if new_resource.invites_specified? || new_resource.members_specified? + outstanding_invites.keys - (new_resource.invites | new_resource.members) + else + [] + end + else + new_resource.remove_members + end + end + + def members_to_remove + if new_resource.complete + if new_resource.members_specified? + existing_members - (new_resource.invites | new_resource.members) + else + [] + end + else + new_resource.remove_members + end + end + end + + action :delete do + if current_resource_exists? + converge_by "delete organization #{new_resource.name} at #{rest.url}" do + rest.delete("#{rest.root_url}/organizations/#{new_resource.name}") + end + end + end + + action_class.class_eval do + def load_current_resource + begin + @current_resource = json_to_resource(rest.get("#{rest.root_url}/organizations/#{new_resource.name}")) + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_resource = not_found_resource + else + raise + end + end + end + + # + # Helpers + # + + def resource_class + Chef::Resource::ChefOrganization + end + + def data_handler + OrganizationDataHandler.new + end + + def keys + { + 'name' => :name, + 'full_name' => :full_name + } + end + + class OrganizationDataHandler < Chef::ChefFS::DataHandler::DataHandlerBase + def normalize(organization, entry) + # Normalize the order of the keys for easier reading + normalize_hash(organization, { + 'name' => remove_dot_json(entry.name), + 'full_name' => remove_dot_json(entry.name), + 'org_type' => 'Business', + 'clientname' => "#{remove_dot_json(entry.name)}-validator", + 'billing_plan' => 'platform-free' + }) + end + end + end + end end end diff --git a/lib/chef/resource/chef_resolved_cookbooks.rb b/lib/chef/resource/chef_resolved_cookbooks.rb index a786687..4e59f9d 100644 --- a/lib/chef/resource/chef_resolved_cookbooks.rb +++ b/lib/chef/resource/chef_resolved_cookbooks.rb @@ -1,13 +1,11 @@ -require 'chef_compat/resource' +require 'cheffish/base_resource' +require 'chef_zero' class Chef class Resource - class ChefResolvedCookbooks < ChefCompat::Resource + class ChefResolvedCookbooks < Cheffish::BaseResource resource_name :chef_resolved_cookbooks - allowed_actions :resolve, :nothing - default_action :resolve - def initialize(*args) super require 'berkshelf' @@ -30,6 +28,42 @@ def cookbooks_from(path = nil) property :berksfile property :chef_server + + + action :resolve do + new_resource.cookbooks_from.each do |path| + ::Dir.entries(path).each do |name| + if ::File.directory?(::File.join(path, name)) && name != '.' && name != '..' + new_resource.berksfile.cookbook name, :path => ::File.join(path, name) + end + end + end + + new_resource.berksfile.install + + # Ridley really really wants a key :/ + if new_resource.chef_server[:options][:signing_key_filename] + new_resource.berksfile.upload( + :server_url => new_resource.chef_server[:chef_server_url], + :client_name => new_resource.chef_server[:options][:client_name], + :client_key => new_resource.chef_server[:options][:signing_key_filename]) + else + file = Tempfile.new('privatekey') + begin + file.write(ChefZero::PRIVATE_KEY) + file.close + + new_resource.berksfile.upload( + :server_url => new_resource.chef_server[:chef_server_url], + :client_name => new_resource.chef_server[:options][:client_name] || 'me', + :client_key => file.path) + + ensure + file.close + file.unlink + end + end + end end end end diff --git a/lib/chef/resource/chef_role.rb b/lib/chef/resource/chef_role.rb index 60e53ee..0217a6e 100644 --- a/lib/chef/resource/chef_role.rb +++ b/lib/chef/resource/chef_role.rb @@ -1,15 +1,13 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/base_resource' require 'chef/run_list/run_list_item' +require 'chef/chef_fs/data_handler/role_data_handler' class Chef class Resource - class ChefRole < ChefCompat::Resource + class ChefRole < Cheffish::BaseResource resource_name :chef_role - allowed_actions :create, :delete, :nothing - default_action :create - # Grab environment from with_environment def initialize(*args) super @@ -105,6 +103,77 @@ def remove_role(*roles) @run_list_removers ||= [] @run_list_removers += roles.map { |recipe| Chef::RunList::RunListItem.new("role[#{role}]") } end + + action :create do + differences = json_differences(current_json, new_json) + + if current_resource_exists? + if differences.size > 0 + description = [ "update role #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.put("roles/#{new_resource.name}", normalize_for_put(new_json)) + end + end + else + description = [ "create role #{new_resource.name} at #{rest.url}" ] + differences + converge_by description do + rest.post("roles", normalize_for_post(new_json)) + end + end + end + + action :delete do + if current_resource_exists? + converge_by "delete role #{new_resource.name} at #{rest.url}" do + rest.delete("roles/#{new_resource.name}") + end + end + end + + action_class.class_eval do + def load_current_resource + begin + @current_resource = json_to_resource(rest.get("roles/#{new_resource.name}")) + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_resource = not_found_resource + else + raise + end + end + end + + def augment_new_json(json) + # Apply modifiers + json['run_list'] = apply_run_list_modifiers(new_resource.run_list_modifiers, new_resource.run_list_removers, json['run_list']) + json['default_attributes'] = apply_modifiers(new_resource.default_attribute_modifiers, json['default_attributes']) + json['override_attributes'] = apply_modifiers(new_resource.override_attribute_modifiers, json['override_attributes']) + json + end + + # + # Helpers + # + + def resource_class + Chef::Resource::ChefRole + end + + def data_handler + Chef::ChefFS::DataHandler::RoleDataHandler.new + end + + def keys + { + 'name' => :name, + 'description' => :description, + 'run_list' => :run_list, + 'env_run_lists' => :env_run_lists, + 'default_attributes' => :default_attributes, + 'override_attributes' => :override_attributes + } + end + end end end end diff --git a/lib/chef/resource/chef_user.rb b/lib/chef/resource/chef_user.rb index 86f6a0e..b2212ce 100644 --- a/lib/chef/resource/chef_user.rb +++ b/lib/chef/resource/chef_user.rb @@ -1,14 +1,11 @@ require 'cheffish' -require 'chef_compat/resource' +require 'cheffish/chef_actor_base' class Chef class Resource - class ChefUser < ChefCompat::Resource + class ChefUser < Cheffish::ChefActorBase resource_name :chef_user - allowed_actions :create, :delete, :nothing - default_action :create - # Grab environment from with_environment def initialize(*args) super @@ -51,6 +48,52 @@ def before(&block) def after(&block) block ? @after = block : @after end + + + action :create do + create_actor + end + + action :delete do + delete_actor + end + + action_class.class_eval do + # + # Helpers + # + # Gives us new_json, current_json, not_found_json, etc. + + def actor_type + 'user' + end + + def actor_path + "#{rest.root_url}/users" + end + + def resource_class + Chef::Resource::ChefUser + end + + def data_handler + Chef::ChefFS::DataHandler::UserDataHandler.new + end + + def keys + { + 'name' => :name, + 'username' => :name, + 'display_name' => :display_name, + 'admin' => :admin, + 'email' => :email, + 'password' => :password, + 'external_authentication_uid' => :external_authentication_uid, + 'recovery_authentication_enabled' => :recovery_authentication_enabled, + 'public_key' => :source_key + } + end + end end end end diff --git a/lib/chef/resource/private_key.rb b/lib/chef/resource/private_key.rb index be2a760..8428373 100644 --- a/lib/chef/resource/private_key.rb +++ b/lib/chef/resource/private_key.rb @@ -1,9 +1,11 @@ require 'openssl/cipher' -require 'chef_compat/resource' +require 'cheffish/base_resource' +require 'openssl' +require 'cheffish/key_formatter' class Chef class Resource - class PrivateKey < ChefCompat::Resource + class PrivateKey < Cheffish::BaseResource resource_name :private_key allowed_actions :create, :delete, :regenerate, :nothing @@ -43,6 +45,217 @@ def after(&block) def load_prior_resource(*args) Chef::Log.debug("Overloading #{resource_name}.load_prior_resource with NOOP") end + + + action :create do + create_key(false, :create) + end + + action :regenerate do + create_key(true, :regenerate) + end + + action :delete do + if current_resource.path + converge_by "delete private key #{new_path}" do + ::File.unlink(new_path) + end + end + end + + action_class.class_eval do + def create_key(regenerate, action) + if @should_create_directory + Cheffish.inline_resource(self, action) do + directory run_context.config[:private_key_write_path] + end + end + + final_private_key = nil + if new_source_key + # + # Create private key from source + # + desired_output = encode_private_key(new_source_key) + if current_resource.path == :none || desired_output != IO.read(new_path) + converge_by "reformat key at #{new_resource.source_key_path} to #{new_resource.format} private key #{new_path} (#{new_resource.pass_phrase ? ", #{new_resource.cipher} password" : ""})" do + IO.write(new_path, desired_output) + end + end + + final_private_key = new_source_key + + else + # + # Generate a new key + # + if current_resource.action == [ :delete ] || regenerate || + (new_resource.regenerate_if_different && + (!current_private_key || + current_resource.size != new_resource.size || + current_resource.type != new_resource.type)) + + case new_resource.type + when :rsa + if new_resource.exponent + final_private_key = OpenSSL::PKey::RSA.generate(new_resource.size, new_resource.exponent) + else + final_private_key = OpenSSL::PKey::RSA.generate(new_resource.size) + end + when :dsa + final_private_key = OpenSSL::PKey::DSA.generate(new_resource.size) + end + + generated_key = true + elsif !current_private_key + raise "Could not read private key from #{current_resource.path}: missing pass phrase?" + else + final_private_key = current_private_key + generated_key = false + end + + if generated_key + generated_description = " (#{new_resource.size} bits#{new_resource.pass_phrase ? ", #{new_resource.cipher} password" : ""})" + + if new_path != :none + action = current_resource.path == :none ? 'create' : 'overwrite' + converge_by "#{action} #{new_resource.type} private key #{new_path}#{generated_description}" do + write_private_key(final_private_key) + end + else + converge_by "generate private key#{generated_description}" do + end + end + else + # Warn if existing key has different characteristics than expected + if current_resource.size != new_resource.size + Chef::Log.warn("Mismatched key size! #{current_resource.path} is #{current_resource.size} bytes, desired is #{new_resource.size} bytes. Use action :regenerate to force key regeneration.") + elsif current_resource.type != new_resource.type + Chef::Log.warn("Mismatched key type! #{current_resource.path} is #{current_resource.type}, desired is #{new_resource.type} bytes. Use action :regenerate to force key regeneration.") + end + + if current_resource.format != new_resource.format + converge_by "change format of #{new_resource.type} private key #{new_path} from #{current_resource.format} to #{new_resource.format}" do + write_private_key(current_private_key) + end + elsif (@current_file_mode & 0077) != 0 + new_mode = @current_file_mode & 07700 + converge_by "change mode of private key #{new_path} to #{new_mode.to_s(8)}" do + ::File.chmod(new_mode, new_path) + end + end + end + end + + if new_resource.public_key_path + public_key_path = new_resource.public_key_path + public_key_format = new_resource.public_key_format + Cheffish.inline_resource(self, action) do + public_key public_key_path do + source_key final_private_key + format public_key_format + end + end + end + + if new_resource.after + new_resource.after.call(new_resource, final_private_key) + end + end + + def encode_private_key(key) + key_format = {} + key_format[:format] = new_resource.format if new_resource.format + key_format[:pass_phrase] = new_resource.pass_phrase if new_resource.pass_phrase + key_format[:cipher] = new_resource.cipher if new_resource.cipher + Cheffish::KeyFormatter.encode(key, key_format) + end + + def write_private_key(key) + ::File.open(new_path, 'w') do |file| + file.chmod(0600) + file.write(encode_private_key(key)) + end + end + + def new_source_key + @new_source_key ||= begin + if new_resource.source_key.is_a?(String) + source_key, source_key_format = Cheffish::KeyFormatter.decode(new_resource.source_key, new_resource.source_key_pass_phrase) + source_key + elsif new_resource.source_key + new_resource.source_key + elsif new_resource.source_key_path + source_key, source_key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.source_key_path), new_resource.source_key_pass_phrase, new_resource.source_key_path) + source_key + else + nil + end + end + end + + attr_reader :current_private_key + + def new_path + new_key_with_path[1] + end + + def new_key_with_path + path = new_resource.path + if path.is_a?(Symbol) + return [ nil, path ] + elsif Pathname.new(path).relative? + private_key, private_key_path = Cheffish.get_private_key_with_path(path, run_context.config) + if private_key + return [ private_key, (private_key_path || :none) ] + elsif run_context.config[:private_key_write_path] + @should_create_directory = true + path = ::File.join(run_context.config[:private_key_write_path], path) + return [ nil, path ] + else + raise "Could not find key #{path} and Chef::Config.private_key_write_path is not set." + end + elsif ::File.exist?(path) + return [ IO.read(path), path ] + else + return [ nil, path ] + end + end + + def load_current_resource + resource = Chef::Resource::PrivateKey.new(new_resource.name, run_context) + + new_key, new_path = new_key_with_path + if new_path != :none && ::File.exist?(new_path) + resource.path new_path + @current_file_mode = ::File.stat(new_path).mode + else + resource.path :none + end + + if new_key + begin + key, key_format = Cheffish::KeyFormatter.decode(new_key, new_resource.pass_phrase, new_path) + if key + @current_private_key = key + resource.format key_format[:format] + resource.type key_format[:type] + resource.size key_format[:size] + resource.exponent key_format[:exponent] + resource.pass_phrase key_format[:pass_phrase] + resource.cipher key_format[:cipher] + end + rescue + # If there's an error reading, we assume format and type are wrong and don't futz with them + Chef::Log.warn("Error reading #{new_path}: #{$!}") + end + else + resource.action :delete + end + + @current_resource = resource + end + end end end end diff --git a/lib/chef/resource/public_key.rb b/lib/chef/resource/public_key.rb index e6b0608..4e661ae 100644 --- a/lib/chef/resource/public_key.rb +++ b/lib/chef/resource/public_key.rb @@ -1,9 +1,11 @@ require 'openssl/cipher' -require 'chef_compat/resource' +require 'cheffish/base_resource' +require 'openssl' +require 'cheffish/key_formatter' class Chef class Resource - class PublicKey < ChefCompat::Resource + class PublicKey < Cheffish::BaseResource resource_name :public_key allowed_actions :create, :delete, :nothing @@ -20,6 +22,83 @@ class PublicKey < ChefCompat::Resource def load_prior_resource(*args) Chef::Log.debug("Overloading #{resource_name}.load_prior_resource with NOOP") end + + + action :create do + if !new_source_key + raise "No source key specified" + end + desired_output = encode_public_key(new_source_key) + if Array(current_resource.action) == [ :delete ] || desired_output != IO.read(new_resource.path) + converge_by "write #{new_resource.format} public key #{new_resource.path} from #{new_source_key_publicity} key #{new_resource.source_key_path}" do + IO.write(new_resource.path, desired_output) + # TODO permissions on file? + end + end + end + + action :delete do + if Array(current_resource.action) == [ :create ] + converge_by "delete public key #{new_resource.path}" do + ::File.unlink(new_resource.path) + end + end + end + + action_class.class_eval do + def encode_public_key(key) + key_format = {} + key_format[:format] = new_resource.format if new_resource.format + Cheffish::KeyFormatter.encode(key, key_format) + end + + attr_reader :current_public_key + attr_reader :new_source_key_publicity + + def new_source_key + @new_source_key ||= begin + if new_resource.source_key.is_a?(String) + source_key, source_key_format = Cheffish::KeyFormatter.decode(new_resource.source_key, new_resource.source_key_pass_phrase) + elsif new_resource.source_key + source_key = new_resource.source_key + elsif new_resource.source_key_path + source_key, source_key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.source_key_path), new_resource.source_key_pass_phrase, new_resource.source_key_path) + else + return nil + end + + if source_key.private? + @new_source_key_publicity = 'private' + source_key.public_key + else + @new_source_key_publicity = 'public' + source_key + end + end + end + + def load_current_resource + if ::File.exist?(new_resource.path) + resource = Chef::Resource::PublicKey.new(new_resource.path, run_context) + begin + key, key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.path), nil, new_resource.path) + if key + @current_public_key = key + resource.format key_format[:format] + end + rescue + # If there is an error reading we assume format and such is broken + end + + @current_resource = resource + else + not_found_resource = Chef::Resource::PublicKey.new(new_resource.path, run_context) + not_found_resource.action :delete + @current_resource = not_found_resource + end + end + end + end end end diff --git a/lib/cheffish/actor_provider_base.rb b/lib/cheffish/actor_provider_base.rb deleted file mode 100644 index 1b55136..0000000 --- a/lib/cheffish/actor_provider_base.rb +++ /dev/null @@ -1,131 +0,0 @@ -require 'cheffish/key_formatter' -require 'cheffish/chef_provider_base' - -class Cheffish::ActorProviderBase < Cheffish::ChefProviderBase - - def create_actor - if new_resource.before - new_resource.before.call(new_resource) - end - - # Create or update the client/user - current_public_key = new_json['public_key'] - differences = json_differences(current_json, new_json) - if current_resource_exists? - # Update the actor if it's different - if differences.size > 0 - description = [ "update #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences - converge_by description do - result = rest.put("#{actor_path}/#{new_resource.name}", normalize_for_put(new_json)) - current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key'] - end - end - else - # Create the actor if it's missing - if !new_public_key - raise "You must specify a public key to create a #{actor_type}! Use the private_key resource to create a key, and pass it in with source_key_path." - end - description = [ "create #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences - converge_by description do - result = rest.post("#{actor_path}", normalize_for_post(new_json)) - current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key'] - end - end - - # Write out the public key - if new_resource.output_key_path - # TODO use inline_resource - key_content = Cheffish::KeyFormatter.encode(current_public_key, { :format => new_resource.output_key_format }) - if !current_resource.output_key_path - action = 'create' - elsif key_content != IO.read(current_resource.output_key_path) - action = 'overwrite' - else - action = nil - end - if action - converge_by "#{action} public key #{new_resource.output_key_path}" do - IO.write(new_resource.output_key_path, key_content) - end - end - # TODO permissions? - end - - if new_resource.after - new_resource.after.call(self, new_json, server_private_key, server_public_key) - end - end - - def delete_actor - if current_resource_exists? - converge_by "delete #{actor_type} #{new_resource.name} at #{actor_path}" do - rest.delete("#{actor_path}/#{new_resource.name}") - Chef::Log.info("#{new_resource} deleted #{actor_type} #{new_resource.name} at #{rest.url}") - end - end - if current_resource.output_key_path - converge_by "delete public key #{current_resource.output_key_path}" do - ::File.unlink(current_resource.output_key_path) - end - end - end - - def new_public_key - @new_public_key ||= begin - if new_resource.source_key - if new_resource.source_key.is_a?(String) - key, key_format = Cheffish::KeyFormatter.decode(new_resource.source_key) - - if key.private? - key.public_key - else - key - end - elsif new_resource.source_key.private? - new_resource.source_key.public_key - else - new_resource.source_key - end - elsif new_resource.source_key_path - source_key_path = new_resource.source_key_path - if Pathname.new(source_key_path).relative? - source_key_str, source_key_path = Cheffish.get_private_key_with_path(source_key_path, run_context.config) - else - source_key_str = IO.read(source_key_path) - end - source_key, source_key_format = Cheffish::KeyFormatter.decode(source_key_str, new_resource.source_key_pass_phrase, source_key_path) - if source_key.private? - source_key.public_key - else - source_key - end - else - nil - end - end - end - - def augment_new_json(json) - if new_public_key - json['public_key'] = new_public_key.to_pem - end - json - end - - def load_current_resource - begin - json = rest.get("#{actor_path}/#{new_resource.name}") - @current_resource = json_to_resource(json) - rescue Net::HTTPServerException => e - if e.response.code == "404" - @current_resource = not_found_resource - else - raise - end - end - - if new_resource.output_key_path && ::File.exist?(new_resource.output_key_path) - current_resource.output_key_path = new_resource.output_key_path - end - end -end diff --git a/lib/cheffish/base_resource.rb b/lib/cheffish/base_resource.rb new file mode 100644 index 0000000..6c0fe7d --- /dev/null +++ b/lib/cheffish/base_resource.rb @@ -0,0 +1,241 @@ +require 'chef_compat/resource' + +module Cheffish + class BaseResource < ChefCompat::Resource + declare_action_class.class_eval do + def rest + @rest ||= Cheffish.chef_server_api(new_resource.chef_server) + end + + def current_resource_exists? + Array(current_resource.action) != [ :delete ] + end + + def not_found_resource + resource = resource_class.new(new_resource.name, run_context) + resource.action :delete + resource + end + + def normalize_for_put(json) + data_handler.normalize_for_put(json, fake_entry) + end + + def normalize_for_post(json) + data_handler.normalize_for_post(json, fake_entry) + end + + def new_json + @new_json ||= begin + if new_resource.complete + result = normalize(resource_to_json(new_resource)) + else + # If the resource is incomplete, we use the current json to fill any holes + result = current_json.merge(resource_to_json(new_resource)) + end + augment_new_json(result) + end + end + + # Meant to be overridden + def augment_new_json(json) + json + end + + def current_json + @current_json ||= begin + result = normalize(resource_to_json(current_resource)) + result = augment_current_json(result) + result + end + end + + # Meant to be overridden + def augment_current_json(json) + json + end + + def resource_to_json(resource) + json = resource.raw_json || {} + keys.each do |json_key, resource_key| + value = resource.send(resource_key) + # This takes care of Chef ImmutableMash and ImmutableArray + value = value.to_hash if value.is_a?(Hash) + value = value.to_a if value.is_a?(Array) + json[json_key] = value if value + end + json + end + + def json_to_resource(json) + resource = resource_class.new(new_resource.name, run_context) + keys.each do |json_key, resource_key| + resource.send(resource_key, json.delete(json_key)) + end + # Set the leftover to raw_json + resource.raw_json json + resource + end + + def normalize(json) + data_handler.normalize(json, fake_entry) + end + + def json_differences(old_json, new_json, print_values=true, name = '', result = nil) + result ||= [] + json_differences_internal(old_json, new_json, print_values, name, result) + result + end + + def json_differences_internal(old_json, new_json, print_values, name, result) + if old_json.kind_of?(Hash) && new_json.kind_of?(Hash) + removed_keys = old_json.keys.inject({}) { |hash, key| hash[key] = true; hash } + new_json.each_pair do |new_key, new_value| + if old_json.has_key?(new_key) + removed_keys.delete(new_key) + if new_value != old_json[new_key] + json_differences_internal(old_json[new_key], new_value, print_values, name == '' ? new_key : "#{name}.#{new_key}", result) + end + else + if print_values + result << " add #{name == '' ? new_key : "#{name}.#{new_key}"} = #{new_value.inspect}" + else + result << " add #{name == '' ? new_key : "#{name}.#{new_key}"}" + end + end + end + removed_keys.keys.each do |removed_key| + result << " remove #{name == '' ? removed_key : "#{name}.#{removed_key}"}" + end + else + old_json = old_json.to_s if old_json.kind_of?(Symbol) + new_json = new_json.to_s if new_json.kind_of?(Symbol) + if old_json != new_json + if print_values + result << " update #{name} from #{old_json.inspect} to #{new_json.inspect}" + else + result << " update #{name}" + end + end + end + end + + def apply_modifiers(modifiers, json) + return json if !modifiers || modifiers.size == 0 + + # If the attributes have nothing, set them to {} so we have something to add to + if json + json = Marshal.load(Marshal.dump(json)) # Deep copy + else + json = {} + end + + modifiers.each do |path, value| + path = [path] if !path.kind_of?(Array) + path = path.map { |path_part| path_part.to_s } + parent = 0.upto(path.size-2).inject(json) do |hash, index| + if hash.nil? + nil + elsif !hash.is_a?(Hash) + raise "Attempt to set #{path} to #{value} when #{path[0..index-1]} is not a hash" + else + hash[path[index]] + end + end + if !parent.nil? && !parent.is_a?(Hash) + raise "Attempt to set #{path} to #{value} when #{path[0..-2]} is not a hash" + end + existing_value = parent ? parent[path[-1]] : nil + + if value.is_a?(Proc) + value = value.call(existing_value) + end + if value == :delete + parent.delete(path[-1]) if parent + else + # Create parent if necessary, overwriting values + parent = path[0..-2].inject(json) do |hash, path_part| + hash[path_part] = {} if !hash[path_part] + hash[path_part] + end + if path.size > 0 + parent[path[-1]] = value + else + json = value + end + end + end + json + end + + def apply_run_list_modifiers(add_to_run_list, delete_from_run_list, run_list) + return run_list if (!add_to_run_list || add_to_run_list.size == 0) && (!delete_from_run_list || !delete_from_run_list.size) + delete_from_run_list ||= [] + add_to_run_list ||= [] + + run_list = Chef::RunList.new(*run_list) + + result = [] + add_to_run_list_index = 0 + run_list_index = 0 + while run_list_index < run_list.run_list_items.size do + # See if the desired run list has this item + found_desired = add_to_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) } + if found_desired + # If so, copy all items up to that desired run list (to preserve order). + # If a run list item is out of order (run_list = X, B, Y, A, Z and desired = A, B) + # then this will give us X, A, B. When A is found later, nothing will be copied + # because found_desired will be less than add_to_run_list_index. The result will + # be X, A, B, Y, Z. + if found_desired >= add_to_run_list_index + result += add_to_run_list[add_to_run_list_index..found_desired].map { |item| item.to_s } + add_to_run_list_index = found_desired+1 + end + else + # If not, just copy it in + unless delete_from_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) } + result << run_list[run_list_index].to_s + end + end + run_list_index += 1 + end + + # Copy any remaining desired items at the end + result += add_to_run_list[add_to_run_list_index..-1].map { |item| item.to_s } + result + end + + def same_run_list_item(a, b) + a_name = a.name + b_name = b.name + # Handle "a::default" being the same as "a" + if a.type == :recipe && a_name =~ /(.+)::default$/ + a_name = $1 + elsif b.type == :recipe && b_name =~ /(.+)::default$/ + b_name = $1 + end + + a_name == b_name && a.type == b.type # We want to replace things with same name and different version + end + + private + + # Needed to be able to use DataHandler classes + def fake_entry + FakeEntry.new("#{new_resource.send(keys.values.first)}.json") + end + + class FakeEntry + def initialize(name, parent = nil) + @name = name + @parent = parent + @org = nil + end + + attr_reader :name + attr_reader :parent + attr_reader :org + end + end + end +end diff --git a/lib/cheffish/chef_actor_base.rb b/lib/cheffish/chef_actor_base.rb new file mode 100644 index 0000000..fcfaff2 --- /dev/null +++ b/lib/cheffish/chef_actor_base.rb @@ -0,0 +1,135 @@ +require 'cheffish/key_formatter' +require 'cheffish/base_resource' + +module Cheffish + class ChefActorBase < Cheffish::BaseResource + + action_class.class_eval do + def create_actor + if new_resource.before + new_resource.before.call(new_resource) + end + + # Create or update the client/user + current_public_key = new_json['public_key'] + differences = json_differences(current_json, new_json) + if current_resource_exists? + # Update the actor if it's different + if differences.size > 0 + description = [ "update #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences + converge_by description do + result = rest.put("#{actor_path}/#{new_resource.name}", normalize_for_put(new_json)) + current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key'] + end + end + else + # Create the actor if it's missing + if !new_public_key + raise "You must specify a public key to create a #{actor_type}! Use the private_key resource to create a key, and pass it in with source_key_path." + end + description = [ "create #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences + converge_by description do + result = rest.post("#{actor_path}", normalize_for_post(new_json)) + current_public_key, current_public_key_format = Cheffish::KeyFormatter.decode(result['public_key']) if result['public_key'] + end + end + + # Write out the public key + if new_resource.output_key_path + # TODO use inline_resource + key_content = Cheffish::KeyFormatter.encode(current_public_key, { :format => new_resource.output_key_format }) + if !current_resource.output_key_path + action = 'create' + elsif key_content != IO.read(current_resource.output_key_path) + action = 'overwrite' + else + action = nil + end + if action + converge_by "#{action} public key #{new_resource.output_key_path}" do + IO.write(new_resource.output_key_path, key_content) + end + end + # TODO permissions? + end + + if new_resource.after + new_resource.after.call(self, new_json, server_private_key, server_public_key) + end + end + + def delete_actor + if current_resource_exists? + converge_by "delete #{actor_type} #{new_resource.name} at #{actor_path}" do + rest.delete("#{actor_path}/#{new_resource.name}") + Chef::Log.info("#{new_resource} deleted #{actor_type} #{new_resource.name} at #{rest.url}") + end + end + if current_resource.output_key_path + converge_by "delete public key #{current_resource.output_key_path}" do + ::File.unlink(current_resource.output_key_path) + end + end + end + + def new_public_key + @new_public_key ||= begin + if new_resource.source_key + if new_resource.source_key.is_a?(String) + key, key_format = Cheffish::KeyFormatter.decode(new_resource.source_key) + + if key.private? + key.public_key + else + key + end + elsif new_resource.source_key.private? + new_resource.source_key.public_key + else + new_resource.source_key + end + elsif new_resource.source_key_path + source_key_path = new_resource.source_key_path + if Pathname.new(source_key_path).relative? + source_key_str, source_key_path = Cheffish.get_private_key_with_path(source_key_path, run_context.config) + else + source_key_str = IO.read(source_key_path) + end + source_key, source_key_format = Cheffish::KeyFormatter.decode(source_key_str, new_resource.source_key_pass_phrase, source_key_path) + if source_key.private? + source_key.public_key + else + source_key + end + else + nil + end + end + end + + def augment_new_json(json) + if new_public_key + json['public_key'] = new_public_key.to_pem + end + json + end + + def load_current_resource + begin + json = rest.get("#{actor_path}/#{new_resource.name}") + @current_resource = json_to_resource(json) + rescue Net::HTTPServerException => e + if e.response.code == "404" + @current_resource = not_found_resource + else + raise + end + end + + if new_resource.output_key_path && ::File.exist?(new_resource.output_key_path) + current_resource.output_key_path = new_resource.output_key_path + end + end + end + end +end diff --git a/lib/cheffish/chef_provider_base.rb b/lib/cheffish/chef_provider_base.rb deleted file mode 100644 index 877e991..0000000 --- a/lib/cheffish/chef_provider_base.rb +++ /dev/null @@ -1,246 +0,0 @@ -require 'chef/config' -require 'chef/run_list' -require 'chef/provider/lwrp_base' - -module Cheffish - class ChefProviderBase < Chef::Provider::LWRPBase - def rest - @rest ||= Cheffish.chef_server_api(new_resource.chef_server) - end - - def current_resource_exists? - Array(current_resource.action) != [ :delete ] - end - - def not_found_resource - resource = resource_class.new(new_resource.name, run_context) - resource.action :delete - resource - end - - def normalize_for_put(json) - data_handler.normalize_for_put(json, fake_entry) - end - - def normalize_for_post(json) - data_handler.normalize_for_post(json, fake_entry) - end - - def new_json - @new_json ||= begin - if new_resource.complete - result = normalize(resource_to_json(new_resource)) - else - # If the resource is incomplete, we use the current json to fill any holes - result = current_json.merge(resource_to_json(new_resource)) - end - augment_new_json(result) - end - end - - # Meant to be overridden - def augment_new_json(json) - json - end - - def current_json - @current_json ||= begin - result = normalize(resource_to_json(current_resource)) - result = augment_current_json(result) - result - end - end - - # Meant to be overridden - def augment_current_json(json) - json - end - - def resource_to_json(resource) - json = resource.raw_json || {} - keys.each do |json_key, resource_key| - value = resource.send(resource_key) - # This takes care of Chef ImmutableMash and ImmutableArray - value = value.to_hash if value.is_a?(Hash) - value = value.to_a if value.is_a?(Array) - json[json_key] = value if value - end - json - end - - def json_to_resource(json) - resource = resource_class.new(new_resource.name, run_context) - keys.each do |json_key, resource_key| - resource.send(resource_key, json.delete(json_key)) - end - # Set the leftover to raw_json - resource.raw_json json - resource - end - - def normalize(json) - data_handler.normalize(json, fake_entry) - end - - def json_differences(old_json, new_json, print_values=true, name = '', result = nil) - result ||= [] - json_differences_internal(old_json, new_json, print_values, name, result) - result - end - - def json_differences_internal(old_json, new_json, print_values, name, result) - if old_json.kind_of?(Hash) && new_json.kind_of?(Hash) - removed_keys = old_json.keys.inject({}) { |hash, key| hash[key] = true; hash } - new_json.each_pair do |new_key, new_value| - if old_json.has_key?(new_key) - removed_keys.delete(new_key) - if new_value != old_json[new_key] - json_differences_internal(old_json[new_key], new_value, print_values, name == '' ? new_key : "#{name}.#{new_key}", result) - end - else - if print_values - result << " add #{name == '' ? new_key : "#{name}.#{new_key}"} = #{new_value.inspect}" - else - result << " add #{name == '' ? new_key : "#{name}.#{new_key}"}" - end - end - end - removed_keys.keys.each do |removed_key| - result << " remove #{name == '' ? removed_key : "#{name}.#{removed_key}"}" - end - else - old_json = old_json.to_s if old_json.kind_of?(Symbol) - new_json = new_json.to_s if new_json.kind_of?(Symbol) - if old_json != new_json - if print_values - result << " update #{name} from #{old_json.inspect} to #{new_json.inspect}" - else - result << " update #{name}" - end - end - end - end - - def apply_modifiers(modifiers, json) - return json if !modifiers || modifiers.size == 0 - - # If the attributes have nothing, set them to {} so we have something to add to - if json - json = Marshal.load(Marshal.dump(json)) # Deep copy - else - json = {} - end - - modifiers.each do |path, value| - path = [path] if !path.kind_of?(Array) - path = path.map { |path_part| path_part.to_s } - parent = 0.upto(path.size-2).inject(json) do |hash, index| - if hash.nil? - nil - elsif !hash.is_a?(Hash) - raise "Attempt to set #{path} to #{value} when #{path[0..index-1]} is not a hash" - else - hash[path[index]] - end - end - if !parent.nil? && !parent.is_a?(Hash) - raise "Attempt to set #{path} to #{value} when #{path[0..-2]} is not a hash" - end - existing_value = parent ? parent[path[-1]] : nil - - if value.is_a?(Proc) - value = value.call(existing_value) - end - if value == :delete - parent.delete(path[-1]) if parent - else - # Create parent if necessary, overwriting values - parent = path[0..-2].inject(json) do |hash, path_part| - hash[path_part] = {} if !hash[path_part] - hash[path_part] - end - if path.size > 0 - parent[path[-1]] = value - else - json = value - end - end - end - json - end - - def apply_run_list_modifiers(add_to_run_list, delete_from_run_list, run_list) - return run_list if (!add_to_run_list || add_to_run_list.size == 0) && (!delete_from_run_list || !delete_from_run_list.size) - delete_from_run_list ||= [] - add_to_run_list ||= [] - - run_list = Chef::RunList.new(*run_list) - - result = [] - add_to_run_list_index = 0 - run_list_index = 0 - while run_list_index < run_list.run_list_items.size do - # See if the desired run list has this item - found_desired = add_to_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) } - if found_desired - # If so, copy all items up to that desired run list (to preserve order). - # If a run list item is out of order (run_list = X, B, Y, A, Z and desired = A, B) - # then this will give us X, A, B. When A is found later, nothing will be copied - # because found_desired will be less than add_to_run_list_index. The result will - # be X, A, B, Y, Z. - if found_desired >= add_to_run_list_index - result += add_to_run_list[add_to_run_list_index..found_desired].map { |item| item.to_s } - add_to_run_list_index = found_desired+1 - end - else - # If not, just copy it in - unless delete_from_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) } - result << run_list[run_list_index].to_s - end - end - run_list_index += 1 - end - - # Copy any remaining desired items at the end - result += add_to_run_list[add_to_run_list_index..-1].map { |item| item.to_s } - result - end - - def same_run_list_item(a, b) - a_name = a.name - b_name = b.name - # Handle "a::default" being the same as "a" - if a.type == :recipe && a_name =~ /(.+)::default$/ - a_name = $1 - elsif b.type == :recipe && b_name =~ /(.+)::default$/ - b_name = $1 - end - - a_name == b_name && a.type == b.type # We want to replace things with same name and different version - end - - private - - # Needed to be able to use DataHandler classes - def fake_entry - FakeEntry.new("#{new_resource.send(keys.values.first)}.json") - end - - class FakeEntry - def initialize(name, parent = nil) - @name = name - @parent = parent - @org = nil - end - - attr_reader :name - attr_reader :parent - attr_reader :org - end - end - - # We are not interested in Chef's cloning behavior here. - def load_prior_resource(*args) - Chef::Log.debug("Overloading #{resource_name}.load_prior_resource with NOOP") - end -end diff --git a/lib/cheffish/recipe_dsl.rb b/lib/cheffish/recipe_dsl.rb index 8139ba0..181222b 100644 --- a/lib/cheffish/recipe_dsl.rb +++ b/lib/cheffish/recipe_dsl.rb @@ -24,20 +24,6 @@ require 'chef/resource/chef_user' require 'chef/resource/private_key' require 'chef/resource/public_key' -require 'chef/provider/chef_acl' -require 'chef/provider/chef_client' -require 'chef/provider/chef_container' -require 'chef/provider/chef_data_bag' -require 'chef/provider/chef_data_bag_item' -require 'chef/provider/chef_environment' -require 'chef/provider/chef_group' -require 'chef/provider/chef_mirror' -require 'chef/provider/chef_node' -require 'chef/provider/chef_organization' -require 'chef/provider/chef_role' -require 'chef/provider/chef_user' -require 'chef/provider/private_key' -require 'chef/provider/public_key' class Chef diff --git a/spec/integration/chef_acl_spec.rb b/spec/integration/chef_acl_spec.rb index d6c0308..d0c787b 100644 --- a/spec/integration/chef_acl_spec.rb +++ b/spec/integration/chef_acl_spec.rb @@ -1,7 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/chef_acl' -require 'chef/provider/chef_acl' require 'chef_zero/version' require 'uri' diff --git a/spec/integration/chef_client_spec.rb b/spec/integration/chef_client_spec.rb index 9e439f7..40151c8 100644 --- a/spec/integration/chef_client_spec.rb +++ b/spec/integration/chef_client_spec.rb @@ -2,7 +2,6 @@ require 'cheffish/rspec/chef_run_support' require 'support/key_support' require 'chef/resource/chef_client' -require 'chef/provider/chef_client' repo_path = Dir.mktmpdir('chef_repo') diff --git a/spec/integration/chef_container_spec.rb b/spec/integration/chef_container_spec.rb index d892631..62d9485 100644 --- a/spec/integration/chef_container_spec.rb +++ b/spec/integration/chef_container_spec.rb @@ -1,7 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/chef_container' -require 'chef/provider/chef_container' describe Chef::Resource::ChefContainer do extend Cheffish::RSpec::ChefRunSupport diff --git a/spec/integration/chef_group_spec.rb b/spec/integration/chef_group_spec.rb index a737ca7..2814320 100644 --- a/spec/integration/chef_group_spec.rb +++ b/spec/integration/chef_group_spec.rb @@ -1,7 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/chef_group' -require 'chef/provider/chef_group' describe Chef::Resource::ChefGroup do extend Cheffish::RSpec::ChefRunSupport diff --git a/spec/integration/chef_mirror_spec.rb b/spec/integration/chef_mirror_spec.rb index 431b0f7..0817532 100644 --- a/spec/integration/chef_mirror_spec.rb +++ b/spec/integration/chef_mirror_spec.rb @@ -1,7 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/chef_mirror' -require 'chef/provider/chef_mirror' describe Chef::Resource::ChefMirror do extend Cheffish::RSpec::ChefRunSupport diff --git a/spec/integration/chef_node_spec.rb b/spec/integration/chef_node_spec.rb index ca7d546..8a883b8 100644 --- a/spec/integration/chef_node_spec.rb +++ b/spec/integration/chef_node_spec.rb @@ -1,7 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/chef_node' -require 'chef/provider/chef_node' describe Chef::Resource::ChefNode do extend Cheffish::RSpec::ChefRunSupport diff --git a/spec/integration/chef_organization_spec.rb b/spec/integration/chef_organization_spec.rb index 04ee465..43de7f9 100644 --- a/spec/integration/chef_organization_spec.rb +++ b/spec/integration/chef_organization_spec.rb @@ -1,7 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/chef_organization' -require 'chef/provider/chef_organization' describe Chef::Resource::ChefOrganization do extend Cheffish::RSpec::ChefRunSupport diff --git a/spec/integration/chef_role_spec.rb b/spec/integration/chef_role_spec.rb index 30df3f7..505caf5 100644 --- a/spec/integration/chef_role_spec.rb +++ b/spec/integration/chef_role_spec.rb @@ -1,7 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/chef_role' -require 'chef/provider/chef_role' describe Chef::Resource::ChefRole do extend Cheffish::RSpec::ChefRunSupport diff --git a/spec/integration/chef_user_spec.rb b/spec/integration/chef_user_spec.rb index b541f50..3a0e0ae 100644 --- a/spec/integration/chef_user_spec.rb +++ b/spec/integration/chef_user_spec.rb @@ -1,8 +1,6 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' require 'support/key_support' -require 'chef/resource/chef_user' -require 'chef/provider/chef_user' repo_path = Dir.mktmpdir('chef_repo') diff --git a/spec/integration/private_key_spec.rb b/spec/integration/private_key_spec.rb index 7b2926e..5d41d39 100644 --- a/spec/integration/private_key_spec.rb +++ b/spec/integration/private_key_spec.rb @@ -1,9 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/private_key' -require 'chef/provider/private_key' -require 'chef/resource/public_key' -require 'chef/provider/public_key' require 'support/key_support' repo_path = Dir.mktmpdir('chef_repo') diff --git a/spec/integration/recipe_dsl_spec.rb b/spec/integration/recipe_dsl_spec.rb index 191d1c1..a97b2d5 100644 --- a/spec/integration/recipe_dsl_spec.rb +++ b/spec/integration/recipe_dsl_spec.rb @@ -1,7 +1,5 @@ require 'support/spec_support' require 'cheffish/rspec/chef_run_support' -require 'chef/resource/chef_node' -require 'chef/provider/chef_node' require 'tmpdir' describe 'Cheffish Recipe DSL' do diff --git a/spec/support/spec_support.rb b/spec/support/spec_support.rb index 06951b5..2266f94 100644 --- a/spec/support/spec_support.rb +++ b/spec/support/spec_support.rb @@ -1,7 +1,6 @@ require 'cheffish/rspec' require 'cheffish' -require 'chef/provider/chef_acl' RSpec.configure do |config| config.filter_run :focus => true