cheffish-4.0.0/0000755000004100000410000000000012767772421013341 5ustar www-datawww-datacheffish-4.0.0/Rakefile0000644000004100000410000000177212767772421015015 0ustar www-datawww-datarequire 'bundler' require 'rubygems' require 'rubygems/package_task' require 'rdoc/task' require 'rspec/core/rake_task' Bundler::GemHelper.install_tasks task :default => :spec desc "Run specs" RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' end gem_spec = eval(File.read("cheffish.gemspec")) RDoc::Task.new do |rdoc| rdoc.rdoc_dir = 'rdoc' rdoc.title = "cheffish #{gem_spec.version}" rdoc.rdoc_files.include('README*') rdoc.rdoc_files.include('lib/**/*.rb') end begin require 'github_changelog_generator/task' GitHubChangelogGenerator::RakeTask.new :changelog do |config| require "cheffish/version" config.future_release = Cheffish::VERSION config.enhancement_labels = "enhancement,Enhancement,New Feature,Feature".split(",") config.bug_labels = "bug,Bug,Improvement,Upstream Bug".split(",") config.exclude_labels = "duplicate,question,invalid,wontfix,no_changelog,Exclude From Changelog,Question,Discussion".split(",") end rescue LoadError end cheffish-4.0.0/Gemfile0000644000004100000410000000070612767772421014637 0ustar www-datawww-datasource 'https://rubygems.org' gemspec group :changelog do gem 'github_changelog_generator' end group :development do gem 'rake' gem 'rspec', '~> 3.0' end # Allow Travis to run tests with different dependency versions if ENV['GEMFILE_MOD'] puts ENV['GEMFILE_MOD'] instance_eval(ENV['GEMFILE_MOD']) else group :development do gem "chef", git: "https://github.com/chef/chef" # until a version allowing chef-zero 5 is released end end cheffish-4.0.0/spec/0000755000004100000410000000000012767772421014273 5ustar www-datawww-datacheffish-4.0.0/spec/integration/0000755000004100000410000000000012767772421016616 5ustar www-datawww-datacheffish-4.0.0/spec/integration/chef_container_spec.rb0000644000004100000410000000155512767772421023132 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe Chef::Resource::ChefContainer do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server 'is in multi-org mode' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end it 'Converging chef_container "x" creates the container' do expect_recipe { chef_container 'x' }.to have_updated('chef_container[x]', :create) expect { get('containers/x') }.not_to raise_error end context 'and already has a container named x' do container 'x', {} it 'Converging chef_container "x" changes nothing' do expect_recipe { chef_container 'x' }.not_to have_updated('chef_container[x]', :create) end end end end cheffish-4.0.0/spec/integration/chef_group_spec.rb0000644000004100000410000002126112767772421022300 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe Chef::Resource::ChefGroup do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server 'is in multi-org mode' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end context 'and is empty' do group 'g', {} user 'u', {} client 'c', {} it 'Converging chef_group "x" creates the group with no members' do expect_recipe { chef_group 'x' }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => [], 'groups' => [], 'users' => [], 'clients' => [] }) end it 'chef_group "x" action :delete does nothing' do expect_recipe { chef_group 'x' do action :delete end }.to not_have_updated('chef_group[x]', :delete).and not_have_updated('chef_group[x]', :create) expect { get('groups/x') }.to raise_error(Net::HTTPServerException) end it 'Converging chef_group "x" creates the group with the given members' do expect_recipe { chef_group 'x' do groups 'g' users 'u' clients 'c' end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c u), 'groups' => %w(g), 'users' => %w(u), 'clients' => %w(c) }) end end context 'and has a group named x' do group 'g', {} group 'g2', {} group 'g3', {} group 'g4', {} user 'u', {} user 'u2', {} user 'u3', {} user 'u4', {} client 'c', {} client 'c2', {} client 'c3', {} client 'c4', {} group 'x', { 'users' => %w(u u2), 'clients' => %w(c c2), 'groups' => %w(g g2) } it 'Converging chef_group "x" changes nothing' do expect_recipe { chef_group 'x' }.not_to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c c2 u u2), 'groups' => %w(g g2), 'users' => %w(u u2), 'clients' => %w(c c2) }) end it 'chef_group "x" action :delete deletes the group' do expect_recipe { chef_group 'x' do action :delete end }.to have_updated('chef_group[x]', :delete) expect { get('groups/x') }.to raise_error(Net::HTTPServerException) end it 'Converging chef_group "x" with existing users changes nothing' do expect_recipe { chef_group 'x' do users 'u' clients 'c' groups 'g' end }.not_to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c c2 u u2), 'groups' => %w(g g2), 'users' => %w(u u2), 'clients' => %w(c c2) }) end it 'Converging chef_group "x" adds new users' do expect_recipe { chef_group 'x' do users 'u3' clients 'c3' groups 'g3' end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c c2 c3 u u2 u3), 'groups' => %w(g g2 g3), 'users' => %w(u u2 u3), 'clients' => %w(c c2 c3) }) end it 'Converging chef_group "x" with multiple users adds new users' do expect_recipe { chef_group 'x' do users 'u3', 'u4' clients 'c3', 'c4' groups 'g3', 'g4' end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c c2 c3 c4 u u2 u3 u4), 'groups' => %w(g g2 g3 g4), 'users' => %w(u u2 u3 u4), 'clients' => %w(c c2 c3 c4) }) end it 'Converging chef_group "x" with multiple users in an array adds new users' do expect_recipe { chef_group 'x' do users [ 'u3', 'u4' ] clients [ 'c3', 'c4' ] groups [ 'g3', 'g4' ] end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c c2 c3 c4 u u2 u3 u4), 'groups' => %w(g g2 g3 g4), 'users' => %w(u u2 u3 u4), 'clients' => %w(c c2 c3 c4) }) end it 'Converging chef_group "x" with multiple users declarations adds new users' do expect_recipe { chef_group 'x' do users 'u3' users 'u4' clients 'c3' clients 'c4' groups 'g3' groups 'g4' end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c c2 c3 c4 u u2 u3 u4), 'groups' => %w(g g2 g3 g4), 'users' => %w(u u2 u3 u4), 'clients' => %w(c c2 c3 c4) }) end it 'Converging chef_group "x" removes desired users' do expect_recipe { chef_group 'x' do remove_users 'u2' remove_clients 'c2' remove_groups 'g2' end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c u), 'groups' => %w(g), 'users' => %w(u), 'clients' => %w(c) }) end it 'Converging chef_group "x" with multiple users removes desired users' do expect_recipe { chef_group 'x' do remove_users 'u', 'u2' remove_clients 'c', 'c2' remove_groups 'g', 'g2' end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => [], 'groups' => [], 'users' => [], 'clients' => [] }) end it 'Converging chef_group "x" with multiple users in an array removes desired users' do expect_recipe { chef_group 'x' do remove_users [ 'u', 'u2' ] remove_clients [ 'c', 'c2' ] remove_groups [ 'g', 'g2' ] end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => [], 'groups' => [], 'users' => [], 'clients' => [] }) end it 'Converging chef_group "x" with multiple remove_ declarations removes desired users' do expect_recipe { chef_group 'x' do remove_users 'u' remove_users 'u2' remove_clients 'c' remove_clients 'c2' remove_groups 'g' remove_groups 'g2' end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => [], 'groups' => [], 'users' => [], 'clients' => [] }) end it 'Converging chef_group "x" adds and removes desired users' do expect_recipe { chef_group 'x' do users 'u3' clients 'c3' groups 'g3' remove_users 'u' remove_clients 'c' remove_groups 'g' end }.to have_updated('chef_group[x]', :create) expect(get('groups/x')).to eq({ 'name' => 'x', 'groupname' => 'x', 'orgname' => 'foo', 'actors' => %w(c2 c3 u2 u3), 'groups' => %w(g2 g3), 'users' => %w(u2 u3), 'clients' => %w(c2 c3) }) end end end end cheffish-4.0.0/spec/integration/chef_organization_spec.rb0000644000004100000410000002233212767772421023650 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe Chef::Resource::ChefOrganization do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server 'is in multi-org mode' do context 'and chef_server_url is pointed at the top level' do user 'u', {} user 'u2', {} it 'chef_organization "x" creates the organization' do expect_recipe { chef_organization 'x' }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('x') end end context 'and chef_server_url is pointed at /organizations/foo' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end context 'and is empty' do user 'u', {} user 'u2', {} it 'chef_organization "x" creates the organization' do expect_recipe { chef_organization 'x' }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('x') end it 'chef_organization "x" with full_name creates the organization' do expect_recipe { chef_organization 'x' do full_name 'Hi' end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('Hi') end it 'chef_organization "x" and inviting users creates the invites' do expect_recipe { chef_organization 'x' do invites 'u', 'u2' end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(u u2)) end it 'chef_organization "x" adds members' do expect_recipe { chef_organization 'x' do members 'u', 'u2' end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(u u2)) end end context 'and already has an organization named x' do user 'u', {} user 'u2', {} user 'u3', {} user 'member', {} user 'member2', {} user 'invited', {} user 'invited2', {} organization 'x', { 'full_name' => 'Lo' } do org_member 'member', 'member2' org_invite 'invited', 'invited2' end it 'chef_organization "x" changes nothing' do expect_recipe { chef_organization 'x' }.not_to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('Lo') end it 'chef_organization "x" with "complete true" reverts the full_name' do expect_recipe { chef_organization 'x' do complete true end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('x') end it 'chef_organization "x" with new full_name updates the organization' do expect_recipe { chef_organization 'x' do full_name 'Hi' end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('Hi') end context 'invites and membership tests' do it 'chef_organization "x" and inviting users creates the invites' do expect_recipe { chef_organization 'x' do invites 'u', 'u2' end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(invited invited2 u u2)) end it 'chef_organization "x" adds members' do expect_recipe { chef_organization 'x' do members 'u', 'u2' end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(member member2 u u2)) end it 'chef_organization "x" does nothing when inviting already-invited users and members' do expect_recipe { chef_organization 'x' do invites 'invited', 'member' end }.not_to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(invited invited2)) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(member member2)) end it 'chef_organization "x" does nothing when adding members who are already members' do expect_recipe { chef_organization 'x' do members 'member' end }.not_to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(invited invited2)) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(member member2)) end it 'chef_organization "x" upgrades invites to members when asked' do expect_recipe { chef_organization 'x' do members 'invited' end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(invited member member2)) expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(invited2)) end it 'chef_organization "x" removes members and invites when asked' do expect_recipe { chef_organization 'x' do remove_members 'invited', 'member' end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(invited2)) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(member2)) end it 'chef_organization "x" does nothing when asked to remove non-members' do expect_recipe { chef_organization 'x' do remove_members 'u', 'u2' end }.not_to have_updated('chef_organization[x]', :create) expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(invited invited2)) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(member member2)) end it 'chef_organization "x" with "complete true" reverts the full_name but does not remove invites or members' do expect_recipe { chef_organization 'x' do complete true end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('x') expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(invited invited2)) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(member member2)) end it 'chef_organization "x" with members [] and "complete true" removes invites and members' do expect_recipe { chef_organization 'x' do members [] complete true end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('x') expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq([]) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq([]) end it 'chef_organization "x" with invites [] and "complete true" removes invites but not members' do expect_recipe { chef_organization 'x' do invites [] complete true end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('x') expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq([]) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(member member2)) end it 'chef_organization "x" with invites, members and "complete true" removes all non-specified invites and members' do expect_recipe { chef_organization 'x' do invites 'invited', 'u' members 'member', 'u2' complete true end }.to have_updated('chef_organization[x]', :create) expect(get('/organizations/x')['full_name']).to eq('x') expect(get('/organizations/x/association_requests').map { |u| u['username'] }).to eq(%w(invited u)) expect(get('/organizations/x/users').map { |u| u['user']['username'] }).to eq(%w(member u2)) end end end end end end cheffish-4.0.0/spec/integration/private_key_spec.rb0000644000004100000410000003577612767772421022521 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' require 'support/key_support' repo_path = Dir.mktmpdir('chef_repo') describe Chef::Resource::PrivateKey do extend Cheffish::RSpec::ChefRunSupport before :each do FileUtils.remove_entry_secure(repo_path) Dir.mkdir(repo_path) end context 'with a recipe with a private_key' do it 'the private_key is created in pem format' do expect_recipe { private_key "#{repo_path}/blah" }.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN') expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) end end context 'with a private_key "blah" resource' do before :each do Dir.mkdir("#{repo_path}/other_keys") Chef::Config.private_key_paths = [ repo_path, "#{repo_path}/other_keys" ] end it 'the private key is created in the private_key_write_path' do expect_recipe { private_key 'blah' }.to have_updated "private_key[blah]", :create expect(Chef::Config.private_key_write_path).to eq(repo_path) expect(File.exist?("#{repo_path}/blah")).to be true expect(File.exist?("#{repo_path}/other_keys/blah")).to be false expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) expect(OpenSSL::PKey.read(Cheffish.get_private_key('blah'))).to be_kind_of(OpenSSL::PKey::RSA) end context 'and the private key already exists somewhere not in the write path' do before :each do recipe { private_key "#{repo_path}/other_keys/blah" }.converge end it 'the private expect(key).to not update' do expect_recipe { private_key 'blah' }.not_to have_updated "private_key[blah]", :create expect(File.exist?("#{repo_path}/blah")).to be false expect(File.exist?("#{repo_path}/other_keys/blah")).to be true end end end context 'with a private key' do before :each do Cheffish::BasicChefClient.converge_block do private_key "#{repo_path}/blah" end end context 'and a private_key that copies it in der format' do it 'the private_key is copied in der format and is identical' do expect_recipe { private_key "#{repo_path}/blah.der" do source_key_path "#{repo_path}/blah" format :der end }.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with('-----BEGIN') expect(key_str).not_to start_with('ssh-') expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah") end end it 'a private_key that copies it from in-memory as a string succeeds' do expect_recipe { private_key "#{repo_path}/blah.der" do source_key IO.read("#{repo_path}/blah") format :der end }.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with('-----BEGIN') expect(key_str).not_to start_with('ssh-') expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah") end it 'a private_key that copies it from in-memory as a key succeeds' do key = OpenSSL::PKey.read(IO.read("#{repo_path}/blah")) expect_recipe { private_key "#{repo_path}/blah.der" do source_key key format :der end }.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with('-----BEGIN') expect(key_str).not_to start_with('ssh-') expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah") end context 'and a public_key recipe' do it 'the public_key is created' do expect_recipe { public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" end }.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end context 'and a public key' do before :each do Cheffish::BasicChefClient.converge_block do public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" end end end context 'and public_key resource based off the public key file' do it 'the second public_key is created' do expect_recipe { public_key "#{repo_path}/blah.pub2" do source_key_path "#{repo_path}/blah.pub" end }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end context 'and another public_key based off the first public_key in-memory in a string' do it 'the second public_key is created' do expect_recipe { public_key "#{repo_path}/blah.pub2" do source_key IO.read("#{repo_path}/blah.pub") end }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end it 'and another public_key based off the first public_key in-memory in a key, the second public_key is created' do key, format = Cheffish::KeyFormatter.decode(IO.read("#{repo_path}/blah.pub")) expect_recipe { public_key "#{repo_path}/blah.pub2" do source_key key end }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end context 'and another public_key in :pem format based off the first public_key' do it 'the second public_key is created' do expect_recipe { public_key "#{repo_path}/blah.pub2" do source_key_path "#{repo_path}/blah.pub" format :pem end }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end context 'and another public_key in :der format based off the first public_key' do it 'the second public_key is created' do expect_recipe { public_key "#{repo_path}/blah.pub2" do source_key_path "#{repo_path}/blah.pub" format :pem end }.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end end context 'and a public_key resource in pem format' do it 'the public_key is created' do expect_recipe { public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" format :pem end }.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('-----BEGIN') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end context 'and a public_key resource in der format' do it 'the public_key is created in openssh format' do expect_recipe { public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" format :der end }.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).not_to start_with('-----BEGIN') expect(IO.read("#{repo_path}/blah.pub")).not_to start_with('ssh-rsa') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end end context 'with a recipe with a private_key in der format' do it 'the private_key is created' do expect_recipe { private_key "#{repo_path}/blah" do format :der end }.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).not_to start_with('-----BEGIN') expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) end end context 'with a private key in der format' do before :each do Cheffish::BasicChefClient.converge_block do private_key "#{repo_path}/blah" do format :der end end end context 'and a public_key' do it 'the public_key is created in openssh format' do expect_recipe { public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" end }.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end end context 'with a recipe with a private_key with a pass_phrase' do it 'the private_key is created' do expect_recipe { private_key "#{repo_path}/blah" do pass_phrase 'hello' end }.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN') expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"), 'hello')).to be_kind_of(OpenSSL::PKey::RSA) end end context 'with a private key with a pass phrase' do before :each do Cheffish::BasicChefClient.converge_block do private_key "#{repo_path}/blah" do pass_phrase 'hello' end end end context 'and a private_key that copies it in der format' do it 'the private_key is copied in der format and is identical' do expect_recipe { private_key "#{repo_path}/blah.der" do source_key_path "#{repo_path}/blah" source_key_pass_phrase 'hello' format :der end }.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with('-----BEGIN') expect(key_str).not_to start_with('ssh-') expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah", 'hello') end end context 'and a private_key resource pointing at it without a pass_phrase' do it 'the run fails with an exception' do expect { converge { private_key "#{repo_path}/blah" } }.to raise_error /missing pass phrase?/ end end context 'and a private_key resource with no pass phrase and regenerate_if_different' do it 'the private_key is regenerated' do expect_recipe { private_key "#{repo_path}/blah" do regenerate_if_different true end }.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN') expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) end end it 'a private_key resource that copies it from in-memory as a string succeeds' do expect_recipe { private_key "#{repo_path}/blah.der" do source_key IO.read("#{repo_path}/blah") source_key_pass_phrase 'hello' format :der end }.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with('-----BEGIN') expect(key_str).not_to start_with('ssh-') expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah", 'hello') end context 'and a public_key' do it 'the public_key is created in openssh format' do expect_recipe { public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" source_key_pass_phrase 'hello' end }.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah", 'hello' end end context 'and a public_key derived from the private key in an in-memory string' do it 'the public_key is created in openssh format' do expect_recipe { public_key "#{repo_path}/blah.pub" do source_key IO.read("#{repo_path}/blah") source_key_pass_phrase 'hello' end }.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah", 'hello' end end end context 'with a recipe with a private_key and public_key_path' do it 'the private_key and public_key are created' do expect_recipe { private_key "#{repo_path}/blah" do public_key_path "#{repo_path}/blah.pub" end }.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN') expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for("#{repo_path}/blah") end end context 'with a recipe with a private_key and public_key_path and public_key_format' do it 'the private_key and public_key are created' do expect_recipe { private_key "#{repo_path}/blah" do public_key_path "#{repo_path}/blah.pub.der" public_key_format :der end }.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with('-----BEGIN') expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) expect(IO.read("#{repo_path}/blah.pub.der")).not_to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub.der").to be_public_key_for("#{repo_path}/blah") end end context 'with a recipe with a private_key with path :none' do it 'the private_key is created' do got_private_key = nil expect_recipe { private_key 'in_memory' do path :none after { |resource, private_key| got_private_key = private_key } end }.to have_updated "private_key[in_memory]", :create expect(got_private_key).to be_kind_of(OpenSSL::PKey::RSA) end end end cheffish-4.0.0/spec/integration/chef_client_spec.rb0000644000004100000410000000667312767772421022434 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' require 'support/key_support' require 'chef/resource/chef_client' repo_path = Dir.mktmpdir('chef_repo') describe Chef::Resource::ChefClient do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server 'is in multi-org mode' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end context 'and is empty' do context 'and we have a private key with a path' do with_converge do private_key "#{repo_path}/blah.pem" end context 'and we run a recipe that creates client "blah"' do it 'the client gets created' do expect_recipe { chef_client 'blah' do source_key_path "#{repo_path}/blah.pem" end }.to have_updated 'chef_client[blah]', :create client = get('clients/blah') expect(client['name']).to eq('blah') key, format = Cheffish::KeyFormatter.decode(client['public_key']) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end context 'and we run a recipe that creates client "blah" with output_key_path' do with_converge do chef_client 'blah' do source_key_path "#{repo_path}/blah.pem" output_key_path "#{repo_path}/blah.pub" end end it 'the output public key gets created' do expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for("#{repo_path}/blah.pem") end end end context "and a private_key 'blah' resource" do before :each do Chef::Config.private_key_paths = [ repo_path ] end with_converge do private_key 'blah' end context "and a chef_client 'foobar' resource with source_key_path 'blah'" do it 'the client is accessible via the given private key' do expect_recipe { chef_client 'foobar' do source_key_path 'blah' end }.to have_updated 'chef_client[foobar]', :create client = get('clients/foobar') key, format = Cheffish::KeyFormatter.decode(client['public_key']) expect(key).to be_public_key_for("#{repo_path}/blah.pem") private_key = Cheffish::KeyFormatter.decode(Cheffish.get_private_key('blah')) expect(key).to be_public_key_for(private_key) end end end end end when_the_chef_server 'is in OSC mode' do context 'and is empty' do context 'and we have a private key with a path' do with_converge do private_key "#{repo_path}/blah.pem" end context 'and we run a recipe that creates client "blah"' do it 'the client gets created' do expect_recipe { chef_client 'blah' do source_key_path "#{repo_path}/blah.pem" end }.to have_updated 'chef_client[blah]', :create client = get('clients/blah') expect(client['name']).to eq('blah') key, format = Cheffish::KeyFormatter.decode(client['public_key']) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end end end end end cheffish-4.0.0/spec/integration/recipe_dsl_spec.rb0000644000004100000410000000126112767772421022266 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' require 'tmpdir' describe 'Cheffish Recipe DSL' do extend Cheffish::RSpec::ChefRunSupport context 'when we include with_chef_local_server' do before :each do @tmp_repo = tmp_repo = Dir.mktmpdir('chef_repo') end after :each do FileUtils.remove_entry_secure @tmp_repo end it 'chef_nodes get put into said server' do tmp_repo = @tmp_repo expect_recipe { with_chef_local_server :chef_repo_path => tmp_repo chef_node 'blah' }.to have_updated 'chef_node[blah]', :create expect(File).to exist("#{@tmp_repo}/nodes/blah.json") end end end cheffish-4.0.0/spec/integration/chef_node_spec.rb0000644000004100000410000006276012767772421022102 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe Chef::Resource::ChefNode do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server 'is in multi-org mode' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end context 'and is empty' do context 'and we run a recipe that creates node "blah"' do it 'the node gets created' do expect_recipe { chef_node 'blah' }.to have_updated 'chef_node[blah]', :create expect(get('nodes/blah')['name']).to eq('blah') end end # TODO why-run mode context 'and another chef server is running on port 8899' do before :each do @server = ChefZero::Server.new(:port => 8899) @server.start_background end after :each do @server.stop end context 'and a recipe is run that creates node "blah" on the second chef server using with_chef_server' do it 'the node is created on the second chef server but not the first' do expect_recipe { with_chef_server 'http://127.0.0.1:8899' chef_node 'blah' }.to have_updated 'chef_node[blah]', :create expect { get('nodes/blah') }.to raise_error(Net::HTTPServerException) expect(get('http://127.0.0.1:8899/nodes/blah')['name']).to eq('blah') end end context 'and a recipe is run that creates node "blah" on the second chef server using chef_server' do it 'the node is created on the second chef server but not the first' do expect_recipe { chef_node 'blah' do chef_server({ :chef_server_url => 'http://127.0.0.1:8899' }) end }.to have_updated 'chef_node[blah]', :create expect { get('nodes/blah') }.to raise_error(Net::HTTPServerException) expect(get('http://127.0.0.1:8899/nodes/blah')['name']).to eq('blah') end end end end context 'and has a node named "blah"' do node 'blah', {} it 'chef_node "blah" does not get created or updated' do expect_recipe { chef_node 'blah' }.not_to have_updated 'chef_node[blah]', :create end end context 'and has a node named "blah" with tags' do node 'blah', { 'normal' => { 'tags' => [ 'a', 'b' ] } } context 'with chef_node "blah" that sets attributes' do with_converge do chef_node 'blah' do attributes({}) end end it 'the tags in attributes are used' do expect(get('nodes/blah')['normal']['tags']).to eq([ 'a', 'b' ]) end end context 'with chef_node "blah" that sets attributes with tags in them' do with_converge do chef_node 'blah' do attributes 'tags' => [ 'c', 'd' ] end end it 'the tags in attributes are used' do expect(get('nodes/blah')['normal']['tags']).to eq([ 'c', 'd' ]) end end end describe '#complete' do context 'when the Chef server has a node named "blah" with everything in it' do node 'blah', { 'chef_environment' => 'blah', 'run_list' => [ 'recipe[bjork]' ], 'normal' => { 'foo' => 'bar', 'tags' => [ 'a', 'b' ] }, 'default' => { 'foo2' => 'bar2' }, 'automatic' => { 'foo3' => 'bar3' }, 'override' => { 'foo4' => 'bar4' } } it 'chef_node with no attributes modifies nothing' do expect_recipe { chef_node 'blah' }.to be_up_to_date expect(get('nodes/blah')).to include( 'name' => 'blah', 'chef_environment' => 'blah', 'run_list' => [ 'recipe[bjork]' ], 'normal' => { 'foo' => 'bar', 'tags' => [ 'a', 'b' ] }, 'default' => { 'foo2' => 'bar2' }, 'automatic' => { 'foo3' => 'bar3' }, 'override' => { 'foo4' => 'bar4' } ) end it 'chef_node with complete true removes everything except default, automatic and override' do expect_recipe { chef_node 'blah' do complete true end }.to be_updated expect(get('nodes/blah')).to include( 'name' => 'blah', 'chef_environment' => '_default', 'run_list' => [ ], 'normal' => { 'tags' => [ 'a', 'b' ] }, 'default' => { 'foo2' => 'bar2' }, 'automatic' => { 'foo3' => 'bar3' }, 'override' => { 'foo4' => 'bar4' } ) end it 'chef_node with complete true sets the given attributes' do expect_recipe { chef_node 'blah' do chef_environment 'x' run_list [ 'recipe[y]' ] attributes 'a' => 'b' tags 'c', 'd' complete true end }.to be_updated expect(get('nodes/blah')).to include( 'name' => 'blah', 'chef_environment' => 'x', 'run_list' => [ 'recipe[y]' ], 'normal' => { 'a' => 'b', 'tags' => [ 'c', 'd' ] }, 'default' => { 'foo2' => 'bar2' }, 'automatic' => { 'foo3' => 'bar3' }, 'override' => { 'foo4' => 'bar4' } ) end it 'chef_node with complete true and partial attributes sets the given attributes' do expect_recipe { chef_node 'blah' do chef_environment 'x' recipe 'y' attribute 'a', 'b' tags 'c', 'd' complete true end }.to be_updated expect(get('nodes/blah')).to include( 'name' => 'blah', 'chef_environment' => 'x', 'run_list' => [ 'recipe[y]' ], 'normal' => { 'a' => 'b', 'tags' => [ 'c', 'd' ] }, 'default' => { 'foo2' => 'bar2' }, 'automatic' => { 'foo3' => 'bar3' }, 'override' => { 'foo4' => 'bar4' } ) end end end describe '#attributes' do context 'with a node with normal attributes a => b and c => { d => e }' do node 'blah', { 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' } it 'chef_node with attributes {} removes all normal attributes but leaves tags, automatic and environment alone' do expect_recipe { chef_node 'blah' do attributes({}) end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attributes { c => d } replaces normal but not tags/automatic/environment' do expect_recipe { chef_node 'blah' do attributes 'c' => 'd' end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'c' => 'd', 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attributes { c => f => g, y => z } replaces normal but not tags/automatic/environment' do expect_recipe { chef_node 'blah' do attributes 'c' => { 'f' => 'g' }, 'y' => 'z' end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'c' => { 'f' => 'g' }, 'y' => 'z', 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attributes { tags => [ "x" ] } replaces normal and tags but not automatic/environment' do expect_recipe { chef_node 'blah' do attributes 'tags' => [ 'x' ] end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'tags' => [ 'x' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with tags "x" and attributes { "tags" => [ "y" ] } sets tags to "x"' do expect_recipe { chef_node 'blah' do tags 'x' attributes 'tags' => [ 'y' ] end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'tags' => [ 'x' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end end end describe '#attribute' do context 'with a node with normal attributes a => b and c => { d => e }' do node 'blah', { 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' } context 'basic scenarios' do it 'chef_node with no attributes, leaves it alone' do expect_recipe { chef_node 'blah' }.not_to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute d, e adds the attribute' do expect_recipe { chef_node 'blah' do attribute 'd', 'e' end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'd' => 'e', 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute tags, [ "x" ] replaces tags' do expect_recipe { chef_node 'blah' do attribute 'tags', [ 'x' ] end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'tags' => [ 'x' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute c, x replaces the attribute' do expect_recipe { chef_node 'blah' do attribute 'c', 'x' end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => 'x', 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute c, { d => x } replaces the attribute' do expect_recipe { chef_node 'blah' do attribute 'c', { 'd' => 'x' } end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'x' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [ c, d ], x replaces the attribute' do expect_recipe { chef_node 'blah' do attribute [ 'c', 'd' ], 'x' end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'x' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [ a, b ], x raises an error' do expect { converge { chef_node 'blah' do attribute [ 'a', 'b' ], 'x' end } }.to raise_error /Attempt to set \["a", "b"\] to x when \["a"\] is not a hash/ end it 'chef_node with attribute [ a, b, c ], x raises an error' do expect { converge { chef_node 'blah' do attribute [ 'a', 'b', 'c' ], 'x' end } }.to raise_error /Attempt to set \["a", "b", "c"\] to x when \["a"\] is not a hash/ end it 'chef_node with attribute [ x, y ], z adds a new attribute' do expect_recipe { chef_node 'blah' do attribute [ 'x', 'y' ], 'z' end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'x' => { 'y' => 'z' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [], {} clears all attributes' do expect_recipe { chef_node 'blah' do attribute([], {}) end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end end context 'delete' do it 'chef_node with attribute a, :delete deletes the attribute' do expect_recipe { chef_node 'blah' do attribute 'a', :delete end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute c, :delete deletes the attribute' do expect_recipe { chef_node 'blah' do attribute 'c', :delete end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [ c, d ], :delete deletes the attribute' do expect_recipe { chef_node 'blah' do attribute [ 'c', 'd' ], :delete end }.to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => {}, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute xyz, :delete does nothing' do expect_recipe { chef_node 'blah' do attribute 'xyz', :delete end }.not_to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [ c, x ], :delete does nothing' do expect_recipe { chef_node 'blah' do attribute [ 'c', 'x' ], :delete end }.not_to have_updated('chef_node[blah]', :create) expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end end context 'types' do it 'chef_node with attribute a, true sets a to true' do expect_recipe { chef_node 'blah' do attribute 'a', true end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => true, 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute a, 1 sets a to 1' do expect_recipe { chef_node 'blah' do attribute 'a', 1 end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 1, 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute a, "1" sets a to "1"' do expect_recipe { chef_node 'blah' do attribute 'a', "1" end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => "1", 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute a, "" sets a to ""' do expect_recipe { chef_node 'blah' do attribute 'a', "" end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => "", 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute a, nil sets a to nil' do expect_recipe { chef_node 'blah' do attribute 'a', nil end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => nil, 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end end context 'multiple attribute definitions' do it 'chef_node with attribute a, x and c, y replaces both attributes' do expect_recipe { chef_node 'blah' do attribute 'a', 'x' attribute 'c', 'y' end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'x', 'c' => 'y', 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute m, x and n, y adds both attributes' do expect_recipe { chef_node 'blah' do attribute 'm', 'x' attribute 'n', 'y' end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'm' => 'x', 'n' => 'y', 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [x, y], z and [x, yy], zz adds both attributes' do expect_recipe { chef_node 'blah' do attribute [ 'x', 'y' ], 'z' attribute [ 'x', 'yy' ], 'zz' end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'x' => { 'y' => 'z', 'yy' => 'zz' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end describe 'precedence' do it 'chef_node with attribute a, 1 and a, 2 sets a to 2' do expect_recipe { chef_node 'blah' do attribute 'a', 1 attribute 'a', 2 end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 2, 'c' => { 'd' => 'e' }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [ x, y ], 1 and [ x, y ], 2 sets [ x, y ], 2' do expect_recipe { chef_node 'blah' do attribute [ 'x', 'y' ], 1 attribute [ 'x', 'y' ], 2 end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e' }, 'x' => { 'y' => 2 }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [ c, e ], { a => 1 }, [ c, e ], { b => 2 } sets b only' do expect_recipe { chef_node 'blah' do attribute [ 'c', 'e' ], { 'a' => 1 } attribute [ 'c', 'e' ], { 'b' => 2 } end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e', 'e' => { 'b' => 2 } }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [ c, e ], { a => 1 }, [ c, e, b ], 2 sets both' do expect_recipe { chef_node 'blah' do attribute [ 'c', 'e' ], { 'a' => 1 } attribute [ 'c', 'e', 'b' ], 2 end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e', 'e' => { 'a' => 1, 'b' => 2 } }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end it 'chef_node with attribute [ c, e, b ], 2, [ c, e ], { a => 1 } sets a only' do expect_recipe { chef_node 'blah' do attribute [ 'c', 'e', 'b' ], 2 attribute [ 'c', 'e' ], { 'a' => 1 } end }.to be_updated expect(get('nodes/blah')).to include( 'normal' => { 'a' => 'b', 'c' => { 'd' => 'e', 'e' => { 'a' => 1 } }, 'tags' => [ 'a', 'b' ] }, 'automatic' => { 'x' => 'y' }, 'chef_environment' => 'desert' ) end end end end end end when_the_chef_server 'is in OSC mode' do context 'and is empty' do context 'and we run a recipe that creates node "blah"' do it 'the node gets created' do expect_recipe { chef_node 'blah' }.to have_updated 'chef_node[blah]', :create expect(get('nodes/blah')['name']).to eq('blah') end end end end end cheffish-4.0.0/spec/integration/chef_acl_spec.rb0000644000004100000410000010316412767772421021706 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' require 'chef_zero/version' require 'uri' if Gem::Version.new(ChefZero::VERSION) >= Gem::Version.new('3.1') describe Chef::Resource::ChefAcl do extend Cheffish::RSpec::ChefRunSupport # let(:chef_config) { super().merge(log_level: :debug, stdout: STDOUT, stderr: STDERR, log_location: STDOUT) } context "Rights attributes" do when_the_chef_server 'has a node named x', :osc_compat => false do node 'x', {} it 'Converging chef_acl "nodes/x" changes nothing' do expect_recipe { chef_acl 'nodes/x' }.to be_up_to_date expect(get('nodes/x/_acl')).to partially_match({}) end it 'Converging chef_acl "nodes/x" with "complete true" and no rights raises an error' do expect_converge { chef_acl 'nodes/x' do complete true end }.to raise_error(RuntimeError) end it 'Removing all :grant rights from a node raises an error' do expect_converge { chef_acl 'nodes/x' do remove_rights :grant, users: %w(pivotal), groups: %w(admins users clients) end }.to raise_error(RuntimeError) end context 'and a user "blarghle"' do user 'blarghle', {} it 'Converging chef_acl "nodes/x" with user "blarghle" adds the user' do expect_recipe { chef_acl 'nodes/x' do rights :read, users: %w(blarghle) end }.to be_updated expect(get('nodes/x/_acl')).to partially_match('read' => { 'actors' => %w(blarghle) }) end it 'Converging chef_acl "nodes/x" with "complete true" removes all ACLs except those specified' do expect_recipe { chef_acl 'nodes/x' do rights :grant, users: %w(blarghle) complete true end }.to be_updated expect(get('nodes/x/_acl')).to eq( "create"=>{"actors"=>[], "groups"=>[]}, "read" =>{"actors"=>[], "groups"=>[]}, "update"=>{"actors"=>[], "groups"=>[]}, "delete"=>{"actors"=>[], "groups"=>[]}, "grant" =>{"actors"=>["blarghle"], "groups"=>[]} ) end end it 'Converging chef_acl "nodes/x" with "complete true" removes all ACLs except those specified in :all' do expect_recipe { chef_acl 'nodes/x' do rights :all, users: %w(blarghle) complete true end }.to be_updated expect(get('nodes/x/_acl')).to eq( "create"=>{"actors"=>["blarghle"], "groups"=>[]}, "read" =>{"actors"=>["blarghle"], "groups"=>[]}, "update"=>{"actors"=>["blarghle"], "groups"=>[]}, "delete"=>{"actors"=>["blarghle"], "groups"=>[]}, "grant" =>{"actors"=>["blarghle"], "groups"=>[]} ) end context 'and a client "blarghle"' do user 'blarghle', {} it 'Converging chef_acl "nodes/x" with client "blarghle" adds the client' do expect_recipe { chef_acl 'nodes/x' do rights :read, clients: %w(blarghle) end }.to be_updated expect(get('nodes/x/_acl')).to partially_match('read' => { 'actors' => %w(blarghle) }) end end context 'and a group "blarghle"' do group 'blarghle', {} it 'Converging chef_acl "nodes/x" with group "blarghle" adds the group' do expect_recipe { chef_acl 'nodes/x' do rights :read, groups: %w(blarghle) end }.to be_updated expect(get('nodes/x/_acl')).to partially_match('read' => { 'groups' => %w(blarghle) }) end end context 'and multiple users and groups' do user 'u1', {} user 'u2', {} user 'u3', {} client 'c1', {} client 'c2', {} client 'c3', {} group 'g1', {} group 'g2', {} group 'g3', {} it 'Converging chef_acls should ignore order of the values in the acls' do expect_recipe { chef_acl 'nodes/x' do rights :create, users: [ 'u1', 'u2', 'u3' ], clients: [ 'c1', 'c2', 'c3' ], groups: [ 'g1', 'g2', 'g3' ] end }.to be_updated expect_recipe { chef_acl 'nodes/x' do rights :create, users: [ 'u2', 'u3', 'u1' ], clients: [ 'c3', 'c2', 'c1' ], groups: [ 'g1', 'g2', 'g3' ] end }.to be_up_to_date end it 'Converging chef_acl "nodes/x" with multiple groups, users and clients in an acl makes the appropriate changes' do expect_recipe { chef_acl 'nodes/x' do rights :create, users: [ 'u1', 'u2', 'u3' ], clients: [ 'c1', 'c2', 'c3' ], groups: [ 'g1', 'g2', 'g3' ] end }.to be_updated expect(get('nodes/x/_acl')).to partially_match( 'create' => { 'groups' => %w(g1 g2 g3), 'actors' => %w(u1 u2 u3 c1 c2 c3) } ) end it 'Converging chef_acl "nodes/x" with multiple groups, users and clients across multiple "rights" groups makes the appropriate changes' do expect_recipe { chef_acl 'nodes/x' do rights :create, users: %w(u1), clients: %w(c1), groups: %w(g1) rights :create, users: %w(u2 u3), clients: %w(c2 c3), groups: %w(g2) rights :read, users: %w(u1) rights :read, groups: %w(g1) end }.to be_updated expect(get('nodes/x/_acl')).to partially_match( 'create' => { 'groups' => %w(g1 g2), 'actors' => %w(u1 u2 u3 c1 c2 c3) }, 'read' => { 'groups' => %w(g1), 'actors' => %w(u1) } ) end it 'Converging chef_acl "nodes/x" with rights [ :read, :create, :update, :delete, :grant ] modifies all rights' do expect_recipe { chef_acl 'nodes/x' do rights [ :create, :read, :update, :delete, :grant ], users: %w(u1 u2), clients: %w(c1), groups: %w(g1) end }.to be_updated expect(get('nodes/x/_acl')).to partially_match( 'create' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, 'read' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, 'update' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, 'delete' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, 'grant' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, ) end it 'Converging chef_acl "nodes/x" with rights :all modifies all rights' do expect_recipe { chef_acl 'nodes/x' do rights :all, users: %w(u1 u2), clients: %w(c1), groups: %w(g1) end }.to be_updated expect(get('nodes/x/_acl')).to partially_match( 'create' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, 'read' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, 'update' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, 'delete' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, 'grant' => { 'groups' => %w(g1), 'actors' => %w(u1 u2 c1) }, ) end end it 'Converging chef_acl "nodes/y" throws a 404' do expect_converge { chef_acl 'nodes/y' }.to raise_error(Net::HTTPServerException) end end when_the_chef_server 'has a node named x with user blarghle in its acl', :osc_compat => false do user 'blarghle', {} node 'x', {} do acl 'read' => { 'actors' => %w(blarghle) } end it 'Converging chef_acl "nodes/x" with that user changes nothing' do expect_recipe { chef_acl 'nodes/x' do rights :read, users: %w(blarghle) end }.to be_up_to_date expect(get('nodes/x/_acl')).to partially_match({}) end end when_the_chef_server 'has a node named x with users foo and bar in all its acls', :osc_compat => false do user 'foo', {} user 'bar', {} node 'x', {} do acl 'create' => { 'actors' => %w(foo bar) }, 'read' => { 'actors' => %w(foo bar) }, 'update' => { 'actors' => %w(foo bar) }, 'delete' => { 'actors' => %w(foo bar) }, 'grant' => { 'actors' => %w(foo bar) } end it 'Converging chef_acl "nodes/x" with remove_rights :all removes foo from everything' do expect_recipe { chef_acl 'nodes/x' do remove_rights :all, users: %w(foo) end }.to be_updated expect(get('nodes/x/_acl')).to partially_match( 'create' => { 'actors' => exclude('foo') }, 'read' => { 'actors' => exclude('foo') }, 'update' => { 'actors' => exclude('foo') }, 'delete' => { 'actors' => exclude('foo') }, 'grant' => { 'actors' => exclude('foo') }, ) end end ::RSpec::Matchers.define_negated_matcher :exclude, :include context 'recursive' do when_the_chef_server 'has a nodes container with user blarghle in its acl', :osc_compat => false do user 'blarghle', {} acl_for 'containers/nodes', 'read' => { 'actors' => %w(blarghle) } node 'x', {} do acl 'read' => { 'actors' => [] } end it 'Converging chef_acl "nodes" makes no changes' do expect { expect_recipe { chef_acl 'nodes' do rights :read, users: %w(blarghle) end }.to be_up_to_date }.to not_change { get('containers/nodes/_acl') }. and not_change { get('nodes/x/_acl') } end RSpec::Matchers.define_negated_matcher :not_change, :change it 'Converging chef_acl "nodes" with recursive :on_change makes no changes' do expect { expect_recipe { chef_acl 'nodes' do rights :read, users: %w(blarghle) recursive :on_change end }.to be_up_to_date }.to not_change { get('containers/nodes/_acl') }. and not_change { get('nodes/x/_acl') } end it 'Converging chef_acl "nodes" with recursive true changes nodes/x\'s acls' do expect_recipe { chef_acl 'nodes' do rights :read, users: %w(blarghle) recursive true end }.to be_updated expect(get('nodes/x/_acl')).to partially_match('read' => { 'actors' => %w(blarghle) }) end it 'Converging chef_acl "" with recursive false does not change nodes/x\'s acls' do expect_recipe { chef_acl '' do rights :read, users: %w(blarghle) recursive false end }.to be_updated expect(get('containers/nodes/_acl')).to partially_match({}) expect(get('nodes/x/_acl')).to partially_match({}) end it 'Converging chef_acl "" with recursive :on_change does not change nodes/x\'s acls' do expect_recipe { chef_acl '' do rights :read, users: %w(blarghle) recursive :on_change end }.to be_updated expect(get('containers/nodes/_acl')).to partially_match({}) expect(get('nodes/x/_acl')).to partially_match({}) end it 'Converging chef_acl "" with recursive true changes nodes/x\'s acls' do expect_recipe { chef_acl '' do rights :read, users: %w(blarghle) recursive true end }.to be_updated expect(get('/organizations/_acl')).to partially_match('read' => { 'actors' => %w(blarghle) }) expect(get('nodes/x/_acl')).to partially_match('read' => { 'actors' => %w(blarghle) }) end end end end context 'ACLs on each type of thing' do when_the_chef_server 'has an organization named foo', :osc_compat => false, :single_org => false do organization 'foo' do user 'u', {} client 'x', {} container 'x', {} cookbook 'x', '1.0.0', {} data_bag 'x', { 'y' => {} } environment 'x', {} group 'x', {} node 'x', {} role 'x', {} sandbox 'x', {} user 'x', {} end organization 'bar' do user 'u', {} node 'x', {} end context 'and the chef server URL points at /organizations/foo' do before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end context 'relative paths' do it "chef_acl 'nodes/x' changes the acls" do expect_recipe { chef_acl "nodes/x" do rights :read, users: %w(u) end }.to be_updated expect(get("nodes/x/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end it "chef_acl '*/*' changes the acls" do expect_recipe { chef_acl "*/*" do rights :read, users: %w(u) end }.to be_updated %w(clients containers cookbooks data environments groups nodes roles).each do |type| expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match( 'read' => { 'actors' => %w(u) }) end end end context 'absolute paths' do %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| it "chef_acl '/organizations/foo/#{type}/x' changes the acl" do expect_recipe { chef_acl "/organizations/foo/#{type}/x" do rights :read, users: %w(u) end }.to be_updated expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| it "chef_acl '/organizations/foo/#{type}/x' changes the acl" do expect_recipe { chef_acl "/organizations/foo/#{type}/x" do rights :read, users: %w(u) end }.to be_updated expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end %w(clients containers cookbooks data environments groups nodes roles).each do |type| it "chef_acl '/*/*/#{type}/*' changes the acl" do expect_recipe { chef_acl "/*/*/#{type}/*" do rights :read, users: %w(u) end }.to be_updated expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end it "chef_acl '/*/*/*/x' changes the acls" do expect_recipe { chef_acl "/*/*/*/x" do rights :read, users: %w(u) end }.to be_updated %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match( 'read' => { 'actors' => %w(u) }) end end it "chef_acl '/*/*/*/*' changes the acls" do expect_recipe { chef_acl "/*/*/*/*" do rights :read, users: %w(u) end }.to be_updated %w(clients containers cookbooks data environments groups nodes roles).each do |type| expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match( 'read' => { 'actors' => %w(u) }) end end it 'chef_acl "/organizations/foo/data_bags/x" changes the acl' do expect_recipe { chef_acl '/organizations/foo/data_bags/x' do rights :read, users: %w(u) end }.to be_updated expect(get('/organizations/foo/data/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "/*/*/data_bags/*" changes the acl' do expect_recipe { chef_acl '/*/*/data_bags/*' do rights :read, users: %w(u) end }.to be_updated expect(get('/organizations/foo/data/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it "chef_acl '/organizations/foo/cookbooks/x/1.0.0' raises an error" do expect_converge { chef_acl "/organizations/foo/cookbooks/x/1.0.0" do rights :read, users: %w(u) end }.to raise_error(/ACLs cannot be set on children of \/organizations\/foo\/cookbooks\/x/) end it "chef_acl '/organizations/foo/cookbooks/*/*' raises an error" do pending expect_converge { chef_acl "/organizations/foo/cookbooks/*/*" do rights :read, users: %w(u) end }.to raise_error(/ACLs cannot be set on children of \/organizations\/foo\/cookbooks\/*/) end it 'chef_acl "/organizations/foo/data/x/y" raises an error' do expect_converge { chef_acl '/organizations/foo/data/x/y' do rights :read, users: %w(u) end }.to raise_error(/ACLs cannot be set on children of \/organizations\/foo\/data\/x/) end it 'chef_acl "/organizations/foo/data/*/*" raises an error' do pending expect_converge { chef_acl '/organizations/foo/data/*/*' do rights :read, users: %w(u) end }.to raise_error(/ACLs cannot be set on children of \/organizations\/foo\/data\/*/) end it 'chef_acl "/organizations/foo" changes the acl' do expect_recipe { chef_acl '/organizations/foo' do rights :read, users: %w(u) end }.to be_updated expect(get('/organizations/foo/organizations/_acl')).to partially_match('read' => { 'actors' => %w(u) }) expect(get('/organizations/foo/nodes/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "/organizations/*" changes the acl' do expect_recipe { chef_acl '/organizations/*' do rights :read, users: %w(u) end }.to be_updated expect(get('/organizations/foo/organizations/_acl')).to partially_match('read' => { 'actors' => %w(u) }) expect(get('/organizations/foo/nodes/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "/users/x" changes the acl' do expect_recipe { chef_acl '/users/x' do rights :read, users: %w(u) end }.to be_updated expect(get('/users/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "/users/*" changes the acl' do expect_recipe { chef_acl '/users/*' do rights :read, users: %w(u) end }.to be_updated expect(get('/users/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "/*/x" changes the acl' do expect_recipe { chef_acl '/*/x' do rights :read, users: %w(u) end }.to be_updated expect(get('/users/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "/*/*" changes the acl' do expect_recipe { chef_acl '/*/*' do rights :read, users: %w(u) end }.to be_updated expect(get('/organizations/foo/organizations/_acl')).to partially_match('read' => { 'actors' => %w(u) }) expect(get('/users/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end end end context 'and the chef server URL points at /organizations/bar' do before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url.to_s, '/organizations/bar').to_s end it "chef_acl '/organizations/foo/nodes/*' changes the acl" do expect_recipe { chef_acl "/organizations/foo/nodes/*" do rights :read, users: %w(u) end }.to be_updated expect(get("/organizations/foo/nodes/x/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end context 'and the chef server URL points at /' do before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url.to_s, '/').to_s end it "chef_acl '/organizations/foo/nodes/*' changes the acl" do expect_recipe { chef_acl "/organizations/foo/nodes/*" do rights :read, users: %w(u) end }.to be_updated expect(get("/organizations/foo/nodes/x/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end end when_the_chef_server 'has a user "u" in single org mode', :osc_compat => false do user 'u', {} client 'x', {} container 'x', {} cookbook 'x', '1.0.0', {} data_bag 'x', { 'y' => {} } environment 'x', {} group 'x', {} node 'x', {} role 'x', {} sandbox 'x', {} user 'x', {} %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| it "chef_acl #{type}/x' changes the acl" do expect_recipe { chef_acl "#{type}/x" do rights :read, users: %w(u) end }.to be_updated expect(get("#{type}/x/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end %w(clients containers cookbooks data environments groups nodes roles).each do |type| it "chef_acl '#{type}/*' changes the acl" do expect_recipe { chef_acl "#{type}/*" do rights :read, users: %w(u) end }.to be_updated expect(get("#{type}/x/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end it "chef_acl '*/x' changes the acls" do expect_recipe { chef_acl "*/x" do rights :read, users: %w(u) end }.to be_updated %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| expect(get("#{type}/x/_acl")).to partially_match( 'read' => { 'actors' => %w(u) }) end end it "chef_acl '*/*' changes the acls" do expect_recipe { chef_acl "*/*" do rights :read, users: %w(u) end }.to be_updated %w(clients containers cookbooks data environments groups nodes roles).each do |type| expect(get("#{type}/x/_acl")).to partially_match( 'read' => { 'actors' => %w(u) }) end end it "chef_acl 'groups/*' changes the acl" do expect_recipe { chef_acl "groups/*" do rights :read, users: %w(u) end }.to be_updated %w(admins billing-admins clients users x).each do |n| expect(get("groups/#{n}/_acl")).to partially_match( 'read' => { 'actors' => %w(u) }) end end it 'chef_acl "data_bags/x" changes the acl' do expect_recipe { chef_acl 'data_bags/x' do rights :read, users: %w(u) end }.to be_updated expect(get('data/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "data_bags/*" changes the acl' do expect_recipe { chef_acl 'data_bags/*' do rights :read, users: %w(u) end }.to be_updated expect(get('data/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "" changes the organization acl' do expect_recipe { chef_acl '' do rights :read, users: %w(u) end }.to be_updated expect(get('/organizations/_acl')).to partially_match('read' => { 'actors' => %w(u) }) expect(get('nodes/x/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end end end context 'ACLs on each container type' do when_the_chef_server 'has an organization named foo', :osc_compat => false, :single_org => false do organization 'foo' do user 'u', {} client 'x', {} container 'x', {} cookbook 'x', '1.0.0', {} data_bag 'x', { 'y' => {} } environment 'x', {} group 'x', {} node 'x', {} role 'x', {} sandbox 'x', {} user 'x', {} end %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| it "chef_acl '/organizations/foo/#{type}' changes the acl" do expect_recipe { chef_acl "/organizations/foo/#{type}" do rights :read, users: %w(u) end }.to be_updated expect(get("/organizations/foo/containers/#{type}/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end %w(clients containers cookbooks data environments groups nodes roles).each do |type| it "chef_acl '/*/*/#{type}' changes the acl" do expect_recipe { chef_acl "/*/*/#{type}" do rights :read, users: %w(u) end }.to be_updated expect(get("/organizations/foo/containers/#{type}/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end it "chef_acl '/*/*/*' changes the acls" do expect_recipe { chef_acl "/*/*/*" do rights :read, users: %w(u) end }.to be_updated %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| expect(get("/organizations/foo/containers/#{type}/_acl")).to partially_match( 'read' => { 'actors' => %w(u) }) end end it 'chef_acl "/organizations/foo/data_bags" changes the acl' do expect_recipe { chef_acl '/organizations/foo/data_bags' do rights :read, users: %w(u) end }.to be_updated expect(get('/organizations/foo/containers/data/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end it 'chef_acl "/*/*/data_bags" changes the acl' do expect_recipe { chef_acl '/*/*/data_bags' do rights :read, users: %w(u) end }.to be_updated expect(get('/organizations/foo/containers/data/_acl')).to partially_match('read' => { 'actors' => %w(u) }) end end when_the_chef_server 'has a user "u" in single org mode', :osc_compat => false do user 'u', {} client 'x', {} container 'x', {} cookbook 'x', '1.0.0', {} data_bag 'x', { 'y' => {} } environment 'x', {} group 'x', {} node 'x', {} role 'x', {} sandbox 'x', {} user 'x', {} %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| it "chef_acl #{type}' changes the acl" do expect_recipe { chef_acl "#{type}" do rights :read, users: %w(u) end }.to be_updated expect(get("containers/#{type}/_acl")).to partially_match('read' => { 'actors' => %w(u) }) end end it "chef_acl '*' changes the acls" do expect_recipe { chef_acl "*" do rights :read, users: %w(u) end }.to be_updated %w(clients containers cookbooks data environments groups nodes roles sandboxes).each do |type| expect(get("containers/#{type}/_acl")).to partially_match( 'read' => { 'actors' => %w(u) }) end end end end context 'remove_rights' do when_the_chef_server 'has a node "x" with "u", "c" and "g" in its acl', :osc_compat => false do user 'u', {} user 'u2', {} client 'c', {} client 'c2', {} group 'g', {} group 'g2', {} node 'x', {} do acl 'create' => { 'actors' => [ 'u', 'c' ], 'groups' => [ 'g' ] }, 'read' => { 'actors' => [ 'u', 'c' ], 'groups' => [ 'g' ] }, 'update' => { 'actors' => [ 'u', 'c' ], 'groups' => [ 'g' ] } end it 'chef_acl with remove_rights "u" removes the user\'s rights' do expect_recipe { chef_acl "nodes/x" do remove_rights :read, users: %w(u) end }.to be_updated expect(get("nodes/x/_acl")).to partially_match('read' => { 'actors' => exclude('u') }) end it 'chef_acl with remove_rights "c" removes the client\'s rights' do expect_recipe { chef_acl "nodes/x" do remove_rights :read, clients: %w(c) end }.to be_updated expect(get("nodes/x/_acl")).to partially_match('read' => { 'actors' => exclude('c') }) end it 'chef_acl with remove_rights "g" removes the group\'s rights' do expect_recipe { chef_acl "nodes/x" do remove_rights :read, groups: %w(g) end }.to be_updated expect(get("nodes/x/_acl")).to partially_match( 'read' => { 'groups' => exclude('g') } ) end it 'chef_acl with remove_rights [ :create, :read ], "u", "c", "g" removes all three' do expect_recipe { chef_acl "nodes/x" do remove_rights [ :create, :read ], users: %w(u), clients: %w(c), groups: %w(g) end }.to be_updated expect(get("nodes/x/_acl")).to partially_match( 'create' => { 'actors' => exclude('u').and(exclude('c')), 'groups' => exclude('g') }, 'read' => { 'actors' => exclude('u').and(exclude('c')), 'groups' => exclude('g') } ) end it 'chef_acl with remove_rights "u2", "c2", "g2" has no effect' do expect { expect_recipe { chef_acl "nodes/x" do remove_rights :read, users: %w(u2), clients: %w(c2), groups: %w(g2) end }.to be_up_to_date }.not_to change { get("nodes/x/_acl") } end end end when_the_chef_server 'has a node named data_bags', :osc_compat => false do user 'blarghle', {} node 'data_bags', {} it 'Converging chef_acl "nodes/data_bags" with user "blarghle" adds the user' do expect_recipe { chef_acl 'nodes/data_bags' do rights :read, users: %w(blarghle) end }.to be_updated expect(get('nodes/data_bags/_acl')).to partially_match('read' => { 'actors' => %w(blarghle) }) end end when_the_chef_server 'has a node named data_bags in multi-org mode', :osc_compat => false, :single_org => false do user 'blarghle', {} organization 'foo' do node 'data_bags', {} end it 'Converging chef_acl "/organizations/foo/nodes/data_bags" with user "blarghle" adds the user' do expect_recipe { chef_acl '/organizations/foo/nodes/data_bags' do rights :read, users: %w(blarghle) end }.to be_updated expect(get('/organizations/foo/nodes/data_bags/_acl')).to partially_match('read' => { 'actors' => %w(blarghle) }) end end when_the_chef_server 'has a user named data_bags in multi-org mode', :osc_compat => false, :single_org => false do user 'data_bags', {} user 'blarghle', {} it 'Converging chef_acl "/users/data_bags" with user "blarghle" adds the user' do expect_recipe { chef_acl '/users/data_bags' do rights :read, users: %w(blarghle) end }.to be_updated expect(get('/users/data_bags/_acl')).to partially_match('read' => { 'actors' => %w(blarghle) }) end end end end cheffish-4.0.0/spec/integration/chef_role_spec.rb0000644000004100000410000000474212767772421022112 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe Chef::Resource::ChefRole do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server 'is in multi-org mode' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end context 'and is empty' do context 'and we run a recipe that creates role "blah"' do it 'the role gets created' do expect_recipe { chef_role 'blah' }.to have_updated 'chef_role[blah]', :create expect(get('roles/blah')['name']).to eq('blah') end end # TODO why-run mode context 'and another chef server is running on port 8899' do before :each do @server = ChefZero::Server.new(:port => 8899) @server.start_background end after :each do @server.stop end context 'and a recipe is run that creates role "blah" on the second chef server using with_chef_server' do it 'the role is created on the second chef server but not the first' do expect_recipe { with_chef_server 'http://127.0.0.1:8899' chef_role 'blah' }.to have_updated 'chef_role[blah]', :create expect { get('roles/blah') }.to raise_error(Net::HTTPServerException) expect(get('http://127.0.0.1:8899/roles/blah')['name']).to eq('blah') end end context 'and a recipe is run that creates role "blah" on the second chef server using chef_server' do it 'the role is created on the second chef server but not the first' do expect_recipe { chef_role 'blah' do chef_server({ :chef_server_url => 'http://127.0.0.1:8899' }) end }.to have_updated 'chef_role[blah]', :create expect { get('roles/blah') }.to raise_error(Net::HTTPServerException) expect(get('http://127.0.0.1:8899/roles/blah')['name']).to eq('blah') end end end end end when_the_chef_server 'is in OSC mode' do context 'and is empty' do context 'and we run a recipe that creates role "blah"' do it 'the role gets created' do expect_recipe { chef_role 'blah' }.to have_updated 'chef_role[blah]', :create expect(get('roles/blah')['name']).to eq('blah') end end end end end cheffish-4.0.0/spec/integration/chef_data_bag_item_spec.rb0000644000004100000410000000143312767772421023703 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe Chef::Resource::ChefDataBagItem do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server 'foo' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end context 'when data bag "bag" exists' do with_converge { chef_data_bag 'bag' } it 'runs a recipe that creates a chef_data_bag_item "bag/item"' do expect_recipe { chef_data_bag_item 'bag/item' }.to have_updated 'chef_data_bag_item[bag/item]', :create # expect(get('data_bags/bag')['name']).to eq('bag') # expect(get('data_bags/bag/item')['id']).to eq('item') end end end end cheffish-4.0.0/spec/integration/rspec/0000755000004100000410000000000012767772421017732 5ustar www-datawww-datacheffish-4.0.0/spec/integration/rspec/converge_spec.rb0000644000004100000410000001171412767772421023105 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe 'Cheffish::RSpec::ChefRunSupport' do extend Cheffish::RSpec::ChefRunSupport let(:temp_file) { Tempfile.new('test') } context "#recipe" do it "recipe { file ... } updates the file" do result = recipe { file temp_file.path do content 'test' end } expect(result.updated?).to be_falsey expect(IO.read(temp_file.path)).to eq '' end it "recipe 'file ...' does not update the file" do result = recipe <<-EOM file temp_file.path do content 'test' end EOM expect(result.updated?).to be_falsey expect(IO.read(temp_file.path)).to eq '' end it "recipe 'file ...' with file and line number does not update the file" do result = recipe(<<-EOM, __FILE__, __LINE__+1) file temp_file.path do content 'test' end EOM expect(result.updated?).to be_falsey expect(IO.read(temp_file.path)).to eq '' end end context "#converge" do it "converge { file ... } updates the file" do result = converge { file temp_file.path do content 'test' end } expect(result.updated?).to be_truthy expect(IO.read(temp_file.path)).to eq 'test' end it "converge 'file ...' updates the file" do result = converge <<-EOM file temp_file.path do content 'test' end EOM expect(result.updated?).to be_truthy expect(IO.read(temp_file.path)).to eq 'test' end it "converge 'file ...' with file and line number updates the file" do result = converge(<<-EOM, __FILE__, __LINE__+1) file temp_file.path do content 'test' end EOM expect(result.updated?).to be_truthy expect(IO.read(temp_file.path)).to eq 'test' end end context "#expect_recipe" do it "expect_recipe { file ... }.to be_updated updates the file, and be_idempotent does not fail" do expect_recipe { file temp_file.path do content 'test' end }.to be_updated.and be_idempotent expect(IO.read(temp_file.path)).to eq 'test' end it "expect_recipe 'file ...'.to be_updated updates the file, and be_idempotent does not fail" do expect_recipe(<<-EOM).to be_updated.and be_idempotent file temp_file.path do content 'test' end EOM expect(IO.read(temp_file.path)).to eq 'test' end it "expect_recipe('file ...', file, line).to be_updated updates the file, and be_idempotent does not fail" do expect_recipe(<<-EOM, __FILE__, __LINE__+1).to be_updated.and be_idempotent file temp_file.path do content 'test' end EOM expect(IO.read(temp_file.path)).to eq 'test' end it "expect_recipe { file ... }.to be_up_to_date fails" do expect { expect_recipe { file temp_file.path do content 'test' end }.to be_up_to_date }.to raise_error RSpec::Expectations::ExpectationNotMetError end it "expect_recipe { }.to be_updated fails" do expect { expect_recipe { }.to be_updated }.to raise_error RSpec::Expectations::ExpectationNotMetError end it "expect_recipe { }.to be_up_to_date succeeds" do expect_recipe { }.to be_up_to_date end it "expect_recipe { }.to be_idempotent succeeds" do expect_recipe { }.to be_idempotent end end context "#expect_converge" do it "expect_converge { file ... }.not_to raise_error updates the file" do expect_converge { file temp_file.path do content 'test' end }.not_to raise_error expect(IO.read(temp_file.path)).to eq 'test' end it "expect_converge('file ...').not_to raise_error updates the file" do expect_converge(<<-EOM).not_to raise_error file temp_file.path do content 'test' end EOM expect(IO.read(temp_file.path)).to eq 'test' end it "expect_converge('file ...', file, line).not_to raise_error updates the file" do expect_converge(<<-EOM, __FILE__, __LINE__+1).not_to raise_error file temp_file.path do content 'test' end EOM expect(IO.read(temp_file.path)).to eq 'test' end it "expect_converge { raise 'oh no' }.to raise_error passes" do expect_converge { raise 'oh no' }.to raise_error('oh no') end end context "when there is a let variable" do let(:let_variable) { "test" } it "converge { let_variable } accesses it" do # Capture the variable outside x = nil converge { x = let_variable } expect(x).to eq 'test' end it "converge with a file resource referencing let_variable accesses let_variable" do converge { file temp_file.path do content let_variable end } expect(IO.read(temp_file.path)).to eq 'test' end end end cheffish-4.0.0/spec/integration/chef_mirror_spec.rb0000644000004100000410000004521312767772421022461 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe Chef::Resource::ChefMirror do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server 'is in multi-org mode' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end describe 'basic download and upload' do when_the_repository 'is full of stuff' do file 'nodes/x.json', {} file 'roles/x.json', {} directory 'cookbooks/x' do file 'metadata.rb', 'name "x"; version "2.0.0"' end it "Download grabs defaults" do expect_recipe { chef_mirror '' do action :download end }.to have_updated('chef_mirror[]', :download) expect(File.exist?(path_to('groups/admins.json'))).to be true expect(File.exist?(path_to('environments/_default.json'))).to be true end it "Upload uploads everything" do expect_recipe { chef_mirror '' do action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('nodes/x') }.not_to raise_error expect { get('roles/x') }.not_to raise_error expect { get('cookbooks/x/2.0.0') }.not_to raise_error end it 'chef_mirror with concurrency 0 fails with a reasonable message' do expect { converge { chef_mirror '' do concurrency 0 action :download end } }.to raise_error /chef_mirror.concurrency must be above 0/ end end end context 'and the Chef server has a node and role in it' do node 'x', {} role 'x', {} when_the_repository 'is empty' do it "Download grabs the node and role" do expect_recipe { chef_mirror '' do action :download end }.to have_updated('chef_mirror[]', :download) expect(File.exist?(path_to('nodes/x.json'))).to be true expect(File.exist?(path_to('roles/x.json'))).to be true end it "Upload uploads nothing" do expect_recipe { chef_mirror '' do action :upload end }.not_to have_updated('chef_mirror[]', :upload) end end end context 'and the Chef server has nodes and roles named x' do node 'x', {} role 'x', {} when_the_repository 'has nodes and roles named y' do file 'nodes/y.json', {} file 'roles/y.json', {} it "Download grabs the x's" do expect_recipe { chef_mirror '' do action :download end }.to have_updated('chef_mirror[]', :download) expect(File.exist?(path_to('nodes/x.json'))).to be true expect(File.exist?(path_to('roles/x.json'))).to be true expect(File.exist?(path_to('nodes/y.json'))).to be true expect(File.exist?(path_to('roles/y.json'))).to be true end it "Upload uploads the y's" do expect_recipe { chef_mirror '' do action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('nodes/x') }.not_to raise_error expect { get('roles/x') }.not_to raise_error expect { get('nodes/y') }.not_to raise_error expect { get('roles/y') }.not_to raise_error end it "Download with purge grabs the x's and deletes the y's" do expect_recipe { chef_mirror '' do purge true action :download end }.to have_updated('chef_mirror[]', :download) expect(File.exist?(path_to('nodes/x.json'))).to be true expect(File.exist?(path_to('roles/x.json'))).to be true end it "Upload with :purge uploads the y's and deletes the x's" do expect_recipe { chef_mirror '*/*.json' do purge true action :upload end }.to have_updated('chef_mirror[*/*.json]', :upload) expect { get('nodes/y') }.not_to raise_error expect { get('roles/y') }.not_to raise_error end end end describe "chef_repo_path" do when_the_repository 'has stuff but no chef_repo_path' do file 'repo/nodes/x.json', {} file 'repo/roles/x.json', {} file 'repo2/nodes/y.json', {} file 'repo2/roles/y.json', {} before do Chef::Config.delete(:chef_repo_path) Chef::Config.delete(:node_path) Chef::Config.delete(:cookbook_path) Chef::Config.delete(:role_path) end it "Upload with chef_repo_path('repo') uploads everything" do repo_path = path_to('repo') expect_recipe { chef_mirror '' do chef_repo_path repo_path action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('nodes/x') }.not_to raise_error expect { get('roles/x') }.not_to raise_error expect { get('nodes/y') }.to raise_error /404/ expect { get('roles/y') }.to raise_error /404/ end it "Upload with chef_repo_path(:chef_repo_path) with multiple paths uploads everything" do repo_path = path_to('repo') repo2_path = path_to('repo2') expect_recipe { chef_mirror '' do chef_repo_path :chef_repo_path => [ repo_path, repo2_path ] action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('nodes/x') }.not_to raise_error expect { get('roles/x') }.not_to raise_error expect { get('nodes/y') }.not_to raise_error expect { get('roles/y') }.not_to raise_error end it "Upload with chef_repo_path(:node_path, :role_path) uploads everything" do repo_path = path_to('repo') repo2_path = path_to('repo2') expect_recipe { chef_mirror '' do chef_repo_path :chef_repo_path => '/blahblah', :node_path => "#{repo_path}/nodes", :role_path => "#{repo2_path}/roles" action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('nodes/x') }.not_to raise_error expect { get('roles/x') }.to raise_error /404/ expect { get('nodes/y') }.to raise_error /404/ expect { get('roles/y') }.not_to raise_error end it "Upload with chef_repo_path(:chef_repo_path, :role_path) uploads everything" do repo_path = path_to('repo') repo2_path = path_to('repo2') expect_recipe { chef_mirror '' do chef_repo_path :chef_repo_path => repo_path, :role_path => "#{repo2_path}/roles" action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('nodes/x') }.not_to raise_error expect { get('roles/x') }.to raise_error /404/ expect { get('nodes/y') }.to raise_error /404/ expect { get('roles/y') }.not_to raise_error end it "Upload with chef_repo_path(:node_path, :role_path) with multiple paths uploads everything" do repo_path = path_to('repo') repo2_path = path_to('repo2') expect_recipe { chef_mirror '' do chef_repo_path :chef_repo_path => [ 'foo', 'bar' ], :node_path => [ "#{repo_path}/nodes", "#{repo2_path}/nodes" ], :role_path => [ "#{repo_path}/roles", "#{repo2_path}/roles" ] action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('nodes/x') }.not_to raise_error expect { get('roles/x') }.not_to raise_error expect { get('nodes/y') }.not_to raise_error expect { get('roles/y') }.not_to raise_error end end end describe "cookbook upload, chef_repo_path and versioned_cookbooks" do when_the_repository 'has cookbooks in non-versioned format' do file 'cookbooks/x-1.0.0/metadata.rb', 'name "x-1.0.0"; version "2.0.0"' file 'cookbooks/y-1.0.0/metadata.rb', 'name "y-3.0.0"; version "4.0.0"' it "chef_mirror :upload uploads everything" do expect_recipe { chef_mirror '' do action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('cookbooks/x-1.0.0/2.0.0') }.not_to raise_error expect { get('cookbooks/y-3.0.0/4.0.0') }.not_to raise_error end context 'and Chef::Config.versioned_cookbooks is false' do before do Chef::Config.versioned_cookbooks false end it "chef_mirror :upload uploads everything" do expect_recipe { chef_mirror '' do action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('cookbooks/x-1.0.0/2.0.0') }.not_to raise_error expect { get('cookbooks/y-3.0.0/4.0.0') }.not_to raise_error end end context 'and Chef::Config.chef_repo_path is not set but versioned_cookbooks is false' do before do Chef::Config.delete(:chef_repo_path) Chef::Config.versioned_cookbooks false end it "chef_mirror :upload with chef_repo_path and versioned_cookbooks false uploads cookbooks with name including version" do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir versioned_cookbooks false action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('cookbooks/x-1.0.0/2.0.0') }.not_to raise_error expect { get('cookbooks/y-3.0.0/4.0.0') }.not_to raise_error end end end when_the_repository 'has cookbooks in versioned_cookbook format' do file 'cookbooks/x-1.0.0/metadata.rb', 'name "x"; version "1.0.0"' file 'cookbooks/x-2.0.0/metadata.rb', 'name "x"; version "2.0.0"' context 'and Chef::Config.versioned_cookbooks is true' do before do Chef::Config.versioned_cookbooks true end it "chef_mirror :upload uploads everything" do expect_recipe { chef_mirror '' do action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('cookbooks/x/1.0.0') }.not_to raise_error expect { get('cookbooks/x/2.0.0') }.not_to raise_error end end context 'and Chef::Config.chef_repo_path set somewhere else' do before do Chef::Config.chef_repo_path = '/x/y/z' end it "chef_mirror :upload with chef_repo_path uploads cookbooks" do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('cookbooks/x/1.0.0') }.not_to raise_error expect { get('cookbooks/x/2.0.0') }.not_to raise_error end end context 'and Chef::Config.chef_repo_path is not set but versioned_cookbooks is false' do before do Chef::Config.delete(:chef_repo_path) Chef::Config.versioned_cookbooks false end it "chef_mirror :upload with chef_repo_path uploads cookbooks with name split from version" do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('cookbooks/x/1.0.0') }.not_to raise_error expect { get('cookbooks/x/2.0.0') }.not_to raise_error end it "chef_mirror :upload with chef_repo_path and versioned_cookbooks uploads cookbooks with name split from version" do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir versioned_cookbooks true action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('cookbooks/x/1.0.0') }.not_to raise_error expect { get('cookbooks/x/2.0.0') }.not_to raise_error end end context 'and Chef::Config.chef_repo_path is not set but versioned_cookbooks is true' do before do Chef::Config.delete(:chef_repo_path) Chef::Config.versioned_cookbooks true end it "chef_mirror :upload with chef_repo_path uploads cookbooks with name split from version" do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir action :upload end }.to have_updated('chef_mirror[]', :upload) expect { get('cookbooks/x/1.0.0') }.not_to raise_error expect { get('cookbooks/x/2.0.0') }.not_to raise_error end end end end describe 'cookbook download, chef_repo_path, and versioned_cookbooks' do context 'when the Chef server has a cookbook with multiple versions' do cookbook 'x', '1.0.0', 'metadata.rb' => 'name "x"; version "1.0.0"' cookbook 'x', '2.0.0', 'metadata.rb' => 'name "x"; version "2.0.0"' when_the_repository 'is empty' do it 'chef_mirror :download downloads the latest version of the cookbook' do expect_recipe { chef_mirror '' do action :download end }.to have_updated('chef_mirror[]', :download) expect(File.read(path_to('cookbooks/x/metadata.rb'))).to eq('name "x"; version "2.0.0"') end it 'chef_mirror :download with versioned_cookbooks = true downloads all versions of the cookbook' do expect_recipe { chef_mirror '' do versioned_cookbooks true action :download end }.to have_updated('chef_mirror[]', :download) expect(File.read(path_to('cookbooks/x-1.0.0/metadata.rb'))).to eq('name "x"; version "1.0.0"') expect(File.read(path_to('cookbooks/x-2.0.0/metadata.rb'))).to eq('name "x"; version "2.0.0"') end context 'and Chef::Config.chef_repo_path is set elsewhere' do before do Chef::Config.chef_repo_path = '/x/y/z' end it 'chef_mirror :download with chef_repo_path downloads all versions of the cookbook' do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir action :download end }.to have_updated('chef_mirror[]', :download) expect(File.read(path_to('cookbooks/x-1.0.0/metadata.rb'))).to eq('name "x"; version "1.0.0"') expect(File.read(path_to('cookbooks/x-2.0.0/metadata.rb'))).to eq('name "x"; version "2.0.0"') end it 'chef_mirror :download with chef_repo_path and versioned_cookbooks = false downloads the latest version of the cookbook' do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir versioned_cookbooks false action :download end }.to have_updated('chef_mirror[]', :download) expect(File.read(path_to('cookbooks/x/metadata.rb'))).to eq('name "x"; version "2.0.0"') end end context 'and Chef::Config.versioned_cookbooks is true' do before do Chef::Config.versioned_cookbooks = true end it 'chef_mirror :download downloads all versions of the cookbook' do expect_recipe { chef_mirror '' do action :download end }.to have_updated('chef_mirror[]', :download) expect(File.read(path_to('cookbooks/x-1.0.0/metadata.rb'))).to eq('name "x"; version "1.0.0"') expect(File.read(path_to('cookbooks/x-2.0.0/metadata.rb'))).to eq('name "x"; version "2.0.0"') end it 'chef_mirror :download with versioned_cookbooks = false downloads the latest version of the cookbook' do expect_recipe { chef_mirror '' do versioned_cookbooks false action :download end }.to have_updated('chef_mirror[]', :download) expect(File.read(path_to('cookbooks/x/metadata.rb'))).to eq('name "x"; version "2.0.0"') end context 'and Chef::Config.chef_repo_path is set elsewhere' do before do Chef::Config.chef_repo_path = '/x/y/z' end it 'chef_mirror :download with chef_repo_path downloads all versions of the cookbook' do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir action :download end }.to have_updated('chef_mirror[]', :download) expect(File.read(path_to('cookbooks/x-1.0.0/metadata.rb'))).to eq('name "x"; version "1.0.0"') expect(File.read(path_to('cookbooks/x-2.0.0/metadata.rb'))).to eq('name "x"; version "2.0.0"') end it 'chef_mirror :download with chef_repo_path and versioned_cookbooks = false downloads the latest version of the cookbook' do repository_dir = @repository_dir expect_recipe { chef_mirror '' do chef_repo_path repository_dir versioned_cookbooks false action :download end }.to have_updated('chef_mirror[]', :download) expect(File.read(path_to('cookbooks/x/metadata.rb'))).to eq('name "x"; version "2.0.0"') end end end end end end end end cheffish-4.0.0/spec/integration/chef_user_spec.rb0000644000004100000410000000530512767772421022123 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' require 'support/key_support' repo_path = Dir.mktmpdir('chef_repo') describe Chef::Resource::ChefUser do extend Cheffish::RSpec::ChefRunSupport with_converge do private_key "#{repo_path}/blah.pem" end when_the_chef_server 'is empty' do context 'and we run a recipe that creates user "blah"'do it 'the user gets created' do expect_recipe { chef_user 'blah' do source_key_path "#{repo_path}/blah.pem" end }.to have_updated 'chef_user[blah]', :create user = get('/users/blah') expect(user['name']).to eq('blah') key, format = Cheffish::KeyFormatter.decode(user['public_key']) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end context 'and we run a recipe that creates user "blah" with output_key_path' do with_converge do chef_user 'blah' do source_key_path "#{repo_path}/blah.pem" output_key_path "#{repo_path}/blah.pub" end end it 'the output public key gets created' do expect(IO.read("#{repo_path}/blah.pub")).to start_with('ssh-rsa ') expect("#{repo_path}/blah.pub").to be_public_key_for("#{repo_path}/blah.pem") end end end when_the_chef_12_server 'is in multi-org mode' do context 'and chef_server_url is pointed at the top level' do context 'and we run a recipe that creates user "blah"'do it 'the user gets created' do expect_recipe { chef_user 'blah' do source_key_path "#{repo_path}/blah.pem" end }.to have_updated 'chef_user[blah]', :create user = get('/users/blah') expect(user['name']).to eq('blah') key, format = Cheffish::KeyFormatter.decode(user['public_key']) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end end context 'and chef_server_url is pointed at /organizations/foo' do organization 'foo' before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, '/organizations/foo').to_s end context 'and we run a recipe that creates user "blah"'do it 'the user gets created' do expect_recipe { chef_user 'blah' do source_key_path "#{repo_path}/blah.pem" end }.to have_updated 'chef_user[blah]', :create user = get('/users/blah') expect(user['name']).to eq('blah') key, format = Cheffish::KeyFormatter.decode(user['public_key']) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end end end end cheffish-4.0.0/spec/unit/0000755000004100000410000000000012767772421015252 5ustar www-datawww-datacheffish-4.0.0/spec/unit/recipe_run_wrapper_spec.rb0000644000004100000410000000177212767772421022513 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' # require 'cheffish/rspec/recipe_run_wrapper' module MyModule def respond_to_missing?(name, *args) if name == :allowable_method true else false end end end describe Cheffish::RSpec::RecipeRunWrapper do extend Cheffish::RSpec::ChefRunSupport let(:run_wrapper) { Cheffish::RSpec::RecipeRunWrapper.new(chef_config) do log "test recipe in specs" end } context "defines #respond_to_missing? on the client" do it "calls the new super.respond_to_missing" do run_wrapper.client.extend MyModule expect(run_wrapper.client.respond_to?(:allowable_method)).to be_truthy expect(run_wrapper.client.respond_to?(:not_an_allowable_method)).to be_falsey end end context "does not define #respond_to_missing? on the client" do it "calls the original super.respond_to_missing" do expect(run_wrapper.client.respond_to?(:nonexistent_method)).to be_falsey end end end cheffish-4.0.0/spec/unit/get_private_key_spec.rb0000644000004100000410000001035312767772421021774 0ustar www-datawww-datarequire 'support/spec_support' require 'cheffish/rspec/chef_run_support' describe Cheffish do let(:directory_that_exists) { Dir.mktmpdir("cheffish-rspec") } let(:directory_that_does_not_exist) { dir = Dir.mktmpdir("cheffish-rspec") FileUtils.remove_entry dir dir } let(:private_key_contents) { "contents of private key" } let(:private_key_pem_contents) { "contents of private key pem" } let(:private_key_garbage_contents) { "da vinci virus" } def setup_key key_file = File.expand_path("ned_stark", directory_that_exists) File.open(key_file, "w+") do |f| f.write private_key_contents end key_file end def setup_arbitrarily_named_key key_file = File.expand_path("ned_stark.xxx", directory_that_exists) File.open(key_file, "w+") do |f| f.write private_key_contents end key_file end def setup_pem_key key_file = File.expand_path("ned_stark.pem", directory_that_exists) File.open(key_file, "w+") do |f| f.write private_key_pem_contents end key_file end def setup_garbage_key key_file = File.expand_path("ned_stark.pem.bak", directory_that_exists) File.open(key_file, "w+") do |f| f.write private_key_garbage_contents end key_file end shared_examples_for "returning the contents of the key file if it finds one" do it "returns nil if it cannot find the private key file" do expect(Cheffish.get_private_key("ned_stark", config)).to be_nil end it "returns the contents of the key if it doesn't have an extension" do setup_key expect(Cheffish.get_private_key("ned_stark", config)).to eq(private_key_contents) end it "returns the contents of the key if it has an extension" do setup_pem_key expect(Cheffish.get_private_key("ned_stark", config)).to eq(private_key_pem_contents) end it "returns the contents of arbitrarily named keys" do setup_arbitrarily_named_key expect(Cheffish.get_private_key("ned_stark.xxx", config)).to eq(private_key_contents) end # we arbitrarily prefer "ned_stark" over "ned_stark.pem" for deterministic behavior it "returns the contents of the key that does not have an extension if both exist" do setup_key setup_pem_key expect(Cheffish.get_private_key("ned_stark", config)).to eq(private_key_contents) end end describe "#get_private_key" do context "when private_key_paths has a directory which is empty" do let(:config) { { :private_key_paths => [ directory_that_exists ] } } it_behaves_like "returning the contents of the key file if it finds one" context "when it also has a garbage file" do before { setup_garbage_key } it "does not return the da vinci virus if we find only the garbage file" do setup_garbage_key expect(Cheffish.get_private_key("ned_stark", config)).to be_nil end it_behaves_like "returning the contents of the key file if it finds one" end end context "when private_key_paths leads with a directory that does not exist and then an empty directory" do let(:config) { { :private_key_paths => [ directory_that_does_not_exist, directory_that_exists ] } } it_behaves_like "returning the contents of the key file if it finds one" end context "when private_keys is empty" do let(:config) { { :private_keys => {} } } it "returns nil" do expect(Cheffish.get_private_key("ned_stark", config)).to be_nil end end context "when private_keys contains the path to a key" do let(:name) { "ned_stark" } let(:config) { { :private_keys => {name => setup_key} } } it "returns the contents of the key file" do setup_key expect(Cheffish.get_private_key(name, config)).to eq(private_key_contents) end end context "when private_keys contains the path to a key" do let(:name) { "ned_stark" } let(:key) {double("key", :to_pem => private_key_contents)} let(:config) { { :private_keys => {name => key} } } it "returns the contents of the key file" do expect(Cheffish.get_private_key(name, config)).to eq(private_key_contents) end end end end cheffish-4.0.0/spec/support/0000755000004100000410000000000012767772421016007 5ustar www-datawww-datacheffish-4.0.0/spec/support/spec_support.rb0000644000004100000410000000036312767772421021064 0ustar www-datawww-datarequire 'cheffish/rspec' require 'cheffish' RSpec.configure do |config| config.filter_run :focus => true config.run_all_when_everything_filtered = true config.before :each do Chef::Config.reset end end require 'chef/providers' cheffish-4.0.0/spec/support/key_support.rb0000644000004100000410000000227212767772421020723 0ustar www-datawww-dataRSpec::Matchers.define :be_public_key_for do |private_key, pass_phrase| match do |public_key| if public_key.is_a?(String) public_key, public_key_format = Cheffish::KeyFormatter.decode(IO.read(File.expand_path(public_key)), pass_phrase, public_key) end if private_key.is_a?(String) private_key, private_key_format = Cheffish::KeyFormatter.decode(IO.read(File.expand_path(private_key)), pass_phrase, private_key) end encrypted = public_key.public_encrypt('hi there') expect(private_key.private_decrypt(encrypted)).to eq('hi there') end end RSpec::Matchers.define :match_private_key do |expected, pass_phrase| match do |actual| if expected.is_a?(String) expected, format = Cheffish::KeyFormatter.decode(IO.read(File.expand_path(expected)), pass_phrase, expected) end if actual.is_a?(String) actual, format = Cheffish::KeyFormatter.decode(IO.read(File.expand_path(actual)), pass_phrase, actual) end encrypted = actual.public_encrypt('hi there') expect(expected.private_decrypt(encrypted)).to eq('hi there') encrypted = expected.public_encrypt('hi there') expect(actual.private_decrypt(encrypted)).to eq('hi there') end end cheffish-4.0.0/spec/functional/0000755000004100000410000000000012767772421016435 5ustar www-datawww-datacheffish-4.0.0/spec/functional/server_api_spec.rb0000644000004100000410000000043612767772421022136 0ustar www-datawww-datarequire 'cheffish' describe "api version" do let(:server_api) do Cheffish.chef_server_api({:chef_server_url => "my.chef.server"}) end it "is pinned to 0" do expect(Cheffish::ServerAPI).to receive(:new).with("my.chef.server", {api_version: "0"}) server_api end end cheffish-4.0.0/spec/functional/fingerprint_spec.rb0000644000004100000410000001045112767772421022324 0ustar www-datawww-datarequire 'cheffish/key_formatter' require 'support/key_support' describe 'Cheffish fingerprint key formatter' do # Sample key: 0x9a6fa4c43b328c3d04c1fbc0498539218b6728e41cd35f6d27d491ef705f0b2083dc1ac977da19f54ba82b044773f20667e9627c543abb3b41b6eb9e4318ca3c68f487bbd0f1c9eea9a3101b7d1d180983c5440ac4183e78e9e256fa687d8aac63b21617a4b02b35bf5e307a3b76961a16cd8493e923536b34cc2b2da8d45220d57ef2243b081b555b84f1da0ade0e896c2aa96911b41430b59eaf75dbffb7eaa7c5b3a686f2d47a24e3b7f1acb0844f84a2fedc63660ae366b800cd9448093d6b1d96503ebb7807b48257e16c3d8a7c9a8cc5dd63116aa673bd9e09754de09358486e743e34c6a3642eeb64b2208efc96df39151572557a75638bd059c21a55 = 0xd6e92677d4e1d2aa6d14f87b5f49ee6916c6b92411536254fae4a21e82eebb0a40600247c701c1c938b21ca9f25b7b330c35fded57b4de3a951e83329a80bdbf2ba138fe2f190bffce43967b5fa93b179367bcd15cb1db7f9e3ab62caca95dc9489b62bc0a10b53841b932455a43409f96eed90dc80abc8cce5593ead8f0a26d * 0xb7f68cd427045788d5e315375f71d3a416784ec2597776a60ed77c821294d9bd66e96658bdcb43072cee0c849d297bd9f94991738f1a0df313ceb51b093a9372f12a61987f40e7a03d773911deb270916a574962ae8ff4f2d8bfcedee1c885e9c3e54212471636a6330b05b78c3a7ddf96b013be389a08ab7971db2f68fb2689 sample_private_key = < format}) end context 'when computing key fingperprints' do it 'computes the PKCS#8 SHA1 private key fingerprint correctly', :pending => (RUBY_VERSION.to_f >= 2.0) do expect(key_to_format(sample_private_key, :pkcs8sha1fingerprint)).to eq( '88:7e:3a:bd:26:9f:b5:c5:d8:ae:52:f9:df:0b:64:a4:5c:17:0a:87') end it 'computes the PKCS#1 MD5 public key fingerprint correctly' do expect(key_to_format(sample_public_key, :pkcs1md5fingerprint)).to eq( '1f:e8:da:c1:16:c3:72:7d:90:e2:b7:64:c4:b4:55:20') end it 'computes the RFC4716 MD5 public key fingerprint correctly' do expect(key_to_format(sample_public_key, :rfc4716md5fingerprint)).to eq( 'b0:13:4f:da:cf:8c:dc:a7:4a:1f:d2:3a:51:92:cf:6b') end it 'defaults to the PKCS#1 MD5 public key fingerprint' do expect(key_to_format(sample_public_key, :fingerprint)).to eq( key_to_format(sample_public_key, :pkcs1md5fingerprint)) end end end cheffish-4.0.0/spec/functional/merged_config_spec.rb0000644000004100000410000000073712767772421022573 0ustar www-datawww-datarequire 'cheffish/merged_config' describe "merged_config" do let(:config) do Cheffish::MergedConfig.new({:test=>'val'}) end it "returns value in config" do expect(config.test).to eq('val') end it "raises a NoMethodError if calling an unknown method with arguments" do expect{config.merge({:some => 'hash'})}.to raise_error(NoMethodError) end it "has an informative string representation" do expect("#{config}").to eq("{:test=>\"val\"}") end endcheffish-4.0.0/lib/0000755000004100000410000000000012767772421014107 5ustar www-datawww-datacheffish-4.0.0/lib/cheffish/0000755000004100000410000000000012767772421015666 5ustar www-datawww-datacheffish-4.0.0/lib/cheffish/with_pattern.rb0000644000004100000410000000066712767772421020734 0ustar www-datawww-datamodule Cheffish module WithPattern def with(symbol) class_eval < 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 cheffish-4.0.0/lib/cheffish/merged_config.rb0000644000004100000410000000356512767772421021014 0ustar www-datawww-datamodule Cheffish class MergedConfig def initialize(*configs) @configs = configs @merge_arrays = {} end include Enumerable attr_reader :configs def merge_arrays(*symbols) if symbols.size > 0 symbols.each do |symbol| @merge_arrays[symbol] = true end else @merge_arrays end end def [](name) if @merge_arrays[name] configs.select { |c| !c[name].nil? }.collect_concat { |c| c[name] } else result_configs = [] configs.each do |config| value = config[name] if !value.nil? if value.respond_to?(:keys) result_configs << value elsif result_configs.size > 0 return result_configs[0] else return value end end end if result_configs.size > 1 MergedConfig.new(*result_configs) elsif result_configs.size == 1 result_configs[0] else nil end end end def method_missing(name, *args) if args.count > 0 raise NoMethodError, "Unexpected method #{name} for MergedConfig with arguments #{args}" else self[name] end end def key?(name) configs.any? { |config| config.has_key?(name) } end alias_method :has_key?, :key? def keys configs.map { |c| c.keys }.flatten(1).uniq end def values keys.map { |key| self[key] } end def each_pair(&block) each(&block) end def each keys.each do |key| if block_given? yield key, self[key] end end end def to_hash result = {} each_pair do |key, value| result[key] = value end result end def to_h to_hash end def to_s to_hash.to_s end end end cheffish-4.0.0/lib/cheffish/server_api.rb0000644000004100000410000000312412767772421020352 0ustar www-datawww-data# # Author:: John Keiser () # Copyright:: Copyright (c) 2012 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # require 'chef/version' require 'chef/http' require 'chef/http/authenticator' require 'chef/http/cookie_manager' require 'chef/http/decompressor' require 'chef/http/json_input' require 'chef/http/json_output' if Gem::Version.new(Chef::VERSION) >= Gem::Version.new('11.12') require 'chef/http/remote_request_id' end module Cheffish # Exactly like Chef::ServerAPI, but requires you to pass in what keys you want (no defaults) class ServerAPI < Chef::HTTP def initialize(url, options = {}) super(url, options) root_url = URI.parse(url) root_url.path = '' @root_url = root_url.to_s end attr_reader :root_url use Chef::HTTP::JSONInput use Chef::HTTP::JSONOutput use Chef::HTTP::CookieManager use Chef::HTTP::Decompressor use Chef::HTTP::Authenticator if Gem::Version.new(Chef::VERSION) >= Gem::Version.new('11.12') use Chef::HTTP::RemoteRequestID end end end cheffish-4.0.0/lib/cheffish/chef_run.rb0000644000004100000410000001042212767772421020003 0ustar www-datawww-datarequire 'cheffish/basic_chef_client' module Cheffish class ChefRun # # @param chef_config A hash with symbol keys that looks suspiciously similar to `Chef::Config`. # Some possible options: # - stdout: - where to stream stdout to # - stderr: - where to stream stderr to # - log_level: :debug|:info|:warn|:error|:fatal # - log_location: - where to stream logs to # - verbose_logging: true|false - true if you want verbose logging in :debug # def initialize(chef_config={}) @chef_config = chef_config || {} end attr_reader :chef_config class StringIOTee < StringIO def initialize(*streams) super() @streams = streams.flatten.select { |s| !s.nil? } end attr_reader :streams def write(*args, &block) super streams.each { |s| s.write(*args, &block) } end end def client @client ||= begin chef_config = self.chef_config.dup chef_config[:log_level] ||= :debug if !chef_config.has_key?(:log_level) chef_config[:verbose_logging] = false if !chef_config.has_key?(:verbose_logging) chef_config[:stdout] = StringIOTee.new(chef_config[:stdout]) chef_config[:stderr] = StringIOTee.new(chef_config[:stderr]) chef_config[:log_location] = StringIOTee.new(chef_config[:log_location]) @client = ::Cheffish::BasicChefClient.new(nil, [ event_sink, Chef::Formatters.new(:doc, chef_config[:stdout], chef_config[:stderr]) ], chef_config ) end end def event_sink @event_sink ||= EventSink.new end # # output # def stdout @client ? client.chef_config[:stdout].string : nil end def stderr @client ? client.chef_config[:stderr].string : nil end def logs @client ? client.chef_config[:log_location].string : nil end def logged_warnings logs.lines.select { |l| l =~ /^\[[^\]]*\] WARN:/ }.join("\n") end def logged_errors logs.lines.select { |l| l =~ /^\[[^\]]*\] ERROR:/ }.join("\n") end def logged_info logs.lines.select { |l| l =~ /^\[[^\]]*\] INFO:/ }.join("\n") end def resources client.run_context.resource_collection end def compile_recipe(&recipe) client.load_block(&recipe) end def converge begin client.converge @converged = true rescue RuntimeError => e @raised_exception = e raise end end def reset @client = nil @converged = nil @stdout = nil @stderr = nil @logs = nil @raised_exception = nil end def converged? !!@converged end def converge_failed? @raised_exception.nil? ? false : true end def updated? client.updated? end def up_to_date? !client.updated? end def output_for_failure_message message = "" if stdout && !stdout.empty? message << "--- ---\n" message << "--- Chef Client Output ---\n" message << "--- ---\n" message << stdout message << "\n" if !stdout.end_with?("\n") end if stderr && !stderr.empty? message << "--- ---\n" message << "--- Chef Client Error Output ---\n" message << "--- ---\n" message << stderr message << "\n" if !stderr.end_with?("\n") end if logs && !logs.empty? message << "--- ---\n" message << "--- Chef Client Logs ---\n" message << "--- ---\n" message << logs end message end class EventSink def initialize @events = [] end attr_reader :events def method_missing(method, *args) @events << [ method, *args ] end def respond_to_missing?(method_name, include_private = false) # Chef::EventDispatch::Dispatcher calls #respond_to? to see (basically) if we'll accept an event; # obviously, per above #method_missing, we'll accept whatever we're given. if there's a problem, it # will surface higher up the stack. true end end end end cheffish-4.0.0/lib/cheffish/chef_run_listener.rb0000644000004100000410000000076312767772421021717 0ustar www-datawww-datarequire 'chef/event_dispatch/base' module Cheffish class ChefRunListener < Chef::EventDispatch::Base def initialize(node) @node = node end attr_reader :node def run_complete(node) disconnect end def run_failed(exception) disconnect end private def disconnect # Stop the servers if node.run_context node.run_context.cheffish.local_servers.each do |server| server.stop end end end end end cheffish-4.0.0/lib/cheffish/node_properties.rb0000644000004100000410000000645512767772421021426 0ustar www-datawww-datarequire 'cheffish/base_properties' module Cheffish module NodeProperties include Cheffish::BaseProperties # Grab environment from with_environment def initialize(*args) super chef_environment run_context.cheffish.current_environment end property :name, Cheffish::NAME_REGEX, name_property: true property :chef_environment, Cheffish::NAME_REGEX property :run_list, Array # We should let them specify it as a series of parameters too property :attributes, Hash # attribute 'ip_address', '127.0.0.1' # attribute [ 'pushy', 'port' ], '9000' # attribute 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # attribute 'ip_address', :delete attr_accessor :attribute_modifiers def attribute(attribute_path, value=Chef::NOT_PASSED, &block) @attribute_modifiers ||= [] if value != Chef::NOT_PASSED @attribute_modifiers << [ attribute_path, value ] elsif block @attribute_modifiers << [ attribute_path, block ] else raise "attribute requires either a value or a block" end end # Patchy tags # tag 'webserver', 'apache', 'myenvironment' def tag(*tags) attribute 'tags' do |existing_tags| existing_tags ||= [] tags.each do |tag| if !existing_tags.include?(tag.to_s) existing_tags << tag.to_s end end existing_tags end end def remove_tag(*tags) attribute 'tags' do |existing_tags| if existing_tags tags.each do |tag| existing_tags.delete(tag.to_s) end end existing_tags end end # NON-patchy tags # tags :a, :b, :c # removes all other tags def tags(*tags) if tags.size == 0 attribute('tags') else tags = tags[0] if tags.size == 1 && tags[0].kind_of?(Array) attribute 'tags', tags.map { |tag| tag.to_s } end end # Order matters--if two things here are in the wrong order, they will be flipped in the run list # recipe 'apache', 'mysql' # recipe 'recipe@version' # recipe 'recipe' # role '' attr_accessor :run_list_modifiers attr_accessor :run_list_removers def recipe(*recipes) if recipes.size == 0 raise ArgumentError, "At least one recipe must be specified" end @run_list_modifiers ||= [] @run_list_modifiers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") } end def role(*roles) if roles.size == 0 raise ArgumentError, "At least one role must be specified" end @run_list_modifiers ||= [] @run_list_modifiers += roles.map { |role| Chef::RunList::RunListItem.new("role[#{role}]") } end def remove_recipe(*recipes) if recipes.size == 0 raise ArgumentError, "At least one recipe must be specified" end @run_list_removers ||= [] @run_list_removers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") } end def remove_role(*roles) if roles.size == 0 raise ArgumentError, "At least one role must be specified" end @run_list_removers ||= [] @run_list_removers += roles.map { |role| Chef::RunList::RunListItem.new("role[#{role}]") } end end end cheffish-4.0.0/lib/cheffish/chef_actor_base.rb0000644000004100000410000001154012767772421021303 0ustar www-datawww-datarequire '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 cheffish-4.0.0/lib/cheffish/version.rb0000644000004100000410000000005012767772421017673 0ustar www-datawww-datamodule Cheffish VERSION = '4.0.0' end cheffish-4.0.0/lib/cheffish/recipe_dsl.rb0000644000004100000410000001132312767772421020324 0ustar www-datawww-datarequire 'cheffish' require 'chef/version' require 'chef_zero/server' require 'chef/chef_fs/chef_fs_data_store' require 'chef/chef_fs/config' require 'cheffish/chef_run_data' require 'cheffish/chef_run_listener' require 'chef/client' require 'chef/config' require 'chef_zero/version' require 'cheffish/merged_config' require 'chef/resource/chef_acl' require 'chef/resource/chef_client' require 'chef/resource/chef_container' require 'chef/resource/chef_data_bag' require 'chef/resource/chef_data_bag_item' require 'chef/resource/chef_environment' require 'chef/resource/chef_group' require 'chef/resource/chef_mirror' require 'chef/resource/chef_node' require 'chef/resource/chef_organization' require 'chef/resource/chef_role' require 'chef/resource/chef_user' require 'chef/resource/private_key' require 'chef/resource/public_key' class Chef module DSL module Recipe def with_chef_data_bag(name) run_context.cheffish.with_data_bag(name, &block) end def with_chef_environment(name, &block) run_context.cheffish.with_environment(name, &block) end def with_chef_data_bag_item_encryption(encryption_options, &block) run_context.cheffish.with_data_bag_item_encryption(encryption_options, &block) end def with_chef_server(server_url, options = {}, &block) run_context.cheffish.with_chef_server({ :chef_server_url => server_url, :options => options }, &block) end def with_chef_local_server(options, &block) options[:host] ||= '127.0.0.1' options[:log_level] ||= Chef::Log.level options[:port] ||= ChefZero::VERSION.to_f >= 2.2 ? 8901.upto(9900) : 8901 # Create the data store chef-zero will use options[:data_store] ||= begin if !options[:chef_repo_path] raise "chef_repo_path must be specified to with_chef_local_server" end # Ensure all paths are given %w(acl client cookbook container data_bag environment group node role).each do |type| # Set the options as symbol keys and then copy to string keys string_key = "#{type}_path" symbol_key = "#{type}_path".to_sym options[symbol_key] ||= begin if options[:chef_repo_path].kind_of?(String) Chef::Util::PathHelper.join(options[:chef_repo_path], "#{type}s") else options[:chef_repo_path].map { |path| Chef::Util::PathHelper.join(path, "#{type}s")} end end # Copy over to string keys for things that use string keys (ChefFS)... # TODO: Fix ChefFS to take symbols or use something that is insensitive to the difference options[string_key] = options[symbol_key] end chef_fs = Chef::ChefFS::Config.new(options).local_fs chef_fs.write_pretty_json = true Chef::ChefFS::ChefFSDataStore.new(chef_fs) end # Start the chef-zero server Chef::Log.info("Starting chef-zero on port #{options[:port]} with repository at #{options[:data_store].chef_fs.fs_description}") chef_zero_server = ChefZero::Server.new(options) chef_zero_server.start_background run_context.cheffish.local_servers << chef_zero_server with_chef_server(chef_zero_server.url, &block) end def get_private_key(name) Cheffish.get_private_key(name, run_context.config) end end end class Config default(:profile) { ENV['CHEF_PROFILE'] || 'default' } configurable(:private_keys) default(:private_key_paths) { [ Chef::Util::PathHelper.join(config_dir, 'keys'), Chef::Util::PathHelper.join(user_home, '.ssh') ] } default(:private_key_write_path) { private_key_paths.first } end class RunContext def cheffish node.run_state[:cheffish] ||= begin run_data = Cheffish::ChefRunData.new(config) events.register(Cheffish::ChefRunListener.new(node)) run_data end end def config node.run_state[:chef_config] ||= Cheffish.profiled_config(Chef::Config) end end Chef::Client.when_run_starts do |run_status| # Pulling on cheffish_run_data makes it initialize right now run_status.node.run_state[:chef_config] = config = Cheffish.profiled_config(Chef::Config) run_status.node.run_state[:cheffish] = run_data = Cheffish::ChefRunData.new(config) run_status.events.register(Cheffish::ChefRunListener.new(run_status.node)) end end # Chef 12 moved Chef::Config.path_join to PathHelper.join if Chef::VERSION.to_i >= 12 require 'chef/util/path_helper' else require 'chef/config' class Chef class Util class PathHelper def self.join(*args) Chef::Config.path_join(*args) end end end end end cheffish-4.0.0/lib/cheffish/base_properties.rb0000644000004100000410000000071012767772421021377 0ustar www-datawww-datarequire 'chef/mixin/properties' require 'cheffish/array_property' require 'cheffish' module Cheffish module BaseProperties include Chef::Mixin::Properties def initialize(*args) super chef_server run_context.cheffish.current_chef_server end Boolean = property_type(is: [ true, false ]) ArrayType = ArrayProperty.new property :chef_server, Hash property :raw_json, Hash property :complete, Boolean end end cheffish-4.0.0/lib/cheffish/key_formatter.rb0000644000004100000410000000602412767772421021070 0ustar www-datawww-datarequire 'openssl' require 'net/ssh' require 'etc' require 'socket' require 'digest/md5' require 'base64' module Cheffish class KeyFormatter # Returns nil or key, format def self.decode(str, pass_phrase=nil, filename='') key_format = {} key_format[:format] = format_of(str) case key_format[:format] when :openssh key = decode_openssh_key(str, filename) else begin key = OpenSSL::PKey.read(str) { pass_phrase } rescue return nil end end key_format[:type] = type_of(key) if type_of(key) key_format[:size] = size_of(key) if size_of(key) key_format[:pass_phrase] = pass_phrase if pass_phrase # TODO cipher, exponent [key, key_format] end def self.encode(key, key_format) format = key_format[:format] || :pem case format when :openssh encode_openssh_key(key) when :pem if key_format[:pass_phrase] cipher = key_format[:cipher] || 'DES-EDE3-CBC' key.to_pem(OpenSSL::Cipher.new(cipher), key_format[:pass_phrase]) else key.to_pem end when :der key.to_der when :fingerprint, :pkcs1md5fingerprint hexes = Digest::MD5.hexdigest(key.to_der) # Put : between every pair of hexes hexes.scan(/../).join(':') when :rfc4716md5fingerprint type, base64_data, etc = encode_openssh_key(key).split data = Base64.decode64(base64_data) hexes = Digest::MD5.hexdigest(data) hexes.scan(/../).join(':') when :pkcs8sha1fingerprint if RUBY_VERSION.to_f >= 2.0 raise "PKCS8 SHA1 not supported in Ruby #{RUBY_VERSION}" end require 'openssl_pkcs8' pkcs8_pem = key.to_pem_pkcs8 pkcs8_base64 = pkcs8_pem.split("\n").reject { |l| l =~ /^-----/ } pkcs8_data = Base64.decode64(pkcs8_base64.join) hexes = Digest::SHA1.hexdigest(pkcs8_data) hexes.scan(/../).join(':') else raise "Unrecognized key format #{format}" end end private def self.encode_openssh_key(key) # TODO there really isn't a method somewhere in net/ssh or openssl that does this?? type = key.ssh_type data = [ key.to_blob ].pack('m0') "#{type} #{data} #{Etc.getlogin}@#{Socket.gethostname}" end def self.decode_openssh_key(str, filename='') Net::SSH::KeyFactory.load_data_public_key(str, filename) end def self.format_of(key_contents) if key_contents.start_with?('-----BEGIN ') :pem elsif key_contents.start_with?('ssh-rsa ') || key_contents.start_with?('ssh-dss ') :openssh else :der end end def self.type_of(key) case key.class when OpenSSL::PKey::RSA :rsa when OpenSSL::PKey::DSA :dsa else nil end end def self.size_of(key) case key.class when OpenSSL::PKey::RSA key.n.num_bytes * 8 else nil end end end end cheffish-4.0.0/lib/cheffish/rspec/0000755000004100000410000000000012767772421017002 5ustar www-datawww-datacheffish-4.0.0/lib/cheffish/rspec/chef_run_support.rb0000644000004100000410000000407312767772421022720 0ustar www-datawww-datarequire 'chef_zero/rspec' require 'chef/server_api' require 'cheffish/rspec/repository_support' require 'uri' require 'cheffish/chef_run' require 'cheffish/rspec/recipe_run_wrapper' require 'cheffish/rspec/matchers' module Cheffish module RSpec module ChefRunSupport include ChefZero::RSpec include RepositorySupport def self.extended(klass) klass.class_eval do include ChefRunSupportInstanceMethods end end def when_the_chef_12_server(*args, **options, &block) if Gem::Version.new(ChefZero::VERSION) >= Gem::Version.new('3.1') when_the_chef_server(*args, :osc_compat => false, :single_org => false, **options, &block) end end def with_converge(&recipe) before :each do r = recipe(&recipe) r.converge end end module ChefRunSupportInstanceMethods def rest ::Chef::ServerAPI.new(Chef::Config.chef_server_url, api_version: "0") end def get(path, *args) if path[0] == '/' path = URI.join(rest.url, path) end rest.get(path, *args) end def chef_config {} end def expect_recipe(str=nil, file=nil, line=nil, &recipe) r = recipe(str, file, line, &recipe) r.converge expect(r) end def expect_converge(str=nil, file=nil, line=nil, &recipe) expect { converge(str, file, line, &recipe) } end def recipe(str=nil, file=nil, line=nil, &recipe) if !recipe if file && line recipe = proc { eval(str, nil, file, line) } else recipe = proc { eval(str) } end end RecipeRunWrapper.new(chef_config, &recipe) end def converge(str=nil, file=nil, line=nil, &recipe) r = recipe(str, file, line, &recipe) r.converge r end def chef_client @chef_client ||= ChefRun.new(chef_config) end end end end end cheffish-4.0.0/lib/cheffish/rspec/matchers.rb0000644000004100000410000000031612767772421021135 0ustar www-datawww-datarequire 'cheffish/rspec/matchers/have_updated' require 'cheffish/rspec/matchers/be_idempotent' require 'cheffish/rspec/matchers/partially_match' require 'cheffish/rspec/matchers/emit_no_warnings_or_errors' cheffish-4.0.0/lib/cheffish/rspec/recipe_run_wrapper.rb0000644000004100000410000000565012767772421023230 0ustar www-datawww-datarequire 'cheffish/chef_run' require 'forwardable' module Cheffish module RSpec class RecipeRunWrapper < ChefRun def initialize(chef_config, example: nil, &recipe) super(chef_config) @recipe = recipe @example = example || recipe.binding.eval('self') end attr_reader :recipe attr_reader :example def client if !@client super example = self.example # # Support for both resources and rspec example's let variables: # # In 12.4, the elimination of a bunch of metaprogramming in 12.4 # changed how Chef DSL is defined in code: resource methods are now # explicitly defined in `Chef::DSL::Recipe`. In 12.3, no actual # methods were defined and `respond_to?(:file)` would return false. # If we reach `method_missing` here, it means that we either have a # 12.3-ish resource or we want to call a `let` variable. # @client.instance_eval { @rspec_example = example } def @client.method_missing(name, *args, &block) # If there is a let variable, call it. This is because in 12.4, # the parent class is going to call respond_to?(name) to find out # if someone was doing weird things, and then call send(). This # would result in an infinite loop, coming right. Back. Here. # A fix to chef is incoming, but we still need this if we want to # work with Chef 12.4. if Gem::Version.new(Chef::VERSION) >= Gem::Version.new('12.4') if @rspec_example.respond_to?(name) return @rspec_example.public_send(name, *args, &block) end end # In 12.3 or below, method_missing was the only way to call # resources. If we are in 12.4, we still need to call the crazy # method_missing metaprogramming because backcompat. begin super rescue NameError if @rspec_example.respond_to?(name) @rspec_example.public_send(name, *args, &block) else raise end end end # This is called by respond_to?, and is required to make sure the # resource knows that we will in fact call the given method. def @client.respond_to_missing?(name, include_private = false) @rspec_example.respond_to?(name, include_private) || super end # Respond true to is_a?(Chef::Provider) so that Chef::Recipe::DSL.build_resource # will hook resources up to the example let variables as well (via # enclosing_provider). # Please don't hurt me def @client.is_a?(klass) klass == Chef::Provider || super(klass) end @client.load_block(&recipe) end @client end end end end cheffish-4.0.0/lib/cheffish/rspec/matchers/0000755000004100000410000000000012767772421020610 5ustar www-datawww-datacheffish-4.0.0/lib/cheffish/rspec/matchers/have_updated.rb0000644000004100000410000000272312767772421023572 0ustar www-datawww-datarequire 'rspec/matchers' RSpec::Matchers.define :have_updated do |resource_name, *expected_actions| match do |recipe| @recipe = recipe actual = @recipe.event_sink.events actual_actions = actual.select { |event, resource, action| event == :resource_updated && resource.to_s == resource_name }. map { |event, resource, action| action } expect(actual_actions).to eq(expected_actions) end failure_message do actual = @recipe.event_sink.events updates = actual.select { |event, resource, action| event == :resource_updated }.to_a result = "expected that the chef_run would #{expected_actions.join(',')} #{resource_name}." if updates.size > 0 result << " Actual updates were #{updates.map { |event, resource, action| "#{resource.to_s} => #{action.inspect}" }.join(', ')}" else result << " Nothing was updated." end result end failure_message_when_negated do actual = @recipe.event_sink.events updates = actual.select { |event, resource, action| event == :resource_updated }.to_a result = "expected that the chef_run would not #{expected_actions.join(',')} #{resource_name}." if updates.size > 0 result << " Actual updates were #{updates.map { |event, resource, action| "#{resource.to_s} => #{action.inspect}" }.join(', ')}" else result << " Nothing was updated." end result end end RSpec::Matchers.define_negated_matcher :not_have_updated, :have_updated cheffish-4.0.0/lib/cheffish/rspec/matchers/emit_no_warnings_or_errors.rb0000644000004100000410000000053112767772421026572 0ustar www-datawww-datarequire 'rspec/matchers' RSpec::Matchers.define :emit_no_warnings_or_errors do match do |recipe| @recipe = recipe @warn_err = recipe.logs.lines.select { |l| l =~ /warn|err/i }.join("\n") @warn_err.empty? end failure_message { "#{@recipe} emitted warnings and errors!\n#{@warn_err}" } supports_block_expectations end cheffish-4.0.0/lib/cheffish/rspec/matchers/partially_match.rb0000644000004100000410000000325512767772421024317 0ustar www-datawww-datamodule Cheffish module RSpec module Matchers class PartiallyMatch include ::RSpec::Matchers::Composable def initialize(example, expected) @example = example @expected = expected end def matches?(actual) @actual = actual partially_matches_values(@expected, actual) end def failure_message "expected #{@actual} to match #{@expected}" end def failure_message_when_negated "expected #{@actual} not to match #{@expected}" end protected def partially_matches_values(expected, actual) if Hash === actual return partially_matches_hashes(expected, actual) if Hash === expected || Array === expected elsif Array === expected && Enumerable === actual && !(Struct === actual) return partially_matches_arrays(expected, actual) end return true if actual == expected begin expected === actual rescue ArgumentError # Some objects, like 0-arg lambdas on 1.9+, raise # ArgumentError for `expected === actual`. false end end def partially_matches_hashes(expected, actual) expected.all? { |key, value| partially_matches_values(value, actual[key]) } end def partially_matches_arrays(expected, actual) expected.all? { |e| actual.any? { |a| partially_matches_values(e, a) } } end end end end end module RSpec module Matchers def partially_match(expected) Cheffish::RSpec::Matchers::PartiallyMatch.new(self, expected) end end end cheffish-4.0.0/lib/cheffish/rspec/matchers/be_idempotent.rb0000644000004100000410000000054212767772421023754 0ustar www-datawww-datarequire 'rspec/matchers' RSpec::Matchers.define :be_idempotent do match do |recipe| @recipe = recipe recipe.reset recipe.converge recipe.up_to_date? end failure_message { "#{@recipe} is not idempotent! Converging it a second time caused updates.\n#{@recipe.output_for_failure_message}" } supports_block_expectations end cheffish-4.0.0/lib/cheffish/rspec/repository_support.rb0000644000004100000410000000607712767772421023354 0ustar www-datawww-datamodule Cheffish module RSpec module RepositorySupport def when_the_repository(desc, *tags, &block) context("when the chef repo #{desc}", *tags) do include_context "with a chef repo" extend WhenTheRepositoryClassMethods module_eval(&block) end end ::RSpec.shared_context "with a chef repo" do before :each do raise "Can only create one directory per test" if @repository_dir @repository_dir = Dir.mktmpdir('chef_repo') Chef::Config.chef_repo_path = @repository_dir %w(client cookbook data_bag environment node role user).each do |object_name| Chef::Config.delete("#{object_name}_path".to_sym) end end after :each do if @repository_dir begin %w(client cookbook data_bag environment node role user).each do |object_name| Chef::Config.delete("#{object_name}_path".to_sym) end Chef::Config.delete(:chef_repo_path) FileUtils.remove_entry_secure(@repository_dir) ensure @repository_dir = nil end end Dir.chdir(@old_cwd) if @old_cwd end def directory(relative_path, &block) old_parent_path = @parent_path @parent_path = path_to(relative_path) FileUtils.mkdir_p(@parent_path) instance_eval(&block) if block @parent_path = old_parent_path end def file(relative_path, contents) filename = path_to(relative_path) dir = File.dirname(filename) FileUtils.mkdir_p(dir) unless dir == '.' File.open(filename, 'w') do |file| raw = case contents when Hash, Array JSON.pretty_generate(contents) else contents end file.write(raw) end end def symlink(relative_path, relative_dest) filename = path_to(relative_path) dir = File.dirname(filename) FileUtils.mkdir_p(dir) unless dir == '.' dest_filename = path_to(relative_dest) File.symlink(dest_filename, filename) end def path_to(relative_path) File.expand_path(relative_path, (@parent_path || @repository_dir)) end def cwd(relative_path) @old_cwd = Dir.pwd Dir.chdir(path_to(relative_path)) end module WhenTheRepositoryClassMethods def directory(*args, &block) before :each do directory(*args, &block) end end def file(*args, &block) before :each do file(*args, &block) end end def symlink(*args, &block) before :each do symlink(*args, &block) end end def path_to(*args, &block) before :each do file(*args, &block) end end end end end end end cheffish-4.0.0/lib/cheffish/rspec.rb0000644000004100000410000000024212767772421017325 0ustar www-datawww-datarequire 'cheffish/rspec/chef_run_support' require 'cheffish/rspec/repository_support' require 'cheffish/rspec/matchers' module Cheffish module RSpec end end cheffish-4.0.0/lib/cheffish/array_property.rb0000644000004100000410000000157012767772421021300 0ustar www-datawww-datarequire 'chef/property' module Cheffish # A typical array property. Defaults to [], accepts multiple args to setter, accumulates values. class ArrayProperty < Chef::Property def initialize(**options) options[:is] ||= Array options[:default] ||= [] options[:coerce] ||= proc { |v| v.is_a?(Array) ? v : [ v ] } super end # Support my_property 'a', 'b', 'c'; my_property 'a'; and my_property ['a', 'b'] def emit_dsl declared_in.class_eval(<<-EOM, __FILE__, __LINE__+1) def #{name}(*values) property = self.class.properties[#{name.inspect}] if values.empty? property.get(self) elsif property.is_set?(self) property.set(self, property.get(self) + values.flatten) else property.set(self, values.flatten) end end EOM end end end cheffish-4.0.0/lib/cheffish/basic_chef_client.rb0000644000004100000410000001171012767772421021617 0ustar www-datawww-datarequire 'cheffish/version' require 'chef/dsl/recipe' require 'chef/event_dispatch/base' require 'chef/event_dispatch/dispatcher' require 'chef/node' require 'chef/run_context' require 'chef/runner' require 'forwardable' require 'chef/providers' require 'chef/resources' module Cheffish class BasicChefClient include Chef::DSL::Recipe def initialize(node = nil, events = nil, **chef_config) if !node node = Chef::Node.new node.name 'basic_chef_client' node.automatic[:platform] = 'basic_chef_client' node.automatic[:platform_version] = Cheffish::VERSION end # Decide on the config we want for this chef client @chef_config = chef_config with_chef_config do @cookbook_name = 'basic_chef_client' @event_catcher = BasicChefClientEvents.new dispatcher = Chef::EventDispatch::Dispatcher.new(@event_catcher) case events when nil when Array events.each { |e| dispatcher.register(e) } if events else dispatcher.register(events) end @run_context = Chef::RunContext.new(node, {}, dispatcher) @updated = [] @cookbook_name = 'basic_chef_client' end end extend Forwardable # Stuff recipes need attr_reader :chef_config attr_reader :run_context attr_accessor :cookbook_name attr_accessor :recipe_name def add_resource(resource) with_chef_config do resource.run_context = run_context run_context.resource_collection.insert(resource) end end def load_block(&block) with_chef_config do @recipe_name = 'block' instance_eval(&block) end end def converge with_chef_config do Chef::Runner.new(run_context).converge end end def updates @event_catcher.updates end def updated? @event_catcher.updates.size > 0 end # Builds a resource sans context, which can be later used in a new client's # add_resource() method. def self.build_resource(type, name, created_at=nil, &resource_attrs_block) created_at ||= caller[0] result = BasicChefClient.new.tap do |client| client.with_chef_config do client.build_resource(type, name, created_at, &resource_attrs_block) end end result end def self.inline_resource(provider, provider_action, *resources, &block) events = ProviderEventForwarder.new(provider, provider_action) client = BasicChefClient.new(provider.node, events) client.with_chef_config do resources.each do |resource| client.add_resource(resource) end end client.load_block(&block) if block client.converge client.updated? end def self.converge_block(node = nil, events = nil, &block) client = BasicChefClient.new(node, events) client.load_block(&block) client.converge client.updated? end def with_chef_config(&block) old_chef_config = Chef::Config.save if chef_config[:log_location] old_loggers = Chef::Log.loggers Chef::Log.init(chef_config[:log_location]) end if chef_config[:log_level] old_level = Chef::Log.level Chef::Log.level(chef_config[:log_level]) end # if chef_config[:stdout] # old_stdout = $stdout # $stdout = chef_config[:stdout] # end # if chef_config[:stderr] # old_stderr = $stderr # $stderr = chef_config[:stderr] # end begin deep_merge_config(chef_config, Chef::Config) block.call ensure # $stdout = old_stdout if chef_config[:stdout] # $stderr = old_stderr if chef_config[:stderr] if old_loggers Chef::Log.logger = old_loggers.shift old_loggers.each { |l| Chef::Log.loggers.push(l) } elsif chef_config[:log_level] Chef::Log.level = old_level end Chef::Config.restore(old_chef_config) end end def deep_merge_config(src, dest) src.each do |name, value| if value.is_a?(Hash) && dest[name].is_a?(Hash) deep_merge_config(value, dest[name]) else dest[name] = value end end end class BasicChefClientEvents < Chef::EventDispatch::Base def initialize @updates = [] end attr_reader :updates # Called after a resource has been completely converged. def resource_updated(resource, action) updates << [ resource, action ] end end class ProviderEventForwarder < Chef::EventDispatch::Base def initialize(provider, provider_action) @provider = provider @provider_action = provider_action end attr_reader :provider attr_reader :provider_action def resource_update_applied(resource, action, update) provider.run_context.events.resource_update_applied(provider.new_resource, provider_action, update) end end end end cheffish-4.0.0/lib/cheffish/chef_run_data.rb0000644000004100000410000000061012767772421020772 0ustar www-datawww-datarequire 'chef/config' require 'cheffish/with_pattern' module Cheffish class ChefRunData def initialize(config) @local_servers = [] @current_chef_server = Cheffish.default_chef_server(config) end extend Cheffish::WithPattern with :data_bag with :environment with :data_bag_item_encryption with :chef_server attr_reader :local_servers end end cheffish-4.0.0/lib/chef/0000755000004100000410000000000012767772421015014 5ustar www-datawww-datacheffish-4.0.0/lib/chef/resource/0000755000004100000410000000000012767772421016643 5ustar www-datawww-datacheffish-4.0.0/lib/chef/resource/chef_user.rb0000644000004100000410000000454712767772421021145 0ustar www-datawww-datarequire 'cheffish' require 'cheffish/chef_actor_base' class Chef class Resource class ChefUser < Cheffish::ChefActorBase resource_name :chef_user # Client attributes property :name, Cheffish::NAME_REGEX, name_property: true property :display_name, String property :admin, Boolean property :email, String property :external_authentication_uid property :recovery_authentication_enabled, Boolean property :password, String # Hmm. There is no way to idempotentize this. #property :salt # TODO server doesn't support sending or receiving these, but it's the only way to backup / restore a user #property :hashed_password #property :hash_type # Input key property :source_key # String or OpenSSL::PKey::* property :source_key_path, String property :source_key_pass_phrase # Output public key (if so desired) property :output_key_path, String property :output_key_format, [ :pem, :der, :openssh ], default: :openssh # Proc that runs just before the resource executes. Called with (resource) def before(&block) block ? @before = block : @before end # Proc that runs after the resource completes. Called with (resource, json, private_key, public_key) 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 cheffish-4.0.0/lib/chef/resource/chef_node.rb0000644000004100000410000000523512767772421021107 0ustar www-datawww-datarequire 'cheffish' require 'cheffish/base_resource' require 'chef/chef_fs/data_handler/node_data_handler' require 'cheffish/node_properties' class Chef class Resource class ChefNode < Cheffish::BaseResource resource_name :chef_node include Cheffish::NodeProperties 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 cheffish-4.0.0/lib/chef/resource/chef_container.rb0000644000004100000410000000255012767772421022141 0ustar www-datawww-datarequire 'cheffish' require 'cheffish/base_resource' require 'chef/chef_fs/data_handler/container_data_handler' class Chef class Resource class ChefContainer < Cheffish::BaseResource resource_name :chef_container property :name, Cheffish::NAME_REGEX, name_property: true 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 cheffish-4.0.0/lib/chef/resource/chef_group.rb0000644000004100000410000000501212767772421021307 0ustar www-datawww-datarequire 'cheffish' 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 < Cheffish::BaseResource resource_name :chef_group property :name, Cheffish::NAME_REGEX, name_property: true property :users, ArrayType property :clients, ArrayType property :groups, ArrayType property :remove_users, ArrayType property :remove_clients, ArrayType property :remove_groups, ArrayType 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 cheffish-4.0.0/lib/chef/resource/private_key.rb0000644000004100000410000002365612767772421021526 0ustar www-datawww-datarequire 'openssl/cipher' require 'cheffish/base_resource' require 'openssl' require 'cheffish/key_formatter' class Chef class Resource class PrivateKey < Cheffish::BaseResource resource_name :private_key allowed_actions :create, :delete, :regenerate, :nothing default_action :create # Path to private key. Set to :none to create the key in memory and not on disk. property :path, [ String, :none ], name_property: true property :format, [ :pem, :der ], default: :pem property :type, [ :rsa, :dsa ], default: :rsa # TODO support :ec # These specify an optional public_key you can spit out if you want. property :public_key_path, String property :public_key_format, [ :openssh, :pem, :der ], default: :openssh # Specify this if you want to copy another private key but give it a different format / password property :source_key property :source_key_path, String property :source_key_pass_phrase # RSA and DSA property :size, Integer, default: 2048 # RSA-only property :exponent, Integer # For RSA # PEM-only property :pass_phrase, String property :cipher, OpenSSL::Cipher.ciphers, default: 'DES-EDE3-CBC' # Set this to regenerate the key if it does not have the desired characteristics (like size, type, etc.) property :regenerate_if_different, Boolean # Proc that runs after the resource completes. Called with (resource, private_key) def after(&block) block ? @after = block : @after 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 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]) if key_format[:type] resource.size(key_format[:size]) if key_format[:size] resource.exponent(key_format[:exponent]) if key_format[:exponent] resource.pass_phrase(key_format[:pass_phrase]) if key_format[:pass_phrase] resource.cipher(key_format[:cipher]) if 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 cheffish-4.0.0/lib/chef/resource/chef_acl.rb0000644000004100000410000004607412767772421020727 0ustar www-datawww-datarequire 'cheffish' 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 < Cheffish::BaseResource resource_name :chef_acl # Path of the thing being secured, e.g. nodes, nodes/*, nodes/mynode, # */*, **, roles/base, data/secrets, cookbooks/apache2, /users/*, # /organizations/foo/nodes/x property :path, String, name_property: true # Whether to change things recursively. true means it will descend all children # and make the same modifications to them. :on_change will only descend if # the parent has changed. :on_change is the default. property :recursive, [ true, false, :on_change ], default: :on_change # rights :read, :users => 'jkeiser', :groups => [ 'admins', 'users' ] # rights [ :create, :read ], :users => [ 'jkeiser', 'adam' ] # rights :all, :users => 'jkeiser' def rights(*values) if values.size == 0 @rights else args = values.pop args[:permissions] ||= [] values.each do |value| args[:permissions] |= Array(value) end @rights ||= [] @rights << args end end # remove_rights :read, :users => 'jkeiser', :groups => [ 'admins', 'users' ] # remove_rights [ :create, :read ], :users => [ 'jkeiser', 'adam' ] # remove_rights :all, :users => [ 'jkeiser', 'adam' ] def remove_rights(*values) if values.size == 0 @remove_rights else args = values.pop args[:permissions] ||= [] values.each do |value| args[:permissions] |= Array(value) end @remove_rights ||= [] @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 cheffish-4.0.0/lib/chef/resource/chef_resolved_cookbooks.rb0000644000004100000410000000357612767772421024064 0ustar www-datawww-datarequire 'cheffish/base_resource' require 'chef_zero' class Chef class Resource class ChefResolvedCookbooks < Cheffish::BaseResource resource_name :chef_resolved_cookbooks def initialize(*args) super require 'berkshelf' berksfile Berkshelf::Berksfile.new('/tmp/Berksfile') @cookbooks_from = [] end extend Forwardable def_delegators :@berksfile, :cookbook, :extension, :group, :metadata, :source def cookbooks_from(path = nil) if path @cookbooks_from << path else @cookbooks_from end end property :berksfile 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 cheffish-4.0.0/lib/chef/resource/chef_environment.rb0000644000004100000410000001024312767772421022521 0ustar www-datawww-datarequire 'cheffish' require 'cheffish/base_resource' require 'chef/environment' require 'chef/chef_fs/data_handler/environment_data_handler' class Chef class Resource class ChefEnvironment < Cheffish::BaseResource resource_name :chef_environment property :name, Cheffish::NAME_REGEX, name_property: true property :description, String property :cookbook_versions, Hash, callbacks: { "should have valid cookbook versions" => lambda { |value| Chef::Environment.validate_cookbook_versions(value) } } property :default_attributes, Hash property :override_attributes, Hash # default 'ip_address', '127.0.0.1' # default [ 'pushy', 'port' ], '9000' # default 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # default 'ip_address', :delete attr_reader :default_attribute_modifiers def default(attribute_path, value=NOT_PASSED, &block) @default_attribute_modifiers ||= [] if value != NOT_PASSED @default_attribute_modifiers << [ attribute_path, value ] elsif block @default_attribute_modifiers << [ attribute_path, block ] else raise "default requires either a value or a block" end end # override 'ip_address', '127.0.0.1' # override [ 'pushy', 'port' ], '9000' # override 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # override 'ip_address', :delete attr_reader :override_attribute_modifiers def override(attribute_path, value=NOT_PASSED, &block) @override_attribute_modifiers ||= [] if value != NOT_PASSED @override_attribute_modifiers << [ attribute_path, value ] elsif block @override_attribute_modifiers << [ attribute_path, block ] else raise "override requires either a value or a block" end end alias :attributes :default_attributes alias :attribute :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 cheffish-4.0.0/lib/chef/resource/chef_client.rb0000644000004100000410000000322712767772421021437 0ustar www-datawww-datarequire 'cheffish' require 'cheffish/chef_actor_base' class Chef class Resource class ChefClient < Cheffish::ChefActorBase resource_name :chef_client # Client attributes property :name, Cheffish::NAME_REGEX, name_property: true property :admin, Boolean property :validator, Boolean # Input key property :source_key # String or OpenSSL::PKey::* property :source_key_path, String property :source_key_pass_phrase # Output public key (if so desired) property :output_key_path, String property :output_key_format, Symbol, default: :openssh, equal_to: [ :pem, :der, :openssh ] # Proc that runs just before the resource executes. Called with (resource) def before(&block) block ? @before = block : @before end # Proc that runs after the resource completes. Called with (resource, json, private_key, public_key) 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 cheffish-4.0.0/lib/chef/resource/public_key.rb0000644000004100000410000000677112767772421021331 0ustar www-datawww-datarequire 'openssl/cipher' require 'cheffish/base_resource' require 'openssl' require 'cheffish/key_formatter' class Chef class Resource class PublicKey < Cheffish::BaseResource resource_name :public_key allowed_actions :create, :delete, :nothing default_action :create property :path, String, name_property: true property :format, [ :pem, :der, :openssh ], default: :openssh property :source_key property :source_key_path, String property :source_key_pass_phrase # 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 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 cheffish-4.0.0/lib/chef/resource/chef_mirror.rb0000644000004100000410000001652312767772421021476 0ustar www-datawww-datarequire 'cheffish' 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 < Cheffish::BaseResource resource_name :chef_mirror # Path of the data to mirror, e.g. nodes, nodes/*, nodes/mynode, # */*, **, roles/base, data/secrets, cookbooks/apache2, etc. property :path, String, name_property: true # Local path. Can be a string (top level of repository) or hash # (:chef_repo_path, :node_path, etc.) # If neither chef_repo_path nor versioned_cookbooks are set, they default to their # Chef::Config values. If chef_repo_path is set but versioned_cookbooks is not, # versioned_cookbooks defaults to true. property :chef_repo_path, [ String, Hash ] # Whether the repo path should contain cookbooks with versioned names, # i.e. cookbooks/mysql-1.0.0, cookbooks/mysql-1.2.0, etc. # Defaults to true if chef_repo_path is specified, or to Chef::Config.versioned_cookbooks otherwise. property :versioned_cookbooks, Boolean # Whether to purge deleted things: if we do not have cookbooks/x locally and we # *do* have cookbooks/x remotely, then :upload with purge will delete it. # Defaults to false. property :purge, Boolean # Whether to freeze cookbooks on upload property :freeze, Boolean # If this is true, only new files will be copied. File contents will not be # diffed, so changed files will never be uploaded. property :no_diff, Boolean # Number of parallel threads to list/upload/download with. Defaults to 10. property :concurrency, Integer, default: 10, desired_state: false 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 <= 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 - 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 cheffish-4.0.0/lib/chef/resource/chef_data_bag.rb0000644000004100000410000000260612767772421021703 0ustar www-datawww-datarequire 'cheffish' require 'cheffish/base_resource' class Chef class Resource class ChefDataBag < Cheffish::BaseResource resource_name :chef_data_bag property :name, Cheffish::NAME_REGEX, name_property: true 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 cheffish-4.0.0/lib/chef/resource/chef_organization.rb0000644000004100000410000001417212767772421022666 0ustar www-datawww-datarequire 'cheffish' 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 < Cheffish::BaseResource resource_name :chef_organization property :name, Cheffish::NAME_REGEX, name_property: true property :full_name, String # A list of users who must at least be invited to the org (but may already be # members). Invites will be sent to users who are not already invited/in the org. property :invites, ArrayType # A list of users who must be members of the org. This will use the # new Chef 12 POST /organizations/ORG/users endpoint to add them # directly to the org. If you do not have permission to perform # this operation, and the users are not a part of the org, the # resource update will fail. property :members, ArrayType # A list of users who must not be members of the org. These users will be removed # from the org and invites will be revoked (if any). property :remove_members, ArrayType 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.property_is_set?(:invites) || new_resource.property_is_set?(:members) result = outstanding_invites.keys result -= new_resource.invites if new_resource.property_is_set?(:invites) result -= new_resource.members if new_resource.property_is_set?(:members) result else [] end else new_resource.remove_members end end def members_to_remove if new_resource.complete if new_resource.property_is_set?(:members) 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 cheffish-4.0.0/lib/chef/resource/chef_data_bag_item.rb0000644000004100000410000003200012767772421022710 0ustar www-datawww-datarequire 'cheffish' require 'chef/config' 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 < Cheffish::BaseResource resource_name :chef_data_bag_item def initialize(*args) super if !property_is_set?(:data_bag) && run_context.cheffish.current_data_bag data_bag run_context.cheffish.current_data_bag end encryption = run_context.cheffish.current_data_bag_item_encryption if encryption encrypt true if encryption[:encrypt_all] secret encryption[:secret] if encryption[:secret] secret_path encryption[:secret_path] || run_context.config[:encrypted_data_bag_secret] if encryption[:secret_path] || run_context.config[:encrypted_data_bag_secret] encryption_cipher encryption[:encryption_cipher] if encryption[:encryption_cipher] encryption_version encryption[:encryption_version] || run_context.config[:data_bag_encrypt_version] if encryption[:encryption_version] || run_context.config[:data_bag_encrypt_version] old_secret encryption[:old_secret] if encryption[:old_secret] old_secret_path encryption[:old_secret_path] if encryption[:old_secret_path] end end # If data_bag and id are not specified, take them from name. # name can either be id, or data_bag/id property :id, String, default: lazy { name.split('/', 2)[-1] } property :data_bag, String, default: lazy { split = name.split('/', 2) split.size >= 2 ? split[0] : nil } property :raw_data, Hash # If secret or secret_path are set, encrypt is assumed true. encrypt exists mainly for with_secret and with_secret_path property :encrypt, Boolean, default: lazy { if secret.nil? && secret_path.nil? false else true end } property :secret, String property :secret_path, String property :encryption_version, Integer # Old secret (or secrets) to read the old data bag when we are changing keys and re-encrypting data property :old_secret, [String, Array] property :old_secret_path, [String, Array] # value 'ip_address', '127.0.0.1' # value [ 'pushy', 'port' ], '9000' # value 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # value 'ip_address', :delete attr_reader :raw_data_modifiers def value(raw_data_path, value=NOT_PASSED, &block) @raw_data_modifiers ||= [] if value != NOT_PASSED @raw_data_modifiers << [ raw_data_path, value ] elsif block @raw_data_modifiers << [ raw_data_path, block ] else 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) unless new_resource.encrypt.nil? current_resource.encryption_version(new_resource.encryption_version) if 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 cheffish-4.0.0/lib/chef/resource/chef_role.rb0000644000004100000410000001321612767772421021121 0ustar www-datawww-datarequire 'cheffish' 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 < Cheffish::BaseResource resource_name :chef_role property :name, Cheffish::NAME_REGEX, name_property: true property :description, String property :run_list, Array # We should let them specify it as a series of parameters too property :env_run_lists, Hash property :default_attributes, Hash property :override_attributes, Hash # default_attribute 'ip_address', '127.0.0.1' # default_attribute [ 'pushy', 'port' ], '9000' # default_attribute 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # default_attribute 'ip_address', :delete attr_reader :default_attribute_modifiers def default_attribute(attribute_path, value=NOT_PASSED, &block) @default_attribute_modifiers ||= [] if value != NOT_PASSED @default_attribute_modifiers << [ attribute_path, value ] elsif block @default_attribute_modifiers << [ attribute_path, block ] else raise "default_attribute requires either a value or a block" end end # override_attribute 'ip_address', '127.0.0.1' # override_attribute [ 'pushy', 'port' ], '9000' # override_attribute 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # override_attribute 'ip_address', :delete attr_reader :override_attribute_modifiers def override_attribute(attribute_path, value=NOT_PASSED, &block) @override_attribute_modifiers ||= [] if value != NOT_PASSED @override_attribute_modifiers << [ attribute_path, value ] elsif block @override_attribute_modifiers << [ attribute_path, block ] else raise "override_attribute requires either a value or a block" end end # Order matters--if two things here are in the wrong order, they will be flipped in the run list # recipe 'apache', 'mysql' # recipe 'recipe@version' # recipe 'recipe' # role '' attr_reader :run_list_modifiers attr_reader :run_list_removers def recipe(*recipes) if recipes.size == 0 raise ArgumentError, "At least one recipe must be specified" end @run_list_modifiers ||= [] @run_list_modifiers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") } end def role(*roles) if roles.size == 0 raise ArgumentError, "At least one role must be specified" end @run_list_modifiers ||= [] @run_list_modifiers += roles.map { |role| Chef::RunList::RunListItem.new("role[#{role}]") } end def remove_recipe(*recipes) if recipes.size == 0 raise ArgumentError, "At least one recipe must be specified" end @run_list_removers ||= [] @run_list_removers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") } end def remove_role(*roles) if roles.size == 0 raise ArgumentError, "At least one role must be specified" end @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 cheffish-4.0.0/lib/cheffish.rb0000644000004100000410000001131012767772421016207 0ustar www-datawww-datamodule Cheffish NAME_REGEX = /^[.\-[:alnum:]_]+$/ def self.inline_resource(provider, provider_action, *resources, &block) BasicChefClient.inline_resource(provider, provider_action, *resources, &block) end def self.default_chef_server(config = profiled_config) { :chef_server_url => config[:chef_server_url], :options => { :client_name => config[:node_name], :signing_key_filename => config[:client_key] } } end def self.chef_server_api(chef_server = default_chef_server) # Pin the server api version to 0 until https://github.com/chef/cheffish/issues/56 # gets the correct compatibility fix. chef_server[:options] ||= {} chef_server[:options].merge!(api_version: "0") Cheffish::ServerAPI.new(chef_server[:chef_server_url], chef_server[:options]) end def self.profiled_config(config = Chef::Config) if config.profile && config.profiles && config.profiles[config.profile] MergedConfig.new(config.profiles[config.profile], config) else config end end def self.load_chef_config(chef_config = Chef::Config) if ::Gem::Version.new(::Chef::VERSION) >= ::Gem::Version.new('12.0.0') chef_config.config_file = ::Chef::Knife.chef_config_dir else chef_config.config_file = ::Chef::Knife.locate_config_file end config_fetcher = Chef::ConfigFetcher.new(chef_config.config_file, chef_config.config_file_jail) if chef_config.config_file.nil? Chef::Log.warn("No config file found or specified on command line, using command line options.") elsif config_fetcher.config_missing? Chef::Log.warn("Did not find config file: #{chef_config.config_file}, using command line options.") else config_content = config_fetcher.read_config config_file_path = chef_config.config_file begin chef_config.from_string(config_content, config_file_path) rescue Exception => error Chef::Log.fatal("Configuration error #{error.class}: #{error.message}") filtered_trace = error.backtrace.grep(/#{Regexp.escape(config_file_path)}/) filtered_trace.each {|line| Chef::Log.fatal(" " + line )} Chef::Application.fatal!("Aborting due to error in '#{config_file_path}'", 2) end end Cheffish.profiled_config(chef_config) end def self.honor_local_mode(local_mode_default = true, &block) if !Chef::Config.has_key?(:local_mode) && !local_mode_default.nil? Chef::Config.local_mode = local_mode_default end if Chef::Config.local_mode && !Chef::Config.has_key?(:cookbook_path) && !Chef::Config.has_key?(:chef_repo_path) Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd) end begin require 'chef/local_mode' Chef::LocalMode.with_server_connectivity(&block) rescue LoadError Chef::Application.setup_server_connectivity if block_given? begin yield ensure Chef::Application.destroy_server_connectivity end end end end def self.get_private_key(name, config = profiled_config) key, key_path = get_private_key_with_path(name, config) key end def self.get_private_key_with_path(name, config = profiled_config) if config[:private_keys] && config[:private_keys][name] named_key = config[:private_keys][name] if named_key.is_a?(String) Chef::Log.info("Got key #{name} from Chef::Config.private_keys.#{name}, which points at #{named_key}. Reading key from there ...") return [ IO.read(named_key), named_key] else Chef::Log.info("Got key #{name} raw from Chef::Config.private_keys.#{name}.") return [ named_key.to_pem, nil ] end elsif config[:private_key_paths] config[:private_key_paths].each do |private_key_path| next unless File.exist?(private_key_path) Dir.entries(private_key_path).sort.each do |key| ext = File.extname(key) if key == name || ext == '' || ext == '.pem' key_name = key[0..-(ext.length+1)] if key_name == name || key == name Chef::Log.info("Reading key #{name} from file #{private_key_path}/#{key}") return [ IO.read("#{private_key_path}/#{key}"), "#{private_key_path}/#{key}" ] end end end end end nil end def self.node_attributes(klass) klass.include Cheffish::NodeProperties end end # Include all recipe objects so require 'cheffish' brings in the whole recipe DSL require 'chef/run_list/run_list_item' require 'cheffish/basic_chef_client' require 'cheffish/server_api' require 'chef/knife' require 'chef/config_fetcher' require 'chef/log' require 'chef/application' require 'cheffish/recipe_dsl' require 'cheffish/node_properties' cheffish-4.0.0/cheffish.gemspec0000644000004100000410000000143612767772421016471 0ustar www-datawww-data$:.unshift(File.dirname(__FILE__) + '/lib') require 'cheffish/version' Gem::Specification.new do |s| s.name = 'cheffish' s.version = Cheffish::VERSION s.platform = Gem::Platform::RUBY s.extra_rdoc_files = [ 'README.md', 'LICENSE' ] s.summary = 'A library to manipulate Chef in Chef.' s.description = s.summary s.author = 'John Keiser' s.email = 'jkeiser@chef.io' s.homepage = 'http://github.com/chef/cheffish' s.required_ruby_version = '>= 2.2.2' s.add_dependency 'chef-zero', '~> 5.0' s.add_dependency 'net-ssh' s.bindir = "bin" s.executables = %w( ) s.require_path = 'lib' s.files = %w(Gemfile Rakefile LICENSE README.md) + Dir.glob("*.gemspec") + Dir.glob("{distro,lib,tasks,spec}/**/*", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } end cheffish-4.0.0/LICENSE0000644000004100000410000002514212767772421014352 0ustar www-datawww-data Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. cheffish-4.0.0/README.md0000644000004100000410000001155312767772421014625 0ustar www-datawww-data# Cheffish [![Status](https://travis-ci.org/chef/cheffish.svg?branch=master)](https://travis-ci.org/chef/cheffish) [![Gem Version](https://badge.fury.io/rb/cheffish.svg)](http://badge.fury.io/rb/cheffish) This library provides a variety of convergent resources for interacting with the Chef Server; along the way, it happens to provide some very useful and sophisticated ways of running Chef resources as recipes in RSpec examples. **This document may have errors, but it should have enough pointers to get you oriented.** There are essentially 3 collections here: ## Resource/Provider Pairs for Manipulating Chef Servers You'd use these in recipes/cookbooks. They are documented on the [main Chef docs site](https://docs.chef.io). - [chef_acl](https://docs.chef.io/resource_chef_acl.html) - [chef_client](https://docs.chef.io/resource_chef_client.html) - [chef_container](https://docs.chef.io/resource_chef_container.html) - [chef_data_bag](https://docs.chef.io/resource_chef_data_bag.html) - [chef_data_bag_item](https://docs.chef.io/resource_chef_data_bag_item.html) - [chef_environment](https://docs.chef.io/resource_chef_environment.html) - [chef_group](https://docs.chef.io/resource_chef_group.html) - [chef_mirror](https://docs.chef.io/resource_chef_mirror.html) - [chef_node](https://docs.chef.io/resource_chef_node.html) - [chef_organization](https://docs.chef.io/resource_chef_organization.html) - [chef_resolved_cookbooks](https://docs.chef.io/resource_chef_resolved_cookbooks.html) - [chef_role](https://docs.chef.io/resource_chef_role.html) - [chef_user](https://docs.chef.io/resource_chef_user.html) - [private_key](https://docs.chef.io/resource_private_key.html) - [public_key](https://docs.chef.io/resource_public_key.html) ## Base/Helper Classes To support the resource/provider pairs. ## RSpec Support Most of these RSpec...things were developed for testing the resource/provider pairs above; *however*, you can also `require cheffish/rspec/chef_run_support` for any RSpec `expect`s you'd like, as we do for `chef-provisioning` and its drivers (especially `chef-provisioning-aws`). The awesomeness here is that instead of instantiating a `run_context` and a `node` and a `resource` as Ruby objects, you can test your resources in an actual recipe: ```ruby when_the_chef_12_server "exists", organization: 'some-org', server_scope: :context, port: 8900..9000 do file "/tmp/something_important.json" do content "A resource in its native environment." end end ``` An enclosing context that spins up `chef-zero` (local mode) Chef servers as dictated by `server_scope`. `Chef::Config` will be set up with the appropriate server URLs (see the `with_*` operators below). `server_scope`: - `:context` - `:example` *[default?]* - ? `port`: - port number (8900 is the default) - port range (server will continue trying up this range until it finds a free port) ```ruby expect_recipe { # unquoted recipe DSL here. }.to be_truthy # or write your own matchers. ``` Converges the recipe using `expect()` (parentheses), which tests for a value and **cannot** be used with `raise_error`. ```ruby expect_converge { # unquoted recipe DSL here. }.to raise_error(ArgumentException) ``` Converges the recipe using `expect{ }` (curly brackets), which wraps the block in a `begin..rescue..end` to detect when the block raises an exception; hence, this is **only** for `raise_error`. The blocks for the following appear to be mostly optional: what they actually do is set the `Chef::Config` variable in the name to the given value, and if you provide a block, the change is scoped to that block. Probably this would be clearer if it were aliased to (and preferring) `using` rather than `with`. - with_chef_server(server_url, options = {}, &block) - with_chef_local_server(options, &block) - with_chef_environment(name, &block) - with_chef_data_bag_item_encryption(encryption_options, &block) - with_chef_data_bag(name) - Takes a block, though this is not noted in the method signature. get_private_key(name) ### RSpec matchers These are used with `expect_recipe` or `expect_converge`: ```ruby expect_recipe { file "/tmp/a_file.json" do content "Very important content." end }.to be_idempotent.and emit_no_warnings_or_errors ``` `be_idempotent` - Runs the provided recipe *again* (`expect_(recipe|converge)` ran it the first time) and asks the Chef run if it updated anything (using `updated?`, which appears to be defined on `Chef::Resource` instead of `Chef::Client`, so there's some clarification to be done there); the matcher is satisfied if the answer is "no." `emit_no_warnings_or_errors` - Greps the Chef client run's log output for WARN/ERROR lines; matcher is satisfied if there aren't any. `have_updated` - Sifts the recipe's event stream(!) to determine if any resources were updated; matcher is satisfied is the answer is "yes." - This is *not* the opposite of `be_idempotent`. `partially_match` - TBD