summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2018-03-20 10:54:16 -0700
committerGitHub <noreply@github.com>2018-03-20 10:54:16 -0700
commit6635bb644dd03ba7877d2a1b7ba931efa65ca79f (patch)
treebba0ca1acb5e893fecfa690048728c889d4a7ca2
parent51c59ecb887e9122aa849d8153b9984df74b2b09 (diff)
parent7136bc0638e76c97bbbdcd2af3772def2a8dd2a1 (diff)
downloadchef-6635bb644dd03ba7877d2a1b7ba931efa65ca79f.tar.gz
Merge pull request #6963 from chef/macos_resources
Add dmg_package, homebrew_cask, and homebrew_tap resources
-rw-r--r--lib/chef/mixin/homebrew_user.rb16
-rw-r--r--lib/chef/resource/dmg_package.rb161
-rw-r--r--lib/chef/resource/homebrew_cask.rb98
-rw-r--r--lib/chef/resource/homebrew_tap.rb86
-rw-r--r--lib/chef/resources.rb3
-rw-r--r--spec/unit/mixin/homebrew_user_spec.rb6
-rw-r--r--spec/unit/resource/dmg_package_spec.rb35
-rw-r--r--spec/unit/resource/homebrew_cask_spec.rb35
-rw-r--r--spec/unit/resource/homebrew_tap_spec.rb39
9 files changed, 471 insertions, 8 deletions
diff --git a/lib/chef/mixin/homebrew_user.rb b/lib/chef/mixin/homebrew_user.rb
index 6e32043c77..b038dfd3b7 100644
--- a/lib/chef/mixin/homebrew_user.rb
+++ b/lib/chef/mixin/homebrew_user.rb
@@ -34,7 +34,8 @@ class Chef
# 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.
- # @param provided_user [String]
+ # @param [String, Integer] provided_user
+ # @return [Integer] UID of the user
def find_homebrew_uid(provided_user = nil)
# They could provide us a user name or a UID
if provided_user
@@ -42,8 +43,17 @@ class Chef
return Etc.getpwnam(provided_user).uid
end
- @homebrew_owner ||= calculate_owner
- @homebrew_owner
+ @homebrew_owner_uid ||= calculate_owner
+ @homebrew_owner_uid
+ end
+
+ # Use find_homebrew_uid to return the UID and then lookup the
+ # name from that UID because sometimes you want the name not the UID
+ # @param [String, Integer] provided_user
+ # @return [String] username
+ def find_homebrew_username(provided_user = nil)
+ @homebrew_owner_username ||= Etc.getpwuid(find_homebrew_uid(provided_user)).name
+ @homebrew_owner_username
end
private
diff --git a/lib/chef/resource/dmg_package.rb b/lib/chef/resource/dmg_package.rb
new file mode 100644
index 0000000000..cdfe764643
--- /dev/null
+++ b/lib/chef/resource/dmg_package.rb
@@ -0,0 +1,161 @@
+#
+# Author:: Joshua Timberman (<jtimberman@chef.io>)
+# Copyright:: 2011-2018, Chef Software, Inc.
+#
+# 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/resource"
+
+class Chef
+ class Resource
+ class DmgPackage < Chef::Resource
+ resource_name :dmg_package
+
+ description "Use the dmg_package resourceto install a DMG 'Package'. The resource will retrieve the"\
+ " DMG file from a remote URL, mount it using OS X's hdid, copy the application (.app directory)"\
+ " to the specified destination (/Applications), and detach the image using hdiutil. The dmg file"\
+ "will be stored in the Chef::Config[:file_cache_path]."
+ introduced "14.0"
+
+ property :app, String,
+ description: "The name of the application used by default for the /Volumes directory and the .app directory copied to /Applications.",
+ name_property: true
+
+ property :source, String,
+ description: "The remote URL for the dmg to download if specified."
+
+ property :file, String,
+ description: "The local dmg full file path."
+
+ property :owner, String,
+ description: "The owner that should own the package installation."
+
+ property :destination, String,
+ description: "The directory to copy the .app into.",
+ default: "/Applications"
+
+ property :checksum, String,
+ description: "The sha256 checksum of the dmg to download"
+
+ property :volumes_dir, String,
+ description: "The Directory under /Volumes where the dmg is mounted as not all dmgs are mounted into a /Volumes location matching the name of the dmg."
+
+ property :dmg_name, String,
+ description: "The name of the dmg if it is not the same as app, or if the name has spaces."
+
+ property :type, String,
+ description: "The type of package.",
+ equal_to: %w{app pkg mpkg},
+ default: "app"
+
+ property :installed, [TrueClass, FalseClass],
+ default: false, desired_state: false
+
+ property :package_id, String,
+ description: "The package id registered with pkgutil when a pkg or mpkg is installed"
+
+ property :dmg_passphrase, String,
+ description: "Specify a passphrase to use to unencrypt the dmg while mounting."
+
+ property :accept_eula, [TrueClass, FalseClass],
+ description: "Specify whether to accept the EULA. Certain dmgs require acceptance of EULA before mounting.",
+ default: false
+
+ property :headers, [Hash, nil],
+ description: "Allows custom HTTP headers (like cookies) to be set on the remote_file resource.",
+ default: nil
+
+ property :allow_untrusted, [TrueClass, FalseClass],
+ description: "Allows packages with untrusted certs to be installed.",
+ default: false
+
+ load_current_value do |new_resource|
+ if ::File.directory?("#{new_resource.destination}/#{new_resource.app}.app")
+ Chef::Log.info "Already installed; to upgrade, remove \"#{new_resource.destination}/#{new_resource.app}.app\""
+ installed true
+ elsif shell_out("pkgutil --pkgs='#{new_resource.package_id}'").exitstatus == 0
+ Chef::Log.info "Already installed; to upgrade, try \"sudo pkgutil --forget '#{new_resource.package_id}'\""
+ installed true
+ else
+ installed false
+ end
+ end
+
+ action :install do
+ description "Installs the application."
+
+ unless current_resource.installed
+
+ volumes_dir = new_resource.volumes_dir ? new_resource.volumes_dir : new_resource.app
+ dmg_name = new_resource.dmg_name ? new_resource.dmg_name : new_resource.app
+
+ if new_resource.source
+ declare_resource(:remote_file, "#{dmg_file} - #{new_resource.name}") do
+ path dmg_file
+ source new_resource.source
+ headers new_resource.headers if new_resource.headers
+ checksum new_resource.checksum if new_resource.checksum
+ end
+ end
+
+ passphrase_cmd = new_resource.dmg_passphrase ? "-passphrase #{new_resource.dmg_passphrase}" : ""
+ ruby_block "attach #{dmg_file}" do
+ block do
+ cmd = shell_out("hdiutil imageinfo #{passphrase_cmd} '#{dmg_file}' | grep -q 'Software License Agreement: true'")
+ software_license_agreement = cmd.exitstatus == 0
+ raise "Requires EULA Acceptance; add 'accept_eula true' to package resource" if software_license_agreement && !new_resource.accept_eula
+ accept_eula_cmd = new_resource.accept_eula ? "echo Y | PAGER=true" : ""
+ shell_out!("#{accept_eula_cmd} hdiutil attach #{passphrase_cmd} '#{dmg_file}' -mountpoint '/Volumes/#{volumes_dir}' -quiet")
+ end
+ not_if "hdiutil info #{passphrase_cmd} | grep -q 'image-path.*#{dmg_file}'"
+ end
+
+ case new_resource.type
+ when "app"
+ declare_resource(:execute, "rsync --force --recursive --links --perms --executability --owner --group --times '/Volumes/#{volumes_dir}/#{new_resource.app}.app' '#{new_resource.destination}'") do
+ user new_resource.owner if new_resource.owner
+ end
+
+ declare_resource(:file, "#{new_resource.destination}/#{new_resource.app}.app/Contents/MacOS/#{new_resource.app}") do
+ mode "755"
+ ignore_failure true
+ end
+ when "mpkg", "pkg"
+ install_cmd = "installation_file=$(ls '/Volumes/#{volumes_dir}' | grep '.#{new_resource.type}$') && sudo installer -pkg \"/Volumes/#{volumes_dir}/$installation_file\" -target /"
+ install_cmd += " -allowUntrusted" if new_resource.allow_untrusted
+
+ declare_resource(:execute, install_cmd) do
+ # Prevent cfprefsd from holding up hdiutil detach for certain disk images
+ environment("__CFPREFERENCES_AVOID_DAEMON" => "1")
+ end
+ end
+
+ declare_resource(:execute, "hdiutil detach '/Volumes/#{volumes_dir}' || hdiutil detach '/Volumes/#{volumes_dir}' -force")
+ end
+ end
+
+ action_class do
+ def dmg_file
+ @dmg_file ||= begin
+ if new_resource.file.nil?
+ "#{Chef::Config[:file_cache_path]}/#{dmg_name}.dmg"
+ else
+ new_resource.file
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/homebrew_cask.rb b/lib/chef/resource/homebrew_cask.rb
new file mode 100644
index 0000000000..d10ecb04c9
--- /dev/null
+++ b/lib/chef/resource/homebrew_cask.rb
@@ -0,0 +1,98 @@
+#
+# Author:: Joshua Timberman (<jtimberman@chef.io>)
+# Author:: Graeme Mathieson (<mathie@woss.name>)
+#
+# Copyright:: 2011-2018, Chef Software, Inc.
+#
+# 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/resource"
+require "chef/mixin/homebrew_user"
+
+class Chef
+ class Resource
+ class HomebrewCask < Chef::Resource
+ resource_name :homebrew_cask
+
+ description "Use the homebrew_cask resource to install binaries distributed via the Homebrew package manager."
+ introduced "14.0"
+
+ include Chef::Mixin::HomebrewUser
+
+ property :cask_name, String,
+ description: "Cask name to override the resource name.",
+ regex: %r{^[\w/-]+$},
+ name_property: true
+
+ property :options, String,
+ description: "Options to pass to the brew CLI during installation."
+
+ property :install_cask, [TrueClass, FalseClass],
+ description: "Auto install cask tap if necessary.",
+ default: true
+
+ property :homebrew_path, String,
+ description: "The path to the homebrew binary.",
+ default: "/usr/local/bin/brew"
+
+ property :owner, String,
+ description: "The owner of the homebrew installation.",
+ default: lazy { Chef::Mixin::HomebrewUser.find_homebrew_username }
+
+ action :install do
+ description "Install an application packaged as a Homebrew cask."
+
+ homebrew_tap "caskroom/cask" if new_resource.install_cask
+
+ unless casked?
+ converge_by("install cask #{new_resource.name} #{new_resource.options}") do
+ shell_out!("#{new_resource.homebrew_path} cask install #{new_resource.name} #{new_resource.options}",
+ user: new_resource.owner,
+ env: { "HOME" => ::Dir.home(new_resource.owner), "USER" => new_resource.owner },
+ cwd: ::Dir.home(new_resource.owner))
+ end
+ end
+ end
+
+ action :remove do
+ description "Remove an application packaged as a Homebrew cask."
+
+ homebrew_tap "caskroom/cask" if new_resource.install_cask
+
+ if casked?
+ converge_by("uninstall cask #{new_resource.name}") do
+ shell_out!("#{new_resource.homebrew_path} cask uninstall #{new_resource.name}",
+ user: new_resource.owner,
+ env: { "HOME" => ::Dir.home(new_resource.owner), "USER" => new_resource.owner },
+ cwd: ::Dir.home(new_resource.owner))
+ end
+ end
+ end
+
+ action_class do
+ alias_method :action_cask, :action_install
+ alias_method :action_uncask, :action_remove
+ alias_method :action_uninstall, :action_remove
+
+ def casked?
+ unscoped_name = new_resource.name.split("/").last
+ shell_out!('#{new_resource.homebrew_path} cask list 2>/dev/null',
+ user: new_resource.owner,
+ env: { "HOME" => ::Dir.home(new_resource.owner), "USER" => new_resource.owner },
+ cwd: ::Dir.home(new_resource.owner)).stdout.split.include?(unscoped_name)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/chef/resource/homebrew_tap.rb b/lib/chef/resource/homebrew_tap.rb
new file mode 100644
index 0000000000..8ce72e0861
--- /dev/null
+++ b/lib/chef/resource/homebrew_tap.rb
@@ -0,0 +1,86 @@
+#
+# Author:: Joshua Timberman (<jtimberman@chef.io>)
+# Author:: Graeme Mathieson (<mathie@woss.name>)
+#
+# Copyright:: 2011-2018, Chef Software, Inc.
+#
+# 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/resource"
+require "chef/mixin/homebrew_user"
+
+class Chef
+ class Resource
+ class HomebrewTap < Chef::Resource
+ resource_name :homebrew_tap
+
+ description "Use the homebrew_tap resource to add additional formula repositories to the Homebrew package manager."
+ introduced "14.0"
+
+ include Chef::Mixin::HomebrewUser
+
+ property :tap_name, String,
+ description: "Optional tap name to override the resource name",
+ validation_message: "Homebrew tap names must be in the form REPO/TAP",
+ regex: %r{^[\w-]+(?:\/[\w-]+)+$},
+ name_property: true
+
+ property :url, String,
+ description: "URL to the tap."
+
+ property :full, [TrueClass, FalseClass],
+ description: "Perform a full clone rather than a shallow clone on the tap.",
+ default: false
+
+ property :homebrew_path, String,
+ description: "The path to the homebrew binary.",
+ default: "/usr/local/bin/brew"
+
+ property :owner, String,
+ description: "The owner of the homebrew installation",
+ default: lazy { Chef::Mixin::HomebrewUser.find_homebrew_username }
+
+ action :tap do
+ description "Add a Homebrew tap."
+
+ unless tapped?(new_resource.name)
+ converge_by("tap #{new_resource.name}") do
+ shell_out!("#{new_resource.homebrew_path} tap #{new_resource.full ? '--full' : ''} #{new_resource.name} #{new_resource.url || ''}",
+ user: new_resource.owner,
+ env: { "HOME" => ::Dir.home(new_resource.owner), "USER" => new_resource.owner },
+ cwd: ::Dir.home(new_resource.owner))
+ end
+ end
+ end
+
+ action :untap do
+ description "Remove a Homebrew tap."
+
+ if tapped?(new_resource.name)
+ converge_by("untap #{new_resource.name}") do
+ shell_out!("#{new_resource.homebrew_path} untap #{new_resource.name}",
+ user: new_resource.owner,
+ env: { "HOME" => ::Dir.home(new_resource.owner), "USER" => new_resource.owner },
+ cwd: ::Dir.home(new_resource.owner))
+ end
+ end
+ end
+
+ def tapped?(name)
+ tap_dir = name.gsub("/", "/homebrew-")
+ ::File.directory?("/usr/local/Homebrew/Library/Taps/#{tap_dir}")
+ end
+ end
+ end
+end
diff --git a/lib/chef/resources.rb b/lib/chef/resources.rb
index dc235deb06..a5d5423bfe 100644
--- a/lib/chef/resources.rb
+++ b/lib/chef/resources.rb
@@ -31,6 +31,7 @@ require "chef/resource/chocolatey_package"
require "chef/resource/cron"
require "chef/resource/csh"
require "chef/resource/directory"
+require "chef/resource/dmg_package"
require "chef/resource/dpkg_package"
require "chef/resource/dnf_package"
require "chef/resource/dsc_script"
@@ -44,7 +45,9 @@ require "chef/resource/git"
require "chef/resource/group"
require "chef/resource/http_request"
require "chef/resource/hostname"
+require "chef/resource/homebrew_cask"
require "chef/resource/homebrew_package"
+require "chef/resource/homebrew_tap"
require "chef/resource/ifconfig"
require "chef/resource/ksh"
require "chef/resource/launchd"
diff --git a/spec/unit/mixin/homebrew_user_spec.rb b/spec/unit/mixin/homebrew_user_spec.rb
index c9a6e6e909..67d79719aa 100644
--- a/spec/unit/mixin/homebrew_user_spec.rb
+++ b/spec/unit/mixin/homebrew_user_spec.rb
@@ -1,7 +1,7 @@
#
# Author:: Joshua Timberman (<joshua@chef.io>)
#
-# Copyright 2014-2016, Chef Software, Inc <legal@chef.io>
+# Copyright 2014-2018, Chef Software, Inc <legal@chef.io>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -23,10 +23,6 @@ class ExampleHomebrewUser
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 }
diff --git a/spec/unit/resource/dmg_package_spec.rb b/spec/unit/resource/dmg_package_spec.rb
new file mode 100644
index 0000000000..d1d8a71188
--- /dev/null
+++ b/spec/unit/resource/dmg_package_spec.rb
@@ -0,0 +1,35 @@
+#
+# Copyright:: Copyright 2018, Chef Software, 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 "spec_helper"
+
+describe Chef::Resource::DmgPackage do
+
+ let(:resource) { Chef::Resource::DmgPackage.new("myapp") }
+
+ it "has a resource name of :dmg_package" do
+ expect(resource.resource_name).to eql(:dmg_package)
+ end
+
+ it "has a default action of install" do
+ expect(resource.action).to eql([:install])
+ end
+
+ it "the app property is the name property" do
+ expect(resource.app).to eql("myapp")
+ end
+end
diff --git a/spec/unit/resource/homebrew_cask_spec.rb b/spec/unit/resource/homebrew_cask_spec.rb
new file mode 100644
index 0000000000..9b04a0328d
--- /dev/null
+++ b/spec/unit/resource/homebrew_cask_spec.rb
@@ -0,0 +1,35 @@
+#
+# Copyright:: Copyright 2018, Chef Software, 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 "spec_helper"
+
+describe Chef::Resource::HomebrewCask do
+
+ let(:resource) { Chef::Resource::HomebrewCask.new("myapp") }
+
+ it "has a resource name of :homebrew_cask" do
+ expect(resource.resource_name).to eql(:homebrew_cask)
+ end
+
+ it "has a default action of install" do
+ expect(resource.action).to eql([:install])
+ end
+
+ it "the cask_name property is the name property" do
+ expect(resource.cask_name).to eql("myapp")
+ end
+end
diff --git a/spec/unit/resource/homebrew_tap_spec.rb b/spec/unit/resource/homebrew_tap_spec.rb
new file mode 100644
index 0000000000..2b93c11c28
--- /dev/null
+++ b/spec/unit/resource/homebrew_tap_spec.rb
@@ -0,0 +1,39 @@
+#
+# Copyright:: Copyright 2018, Chef Software, 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 "spec_helper"
+
+describe Chef::Resource::HomebrewTap do
+
+ let(:resource) { Chef::Resource::HomebrewTap.new("user/mytap") }
+
+ it "has a resource name of :homebrew_tap" do
+ expect(resource.resource_name).to eql(:homebrew_tap)
+ end
+
+ it "has a default action of tap" do
+ expect(resource.action).to eql([:tap])
+ end
+
+ it "the tap_name property is the name property" do
+ expect(resource.tap_name).to eql("user/mytap")
+ end
+
+ it "fails if tap_name isn't in the USER/TAP format" do
+ expect { resource.tap_name "mytap" }.to raise_error(ArgumentError)
+ end
+end