diff options
-rw-r--r-- | lib/chef/mixin/homebrew_user.rb | 16 | ||||
-rw-r--r-- | lib/chef/resource/dmg_package.rb | 161 | ||||
-rw-r--r-- | lib/chef/resource/homebrew_cask.rb | 98 | ||||
-rw-r--r-- | lib/chef/resource/homebrew_tap.rb | 86 | ||||
-rw-r--r-- | lib/chef/resources.rb | 3 | ||||
-rw-r--r-- | spec/unit/mixin/homebrew_user_spec.rb | 6 | ||||
-rw-r--r-- | spec/unit/resource/dmg_package_spec.rb | 35 | ||||
-rw-r--r-- | spec/unit/resource/homebrew_cask_spec.rb | 35 | ||||
-rw-r--r-- | spec/unit/resource/homebrew_tap_spec.rb | 39 |
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 |