diff options
author | danielsdeleo <dan@getchef.com> | 2015-04-23 11:44:24 -0700 |
---|---|---|
committer | danielsdeleo <dan@getchef.com> | 2015-05-20 15:13:56 -0700 |
commit | e6062caaefe82ff351690af15e882f02efc0f91b (patch) | |
tree | 850f5ed39e9bd7e33303bf907c4ea7389a6a33d7 /chef-config | |
parent | b6f9e5feff3b97576534d70dc2873c4fd62d28e4 (diff) | |
download | chef-e6062caaefe82ff351690af15e882f02efc0f91b.tar.gz |
Move Chef::Config into a subproject
Diffstat (limited to 'chef-config')
-rw-r--r-- | chef-config/.gitignore | 9 | ||||
-rw-r--r-- | chef-config/.rspec | 2 | ||||
-rw-r--r-- | chef-config/.travis.yml | 31 | ||||
-rw-r--r-- | chef-config/Gemfile | 4 | ||||
-rw-r--r-- | chef-config/LICENSE | 201 | ||||
-rw-r--r-- | chef-config/README.md | 4 | ||||
-rw-r--r-- | chef-config/Rakefile | 2 | ||||
-rw-r--r-- | chef-config/chef-config.gemspec | 32 | ||||
-rw-r--r-- | chef-config/lib/chef-config.rb | 20 | ||||
-rw-r--r-- | chef-config/lib/chef-config/config.rb | 744 | ||||
-rw-r--r-- | chef-config/lib/chef-config/exceptions.rb | 26 | ||||
-rw-r--r-- | chef-config/lib/chef-config/logger.rb | 62 | ||||
-rw-r--r-- | chef-config/lib/chef-config/path_helper.rb | 233 | ||||
-rw-r--r-- | chef-config/lib/chef-config/version.rb | 4 | ||||
-rw-r--r-- | chef-config/lib/chef-config/windows.rb | 29 | ||||
-rw-r--r-- | chef-config/spec/spec_helper.rb | 75 | ||||
-rw-r--r-- | chef-config/spec/unit/config_spec.rb | 581 | ||||
-rw-r--r-- | chef-config/spec/unit/path_helper_spec.rb | 291 |
18 files changed, 2350 insertions, 0 deletions
diff --git a/chef-config/.gitignore b/chef-config/.gitignore new file mode 100644 index 0000000000..0cb6eeb067 --- /dev/null +++ b/chef-config/.gitignore @@ -0,0 +1,9 @@ +/.bundle/ +/.yardoc +/Gemfile.lock +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/chef-config/.rspec b/chef-config/.rspec new file mode 100644 index 0000000000..eb3ef03653 --- /dev/null +++ b/chef-config/.rspec @@ -0,0 +1,2 @@ +--color +-fd diff --git a/chef-config/.travis.yml b/chef-config/.travis.yml new file mode 100644 index 0000000000..927580f35f --- /dev/null +++ b/chef-config/.travis.yml @@ -0,0 +1,31 @@ +language: ruby + +sudo: false +# Early warning system to catch if Rubygems breaks something +before_install: + gem update --system + +branches: + only: + - master + +matrix: + include: + - rvm: 2.0 + - rvm: 2.1 + +notifications: + on_change: true + on_failure: true + on_success: change + on_pull_requests: false + irc: + channels: + - chat.freenode.net#chef-hacking + hipchat: + rooms: + # Build Statuses + - secure: G8MNo94L8bmWWwkH2/ViB2QaZnZHZscYM/mEjDbOGd15sqrruwckeARyBoUcRI7P1C6AFmS4IKCNVXa6KzX4Pbh51gQWM92zRpRTZpplwtXz53/1l8ajLFLLMLvEMTlBFAANUKEUFAQPY4dMa14V3Qc5oijfIncN61k4nZNTKpY= + - rvm: 2.2 + # Open Source + - secure: hmcex4PpG5dn8WvjndONO4xCUKOC5kPU/bUEGRrfVbe2YKJE7t0XXbNDC96W/xBgzgnJzvf1Er0zJKDrNf4qEDEWFoozdN00WLcqREgaLLS3Seto2FjR/BpBk5q+sCV0rwwEMms2P4Qk+VSnDCnm9EaeM55hOabqNuOrRzoZLBQ= diff --git a/chef-config/Gemfile b/chef-config/Gemfile new file mode 100644 index 0000000000..d39725ff87 --- /dev/null +++ b/chef-config/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in chef-config.gemspec +gemspec diff --git a/chef-config/LICENSE b/chef-config/LICENSE new file mode 100644 index 0000000000..11069edd79 --- /dev/null +++ b/chef-config/LICENSE @@ -0,0 +1,201 @@ + 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. diff --git a/chef-config/README.md b/chef-config/README.md new file mode 100644 index 0000000000..c36527282e --- /dev/null +++ b/chef-config/README.md @@ -0,0 +1,4 @@ +# ChefConfig + +This repo is experimental. Use at your own risk. + diff --git a/chef-config/Rakefile b/chef-config/Rakefile new file mode 100644 index 0000000000..809eb5616a --- /dev/null +++ b/chef-config/Rakefile @@ -0,0 +1,2 @@ +require "bundler/gem_tasks" + diff --git a/chef-config/chef-config.gemspec b/chef-config/chef-config.gemspec new file mode 100644 index 0000000000..74789a7328 --- /dev/null +++ b/chef-config/chef-config.gemspec @@ -0,0 +1,32 @@ +# coding: utf-8 +lib = File.expand_path('../lib', __FILE__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'chef-config/version' + +Gem::Specification.new do |spec| + spec.name = "chef-config" + spec.version = ChefConfig::VERSION + spec.authors = ["Adam Jacob"] + spec.email = ["adam@chef.io"] + + spec.summary = %q{Chef's default configuration and config loading} + spec.homepage = "TODO: Put your gem's website or public repo URL here." + spec.license = "Apache-2.0" + + spec.require_paths = ["lib"] + + spec.add_dependency "mixlib-shellout", "~> 2.0" + spec.add_dependency "mixlib-config", "~> 2.0" + + spec.add_development_dependency "rake", "~> 10.0" + + %w(rspec-core rspec-expectations rspec-mocks).each do |rspec| + spec.add_development_dependency(rspec, "~> 3.2") + end + + spec.files = %w(Rakefile LICENSE README.md CONTRIBUTING.md) + + Dir.glob("{lib,spec}/**/*", File::FNM_DOTMATCH).reject {|f| File.directory?(f) } + + spec.bindir = "bin" + spec.executables = [] +end diff --git a/chef-config/lib/chef-config.rb b/chef-config/lib/chef-config.rb new file mode 100644 index 0000000000..1f593c868f --- /dev/null +++ b/chef-config/lib/chef-config.rb @@ -0,0 +1,20 @@ +# +# Copyright:: Copyright (c) 2015 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. +# + +module ChefConfig + +end diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb new file mode 100644 index 0000000000..549fac5cbf --- /dev/null +++ b/chef-config/lib/chef-config/config.rb @@ -0,0 +1,744 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: AJ Christensen (<aj@opscode.com>) +# Author:: Mark Mzyk (<mmzyk@opscode.com>) +# Author:: Kyle Goodwin (<kgoodwin@primerevenue.com>) +# Copyright:: Copyright (c) 2008 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 'mixlib/config' +require 'pathname' + +require 'chef-config/logger' +require 'chef-config/windows' +require 'chef-config/path_helper' +require 'mixlib/shellout' + +module ChefConfig + + class Config + + extend Mixlib::Config + + # Evaluates the given string as config. + # + # +filename+ is used for context in stacktraces, but doesn't need to be the name of an actual file. + def self.from_string(string, filename) + self.instance_eval(string, filename, 1) + end + + def self.inspect + configuration.inspect + end + + def self.platform_specific_path(path) + path = PathHelper.cleanpath(path) + if ChefConfig.windows? + # turns \etc\chef\client.rb and \var\chef\client.rb into C:/chef/client.rb + if env['SYSTEMDRIVE'] && path[0] == '\\' && path.split('\\')[2] == 'chef' + path = PathHelper.join(env['SYSTEMDRIVE'], path.split('\\', 3)[2]) + end + end + path + end + + def self.add_formatter(name, file_path=nil) + formatters << [name, file_path] + end + + def self.add_event_logger(logger) + event_handlers << logger + end + + # Config file to load (client.rb, knife.rb, etc. defaults set differently in knife, chef-client, etc.) + configurable(:config_file) + + default(:config_dir) do + if config_file + PathHelper.dirname(config_file) + else + PathHelper.join(user_home, ".chef", "") + end + end + + default :formatters, [] + + # Override the config dispatch to set the value of multiple server options simultaneously + # + # === Parameters + # url<String>:: String to be set for all of the chef-server-api URL's + # + configurable(:chef_server_url).writes_value { |url| url.to_s.strip } + + # When you are using ActiveSupport, they monkey-patch 'daemonize' into Kernel. + # So while this is basically identical to what method_missing would do, we pull + # it up here and get a real method written so that things get dispatched + # properly. + configurable(:daemonize).writes_value { |v| v } + + # The root where all local chef object data is stored. cookbooks, data bags, + # environments are all assumed to be in separate directories under this. + # chef-solo uses these directories for input data. knife commands + # that upload or download files (such as knife upload, knife role from file, + # etc.) work. + default :chef_repo_path do + if self.configuration[:cookbook_path] + if self.configuration[:cookbook_path].kind_of?(String) + File.expand_path('..', self.configuration[:cookbook_path]) + else + self.configuration[:cookbook_path].map do |path| + File.expand_path('..', path) + end + end + else + cache_path + end + end + + def self.find_chef_repo_path(cwd) + # In local mode, we auto-discover the repo root by looking for a path with "cookbooks" under it. + # This allows us to run config-free. + path = cwd + until File.directory?(PathHelper.join(path, "cookbooks")) + new_path = File.expand_path('..', path) + if new_path == path + ChefConfig.logger.warn("No cookbooks directory found at or above current directory. Assuming #{Dir.pwd}.") + return Dir.pwd + end + path = new_path + end + ChefConfig.logger.info("Auto-discovered chef repository at #{path}") + path + end + + def self.derive_path_from_chef_repo_path(child_path) + if chef_repo_path.kind_of?(String) + PathHelper.join(chef_repo_path, child_path) + else + chef_repo_path.map { |path| PathHelper.join(path, child_path)} + end + end + + # Location of acls on disk. String or array of strings. + # Defaults to <chef_repo_path>/acls. + # Only applies to Enterprise Chef commands. + default(:acl_path) { derive_path_from_chef_repo_path('acls') } + + # Location of clients on disk. String or array of strings. + # Defaults to <chef_repo_path>/acls. + default(:client_path) { derive_path_from_chef_repo_path('clients') } + + # Location of cookbooks on disk. String or array of strings. + # Defaults to <chef_repo_path>/cookbooks. If chef_repo_path + # is not specified, this is set to [/var/chef/cookbooks, /var/chef/site-cookbooks]). + default(:cookbook_path) do + if self.configuration[:chef_repo_path] + derive_path_from_chef_repo_path('cookbooks') + else + Array(derive_path_from_chef_repo_path('cookbooks')).flatten + + Array(derive_path_from_chef_repo_path('site-cookbooks')).flatten + end + end + + # Location of containers on disk. String or array of strings. + # Defaults to <chef_repo_path>/containers. + # Only applies to Enterprise Chef commands. + default(:container_path) { derive_path_from_chef_repo_path('containers') } + + # Location of data bags on disk. String or array of strings. + # Defaults to <chef_repo_path>/data_bags. + default(:data_bag_path) { derive_path_from_chef_repo_path('data_bags') } + + # Location of environments on disk. String or array of strings. + # Defaults to <chef_repo_path>/environments. + default(:environment_path) { derive_path_from_chef_repo_path('environments') } + + # Location of groups on disk. String or array of strings. + # Defaults to <chef_repo_path>/groups. + # Only applies to Enterprise Chef commands. + default(:group_path) { derive_path_from_chef_repo_path('groups') } + + # Location of nodes on disk. String or array of strings. + # Defaults to <chef_repo_path>/nodes. + default(:node_path) { derive_path_from_chef_repo_path('nodes') } + + # Location of roles on disk. String or array of strings. + # Defaults to <chef_repo_path>/roles. + default(:role_path) { derive_path_from_chef_repo_path('roles') } + + # Location of users on disk. String or array of strings. + # Defaults to <chef_repo_path>/users. + # Does not apply to Enterprise Chef commands. + default(:user_path) { derive_path_from_chef_repo_path('users') } + + # Location of policies on disk. String or array of strings. + # Defaults to <chef_repo_path>/policies. + default(:policy_path) { derive_path_from_chef_repo_path('policies') } + + # Turn on "path sanity" by default. See also: http://wiki.opscode.com/display/chef/User+Environment+PATH+Sanity + default :enforce_path_sanity, true + + # Formatted Chef Client output is a beta feature, disabled by default: + default :formatter, "null" + + # The number of times the client should retry when registering with the server + default :client_registration_retries, 5 + + # An array of paths to search for knife exec scripts if they aren't in the current directory + default :script_path, [] + + # The root of all caches (checksums, cache and backup). If local mode is on, + # this is under the user's home directory. + default(:cache_path) do + if local_mode + PathHelper.join(config_dir, 'local-mode-cache') + else + primary_cache_root = platform_specific_path("/var") + primary_cache_path = platform_specific_path("/var/chef") + # Use /var/chef as the cache path only if that folder exists and we can read and write + # into it, or /var exists and we can read and write into it (we'll create /var/chef later). + # Otherwise, we'll create .chef under the user's home directory and use that as + # the cache path. + unless path_accessible?(primary_cache_path) || path_accessible?(primary_cache_root) + secondary_cache_path = PathHelper.join(user_home, '.chef') + ChefConfig.logger.info("Unable to access cache at #{primary_cache_path}. Switching cache to #{secondary_cache_path}") + secondary_cache_path + else + primary_cache_path + end + end + end + + # Returns true only if the path exists and is readable and writeable for the user. + def self.path_accessible?(path) + File.exists?(path) && File.readable?(path) && File.writable?(path) + end + + # Where cookbook files are stored on the server (by content checksum) + default(:checksum_path) { PathHelper.join(cache_path, "checksums") } + + # Where chef's cache files should be stored + default(:file_cache_path) { PathHelper.join(cache_path, "cache") } + + # Where backups of chef-managed files should go + default(:file_backup_path) { PathHelper.join(cache_path, "backup") } + + # The chef-client (or solo) lockfile. + # + # If your `file_cache_path` resides on a NFS (or non-flock()-supporting + # fs), it's recommended to set this to something like + # '/tmp/chef-client-running.pid' + default(:lockfile) { PathHelper.join(file_cache_path, "chef-client-running.pid") } + + ## Daemonization Settings ## + # What user should Chef run as? + default :user, nil + default :group, nil + default :umask, 0022 + + # Valid log_levels are: + # * :debug + # * :info + # * :warn + # * :fatal + # These work as you'd expect. There is also a special `:auto` setting. + # When set to :auto, Chef will auto adjust the log verbosity based on + # context. When a tty is available (usually because the user is running chef + # in a console), the log level is set to :warn, and output formatters are + # used as the primary mode of output. When a tty is not available, the + # logger is the primary mode of output, and the log level is set to :info + default :log_level, :auto + + # Logging location as either an IO stream or string representing log file path + default :log_location, STDOUT + + # Using `force_formatter` causes chef to default to formatter output when STDOUT is not a tty + default :force_formatter, false + + # Using `force_logger` causes chef to default to logger output when STDOUT is a tty + default :force_logger, false + + default :http_retry_count, 5 + default :http_retry_delay, 5 + default :interval, nil + default :once, nil + default :json_attribs, nil + # toggle info level log items that can create a lot of output + default :verbose_logging, true + default :node_name, nil + default :diff_disabled, false + default :diff_filesize_threshold, 10000000 + default :diff_output_threshold, 1000000 + default :local_mode, false + + default :pid_file, nil + + # Whether Chef Zero local mode should bind to a port. All internal requests + # will go through the socketless code path regardless, so the socket is + # only needed if other processes will connect to the local mode server. + # + # For compatibility this is set to true but it will be changed to false in + # the future. + default :listen, true + + config_context :chef_zero do + config_strict_mode true + default(:enabled) { ChefConfig::Config.local_mode } + default :host, 'localhost' + default :port, 8889.upto(9999) # Will try ports from 8889-9999 until one works + end + default :chef_server_url, "https://localhost:443" + + default(:chef_server_root) do + # if the chef_server_url is a path to an organization, aka + # 'some_url.../organizations/*' then remove the '/organization/*' by default + if self.configuration[:chef_server_url] =~ /\/organizations\/\S*$/ + self.configuration[:chef_server_url].split('/')[0..-3].join('/') + elsif self.configuration[:chef_server_url] # default to whatever chef_server_url is + self.configuration[:chef_server_url] + else + "https://localhost:443" + end + end + + default :rest_timeout, 300 + default :yum_timeout, 900 + default :yum_lock_timeout, 30 + default :solo, false + default :splay, nil + default :why_run, false + default :color, false + default :client_fork, true + default :ez, false + default :enable_reporting, true + default :enable_reporting_url_fatals, false + # Possible values for :audit_mode + # :enabled, :disabled, :audit_only, + # + # TODO: 11 Dec 2014: Currently audit-mode is an experimental feature + # and is disabled by default. When users choose to enable audit-mode, + # a warning is issued in application/client#reconfigure. + # This can be removed when audit-mode is enabled by default. + default :audit_mode, :disabled + + # Chef only needs ohai to run the hostname plugin for the most basic + # functionality. If the rest of the ohai plugins are not needed (like in + # most of our testing scenarios) + default :minimal_ohai, false + + # Policyfile is an experimental feature where a node gets its run list and + # cookbook version set from a single document on the server instead of + # expanding the run list and having the server compute the cookbook version + # set based on environment constraints. + # + # Because this feature is experimental, it is not recommended for + # production use. Developent/release of this feature may not adhere to + # semver guidelines. + default :use_policyfile, false + + # Set these to enable SSL authentication / mutual-authentication + # with the server + + # Client side SSL cert/key for mutual auth + default :ssl_client_cert, nil + default :ssl_client_key, nil + + # Whether or not to verify the SSL cert for all HTTPS requests. When set to + # :verify_peer (default), all HTTPS requests will be validated regardless of other + # SSL verification settings. When set to :verify_none no HTTPS requests will + # be validated. + default :ssl_verify_mode, :verify_peer + + # Whether or not to verify the SSL cert for HTTPS requests to the Chef + # server API. If set to `true`, the server's cert will be validated + # regardless of the :ssl_verify_mode setting. This is set to `true` when + # running in local-mode. + # NOTE: This is a workaround until verify_peer is enabled by default. + default(:verify_api_cert) { ChefConfig::Config.local_mode } + + # Path to the default CA bundle files. + default :ssl_ca_path, nil + default(:ssl_ca_file) do + if ChefConfig.windows? and embedded_path = embedded_dir + cacert_path = File.join(embedded_path, "ssl/certs/cacert.pem") + cacert_path if File.exist?(cacert_path) + else + nil + end + end + + # A directory that contains additional SSL certificates to trust. Any + # certificates in this directory will be added to whatever CA bundle ruby + # is using. Use this to add self-signed certs for your Chef Server or local + # HTTP file servers. + default(:trusted_certs_dir) { PathHelper.join(config_dir, "trusted_certs") } + + # Where should chef-solo download recipes from? + default :recipe_url, nil + + # Sets the version of the signed header authentication protocol to use (see + # the 'mixlib-authorization' project for more detail). Currently, versions + # 1.0 and 1.1 are available; however, the chef-server must first be + # upgraded to support version 1.1 before clients can begin using it. + # + # Version 1.1 of the protocol is required when using a `node_name` greater + # than ~90 bytes (~90 ascii characters), so chef-client will automatically + # switch to using version 1.1 when `node_name` is too large for the 1.0 + # protocol. If you intend to use large node names, ensure that your server + # supports version 1.1. Automatic detection of large node names means that + # users will generally not need to manually configure this. + # + # In the future, this configuration option may be replaced with an + # automatic negotiation scheme. + default :authentication_protocol_version, "1.0" + + # This key will be used to sign requests to the Chef server. This location + # must be writable by Chef during initial setup when generating a client + # identity on the server. + # + # The chef-server will look up the public key for the client using the + # `node_name` of the client. + # + # If chef-zero is enabled, this defaults to nil (no authentication). + default(:client_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/client.pem") } + + # When registering the client, should we allow the client key location to + # be a symlink? eg: /etc/chef/client.pem -> /etc/chef/prod-client.pem + # If the path of the key goes through a directory like /tmp this should + # never be set to true or its possibly an easily exploitable security hole. + default :follow_client_key_symlink, false + + # This secret is used to decrypt encrypted data bag items. + default(:encrypted_data_bag_secret) do + if File.exist?(platform_specific_path("/etc/chef/encrypted_data_bag_secret")) + platform_specific_path("/etc/chef/encrypted_data_bag_secret") + else + nil + end + end + + # As of Chef 11.0, version "1" is the default encrypted data bag item + # format. Version "2" is available which adds encrypt-then-mac protection. + # To maintain compatibility, versions other than 1 must be opt-in. + # + # Set this to `2` if you have chef-client 11.6.0+ in your infrastructure. + # Set this to `3` if you have chef-client 11.?.0+, ruby 2 and OpenSSL >= 1.0.1 in your infrastructure. (TODO) + default :data_bag_encrypt_version, 1 + + # When reading data bag items, any supported version is accepted. However, + # if all encrypted data bags have been generated with the version 2 format, + # it is recommended to disable support for earlier formats to improve + # security. For example, the version 2 format is identical to version 1 + # except for the addition of an HMAC, so an attacker with MITM capability + # could downgrade an encrypted data bag to version 1 as part of an attack. + default :data_bag_decrypt_minimum_version, 0 + + # If there is no file in the location given by `client_key`, chef-client + # will temporarily use the "validator" identity to generate one. If the + # `client_key` is not present and the `validation_key` is also not present, + # chef-client will not be able to authenticate to the server. + # + # The `validation_key` is never used if the `client_key` exists. + # + # If chef-zero is enabled, this defaults to nil (no authentication). + default(:validation_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/validation.pem") } + default :validation_client_name, "chef-validator" + + # When creating a new client via the validation_client account, Chef 11 + # servers allow the client to generate a key pair locally and send the + # public key to the server. This is more secure and helps offload work from + # the server, enhancing scalability. If enabled and the remote server + # implements only the Chef 10 API, client registration will not work + # properly. + # + # The default value is `true`. Set to `false` to disable client-side key + # generation (server generates client keys). + default(:local_key_generation) { true } + + # Zypper package provider gpg checks. Set to true to enable package + # gpg signature checking. This will be default in the + # future. Setting to false disables the warnings. + # Leaving this set to nil or false is a security hazard! + default :zypper_check_gpg, nil + + # Report Handlers + default :report_handlers, [] + + # Event Handlers + default :event_handlers, [] + + default :disable_event_loggers, false + default :event_loggers do + evt_loggers = [] + if ChefConfig::windows? and not ChefConfig::windows_server_2003? + evt_loggers << :win_evt + end + evt_loggers + end + + # Exception Handlers + default :exception_handlers, [] + + # Start handlers + default :start_handlers, [] + + # Syntax Check Cache. Knife keeps track of files that is has already syntax + # checked by storing files in this directory. `syntax_check_cache_path` is + # the new (and preferred) configuration setting. If not set, knife will + # fall back to using cache_options[:path], which is deprecated but exists in + # many client configs generated by pre-Chef-11 bootstrappers. + default(:syntax_check_cache_path) { cache_options[:path] } + + # Deprecated: + # Move this to the default value of syntax_cache_path when this is removed. + default(:cache_options) { { :path => PathHelper.join(config_dir, "syntaxcache") } } + + # Whether errors should be raised for deprecation warnings. When set to + # `false` (the default setting), a warning is emitted but code using + # deprecated methods/features/etc. should work normally otherwise. When set + # to `true`, usage of deprecated methods/features will raise a + # `DeprecatedFeatureError`. This is used by Chef's tests to ensure that + # deprecated functionality is not used internally by Chef. End users + # should generally leave this at the default setting (especially in + # production), but it may be useful when testing cookbooks or other code if + # the user wishes to aggressively address deprecations. + default(:treat_deprecation_warnings_as_errors) do + # Using an environment variable allows this setting to be inherited in + # tests that spawn new processes. + ENV.key?("CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS") + end + + # knife configuration data + config_context :knife do + default :ssh_port, nil + default :ssh_user, nil + default :ssh_attribute, nil + default :ssh_gateway, nil + default :bootstrap_version, nil + default :bootstrap_proxy, nil + default :bootstrap_template, nil + default :secret, nil + default :secret_file, nil + default :identity_file, nil + default :host_key_verify, nil + default :forward_agent, nil + default :sort_status_reverse, nil + default :hints, {} + end + + def self.set_defaults_for_windows + # Those lists of regular expressions define what chef considers a + # valid user and group name + # From http://technet.microsoft.com/en-us/library/cc776019(WS.10).aspx + principal_valid_regex_part = '[^"\/\\\\\[\]\:;|=,+*?<>]+' + default :user_valid_regex, [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ] + default :group_valid_regex, [ /^(#{principal_valid_regex_part}\\)?#{principal_valid_regex_part}$/ ] + + default :fatal_windows_admin_check, false + end + + def self.set_defaults_for_nix + # Those lists of regular expressions define what chef considers a + # valid user and group name + # + # user/group cannot start with '-', '+' or '~' + # user/group cannot contain ':', ',' or non-space-whitespace or null byte + # everything else is allowed (UTF-8, spaces, etc) and we delegate to your O/S useradd program to barf or not + # copies: http://anonscm.debian.org/viewvc/pkg-shadow/debian/trunk/debian/patches/506_relaxed_usernames?view=markup + default :user_valid_regex, [ /^[^-+~:,\t\r\n\f\0]+[^:,\t\r\n\f\0]*$/ ] + default :group_valid_regex, [ /^[^-+~:,\t\r\n\f\0]+[^:,\t\r\n\f\0]*$/ ] + end + + # Those lists of regular expressions define what chef considers a + # valid user and group name + if ChefConfig.windows? + set_defaults_for_windows + else + set_defaults_for_nix + end + + # This provides a hook which rspec can stub so that we can avoid twiddling + # global state in tests. + def self.env + ENV + end + + def self.windows_home_path + ChefConfig.logger.deprecation("Chef::Config.windows_home_path is now deprecated. Consider using Chef::Util::PathHelper.home instead.") + PathHelper.home + end + + # returns a platform specific path to the user home dir if set, otherwise default to current directory. + default( :user_home ) { PathHelper.home || Dir.pwd } + + # Enable file permission fixup for selinux. Fixup will be done + # only if selinux is enabled in the system. + default :enable_selinux_file_permission_fixup, true + + # Use atomic updates (i.e. move operation) while updating contents + # of the files resources. When set to false copy operation is + # used to update files. + default :file_atomic_update, true + + # There are 3 possible values for this configuration setting. + # true => file staging is done in the destination directory + # false => file staging is done via tempfiles under ENV['TMP'] + # :auto => file staging will try using destination directory if possible and + # will fall back to ENV['TMP'] if destination directory is not usable. + default :file_staging_uses_destdir, :auto + + # Exit if another run is in progress and the chef-client is unable to + # get the lock before time expires. If nil, no timeout is enforced. (Exits + # immediately if 0.) + default :run_lock_timeout, nil + + # Number of worker threads for syncing cookbooks in parallel. Increasing + # this number can result in gateway errors from the server (namely 503 and 504). + # If you are seeing this behavior while using the default setting, reducing + # the number of threads will help. + default :cookbook_sync_threads, 10 + + # At the beginning of the Chef Client run, the cookbook manifests are downloaded which + # contain URLs for every file in every relevant cookbook. Most of the files + # (recipes, resources, providers, libraries, etc) are immediately synchronized + # at the start of the run. The handling of "files" and "templates" directories, + # however, have two modes of operation. They can either all be downloaded immediately + # at the start of the run (no_lazy_load==true) or else they can be lazily loaded as + # cookbook_file or template resources are converged which require them (no_lazy_load==false). + # + # The advantage of lazily loading these files is that unnecessary files are not + # synchronized. This may be useful to users with large files checked into cookbooks which + # are only selectively downloaded to a subset of clients which use the cookbook. However, + # better solutions are to either isolate large files into individual cookbooks and only + # include those cookbooks in the run lists of the servers that need them -- or move to + # using remote_file and a more appropriate backing store like S3 for large file + # distribution. + # + # The disadvantages of lazily loading files are that users some time find it + # confusing that their cookbooks are not fully synchronzied to the cache initially, + # and more importantly the time-sensitive URLs which are in the manifest may time + # out on long Chef runs before the resource that uses the file is converged + # (leading to many confusing 403 errors on template/cookbook_file resources). + # + default :no_lazy_load, true + + # Default for the chef_gem compile_time attribute. Nil is the same as true but will emit + # warnings on every use of chef_gem prompting the user to be explicit. If the user sets this to + # true then the user will get backcompat behavior but with a single nag warning that cookbooks + # may break with this setting in the future. The false setting is the recommended setting and + # will become the default. + default :chef_gem_compile_time, nil + + # A whitelisted array of attributes you want sent over the wire when node + # data is saved. + # The default setting is nil, which collects all data. Setting to [] will not + # collect any data for save. + default :automatic_attribute_whitelist, nil + default :default_attribute_whitelist, nil + default :normal_attribute_whitelist, nil + default :override_attribute_whitelist, nil + + config_context :windows_service do + # Set `watchdog_timeout` to the number of seconds to wait for a chef-client run + # to finish + default :watchdog_timeout, 2 * (60 * 60) # 2 hours + end + + # Chef requires an English-language UTF-8 locale to function properly. We attempt + # to use the 'locale -a' command and search through a list of preferences until we + # find one that we can use. On Ubuntu systems we should find 'C.UTF-8' and be + # able to use that even if there is no English locale on the server, but Mac, Solaris, + # AIX, etc do not have that locale. We then try to find an English locale and fall + # back to 'C' if we do not. The choice of fallback is pick-your-poison. If we try + # to do the work to return a non-US UTF-8 locale then we fail inside of providers when + # things like 'svn info' return Japanese and we can't parse them. OTOH, if we pick 'C' then + # we will blow up on UTF-8 characters. Between the warn we throw and the Encoding + # exception that ruby will throw it is more obvious what is broken if we drop UTF-8 by + # default rather than drop English. + # + # If there is no 'locale -a' then we return 'en_US.UTF-8' since that is the most commonly + # available English UTF-8 locale. However, all modern POSIXen should support 'locale -a'. + def self.guess_internal_locale + # https://github.com/opscode/chef/issues/2181 + # Some systems have the `locale -a` command, but the result has + # invalid characters for the default encoding. + # + # For example, on CentOS 6 with ENV['LANG'] = "en_US.UTF-8", + # `locale -a`.split fails with ArgumentError invalid UTF-8 encoding. + cmd = Mixlib::ShellOut.new("locale -a").run_command + cmd.error! + locales = cmd.stdout.split + case + when locales.include?('C.UTF-8') + 'C.UTF-8' + when locales.include?('en_US.UTF-8'), locales.include?('en_US.utf8') + 'en_US.UTF-8' + when locales.include?('en.UTF-8') + 'en.UTF-8' + else + # Will match en_ZZ.UTF-8, en_ZZ.utf-8, en_ZZ.UTF8, en_ZZ.utf8 + guesses = locales.select { |l| l =~ /^en_.*UTF-?8$/i } + unless guesses.empty? + guessed_locale = guesses.first + # Transform into the form en_ZZ.UTF-8 + guessed_locale.gsub(/UTF-?8$/i, "UTF-8") + else + ChefConfig.logger.warn "Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support." + 'C' + end + end + rescue + if ChefConfig.windows? + ChefConfig.logger.debug "Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else." + else + ChefConfig.logger.debug "No usable locale -a command found, assuming you have en_US.UTF-8 installed." + end + 'en_US.UTF-8' + end + + default :internal_locale, guess_internal_locale + + # Force UTF-8 Encoding, for when we fire up in the 'C' locale or other strange locales (e.g. + # japanese windows encodings). If we do not do this, then knife upload will fail when a cookbook's + # README.md has UTF-8 characters that do not encode in whatever surrounding encoding we have been + # passed. Effectively, the Chef Ecosystem is globally UTF-8 by default. Anyone who wants to be + # able to upload Shift_JIS or ISO-8859-1 files needs to mark *those* files explicitly with + # magic tags to make ruby correctly identify the encoding being used. Changing this default will + # break Chef community cookbooks and is very highly discouraged. + default :ruby_encoding, Encoding::UTF_8 + + # If installed via an omnibus installer, this gives the path to the + # "embedded" directory which contains all of the software packaged with + # omnibus. This is used to locate the cacert.pem file on windows. + def self.embedded_dir + Pathname.new(_this_file).ascend do |path| + if path.basename.to_s == "embedded" + return path.to_s + end + end + + nil + end + + # Path to this file in the current install. + def self._this_file + File.expand_path(__FILE__) + end + end +end + + + diff --git a/chef-config/lib/chef-config/exceptions.rb b/chef-config/lib/chef-config/exceptions.rb new file mode 100644 index 0000000000..f5d76d856b --- /dev/null +++ b/chef-config/lib/chef-config/exceptions.rb @@ -0,0 +1,26 @@ +# +# Copyright:: Copyright (c) 2015 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 'chef-config/windows' +require 'chef-config/logger' + +module ChefConfig + + class InvalidPath < StandardError + end + +end diff --git a/chef-config/lib/chef-config/logger.rb b/chef-config/lib/chef-config/logger.rb new file mode 100644 index 0000000000..57f18809ee --- /dev/null +++ b/chef-config/lib/chef-config/logger.rb @@ -0,0 +1,62 @@ +# +# Copyright:: Copyright (c) 2015 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. +# + + +module ChefConfig + + # Implements enough of Logger's API that we can use it in place of a real + # logger for `ChefConfig.logger` + class NullLogger + + def <<(_msg) + end + + def add(_severity, _message = nil, _progname = nil) + end + + def debug(_progname = nil, &block) + end + + def info(_progname = nil, &block) + end + + def warn(_progname = nil, &block) + end + + def deprecation(_progname = nil, &block) + end + + def error(_progname = nil, &block) + end + + def fatal(_progname = nil, &block) + end + + end + + @logger = NullLogger.new + + def self.logger=(new_logger) + @logger = new_logger + end + + def self.logger + @logger + end +end + + diff --git a/chef-config/lib/chef-config/path_helper.rb b/chef-config/lib/chef-config/path_helper.rb new file mode 100644 index 0000000000..acc6b76377 --- /dev/null +++ b/chef-config/lib/chef-config/path_helper.rb @@ -0,0 +1,233 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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 'chef-config/windows' +require 'chef-config/logger' +require 'chef-config/exceptions' + +module ChefConfig + class PathHelper + # Maximum characters in a standard Windows path (260 including drive letter and NUL) + WIN_MAX_PATH = 259 + + def self.dirname(path) + if ChefConfig.windows? + # Find the first slash, not counting trailing slashes + end_slash = path.size + loop do + slash = path.rindex(/[#{Regexp.escape(File::SEPARATOR)}#{Regexp.escape(path_separator)}]/, end_slash - 1) + if !slash + return end_slash == path.size ? '.' : path_separator + elsif slash == end_slash - 1 + end_slash = slash + else + return path[0..slash-1] + end + end + else + ::File.dirname(path) + end + end + + BACKSLASH = '\\'.freeze + + def self.path_separator + if ChefConfig.windows? + File::ALT_SEPARATOR || BACKSLASH + else + File::SEPARATOR + end + end + + def self.join(*args) + path_separator_regex = Regexp.escape(File::SEPARATOR) + unless path_separator == File::SEPARATOR + path_separator_regex << Regexp.escape(path_separator) + end + + trailing_slashes = /[#{path_separator_regex}]+$/ + leading_slashes = /^[#{path_separator_regex}]+/ + + args.flatten.inject() do |joined_path, component| + joined_path = joined_path.sub(trailing_slashes, '') + component = component.sub(leading_slashes, '') + joined_path += "#{path_separator}#{component}" + end + end + + def self.validate_path(path) + if ChefConfig.windows? + unless printable?(path) + msg = "Path '#{path}' contains non-printable characters. Check that backslashes are escaped with another backslash (e.g. C:\\\\Windows) in double-quoted strings." + ChefConfig.logger.error(msg) + raise ChefConfig::InvalidPath, msg + end + + if windows_max_length_exceeded?(path) + ChefConfig.logger.debug("Path '#{path}' is longer than #{WIN_MAX_PATH}, prefixing with'\\\\?\\'") + path.insert(0, "\\\\?\\") + end + end + + path + end + + def self.windows_max_length_exceeded?(path) + # Check to see if paths without the \\?\ prefix are over the maximum allowed length for the Windows API + # http://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx + unless path =~ /^\\\\?\\/ + if path.length > WIN_MAX_PATH + return true + end + end + + false + end + + def self.printable?(string) + # returns true if string is free of non-printable characters (escape sequences) + # this returns false for whitespace escape sequences as well, e.g. \n\t + if string =~ /[^[:print:]]/ + false + else + true + end + end + + # Produces a comparable path. + def self.canonical_path(path, add_prefix=true) + # First remove extra separators and resolve any relative paths + abs_path = File.absolute_path(path) + + if ChefConfig.windows? + # Add the \\?\ API prefix on Windows unless add_prefix is false + # Downcase on Windows where paths are still case-insensitive + abs_path.gsub!(::File::SEPARATOR, path_separator) + if add_prefix && abs_path !~ /^\\\\?\\/ + abs_path.insert(0, "\\\\?\\") + end + + abs_path.downcase! + end + + abs_path + end + + def self.cleanpath(path) + path = Pathname.new(path).cleanpath.to_s + # ensure all forward slashes are backslashes + if ChefConfig.windows? + path = path.gsub(File::SEPARATOR, path_separator) + end + path + end + + def self.paths_eql?(path1, path2) + canonical_path(path1) == canonical_path(path2) + end + + # Paths which may contain glob-reserved characters need + # to be escaped before globbing can be done. + # http://stackoverflow.com/questions/14127343 + def self.escape_glob(*parts) + path = cleanpath(join(*parts)) + path.gsub(/[\\\{\}\[\]\*\?]/) { |x| "\\"+x } + end + + def self.relative_path_from(from, to) + Pathname.new(cleanpath(to)).relative_path_from(Pathname.new(cleanpath(from))) + end + + # Retrieves the "home directory" of the current user while trying to ascertain the existence + # of said directory. The path returned uses / for all separators (the ruby standard format). + # If the home directory doesn't exist or an error is otherwise encountered, nil is returned. + # + # If a set of path elements is provided, they are appended as-is to the home path if the + # homepath exists. + # + # If an optional block is provided, the joined path is passed to that block if the home path is + # valid and the result of the block is returned instead. + # + # Home-path discovery is performed once. If a path is discovered, that value is memoized so + # that subsequent calls to home_dir don't bounce around. + # + # See self.all_homes. + def self.home(*args) + @@home_dir ||= self.all_homes { |p| break p } + if @@home_dir + path = File.join(@@home_dir, *args) + block_given? ? (yield path) : path + end + end + + # See self.home. This method performs a similar operation except that it yields all the different + # possible values of 'HOME' that one could have on this platform. Hence, on windows, if + # HOMEDRIVE\HOMEPATH and USERPROFILE are different, the provided block will be called twice. + # This method goes out and checks the existence of each location at the time of the call. + # + # The return is a list of all the returned values from each block invocation or a list of paths + # if no block is provided. + def self.all_homes(*args) + paths = [] + if ChefConfig.windows? + # By default, Ruby uses the the following environment variables to determine Dir.home: + # HOME + # HOMEDRIVE HOMEPATH + # USERPROFILE + # Ruby only checks to see if the variable is specified - not if the directory actually exists. + # On Windows, HOMEDRIVE HOMEPATH can point to a different location (such as an unavailable network mounted drive) + # while USERPROFILE points to the location where the user application settings and profile are stored. HOME + # is not defined as an environment variable (usually). If the home path actually uses UNC, then the prefix is + # HOMESHARE instead of HOMEDRIVE. + # + # We instead walk down the following and only include paths that actually exist. + # HOME + # HOMEDRIVE HOMEPATH + # HOMESHARE HOMEPATH + # USERPROFILE + + paths << ENV['HOME'] + paths << ENV['HOMEDRIVE'] + ENV['HOMEPATH'] if ENV['HOMEDRIVE'] && ENV['HOMEPATH'] + paths << ENV['HOMESHARE'] + ENV['HOMEPATH'] if ENV['HOMESHARE'] && ENV['HOMEPATH'] + paths << ENV['USERPROFILE'] + end + paths << Dir.home if ENV['HOME'] + + # Depending on what environment variables we're using, the slashes can go in any which way. + # Just change them all to / to keep things consistent. + # Note: Maybe this is a bad idea on some unixy systems where \ might be a valid character depending on + # the particular brand of kool-aid you consume. This code assumes that \ and / are both + # path separators on any system being used. + paths = paths.map { |home_path| home_path.gsub(path_separator, ::File::SEPARATOR) if home_path } + + # Filter out duplicate paths and paths that don't exist. + valid_paths = paths.select { |home_path| home_path && Dir.exists?(home_path) } + valid_paths = valid_paths.uniq + + # Join all optional path elements at the end. + # If a block is provided, invoke it - otherwise just return what we've got. + joined_paths = valid_paths.map { |home_path| File.join(home_path, *args) } + if block_given? + joined_paths.each { |p| yield p } + else + joined_paths + end + end + end +end + diff --git a/chef-config/lib/chef-config/version.rb b/chef-config/lib/chef-config/version.rb new file mode 100644 index 0000000000..ffc0f707c9 --- /dev/null +++ b/chef-config/lib/chef-config/version.rb @@ -0,0 +1,4 @@ +# TODO: license header +module ChefConfig + VERSION = '12.4.0.dev.0' +end diff --git a/chef-config/lib/chef-config/windows.rb b/chef-config/lib/chef-config/windows.rb new file mode 100644 index 0000000000..a2e90067df --- /dev/null +++ b/chef-config/lib/chef-config/windows.rb @@ -0,0 +1,29 @@ +# +# Copyright:: Copyright (c) 2015 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. +# + +module ChefConfig + + def self.windows? + if RUBY_PLATFORM =~ /mswin|mingw|windows/ + true + else + false + end + end + +end + diff --git a/chef-config/spec/spec_helper.rb b/chef-config/spec/spec_helper.rb new file mode 100644 index 0000000000..df9461cde9 --- /dev/null +++ b/chef-config/spec/spec_helper.rb @@ -0,0 +1,75 @@ +require 'chef-config/windows' + +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # These two settings work together to allow you to limit a spec run + # to individual examples or groups you care about by tagging them with + # `:focus` metadata. When nothing is tagged with `:focus`, all examples + # get run. + config.filter_run :focus + config.run_all_when_everything_filtered = true + + config.filter_run_excluding :windows_only => true unless ChefConfig.windows? + config.filter_run_excluding :unix_only => true if ChefConfig.windows? + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax + # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = 'doc' + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + # config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +end diff --git a/chef-config/spec/unit/config_spec.rb b/chef-config/spec/unit/config_spec.rb new file mode 100644 index 0000000000..395fa2618e --- /dev/null +++ b/chef-config/spec/unit/config_spec.rb @@ -0,0 +1,581 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Kyle Goodwin (<kgoodwin@primerevenue.com>) +# Copyright:: Copyright (c) 2008 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 'spec_helper' +require 'chef-config/config' + +RSpec.describe ChefConfig::Config do + before(:each) do + ChefConfig::Config.reset + + # By default, treat deprecation warnings as errors in tests. + ChefConfig::Config.treat_deprecation_warnings_as_errors(true) + + # Set environment variable so the setting persists in child processes + ENV['CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS'] = "1" + end + + describe "config attribute writer: chef_server_url" do + before do + ChefConfig::Config.chef_server_url = "https://junglist.gen.nz" + end + + it "sets the server url" do + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") + end + + context "when the url has a leading space" do + before do + ChefConfig::Config.chef_server_url = " https://junglist.gen.nz" + end + + it "strips the space from the url when setting" do + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") + end + + end + + context "when the url is a frozen string" do + before do + ChefConfig::Config.chef_server_url = " https://junglist.gen.nz".freeze + end + + it "strips the space from the url when setting without raising an error" do + expect(ChefConfig::Config.chef_server_url).to eq("https://junglist.gen.nz") + end + end + end + + describe "when configuring formatters" do + # if TTY and not(force-logger) + # formatter = configured formatter or default formatter + # formatter goes to STDOUT/ERR + # if log file is writeable + # log level is configured level or info + # log location is file + # else + # log level is warn + # log location is STDERR + # end + # elsif not(TTY) and force formatter + # formatter = configured formatter or default formatter + # if log_location specified + # formatter goes to log_location + # else + # formatter goes to STDOUT/ERR + # end + # else + # formatter = "null" + # log_location = configured-value or defualt + # log_level = info or defualt + # end + # + it "has an empty list of formatters by default" do + expect(ChefConfig::Config.formatters).to eq([]) + end + + it "configures a formatter with a short name" do + ChefConfig::Config.add_formatter(:doc) + expect(ChefConfig::Config.formatters).to eq([[:doc, nil]]) + end + + it "configures a formatter with a file output" do + ChefConfig::Config.add_formatter(:doc, "/var/log/formatter.log") + expect(ChefConfig::Config.formatters).to eq([[:doc, "/var/log/formatter.log"]]) + end + + end + + [ false, true ].each do |is_windows| + + context "On #{is_windows ? 'Windows' : 'Unix'}" do + def to_platform(*args) + ChefConfig::Config.platform_specific_path(*args) + end + + before :each do + allow(ChefConfig).to receive(:windows?).and_return(is_windows) + end + + describe "class method: platform_specific_path" do + if is_windows + it "should return a windows path on windows systems" do + path = "/etc/chef/cookbooks" + allow(ChefConfig::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) + # match on a regex that looks for the base path with an optional + # system drive at the beginning (c:) + # system drive is not hardcoded b/c it can change and b/c it is not present on linux systems + expect(ChefConfig::Config.platform_specific_path(path)).to eq("C:\\chef\\cookbooks") + end + else + it "should return given path on non-windows systems" do + path = "/etc/chef/cookbooks" + expect(ChefConfig::Config.platform_specific_path(path)).to eq("/etc/chef/cookbooks") + end + end + end + + describe "default values" do + let :primary_cache_path do + if is_windows + "#{ChefConfig::Config.env['SYSTEMDRIVE']}\\chef" + else + "/var/chef" + end + end + + let :secondary_cache_path do + if is_windows + "#{ChefConfig::Config[:user_home]}\\.chef" + else + "#{ChefConfig::Config[:user_home]}/.chef" + end + end + + before do + if is_windows + allow(ChefConfig::Config).to receive(:env).and_return({ 'SYSTEMDRIVE' => 'C:' }) + ChefConfig::Config[:user_home] = 'C:\Users\charlie' + else + ChefConfig::Config[:user_home] = '/Users/charlie' + end + + allow(ChefConfig::Config).to receive(:path_accessible?).and_return(false) + end + + describe "ChefConfig::Config[:chef_server_root]" do + context "when chef_server_url isn't set manually" do + it "returns the default of 'https://localhost:443'" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://localhost:443") + end + end + + context "when chef_server_url matches '../organizations/*' without a trailing slash" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/organizations/myorg" + end + it "returns the full URL without /organizations/*" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://example.com") + end + end + + context "when chef_server_url matches '../organizations/*' with a trailing slash" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/organizations/myorg/" + end + it "returns the full URL without /organizations/*" do + expect(ChefConfig::Config[:chef_server_root]).to eq("https://example.com") + end + end + + context "when chef_server_url matches '..organizations..' but not '../organizations/*'" do + before do + ChefConfig::Config[:chef_server_url] = "https://organizations.com/organizations" + end + it "returns the full URL without any modifications" do + expect(ChefConfig::Config[:chef_server_root]).to eq(ChefConfig::Config[:chef_server_url]) + end + end + + context "when chef_server_url is a standard URL without the string organization(s)" do + before do + ChefConfig::Config[:chef_server_url] = "https://example.com/some_other_string" + end + it "returns the full URL without any modifications" do + expect(ChefConfig::Config[:chef_server_root]).to eq(ChefConfig::Config[:chef_server_url]) + end + end + end + + describe "ChefConfig::Config[:cache_path]" do + context "when /var/chef exists and is accessible" do + it "defaults to /var/chef" do + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var/chef")).and_return(true) + expect(ChefConfig::Config[:cache_path]).to eq(primary_cache_path) + end + end + + context "when /var/chef does not exist and /var is accessible" do + it "defaults to /var/chef" do + allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(false) + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(true) + expect(ChefConfig::Config[:cache_path]).to eq(primary_cache_path) + end + end + + context "when /var/chef does not exist and /var is not accessible" do + it "defaults to $HOME/.chef" do + allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(false) + allow(ChefConfig::Config).to receive(:path_accessible?).with(to_platform("/var")).and_return(false) + expect(ChefConfig::Config[:cache_path]).to eq(secondary_cache_path) + end + end + + context "when /var/chef exists and is not accessible" do + it "defaults to $HOME/.chef" do + allow(File).to receive(:exists?).with(to_platform("/var/chef")).and_return(true) + allow(File).to receive(:readable?).with(to_platform("/var/chef")).and_return(true) + allow(File).to receive(:writable?).with(to_platform("/var/chef")).and_return(false) + + expect(ChefConfig::Config[:cache_path]).to eq(secondary_cache_path) + end + end + + context "when chef is running in local mode" do + before do + ChefConfig::Config.local_mode = true + end + + context "and config_dir is /a/b/c" do + before do + ChefConfig::Config.config_dir to_platform('/a/b/c') + end + + it "cache_path is /a/b/c/local-mode-cache" do + expect(ChefConfig::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) + end + end + + context "and config_dir is /a/b/c/" do + before do + ChefConfig::Config.config_dir to_platform('/a/b/c/') + end + + it "cache_path is /a/b/c/local-mode-cache" do + expect(ChefConfig::Config.cache_path).to eq(to_platform('/a/b/c/local-mode-cache')) + end + end + end + end + + it "ChefConfig::Config[:file_backup_path] defaults to /var/chef/backup" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) + backup_path = is_windows ? "#{primary_cache_path}\\backup" : "#{primary_cache_path}/backup" + expect(ChefConfig::Config[:file_backup_path]).to eq(backup_path) + end + + it "ChefConfig::Config[:ssl_verify_mode] defaults to :verify_peer" do + expect(ChefConfig::Config[:ssl_verify_mode]).to eq(:verify_peer) + end + + it "ChefConfig::Config[:ssl_ca_path] defaults to nil" do + expect(ChefConfig::Config[:ssl_ca_path]).to be_nil + end + + # On Windows, we'll detect an omnibus build and set this to the + # cacert.pem included in the package, but it's nil if you're on Windows + # w/o omnibus (e.g., doing development on Windows, custom build, etc.) + if !is_windows + it "ChefConfig::Config[:ssl_ca_file] defaults to nil" do + expect(ChefConfig::Config[:ssl_ca_file]).to be_nil + end + end + + it "ChefConfig::Config[:data_bag_path] defaults to /var/chef/data_bags" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) + data_bag_path = is_windows ? "#{primary_cache_path}\\data_bags" : "#{primary_cache_path}/data_bags" + expect(ChefConfig::Config[:data_bag_path]).to eq(data_bag_path) + end + + it "ChefConfig::Config[:environment_path] defaults to /var/chef/environments" do + allow(ChefConfig::Config).to receive(:cache_path).and_return(primary_cache_path) + environment_path = is_windows ? "#{primary_cache_path}\\environments" : "#{primary_cache_path}/environments" + expect(ChefConfig::Config[:environment_path]).to eq(environment_path) + end + + describe "setting the config dir" do + + context "when the config file is /etc/chef/client.rb" do + + before do + ChefConfig::Config.config_file = to_platform("/etc/chef/client.rb") + end + + it "config_dir is /etc/chef" do + expect(ChefConfig::Config.config_dir).to eq(to_platform("/etc/chef")) + end + + context "and chef is running in local mode" do + before do + ChefConfig::Config.local_mode = true + end + + it "config_dir is /etc/chef" do + expect(ChefConfig::Config.config_dir).to eq(to_platform("/etc/chef")) + end + end + + context "when config_dir is set to /other/config/dir/" do + before do + ChefConfig::Config.config_dir = to_platform("/other/config/dir/") + end + + it "yields the explicit value" do + expect(ChefConfig::Config.config_dir).to eq(to_platform("/other/config/dir/")) + end + end + + end + + context "when the user's home dir is /home/charlie/" do + before do + ChefConfig::Config.user_home = to_platform("/home/charlie") + end + + it "config_dir is /home/charlie/.chef/" do + expect(ChefConfig::Config.config_dir).to eq(ChefConfig::PathHelper.join(to_platform("/home/charlie/.chef"), '')) + end + + context "and chef is running in local mode" do + before do + ChefConfig::Config.local_mode = true + end + + it "config_dir is /home/charlie/.chef/" do + expect(ChefConfig::Config.config_dir).to eq(ChefConfig::PathHelper.join(to_platform("/home/charlie/.chef"), '')) + end + end + end + + end + + if is_windows + describe "finding the windows embedded dir" do + let(:default_config_location) { "c:/opscode/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } + let(:alternate_install_location) { "c:/my/alternate/install/place/chef/embedded/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } + let(:non_omnibus_location) { "c:/my/dev/stuff/lib/ruby/gems/1.9.1/gems/chef-11.6.0/lib/chef/config.rb" } + + let(:default_ca_file) { "c:/opscode/chef/embedded/ssl/certs/cacert.pem" } + + it "finds the embedded dir in the default location" do + allow(ChefConfig::Config).to receive(:_this_file).and_return(default_config_location) + expect(ChefConfig::Config.embedded_dir).to eq("c:/opscode/chef/embedded") + end + + it "finds the embedded dir in a custom install location" do + allow(ChefConfig::Config).to receive(:_this_file).and_return(alternate_install_location) + expect(ChefConfig::Config.embedded_dir).to eq("c:/my/alternate/install/place/chef/embedded") + end + + it "doesn't error when not in an omnibus install" do + allow(ChefConfig::Config).to receive(:_this_file).and_return(non_omnibus_location) + expect(ChefConfig::Config.embedded_dir).to be_nil + end + + it "sets the ssl_ca_cert path if the cert file is available" do + allow(ChefConfig::Config).to receive(:_this_file).and_return(default_config_location) + allow(File).to receive(:exist?).with(default_ca_file).and_return(true) + expect(ChefConfig::Config.ssl_ca_file).to eq(default_ca_file) + end + end + end + end + + describe "ChefConfig::Config[:user_home]" do + it "should set when HOME is provided" do + expected = to_platform("/home/kitten") + allow(ChefConfig::PathHelper).to receive(:home).and_return(expected) + expect(ChefConfig::Config[:user_home]).to eq(expected) + end + + it "falls back to the current working directory when HOME and USERPROFILE is not set" do + allow(ChefConfig::PathHelper).to receive(:home).and_return(nil) + expect(ChefConfig::Config[:user_home]).to eq(Dir.pwd) + end + end + + describe "ChefConfig::Config[:encrypted_data_bag_secret]" do + let(:db_secret_default_path){ to_platform("/etc/chef/encrypted_data_bag_secret") } + + before do + allow(File).to receive(:exist?).with(db_secret_default_path).and_return(secret_exists) + end + + context "/etc/chef/encrypted_data_bag_secret exists" do + let(:secret_exists) { true } + it "sets the value to /etc/chef/encrypted_data_bag_secret" do + expect(ChefConfig::Config[:encrypted_data_bag_secret]).to eq db_secret_default_path + end + end + + context "/etc/chef/encrypted_data_bag_secret does not exist" do + let(:secret_exists) { false } + it "sets the value to nil" do + expect(ChefConfig::Config[:encrypted_data_bag_secret]).to be_nil + end + end + end + + describe "ChefConfig::Config[:event_handlers]" do + it "sets a event_handlers to an empty array by default" do + expect(ChefConfig::Config[:event_handlers]).to eq([]) + end + it "should be able to add custom handlers" do + o = Object.new + ChefConfig::Config[:event_handlers] << o + expect(ChefConfig::Config[:event_handlers]).to be_include(o) + end + end + + describe "ChefConfig::Config[:user_valid_regex]" do + context "on a platform that is not Windows" do + it "allows one letter usernames" do + any_match = ChefConfig::Config[:user_valid_regex].any? { |regex| regex.match('a') } + expect(any_match).to be_truthy + end + end + end + + describe "ChefConfig::Config[:internal_locale]" do + let(:shell_out) do + cmd = instance_double("Mixlib::ShellOut", exitstatus: 0, stdout: locales, error!: nil) + allow(cmd).to receive(:run_command).and_return(cmd) + cmd + end + + let(:locales) { locale_array.join("\n") } + + before do + allow(Mixlib::ShellOut).to receive(:new).with("locale -a").and_return(shell_out) + end + + shared_examples_for "a suitable locale" do + it "returns an English UTF-8 locale" do + expect(ChefConfig.logger).to_not receive(:warn).with(/Please install an English UTF-8 locale for Chef to use/) + expect(ChefConfig.logger).to_not receive(:debug).with(/Defaulting to locale en_US.UTF-8 on Windows/) + expect(ChefConfig.logger).to_not receive(:debug).with(/No usable locale -a command found/) + expect(ChefConfig::Config.guess_internal_locale).to eq expected_locale + end + end + + context "when the result includes 'C.UTF-8'" do + include_examples "a suitable locale" do + let(:locale_array) { [expected_locale, "en_US.UTF-8"] } + let(:expected_locale) { "C.UTF-8" } + end + end + + context "when the result includes 'en_US.UTF-8'" do + include_examples "a suitable locale" do + let(:locale_array) { ["en_CA.UTF-8", expected_locale, "en_NZ.UTF-8"] } + let(:expected_locale) { "en_US.UTF-8" } + end + end + + context "when the result includes 'en_US.utf8'" do + include_examples "a suitable locale" do + let(:locale_array) { ["en_CA.utf8", "en_US.utf8", "en_NZ.utf8"] } + let(:expected_locale) { "en_US.UTF-8" } + end + end + + context "when the result includes 'en.UTF-8'" do + include_examples "a suitable locale" do + let(:locale_array) { ["en.ISO8859-1", expected_locale] } + let(:expected_locale) { "en.UTF-8" } + end + end + + context "when the result includes 'en_*.UTF-8'" do + include_examples "a suitable locale" do + let(:locale_array) { [expected_locale, "en_CA.UTF-8", "en_GB.UTF-8"] } + let(:expected_locale) { "en_AU.UTF-8" } + end + end + + context "when the result includes 'en_*.utf8'" do + include_examples "a suitable locale" do + let(:locale_array) { ["en_AU.utf8", "en_CA.utf8", "en_GB.utf8"] } + let(:expected_locale) { "en_AU.UTF-8" } + end + end + + context "when the result does not include 'en_*.UTF-8'" do + let(:locale_array) { ["af_ZA", "af_ZA.ISO8859-1", "af_ZA.ISO8859-15", "af_ZA.UTF-8"] } + + it "should fall back to C locale" do + expect(ChefConfig.logger).to receive(:warn).with("Please install an English UTF-8 locale for Chef to use, falling back to C locale and disabling UTF-8 support.") + expect(ChefConfig::Config.guess_internal_locale).to eq 'C' + end + end + + context "on error" do + let(:locale_array) { [] } + + let(:shell_out_cmd) { instance_double("Mixlib::ShellOut") } + + before do + allow(Mixlib::ShellOut).to receive(:new).and_return(shell_out_cmd) + allow(shell_out_cmd).to receive(:run_command) + allow(shell_out_cmd).to receive(:error!).and_raise(Mixlib::ShellOut::ShellCommandFailed, "this is an error") + end + + it "should default to 'en_US.UTF-8'" do + if is_windows + expect(ChefConfig.logger).to receive(:debug).with("Defaulting to locale en_US.UTF-8 on Windows, until it matters that we do something else.") + else + expect(ChefConfig.logger).to receive(:debug).with("No usable locale -a command found, assuming you have en_US.UTF-8 installed.") + end + expect(ChefConfig::Config.guess_internal_locale).to eq "en_US.UTF-8" + end + end + end + end + end + + describe "Treating deprecation warnings as errors" do + + context "when using our default RSpec configuration" do + + it "defaults to treating deprecation warnings as errors" do + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(true) + end + + it "sets CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS environment variable" do + expect(ENV['CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS']).to eq("1") + end + + it "treats deprecation warnings as errors in child processes when testing" do + # Doing a full integration test where we launch a child process is slow + # and liable to break for weird reasons (bundler env stuff, etc.), so + # we're just checking that the presence of the environment variable + # causes treat_deprecation_warnings_as_errors to be set to true after a + # config reset. + ChefConfig::Config.reset + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(true) + end + + end + + context "outside of our test environment" do + + before do + ENV.delete('CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS') + ChefConfig::Config.reset + end + + it "defaults to NOT treating deprecation warnings as errors" do + expect(ChefConfig::Config[:treat_deprecation_warnings_as_errors]).to be(false) + end + end + + + end + +end diff --git a/chef-config/spec/unit/path_helper_spec.rb b/chef-config/spec/unit/path_helper_spec.rb new file mode 100644 index 0000000000..3e6213597a --- /dev/null +++ b/chef-config/spec/unit/path_helper_spec.rb @@ -0,0 +1,291 @@ +# +# Author:: Bryan McLellan <btm@loftninjas.org> +# Copyright:: Copyright (c) 2014 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 'chef-config/path_helper' +require 'spec_helper' + +RSpec.describe ChefConfig::PathHelper do + + let(:path_helper) { described_class } + + shared_examples_for "common_functionality" do + describe "join" do + + it "joins starting with '' resolve to absolute paths" do + expect(path_helper.join('', 'a', 'b')).to eq("#{path_helper.path_separator}a#{path_helper.path_separator}b") + end + + it "joins ending with '' add a / to the end" do + expect(path_helper.join('a', 'b', '')).to eq("a#{path_helper.path_separator}b#{path_helper.path_separator}") + end + + end + + describe "dirname" do + it "dirname('abc') is '.'" do + expect(path_helper.dirname('abc')).to eq('.') + end + it "dirname('/') is '/'" do + expect(path_helper.dirname(path_helper.path_separator)).to eq(path_helper.path_separator) + end + it "dirname('a/b/c') is 'a/b'" do + expect(path_helper.dirname(path_helper.join('a', 'b', 'c'))).to eq(path_helper.join('a', 'b')) + end + it "dirname('a/b/c/') is 'a/b'" do + expect(path_helper.dirname(path_helper.join('a', 'b', 'c', ''))).to eq(path_helper.join('a', 'b')) + end + it "dirname('/a/b/c') is '/a/b'" do + expect(path_helper.dirname(path_helper.join('', 'a', 'b', 'c'))).to eq(path_helper.join('', 'a', 'b')) + end + end + end + + context "on windows" do + + before(:each) do + allow(ChefConfig).to receive(:windows?).and_return(true) + end + + include_examples("common_functionality") + + it "path_separator is \\" do + expect(path_helper.path_separator).to eq('\\') + end + + describe "platform-specific #join behavior" do + + it "joins components on Windows when some end with unix separators" do + expect(path_helper.join('C:\\foo/', "bar", "baz")).to eq('C:\\foo\\bar\\baz') + end + + it "joins components when some end with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo\\', "bar", "baz")).to eq(expected) + end + + it "joins components when some end and start with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo\\', "bar/", "/baz")).to eq(expected) + end + + it "joins components that don't end in separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expected = "C:#{expected}" + expect(path_helper.join('C:\\foo', "bar", "baz")).to eq(expected) + end + + end + + + it "cleanpath changes slashes into backslashes and leaves backslashes alone" do + expect(path_helper.cleanpath('/a/b\\c/d/')).to eq('\\a\\b\\c\\d') + end + + it "cleanpath does not remove leading double backslash" do + expect(path_helper.cleanpath('\\\\a/b\\c/d/')).to eq('\\\\a\\b\\c\\d') + end + + end + + context "on unix" do + + before(:each) do + allow(ChefConfig).to receive(:windows?).and_return(false) + end + + include_examples("common_functionality") + + it "path_separator is /" do + expect(path_helper.path_separator).to eq('/') + end + + it "cleanpath removes extra slashes alone" do + expect(path_helper.cleanpath('/a///b/c/d/')).to eq('/a/b/c/d') + end + + describe "platform-specific #join behavior" do + + it "joins components when some end with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo/", "bar", "baz")).to eq(expected) + end + + it "joins components when some end and start with separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo/", "bar/", "/baz")).to eq(expected) + end + + it "joins components that don't end in separators" do + expected = path_helper.cleanpath("/foo/bar/baz") + expect(path_helper.join("/foo", "bar", "baz")).to eq(expected) + end + + end + + end + + describe "validate_path" do + context "on windows" do + before(:each) do + # pass by default + allow(ChefConfig).to receive(:windows?).and_return(true) + allow(path_helper).to receive(:printable?).and_return(true) + allow(path_helper).to receive(:windows_max_length_exceeded?).and_return(false) + end + + it "returns the path if the path passes the tests" do + expect(path_helper.validate_path("C:\\ThisIsRigged")).to eql("C:\\ThisIsRigged") + end + + it "does not raise an error if everything looks great" do + expect { path_helper.validate_path("C:\\cool path\\dude.exe") }.not_to raise_error + end + + it "raises an error if the path has invalid characters" do + allow(path_helper).to receive(:printable?).and_return(false) + expect { path_helper.validate_path("Newline!\n") }.to raise_error(ChefConfig::InvalidPath) + end + + it "Adds the \\\\?\\ prefix if the path exceeds MAX_LENGTH and does not have it" do + long_path = "C:\\" + "a" * 250 + "\\" + "b" * 250 + prefixed_long_path = "\\\\?\\" + long_path + allow(path_helper).to receive(:windows_max_length_exceeded?).and_return(true) + expect(path_helper.validate_path(long_path)).to eql(prefixed_long_path) + end + end + end + + describe "windows_max_length_exceeded?" do + it "returns true if the path is too long (259 + NUL) for the API" do + expect(path_helper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 6)).to be_truthy + end + + it "returns false if the path is not too long (259 + NUL) for the standard API" do + expect(path_helper.windows_max_length_exceeded?("C:\\" + "a" * 250 + "\\" + "b" * 5)).to be_falsey + end + + it "returns false if the path is over 259 characters but uses the \\\\?\\ prefix" do + expect(path_helper.windows_max_length_exceeded?("\\\\?\\C:\\" + "a" * 250 + "\\" + "b" * 250)).to be_falsey + end + end + + describe "printable?" do + it "returns true if the string contains no non-printable characters" do + expect(path_helper.printable?("C:\\Program Files (x86)\\Microsoft Office\\Files.lst")).to be_truthy + end + + it "returns true when given 'abc' in unicode" do + expect(path_helper.printable?("\u0061\u0062\u0063")).to be_truthy + end + + it "returns true when given japanese unicode" do + expect(path_helper.printable?("\uff86\uff87\uff88")).to be_truthy + end + + it "returns false if the string contains a non-printable character" do + expect(path_helper.printable?("\my files\work\notes.txt")).to be_falsey + end + + # This isn't necessarily a requirement, but here to be explicit about functionality. + it "returns false if the string contains a newline or tab" do + expect(path_helper.printable?("\tThere's no way,\n\t *no* way,\n\t that you came from my loins.\n")).to be_falsey + end + end + + describe "canonical_path" do + context "on windows", :windows_only do + it "returns an absolute path with backslashes instead of slashes" do + expect(path_helper.canonical_path("\\\\?\\C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") + end + + it "adds the \\\\?\\ prefix if it is missing" do + expect(path_helper.canonical_path("C:/windows/win.ini")).to eq("\\\\?\\c:\\windows\\win.ini") + end + + it "returns a lowercase path" do + expect(path_helper.canonical_path("\\\\?\\C:\\CASE\\INSENSITIVE")).to eq("\\\\?\\c:\\case\\insensitive") + end + end + + context "not on windows", :unix_only do + it "returns a canonical path" do + expect(path_helper.canonical_path("/etc//apache.d/sites-enabled/../sites-available/default")).to eq("/etc/apache.d/sites-available/default") + end + end + end + + describe "paths_eql?" do + it "returns true if the paths are the same" do + allow(path_helper).to receive(:canonical_path).with("bandit").and_return("c:/bandit/bandit") + allow(path_helper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") + expect(path_helper.paths_eql?("bandit", "../bandit/bandit")).to be_truthy + end + + it "returns false if the paths are different" do + allow(path_helper).to receive(:canonical_path).with("bandit").and_return("c:/Bo/Bandit") + allow(path_helper).to receive(:canonical_path).with("../bandit/bandit").and_return("c:/bandit/bandit") + expect(path_helper.paths_eql?("bandit", "../bandit/bandit")).to be_falsey + end + end + + describe "escape_glob" do + it "escapes characters reserved by glob" do + path = "C:\\this\\*path\\[needs]\\escaping?" + escaped_path = "C:\\\\this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" + expect(path_helper.escape_glob(path)).to eq(escaped_path) + end + + context "when given more than one argument" do + it "joins, cleanpaths, and escapes characters reserved by glob" do + args = ["this/*path", "[needs]", "escaping?"] + escaped_path = if ChefConfig.windows? + "this\\\\\\*path\\\\\\[needs\\]\\\\escaping\\?" + else + "this/\\*path/\\[needs\\]/escaping\\?" + end + expect(path_helper).to receive(:join).with(*args).and_call_original + expect(path_helper).to receive(:cleanpath).and_call_original + expect(path_helper.escape_glob(*args)).to eq(escaped_path) + end + end + end + + describe "all_homes" do + before do + stub_const('ENV', env) + allow(ChefConfig).to receive(:windows?).and_return(is_windows) + end + + context "on windows" do + let (:is_windows) { true } + end + + context "on unix" do + let (:is_windows) { false } + + context "when HOME is not set" do + let (:env) { {} } + it "returns an empty array" do + expect(path_helper.all_homes).to eq([]) + end + end + end + end +end |