diff options
author | Amulya <logwolvy+github@gmail.com> | 2021-07-06 20:15:17 +0530 |
---|---|---|
committer | Amulya <logwolvy+github@gmail.com> | 2021-07-06 20:15:17 +0530 |
commit | 66d932f7e5a331434e9353af7788f596824e3eb4 (patch) | |
tree | fed95414dde954c5f2cb4110098e23afb69c5e7e | |
parent | 8457513756bfaed5e23b581990da023dd21aeb7b (diff) | |
download | chef-66d932f7e5a331434e9353af7788f596824e3eb4.tar.gz |
POC: Core Foundation based macos_userdefaults rewritecf-based-macos-userdefaults-rewrite
Signed-off-by: Amulya <atomer@progress.com>
-rw-r--r-- | Gemfile | 2 | ||||
-rw-r--r-- | Gemfile.lock | 12 | ||||
-rw-r--r-- | chef.gemspec | 2 | ||||
-rw-r--r-- | lib/chef/resource/macos_userdefaults.rb | 154 | ||||
-rw-r--r-- | spec/unit/resource/macos_user_defaults_spec.rb | 91 |
5 files changed, 72 insertions, 189 deletions
@@ -7,6 +7,8 @@ gem "ohai", git: "https://github.com/chef/ohai.git", branch: "master" gem "chef-utils", path: File.expand_path("chef-utils", __dir__) if File.exist?(File.expand_path("chef-utils", __dir__)) gem "chef-config", path: File.expand_path("chef-config", __dir__) if File.exist?(File.expand_path("chef-config", __dir__)) +gem "corefoundation", git: "https://github.com/chef/corefoundation.git" + if File.exist?(File.expand_path("chef-bin", __dir__)) # bundling in a git checkout gem "chef-bin", path: File.expand_path("chef-bin", __dir__) diff --git a/Gemfile.lock b/Gemfile.lock index 22e620861e..d04baf52a6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -7,6 +7,13 @@ GIT rubocop (= 1.17.0) GIT + remote: https://github.com/chef/corefoundation.git + revision: 7df25155153ca6b5a14a494e63cfb2b8f7548f8b + specs: + corefoundation (0.2.0) + ffi + +GIT remote: https://github.com/chef/ohai.git revision: faf25314310a9ce27a7ba3e73ffb8301633dec83 branch: master @@ -41,6 +48,7 @@ PATH chef-utils (= 17.3.16) chef-vault chef-zero (>= 14.0.11) + corefoundation diff-lcs (>= 1.2.4, < 1.4.0) erubis (~> 2.7) ffi (>= 1.5.0) @@ -68,6 +76,7 @@ PATH chef-utils (= 17.3.16) chef-vault chef-zero (>= 14.0.11) + corefoundation diff-lcs (>= 1.2.4, < 1.4.0) erubis (~> 2.7) ffi (>= 1.5.0) @@ -419,6 +428,7 @@ DEPENDENCIES chef-vault cheffish (>= 17) chefstyle! + corefoundation! fauxhai-ng inspec-core-bin (~> 4.24) ohai! @@ -432,4 +442,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.2.19 + 2.2.20 diff --git a/chef.gemspec b/chef.gemspec index 76f562cc71..45d6f6b941 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -55,6 +55,8 @@ Gem::Specification.new do |s| s.add_dependency "proxifier", "~> 1.0" + s.add_dependency "corefoundation" + s.bindir = "bin" s.executables = %w{ } diff --git a/lib/chef/resource/macos_userdefaults.rb b/lib/chef/resource/macos_userdefaults.rb index 036bd96d8b..37ad5e5a9c 100644 --- a/lib/chef/resource/macos_userdefaults.rb +++ b/lib/chef/resource/macos_userdefaults.rb @@ -17,6 +17,7 @@ require_relative "../resource" require "chef-utils/dist" unless defined?(ChefUtils::Dist) +require "corefoundation" autoload :Plist, "plist" class Chef @@ -89,10 +90,11 @@ class Chef property :type, String, description: "The value type of the preference key.", + deprecated: true, equal_to: %w{bool string int float array dict}, desired_state: false - property :user, String, + property :user, [String, Symbol], description: "The system user that the default will be applied to.", desired_state: false @@ -102,50 +104,20 @@ class Chef desired_state: false load_current_value do |new_resource| - Chef::Log.debug "#load_current_value: shelling out \"#{defaults_export_cmd(new_resource).join(" ")}\" to determine state" - state = shell_out(defaults_export_cmd(new_resource), user: new_resource.user) - - if state.error? || state.stdout.empty? - Chef::Log.debug "#load_current_value: #{defaults_export_cmd(new_resource).join(" ")} returned stdout: #{state.stdout} and stderr: #{state.stderr}" - current_value_does_not_exist! - end + state = read_preferences(new_resource) - plist_data = ::Plist.parse_xml(state.stdout) - - # handle the situation where the key doesn't exist in the domain - if plist_data.key?(new_resource.key) - key new_resource.key - else + # TODO: error handling + unless state + Chef::Log.debug "#{new_resource.key} could not be found in the domain" current_value_does_not_exist! end - value plist_data[new_resource.key] - end - - # - # The defaults command to export a domain - # - # @return [Array] defaults command - # - def defaults_export_cmd(resource) - state_cmd = ["/usr/bin/defaults"] - - if resource.host == "current" - state_cmd.concat(["-currentHost"]) - elsif resource.host # they specified a non-nil value, which is a hostname - state_cmd.concat(["-host", resource.host]) - end - - state_cmd.concat(["export", resource.domain, "-"]) - state_cmd + value state end action :write, description: "Write the value to the specified domain/key." do converge_if_changed do - cmd = defaults_modify_cmd - Chef::Log.debug("Updating defaults value by shelling out: #{cmd.join(" ")}") - - shell_out!(cmd, user: new_resource.user) + write_preferences(new_resource) end end @@ -153,98 +125,42 @@ class Chef # if it's not there there's nothing to remove return unless current_resource - converge_by("delete domain:#{new_resource.domain} key:#{new_resource.key}") do + # TODO: implement in CF and use here + end - cmd = defaults_modify_cmd - Chef::Log.debug("Removing defaults key by shelling out: #{cmd.join(" ")}") + action_class do + CF_MAPPING = { + current_user: CF::Preferences::CURRENT_USER, + all_users: CF::Preferences::ALL_USERS, + current_host: CF::Preferences::CURRENT_HOST, + all_hosts: CF::Preferences::ALL_HOSTS, + current: CF::Preferences::CURRENT_HOST # TODO: deprecation warning for this option + } + + def read_preferences(new_resource) + CF::Preferences.get(new_resource.key, new_resource.domain, mapped_user, mapped_host) + end - shell_out!(cmd, user: new_resource.user) + def write_preferences(new_resource) + CF::Preferences.set(new_resource.key, new_resource.value, new_resource.domain, mapped_user, mapped_host) end - end - action_class do - # - # The command used to write or delete delete values from domains - # - # @return [Array] Array representation of defaults command to run - # - def defaults_modify_cmd - cmd = ["/usr/bin/defaults"] - - if new_resource.host == :current - cmd.concat(["-currentHost"]) - elsif new_resource.host # they specified a non-nil value, which is a hostname - cmd.concat(["-host", new_resource.host]) - end - - cmd.concat([action.to_s, new_resource.domain, new_resource.key]) - cmd.concat(processed_value) if action == :write - cmd.prepend("sudo") if new_resource.sudo - cmd + def mapped_user + CF_MAPPING[valid_user.to_sym] || valid_user.to_s end - # - # convert the provided value into the format defaults expects - # - # @return [array] array of values starting with the type if applicable - # - def processed_value - type = new_resource.type || value_type(new_resource.value) - - # when dict this creates an array of values ["Key1", "Value1", "Key2", "Value2" ...] - cmd_values = ["-#{type}"] - - case type - when "dict" - cmd_values.concat(new_resource.value.flatten) - when "array" - cmd_values.concat(new_resource.value) - when "bool" - cmd_values.concat(bool_to_defaults_bool(new_resource.value)) - else - cmd_values.concat([new_resource.value]) - end - - cmd_values + def mapped_host + CF_MAPPING[valid_host.to_sym] || valid_host.to_s end - # - # defaults booleans on the CLI must be 'TRUE' or 'FALSE' so convert various inputs to that - # - # @param [String, Integer, Boolean] input <description> - # - # @return [String] TRUE or FALSE - # - def bool_to_defaults_bool(input) - return ["TRUE"] if [true, "TRUE", "1", "true", "YES", "yes"].include?(input) - return ["FALSE"] if [false, "FALSE", "0", "false", "NO", "no"].include?(input) - - # make sure it's very clear bad input was given - raise ArgumentError, "#{input} cannot be converted to a boolean value for use with Apple's defaults command. Acceptable values are: 'TRUE', 'YES', 'true, 'yes', '0', true, 'FALSE', 'false', 'NO', 'no', '1', or false." + def valid_user + # TODO: check backward compatibility and defaults util convention + new_resource.user || :current_user end - # - # convert ruby type to defaults type - # - # @param [Integer, Float, String, TrueClass, FalseClass, Hash, Array] value The value being set - # - # @return [string, nil] the type value used by defaults or nil if not applicable - # - def value_type(value) - case value - when true, false - "bool" - when Integer - "int" - when Float - "float" - when Hash - "dict" - when Array - "array" - when String - "string" - end + def valid_host + # TODO: check backward compatibility and defaults util convention + new_resource.host || :all_hosts end end end diff --git a/spec/unit/resource/macos_user_defaults_spec.rb b/spec/unit/resource/macos_user_defaults_spec.rb index 2c643ab266..883c9b6b85 100644 --- a/spec/unit/resource/macos_user_defaults_spec.rb +++ b/spec/unit/resource/macos_user_defaults_spec.rb @@ -51,86 +51,39 @@ describe Chef::Resource::MacosUserDefaults do expect { resource.action :write }.not_to raise_error end - describe "#defaults_export_cmd" do - it "exports NSGlobalDomain if no domain is set" do - expect(provider.defaults_export_cmd(resource)).to eq(["/usr/bin/defaults", "export", "NSGlobalDomain", "-"]) + describe '#mapped_host' do + it "uses `all_hosts` as default" do + expect(provider.mapped_host).to eq CF::Preferences::ALL_HOSTS end - it "exports a provided domain" do - resource.domain "com.tim" - expect(provider.defaults_export_cmd(resource)).to eq(["/usr/bin/defaults", "export", "com.tim", "-"]) - end - - it "sets -currentHost if host is 'current'" do - resource.host "current" - expect(provider.defaults_export_cmd(resource)).to eq(["/usr/bin/defaults", "-currentHost", "export", "NSGlobalDomain", "-"]) - end - - it "sets -host 'tim-laptop if host is 'tim-laptop'" do - resource.host "tim-laptop" - expect(provider.defaults_export_cmd(resource)).to eq(["/usr/bin/defaults", "-host", "tim-laptop", "export", "NSGlobalDomain", "-"]) - end - end - - describe "#defaults_modify_cmd" do - # avoid needing to set these required values over and over. We'll overwrite them where necessary - before do - resource.key = "foo" - resource.value = "bar" - end - - it "writes to NSGlobalDomain if domain isn't specified" do - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "write", "NSGlobalDomain", "foo", "-string", "bar"]) - end - - it "uses the domain property if set" do - resource.domain = "MyCustomDomain" - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "write", "MyCustomDomain", "foo", "-string", "bar"]) - end - - it "sets host specific values using host property" do - resource.host = "tims_laptop" - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "-host", "tims_laptop", "write", "NSGlobalDomain", "foo", "-string", "bar"]) - end - - it "if host is set to :current it passes CurrentHost" do + it "maps `current` to corresponding constant" do resource.host = :current - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "-currentHost", "write", "NSGlobalDomain", "foo", "-string", "bar"]) + expect(provider.mapped_host).to eq CF::Preferences::CURRENT_HOST end - it "raises ArgumentError if bool is specified, but the value can't be made into a bool" do - resource.type "bool" - expect { provider.defaults_modify_cmd }.to raise_error(ArgumentError) - end - - it "autodetects array type and passes individual values" do - resource.value = %w{one two three} - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "write", "NSGlobalDomain", "foo", "-array", "one", "two", "three"]) - end - - it "autodetects string type and passes a single value" do - resource.value = "one" - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "write", "NSGlobalDomain", "foo", "-string", "one"]) - end - - it "autodetects integer type and passes a single value" do - resource.value = 1 - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "write", "NSGlobalDomain", "foo", "-int", 1]) + it "maps `current_host` to correct corresponding constant" do + resource.host = :current_host + expect(provider.mapped_host).to eq CF::Preferences::CURRENT_HOST end + end - it "autodetects boolean type from TrueClass value and passes a 'TRUE' string" do - resource.value = true - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "write", "NSGlobalDomain", "foo", "-bool", "TRUE"]) + describe '#mapped_user' do + it "uses `current_user` as default" do + expect(provider.mapped_user).to eq CF::Preferences::CURRENT_USER end - it "autodetects boolean type from FalseClass value and passes a 'FALSE' string" do - resource.value = false - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "write", "NSGlobalDomain", "foo", "-bool", "FALSE"]) + it "maps `all_users` to corresponding constant" do + resource.user = :all_users + expect(provider.mapped_user).to eq CF::Preferences::ALL_USERS end + end - it "autodetects dict type from Hash value and flattens keys & values" do - resource.value = { "foo" => "bar" } - expect(provider.defaults_modify_cmd).to eq(["/usr/bin/defaults", "write", "NSGlobalDomain", "foo", "-dict", "foo", "bar"]) + # TODO: should be a functional/integration test + describe "#read_preferences" do + it "reads preference/state" do + resource.domain = "NSGlobalDomain" + resource.key = "AppleKeyboardUIMode" + expect(provider.read_preferences(resource)).to be_nil end end end |