summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTyler Ball <tyleraball@gmail.com>2014-10-07 16:38:59 -0700
committerTyler Ball <tyleraball@gmail.com>2014-10-07 16:38:59 -0700
commit1343bdfff0d54e20b923211f6697d42c484c1627 (patch)
tree382ffff1af107eaf08e830e9ff89d03d0b3ea4e9
parent624a7d31dbc84c3cc8cf4c85f0cc4311b5d53be5 (diff)
parente6a9db3bdadcbef85951677bcc2f7cf7470a9a6c (diff)
downloadchef-1343bdfff0d54e20b923211f6697d42c484c1627.tar.gz
Merge pull request #2102 from opscode/tball/homebrew-owner
`brew` command now ran as user owning executable
-rw-r--r--DOC_CHANGES.md55
-rw-r--r--RELEASE_NOTES.md35
-rw-r--r--lib/chef/mixin/homebrew_owner.rb58
-rw-r--r--lib/chef/mixin/homebrew_user.rb68
-rw-r--r--lib/chef/provider/package/homebrew.rb12
-rw-r--r--lib/chef/resource/homebrew_package.rb11
-rw-r--r--spec/unit/mixin/homebrew_owner_spec.rb65
-rw-r--r--spec/unit/mixin/homebrew_user_spec.rb100
-rw-r--r--spec/unit/provider/package/homebrew_spec.rb14
-rw-r--r--spec/unit/resource/homebrew_package_spec.rb21
10 files changed, 307 insertions, 132 deletions
diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md
index 1c16377ccd..ceccb77cd0 100644
--- a/DOC_CHANGES.md
+++ b/DOC_CHANGES.md
@@ -467,3 +467,58 @@ PathHelper = Chef::Util::PathHelper
Dir.glob(File.join(PathHelper.escape_glob(path), "*")) # ["#{path}\\apache2", "#{path}\\apt", ...]
Dir[PathHelper.escape_glob(path) + "/*"] # ["#{path}\\apache2", "#{path}\\apt", ...]
```
+
+## Mac OS X default package provider is now Homebrew
+
+Per [Chef RFC 016](https://github.com/opscode/chef-rfc/blob/master/rfc016-homebrew-osx-package-provider.md), the default provider for the `package` resource on Mac OS X is now [Homebrew](http://brew.sh). The [homebrew cookbook's](https://supermarket.getchef.com/cookbooks/homebrew) default recipe, or some other method is still required for getting homebrew installed on the system. The cookbook won't be strictly required just to install packages from homebrew on OS X, though. To use this, simply use the `package` resource, or the `homebrew_package` shortcut resource:
+
+```ruby
+package 'emacs'
+```
+
+Or,
+
+```ruby
+homebrew_package 'emacs'
+```
+
+The macports provider will still be available, and can be used with the shortcut resource, or by using the `provider` attribute:
+
+```ruby
+macports_package 'emacs'
+```
+
+Or,
+
+```ruby
+package 'emacs' do
+ provider Chef::Provider::Package::Macports
+end
+```
+
+### Providing `homebrew_user`
+
+Homebrew recommends being ran as a non-root user, whereas Chef recommends being ran with root privileges. The
+`homebrew_package` provider has logic to try and determine which user to install Homebrew packages as.
+
+By default, the `homebrew_package` provider will try to execute the homebrew command as the owner of the `/usr/local/bin/brew`
+executable. If that executable does not exist, Chef will try to find it by executing `which brew`. If that cannot be
+found, Chef then errors. The Homebrew recommendation is the default install, which will place the executable at
+`/usr/local/bin/brew` owned by a non-root user.
+
+You can circumvent this by providing the `homebrew_package` a `homebrew_user` attribute, like:
+
+```ruby
+# provided as a uid
+homebrew_package 'emacs' do
+ homebrew_user 1001
+end
+
+# provided as a string
+homebrew_package 'vim' do
+ homebrew_user 'user1'
+end
+```
+
+Chef will then execute the Homebrew command as that user. The `homebrew_user` attribute can only be provided to the
+`homebrew_package` resource, not the `package` resource. \ No newline at end of file
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 4909414b95..a6d1a65f51 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -94,6 +94,41 @@ package 'emacs' do
end
```
+### Providing `homebrew_user`
+
+Homebrew recommends being ran as a non-root user, whereas Chef recommends being ran with root privileges. The
+`homebrew_package` provider has logic to try and determine which user to install Homebrew packages as.
+
+By default, the `homebrew_package` provider will try to execute the homebrew command as the owner of the `/usr/local/bin/brew`
+executable. If that executable does not exist, Chef will try to find it by executing `which brew`. If that cannot be
+found, Chef then errors. The Homebrew recommendation is the default install, which will place the executable at
+`/usr/local/bin/brew` owned by a non-root user.
+
+You can circumvent this by providing the `homebrew_package` a `homebrew_user` attribute, like:
+
+```ruby
+# provided as a uid
+homebrew_package 'emacs' do
+ homebrew_user 1001
+end
+
+# provided as a string
+homebrew_package 'vim' do
+ homebrew_user 'user1'
+end
+```
+
+Chef will then execute the Homebrew command as that user. The `homebrew_user` attribute can only be provided to the
+`homebrew_package` resource, not the `package` resource.
+
+## DSCL user provider now supports Mac OS X 10.7 and above.
+
+DSCL user provider in Chef has supported setting passwords only on Mac OS X 10.6. In this release, Mac OS X versions 10.7 and above are now supported. Support for Mac OS X 10.6 is dropped from the dscl provider since this version is EOLed by Apple.
+
+In order to support configuring passwords for the users using shadow hashes two new attributes `salt` & `iterations` are added to the user resource. These attributes are required to make the new [SALTED-SHA512-PBKDF2](http://en.wikipedia.org/wiki/PBKDF2) style shadow hashes used in Mac OS X versions 10.8 and above.
+
+User resource on Mac supports setting password both using plain-text password or using the shadow hash. You can simply set the `password` attribute to the plain text password to configure the password for the user. However this is not ideal since including plain text passwords in cookbooks (even if they are private) is not a good idea. In order to set passwords using shadow hash you can follow the instructions below based on your Mac OS X version.
+
### Mac OS X 10.7
10.7 calculates the password hash using **SALTED-SHA512**. Stored shadow hash length is 68 bytes; first 4 bytes being salt and the next 64 bytes being the shadow hash itself. You can use below code in order to calculate password hashes to be used in `password` attribute on Mac OS X 10.7:
diff --git a/lib/chef/mixin/homebrew_owner.rb b/lib/chef/mixin/homebrew_owner.rb
deleted file mode 100644
index 73bb22ddf5..0000000000
--- a/lib/chef/mixin/homebrew_owner.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-#
-# Author:: Joshua Timberman (<joshua@getchef.com>)
-# Author:: Graeme Mathieson (<mathie@woss.name>)
-#
-# Copyright 2011-2013, Opscode, Inc.
-# Copyright 2014, Chef Software, Inc <legal@getchef.com>
-#
-# 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.
-#
-# Ported from the homebrew cookbook's Homebrew::Mixin owner helpers
-#
-# This lives here in Chef::Mixin because Chef's namespacing makes it
-# awkward to use modules elsewhere (e.g., chef/provider/package/homebrew/owner)
-
-class Chef
- module Mixin
- module HomebrewOwner
- def homebrew_owner(node)
- @homebrew_owner ||= calculate_owner(node)
- end
-
- private
-
- def calculate_owner(node)
- owner = homebrew_owner_attr(node) || sudo_user || current_user
- if owner == 'root'
- raise Chef::Exceptions::CannotDetermineHomebrewOwner,
- 'The homebrew owner is not specified and the current user is \"root\"' +
- 'Homebrew does not support root installs, please specify the homebrew' +
- 'owner by setting the attribute `node[\'homebrew\'][\'owner\']`.'
- end
- owner
- end
-
- def homebrew_owner_attr(node)
- node['homebrew']['owner'] if node.attribute?('homebrew') && node['homebrew'].attribute?('owner')
- end
-
- def sudo_user
- ENV['SUDO_USER']
- end
-
- def current_user
- ENV['USER']
- end
- end
- end
-end
diff --git a/lib/chef/mixin/homebrew_user.rb b/lib/chef/mixin/homebrew_user.rb
new file mode 100644
index 0000000000..854a954a90
--- /dev/null
+++ b/lib/chef/mixin/homebrew_user.rb
@@ -0,0 +1,68 @@
+#
+# Author:: Joshua Timberman (<joshua@getchef.com>)
+# Author:: Graeme Mathieson (<mathie@woss.name>)
+#
+# Copyright 2011-2013, Opscode, Inc.
+# Copyright 2014, Chef Software, Inc <legal@getchef.com>
+#
+# 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.
+#
+# Ported from the homebrew cookbook's Homebrew::Mixin owner helpers
+#
+# This lives here in Chef::Mixin because Chef's namespacing makes it
+# awkward to use modules elsewhere (e.g., chef/provider/package/homebrew/owner)
+
+require 'chef/mixin/shell_out'
+require 'etc'
+
+class Chef
+ module Mixin
+ module HomebrewUser
+ include Chef::Mixin::ShellOut
+
+ ##
+ # This tries to find the user to execute brew as. If a user is provided, that overrides the brew
+ # executable user. It is an error condition if the brew executable owner is root or we cannot find
+ # the brew executable.
+ def find_homebrew_uid(provided_user = nil)
+ # They could provide us a user name or a UID
+ unless provided_user.nil?
+ return provided_user if provided_user.is_a? Integer
+ return Etc.getpwnam(provided_user).uid
+ end
+
+ @homebrew_owner ||= calculate_owner
+ @homebrew_owner
+ end
+
+ private
+
+ def calculate_owner
+ default_brew_path = '/usr/local/bin/brew'
+ if ::File.exist?(default_brew_path)
+ # By default, this follows symlinks which is what we want
+ owner = ::File.stat(default_brew_path).uid
+ elsif (brew_path = shell_out("which brew").stdout.strip) && !brew_path.empty?
+ owner = ::File.stat(brew_path).uid
+ else
+ raise Chef::Exceptions::CannotDetermineHomebrewOwner,
+ 'Could not find the "brew" executable in /usr/local/bin or anywhere on the path.'
+ end
+
+ Chef::Log.debug "Found Homebrew owner #{Etc.getpwuid(owner).name}; executing `brew` commands as them"
+ owner
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb
index d964703c87..202e4d2533 100644
--- a/lib/chef/provider/package/homebrew.rb
+++ b/lib/chef/provider/package/homebrew.rb
@@ -19,13 +19,13 @@
#
require 'etc'
-require 'chef/mixin/homebrew_owner'
+require 'chef/mixin/homebrew_user'
class Chef
class Provider
class Package
class Homebrew < Chef::Provider::Package
- include Chef::Mixin::HomebrewOwner
+ include Chef::Mixin::HomebrewUser
def load_current_resource
self.current_resource = Chef::Resource::Package.new(new_resource.name)
current_resource.package_name(new_resource.package_name)
@@ -109,12 +109,14 @@ class Chef
private
def get_response_from_command(command)
- home_dir = Etc.getpwnam(homebrew_owner(node)).dir
+ homebrew_uid = find_homebrew_uid(new_resource.homebrew_user)
+ homebrew_user = Etc.getpwuid(homebrew_uid)
- Chef::Log.debug "Executing '#{command}' as user '#{homebrew_owner(node)}'"
- output = shell_out!(command, :timeout => 1800, :user => homebrew_owner(node), :environment => { 'HOME' => home_dir, 'RUBYOPT' => nil })
+ Chef::Log.debug "Executing '#{command}' as user '#{homebrew_user.name}'"
+ output = shell_out!(command, :timeout => 1800, :user => homebrew_uid, :environment => { 'HOME' => homebrew_user.dir, 'RUBYOPT' => nil })
output.stdout.chomp
end
+
end
end
end
diff --git a/lib/chef/resource/homebrew_package.rb b/lib/chef/resource/homebrew_package.rb
index c3fa9ddffc..e1d50c1739 100644
--- a/lib/chef/resource/homebrew_package.rb
+++ b/lib/chef/resource/homebrew_package.rb
@@ -24,11 +24,22 @@ require 'chef/resource/package'
class Chef
class Resource
class HomebrewPackage < Chef::Resource::Package
+
def initialize(name, run_context=nil)
super
@resource_name = :homebrew_package
@provider = Chef::Provider::Package::Homebrew
+ @homebrew_user = nil
+ end
+
+ def homebrew_user(arg=nil)
+ set_or_return(
+ :homebrew_user,
+ arg,
+ :kind_of => [ String, Integer ]
+ )
end
+
end
end
end
diff --git a/spec/unit/mixin/homebrew_owner_spec.rb b/spec/unit/mixin/homebrew_owner_spec.rb
deleted file mode 100644
index 428cd827d9..0000000000
--- a/spec/unit/mixin/homebrew_owner_spec.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-#
-# Author:: Joshua Timberman (<joshua@getchef.com>)
-#
-# Copyright 2014, Chef Software, Inc <legal@getchef.com>
-#
-# 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 'spec_helper'
-require 'chef/mixin/homebrew_owner'
-
-class ExampleHomebrewOwner
- include Chef::Mixin::HomebrewOwner
-end
-
-describe Chef::Mixin::HomebrewOwner do
- before(:each) do
- node.default['homebrew']['owner'] = nil
- end
-
- let(:homebrew_owner) { ExampleHomebrewOwner.new }
- let(:node) { Chef::Node.new }
-
- describe 'when the homebrew owner node attribute is set' do
- it 'raises an exception if the owner is root' do
- node.default['homebrew']['owner'] = 'root'
- expect { homebrew_owner.homebrew_owner(node) }.to raise_exception(Chef::Exceptions::CannotDetermineHomebrewOwner)
- end
-
- it 'returns the owner set by attribute' do
- node.default['homebrew']['owner'] = 'siouxsie'
- expect(homebrew_owner.homebrew_owner(node)).to eql('siouxsie')
- end
- end
-
- describe 'when the owner attribute is not set and we use sudo' do
- before(:each) do
- ENV.stub(:[]).with('SUDO_USER').and_return('john_lydon')
- end
-
- it 'uses the SUDO_USER environment variable' do
- expect(homebrew_owner.homebrew_owner(node)).to eql('john_lydon')
- end
- end
-
- describe 'when the owner attribute is not set and we arent using sudo' do
- before(:each) do
- ENV.stub(:[]).with('USER').and_return('sid_vicious')
- ENV.stub(:[]).with('SUDO_USER').and_return(nil)
- end
-
- it 'uses the current user' do
- expect(homebrew_owner.homebrew_owner(node)).to eql('sid_vicious')
- end
- end
-end
diff --git a/spec/unit/mixin/homebrew_user_spec.rb b/spec/unit/mixin/homebrew_user_spec.rb
new file mode 100644
index 0000000000..4e30455765
--- /dev/null
+++ b/spec/unit/mixin/homebrew_user_spec.rb
@@ -0,0 +1,100 @@
+#
+# Author:: Joshua Timberman (<joshua@getchef.com>)
+#
+# Copyright 2014, Chef Software, Inc <legal@getchef.com>
+#
+# 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 'spec_helper'
+require 'chef/mixin/homebrew_user'
+
+class ExampleHomebrewUser
+ include Chef::Mixin::HomebrewUser
+end
+
+describe Chef::Mixin::HomebrewUser do
+ before(:each) do
+ node.default['homebrew']['owner'] = nil
+ end
+
+ let(:homebrew_user) { ExampleHomebrewUser.new }
+ let(:node) { Chef::Node.new }
+
+ describe 'when the homebrew user is provided' do
+ let(:uid) { 1001 }
+ let(:user) { "foo" }
+
+ it 'returns the homebrew user without looking at the file when uid is provided' do
+ expect(File).to receive(:exist?).exactly(0).times
+ expect(homebrew_user.find_homebrew_uid(uid)).to eq(uid)
+ end
+
+ it 'returns the homebrew user without looking at the file when name is provided' do
+ expect(File).to receive(:exist?).exactly(0).times
+ Etc.stub_chain(:getpwnam, :uid).and_return(uid)
+ expect(homebrew_user.find_homebrew_uid(user)).to eq(uid)
+ end
+
+ end
+
+ shared_examples "successfully find executable" do
+ let(:user) { nil }
+ let(:brew_owner) { 2001 }
+ let(:default_brew_path) { '/usr/local/bin/brew' }
+ let(:stat_double) {
+ d = double()
+ expect(d).to receive(:uid).and_return(brew_owner)
+ d
+ }
+
+ context "debug statement prints owner name" do
+
+ before do
+ expect(Etc).to receive(:getpwuid).with(brew_owner).and_return(OpenStruct.new(:name => "name"))
+ end
+
+ it 'returns the owner of the brew executable when it is at a default location' do
+ expect(File).to receive(:exist?).with(default_brew_path).and_return(true)
+ expect(File).to receive(:stat).with(default_brew_path).and_return(stat_double)
+ expect(homebrew_user.find_homebrew_uid(user)).to eq(brew_owner)
+ end
+
+ it 'returns the owner of the brew executable when it is not at a default location' do
+ expect(File).to receive(:exist?).with(default_brew_path).and_return(false)
+ homebrew_user.stub_chain(:shell_out, :stdout, :strip).and_return("/foo")
+ expect(File).to receive(:stat).with("/foo").and_return(stat_double)
+ expect(homebrew_user.find_homebrew_uid(user)).to eq(brew_owner)
+ end
+
+ end
+ end
+
+ describe 'when the homebrew user is not provided' do
+
+ it 'raises an error if no executable is found' do
+ expect(File).to receive(:exist?).with(default_brew_path).and_return(false)
+ homebrew_user.stub_chain(:shell_out, :stdout, :strip).and_return("")
+ expect { homebrew_user.find_homebrew_uid(user) }.to raise_error(Chef::Exceptions::CannotDetermineHomebrewOwner)
+ end
+
+ include_examples "successfully find executable"
+
+ context "the executable is owned by root" do
+ include_examples "successfully find executable" do
+ let(:brew_owner) { 0 }
+ end
+ end
+
+ end
+
+end
diff --git a/spec/unit/provider/package/homebrew_spec.rb b/spec/unit/provider/package/homebrew_spec.rb
index 9f105c13b8..d38458546d 100644
--- a/spec/unit/provider/package/homebrew_spec.rb
+++ b/spec/unit/provider/package/homebrew_spec.rb
@@ -28,6 +28,8 @@ describe Chef::Provider::Package::Homebrew do
Chef::Provider::Package::Homebrew.new(new_resource, run_context)
end
+ let(:homebrew_uid) { 1001 }
+
let(:uninstalled_brew_info) do
{
'name' => 'emacs',
@@ -92,8 +94,7 @@ describe Chef::Provider::Package::Homebrew do
end
before(:each) do
- node.default['homebrew']['owner'] = 'sid_vicious'
- allow(Etc).to receive(:getpwnam).with('sid_vicious').and_return('/Users/sid_vicious')
+
end
describe 'load_current_resource' do
@@ -143,13 +144,18 @@ describe Chef::Provider::Package::Homebrew do
end
describe 'brew' do
+ before do
+ expect(provider).to receive(:find_homebrew_uid).and_return(homebrew_uid)
+ expect(Etc).to receive(:getpwuid).with(homebrew_uid).and_return(OpenStruct.new(:name => "name", :dir => "/"))
+ end
+
it 'passes a single to the brew command and return stdout' do
- allow(provider).to receive(:get_response_from_command).and_return('zombo')
+ allow(provider).to receive(:shell_out!).and_return(OpenStruct.new(:stdout => 'zombo'))
expect(provider.brew).to eql('zombo')
end
it 'takes multiple arguments as an array' do
- allow(provider).to receive(:get_response_from_command).and_return('homestarrunner')
+ allow(provider).to receive(:shell_out!).and_return(OpenStruct.new(:stdout => 'homestarrunner'))
expect(provider.brew('info', 'opts', 'bananas')).to eql('homestarrunner')
end
end
diff --git a/spec/unit/resource/homebrew_package_spec.rb b/spec/unit/resource/homebrew_package_spec.rb
index 4b4f9afe5e..bb657607b7 100644
--- a/spec/unit/resource/homebrew_package_spec.rb
+++ b/spec/unit/resource/homebrew_package_spec.rb
@@ -33,4 +33,25 @@ describe Chef::Resource::HomebrewPackage, 'initialize' do
expect(resource.provider).to eql(Chef::Provider::Package::Homebrew)
end
+ it 'sets the homebrew_user to nil' do
+ expect(resource.homebrew_user).to eql(nil)
+ end
+
+ shared_examples 'home_brew user set and returned' do
+ it 'returns the configured homebrew_user' do
+ resource.homebrew_user user
+ expect(resource.homebrew_user).to eql(user)
+ end
+ end
+
+ context 'homebrew_user is set' do
+ let(:user) { 'Captain Picard' }
+ include_examples 'home_brew user set and returned'
+
+ context 'as an integer' do
+ let(:user) { 1001 }
+ include_examples 'home_brew user set and returned'
+ end
+ end
+
end