diff options
378 files changed, 14973 insertions, 5021 deletions
diff --git a/.travis.yml b/.travis.yml index 2910725d7b..d7ad317e28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,6 @@ branches: - master - 10-stable - 11-stable - - 12-stable # do not run expensive spec tests on PRs, only on branches script: " diff --git a/CHANGELOG.md b/CHANGELOG.md index a3605e3084..22f02eee2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,73 @@ +## Unreleased +* [**Ranjib Dey**](https://github.com/ranjib): + [pr#3588](https://github.com/chef/chef/pull/3588) Count skipped resources among total resources in doc formatter +* [**John Kerry**](https://github.com/jkerry): + [pr#3539](https://github.com/chef/chef/pull/3539) Fix issue: registry\_key resource is case sensitive in chef but not on windows +* [**David Eddy**](https://github.com/bahamas10): + [pr#3443](https://github.com/chef/chef/pull/3443) remove extraneous space +* [**margueritepd**](https://github.com/margueritepd): + [pr#3693](https://github.com/chef/chef/pull/3693) Interpolate `%{path}` in verify command +* [**Jeremy Fleischman**](https://github.com/jfly): + [pr#3383](https://github.com/chef/chef/pull/3383) gem\_package should install to the systemwide Ruby when using ChefDK +* [**Stefano Rivera**](https://github.com/stefanor): + [pr#3657](https://github.com/chef/chef/pull/3657) fix upstart status\_commands +* [**ABE Satoru**](https://github.com/polamjag): + [pr#3764](https://github.com/chef/chef/pull/3764) uniquify chef\_repo\_path +* [**Renan Vicente**](https://github.com/renanvicente): + [pr#3771](https://github.com/chef/chef/pull/3771) add depth property for deploy resource +* [**James Belchamber**](https://github.com/JamesBelchamber): + [pr#1796](https://github.com/chef/chef/pull/1796): make mount options aware +* [**Nate Walck**](https://github.com/natewalck): + [pr#3594](https://github.com/chef/chef/pull/3594): Update service provider for OSX 10.11 +* [**Nate Walck**](https://github.com/natewalck): + [pr#3704](https://github.com/chef/chef/pull/3704): Add SIP (OS X 10.11) support +* [**Phil Dibowitz**](https://github.com/jaymzh): + [pr#3805](https://github.com/chef/chef/pull/3805) LWRP parameter validators should use truthiness +* [**Igor Shpakov**](https://github.com/Igorshp): + [pr#3743](https://github.com/chef/chef/pull/3743) speed improvement for `remote_directory` resource + +* [pr#3799](https://github.com/chef/chef/pull/3799) fix supports hash issues in service providers +* [pr#3817](https://github.com/chef/chef/pull/3817) Remove now-useless forcing of ruby Garbage Collector run +* [pr#3774](https://github.com/chef/chef/pull/3774) Add support for yum-deprecated in yum provider +* [pr#3793](https://github.com/chef/chef/pull/3793) CHEF-5372: Support specific `run_levels` for RedHat service +* [pr#2460](https://github.com/chef/chef/pull/2460) add privacy flag +* [pr#1259](https://github.com/chef/chef/pull/1259) CHEF-5012: add methods for template breadcrumbs +* [pr#3656](https://github.com/chef/chef/pull/3656) remove use of self.provides? +* [pr#3455](https://github.com/chef/chef/pull/3455) powershell\_script: do not allow suppression of syntax errors +* [pr#3519](https://github.com/chef/chef/pull/3519) The wording seemed odd. +* [pr#3208](https://github.com/chef/chef/pull/3208) Missing require (require what you use). +* [pr#3449](https://github.com/chef/chef/pull/3449) correcting minor typo in user\_edit knife action +* [pr#3572](https://github.com/chef/chef/pull/3572) Use windows paths without case-sensitivity. +* [pr#3666](https://github.com/chef/chef/pull/3666) Support SNI in `knife ssl check`. +* [pr#3667](https://github.com/chef/chef/pull/3667) Change chef service to start as 'Automatic delayed start'. +* [pr#3683](https://github.com/chef/chef/pull/3683) Correct Windows reboot command to delay in minutes, per the property. +* [pr#3698](https://github.com/chef/chef/pull/3698) Add ability to specify dependencies in chef-service-manager. +* [pr#3728](https://github.com/chef/chef/pull/3728) Rewrite NetLocalGroup things to use FFI +* [pr#3754](https://github.com/chef/chef/pull/3754) Fix functional tests for group resource - fix #3728 +* [pr#3498](https://github.com/chef/chef/pull/3498) Use dpkg-deb directly rather than regex +* [pr#3759](https://github.com/chef/chef/pull/3759) Repair service convergence test on AIX +* [pr#3329](https://github.com/chef/chef/pull/3329) Use ifconfig target property +* [pr#3652](https://github.com/chef/chef/pull/3652) Fix explanation for configuring audit mode in client.rb +* [pr#3687](https://github.com/chef/chef/pull/3687) Add formatter and force-logger/formatter options to chef-apply +* [pr#3768](https://github.com/chef/chef/pull/3768) Make reboot\_pending? look for CBS RebootPending +* [pr#3815](https://github.com/chef/chef/pull/3815) Fix `powershell_script` validation to use correct architecture +* [pr#3772](https://github.com/chef/chef/pull/3772) Add `ps_credential` dsl method to `dsc_script` + +## 12.4.1 + +* [**Noah Kantrowitz**](https://github.com/coderanger): + [pr#3605](https://github.com/chef/chef/pull/3605) Rework `Resource#action` to match 12.3 API + +* [pr#3586](https://github.com/chef/chef/issues/3586) Fix bug preventing light weight resources from being used with heavy weight providers +* [Issue #3593](https://github.com/chef/chef/issues/3593) Fix bug where provider priority map did not take into consideration a provided block +* [pr#3630](https://github.com/chef/chef/pull/3630) Restore Chef::User and Chef::ApiClient namespace to API V0 functionality and move new functionality into Chef::UserV1 and Chef::ApiClientV1 until Chef 13. +* [pr#3611](https://github.com/chef/chef/pull/3611) Call `provides?` even if `provides` is not called +* [pr#3589](https://github.com/chef/chef/pull/3589) Fix errant bashisms +* [pr#3620](https://github.com/chef/chef/pull/3620) Fix issue where recipe names in run list mutate when version constaints are present +* [pr#3623](https://github.com/chef/chef/pull/3623) Allow LWRPs to access the real class when accessed through `Chef::Resource` and `Chef::Provider` +* [pr#3627](https://github.com/chef/chef/pull/3627) Separate priority map and DSL handler map so that `provides` has veto power over priority +* [pr#3638](https://github.com/chef/chef/pull/3638) Deprecate passing more than 1 argument to create a resource + ## 12.4.0 * [**Phil Dibowitz**](https://github.com/jaymzh): diff --git a/DOC_CHANGES.md b/DOC_CHANGES.md index 46712f114c..4c07dea872 100644 --- a/DOC_CHANGES.md +++ b/DOC_CHANGES.md @@ -6,79 +6,392 @@ Example Doc Change: Description of the required change. --> -### Resources now *all* get automatic DSL +### PSCredential Support for `dsc_script` -When you declare a resource (no matter where) you now get automatic DSL for it, based on your class name: +`dsc_script` now supports the use of `ps_credential` to create a PSCredential +object similar to `dsc_resource`. The `ps_credential` helper function takes in +a string and when `to_s` is called on it, produces an object that can be embedded +in your `dsc_script`. For example, you can write: ```ruby -module MyModule - class MyResource < Chef::Resource - # Names the resource "my_resource" +dsc_script 'create-foo-user' do + code <<-EOH + User FooUser + { + UserName = 'FooUser' + Password = #{ps_credential('FooBarBaz1!')} + } + EOH + configuration_data <<-EOH + @{ + AllNodes = @( + @{ + NodeName = "localhost"; + CertificateID = 'A8DB81D8059F349F7EF19104399B898F701D4167' + } + ) + } + EOH +end +``` + +Note, you still need to configure the CertificateID in the LCM. + +### chef-client -j JSON +Add to the [description of chef-client options](https://docs.chef.io/ctl_chef_client.html#options): + +> This option can also be used to set a node's `chef_environment`. For example, +running `chef-client -j /path/to/file.json` where `/path/to/file.json` is +similar to: +``` +{ + "chef_environment": "pre-production" +} +``` +will set the node's environment to `"pre-production"`. + +> *Note that the environment specified by `chef_environment` in your JSON will +take precedence over an environment specified by `-E ENVIROMENT` when both options +are provided.* + +### Resources Made Easy + +Resources are central to Chef. The system is extensible so that you can write +your own reusable resources, and use them in your recipes, and even publish them +so that others can use them too! + +However, writing these resources has not been as easy as we would have liked. In +Chef 12.5, we are fixing this with a large number of DSL improvements designed +to reduce the number of things you need to type and think about when you create +a resource. Resources should be your go-to solution for many Chef problems, and +these changes make them easy enough to dash off in an instant, while retaining +all the power you're accustomed to. + +The process to create a resource is now: + +1. Make a resource file in your cookbook (like `resources/my_resource.rb`). +2. Add the recipes defining your actions using the `action :create <recipe>` DSL. +3. Add properties so the user can tweak some knobs on your resource (like paths, + or preferences), using the `property :my_property, <type>, <options>` DSL. +4. Use the resource in your recipe! + +There are other things you can do, but this is the most basic (and the first) +thing you will start with. + +Let's demonstrate the new features by taking a simple recipe from the awesome +[learnchef tutorial](https://learn.chef.io/learn-the-basics/rhel/configure-a-package-and-service/), +and turning it into a reusable resource: + +```ruby +package 'httpd' + +service 'httpd' do + action [:enable, :start] +end + +file '/var/www/html/index.html' do + content '<html> + <body> + <h1>hello world</h1> + </body> +</html>' +end + +service 'iptables' do + action :stop +end +``` + +We'll design a resource that lets you write this recipe instead: + +```ruby +single_page_website 'mysite' do + homepage '<html> + <body> + <h1>hello world</h1> + </body> + </html>' +end +``` + +#### Declaring the Resource + +The first thing we do is declare the resource. We can do that by creating an +empty file, `resources/single_page_website.rb`, in our cookbook. + +When you do this, the `single_page_website` resource will work in all recipes! + +```ruby +single_page_website 'mysite' +``` + +It won't do anything yet, though :) + +#### Declaring an Action + +Let's make our resource do something. To start with, we'll just have it do exactly +what the learnchef tutorial does, but in the resource. Put this in +`resources/single_page_website.rb`: + +```ruby +action :create do + package 'httpd' + + service 'httpd' do + action [:enable, :start] + end + + file '/var/www/html/index.html' do + content '<html> + <body> + <h1>hello world</h1> + </body> + </html>' + end + + service 'iptables' do + action :stop + end +end +``` + +Now, your simple recipe can use this resource to do what learnchef did: + +```ruby +single_page_website 'mysite' +``` + +We've got ourselves an httpd! + +You will notice the only thing we've done is to add `action :create` around the +recipe. The `action` keyword lets you declare a recipe inline, which will be +executed when the user uses your resource in a recipe. + +#### Declaring a resource property: "homepage" + +This isn't super reusable yet--you might want your webpage to say something other +than "hello world". Let's add a couple of properties for that, by putting this +at the top of `resources/single_page_website`, and modifying the recipe to use +"title" and "body": + +```ruby +property :homepage, String, default: '<h1>hello world</h1>' + +action :create do + package 'httpd' + + service 'httpd' do + action [:enable, :start] + end + + file '/var/www/html/index.html' do + content homepage + end + + service 'iptables' do + action :stop end end ``` -When this happens, the resource can be used in a recipe: +Now you can run this recipe: ```ruby -my_resource 'blah' do +single_page_website 'mysite' do + homepage '<h1>My own page</h1>' end ``` -If you have an abstract class that should *not* have DSL, set `resource_name` to `nil`: +And you've got a website with your stuff! + +What you've done here is add *properties*. Properties are the *desired state* of +a resource, in this case, `homepage` defines the text on the website. When you +add a property, you're letting a user give it whatever value they want. + +When you define a property, there are three bits: +`property :<name>, <type>, <options>`. *Name* defines the name of the property, +so that people can set the property using `name <value>` when they use your +resource. *Type* defines the type of the property: for example, String, Integer +and Array are all possible types. Type is optional. *Options* define a large +number of validation and other options. You've seen `default` already now, +but there are a ton of others. + +#### Adding another property: "not_found_page" + +What if we want a custom 404 page for when people try to go to other pages in +our website? Let's add one more property, to make this even nicer: ```ruby -module MyModule - # This will not have DSL - class MyBaseResource < Chef::Resource - resource_name nil +property :homepage, String, default: '<h1>hello world</h1>' +property :not_found_page, String, default: '<h1>No such page! Sorry. 404.</h1>' + +action :create do + package 'httpd' + + service 'httpd' do + action [:enable, :start] end - # This will have DSL `my_resource` - class MyResource < MyBaseResource + + file '/var/www/html/index.html' do + content homepage + end + + # These together tell Apache to use your custom 404 page: + file '/var/www/html/404.html' do + content not_found_page + end + file '/var/www/html/.htaccess' do + content 'ErrorDocument 404 /404.html' end + + service 'iptables' do + action :stop + end +end +``` + +Now you can run this recipe: + +```ruby +single_page_website 'mysite' do + homepage '<h1>My own page</h1>' + not_found_page '<h1>Grr. Page not found. Sorry. (404)</h1>' end ``` -When you do this, `my_base_resource` will not work in a recipe (but `my_resource` will). +#### Adding another action: "stop" -You can still use `provides` to provide other DSL names: +What if we want to stop the website? Just add another action into the bottom of +`resources/single_page_website.rb`: ```ruby -module MyModule - class MyResource < Chef::Resource - provides :super_resource +action :stop do + service 'httpd' do + action :stop end end ``` -Which enables this recipe: +This action looks a lot like the other. + +There are a ton of other things you can do to create resources, but this should +give you a pretty basic idea. + +### Advanced Resource Capabilities + +#### Ruby Developers: Resources as Classes + +If you are a Ruby developer, we've made it easier to create a Resource outside +of a cookbook (or in a library) by declaring a class! Declare +`class SinglePageWebsite < Chef::Resource` and put the entire resource +declaration inside, and the `single_page_website` resource will work! + +#### Reading the current value: load_current_value + +There is a pitfall inherent in a resource, where users will sometimes omit a +property from a resource, and become surprised when the system overwrites it +with the default! For example, if your website already exists, this recipe +will replace the *homepage* with "hello world": + +```ruby +single_page_website 'mysite' do + not_found_page '<h1>nice</h1>' +end +``` + +It's not at all clear that that's what the user wanted--they didn't say anything +about the homepage, so why did something happen to it? + +To guard against this, you can implement `load_current_value` in your resource. +Put this in `resources/single_page_website.rb`: ```ruby -super_resource 'wowzers' do +load_current_value do + if File.exist?('/var/www/html/index.html') + homepage IO.read('/var/www/html/index.html') + end + if File.exist?('/var/www/html/404.html') + not_found_page IO.read('/var/www/html/404.html') + end end ``` -(Note that when you use provides in this manner, resource_name will be `my_resource` and declared_type will be `super_resource`. This won't affect most people, but it is worth noting as a matter of explanation.) +Now, the above recipe knows what the current homepage is, and will not change it! -Users are encouraged to declare resources in their own namespaces instead of putting them in the `Chef::Resource` namespace. +This capability is also used for several other things, including reporting (to +describe what changed) and pure Ruby actions. -### Resources may now use `allowed_actions` and `default_action` +#### Pure Ruby Actions -Instead of overriding `Chef::Resource.initialize` and setting `@allowed_actions` and `@action` in the constructor, you may now use the `allowed_actions` and `default_action` DSL to declare them: +Some resources need to talk directly to Ruby to do their dirty work, rather than using other resources. In those cases, you need to: + +- Make the updates only if the user specified properties that *need* to change. +- Make sure and call updates if the resource does not exist (need to be created). +- Print useful green text if the update is happening. +- Not actually make any changes in why-run mode! + +`converge_if_changed` handles all of the above by comparing the user's desired +property values against the *current* value as loaded by `load_current_value`. +Simply wrap the part of your recipe that does a set in `converge_if_changed`. +As an example, here is a basic `my_file` resource that creates a file with the +given content: ```ruby -class MyResource < Chef::Resource - allowed_actions :create, :delete - default_action :create +# resources/my_file.rb +property :path, String, name_property: true +property :content, String + +load_current_value do + if File.exist?(path) + content IO.read(path) + end +end + +action :create do + converge_if_changed do + IO.write(path, content) + end end ``` -### LWRPs are no longer automatically placed in the `Chef::Resource` namespace +The above code will only call `IO.write` if the file does not exist, or if the +user specified content that is different from what is on disk. It will print out +something like this, showing the changes: + +```ruby +Recipe: basic_chef_client::block + * my_file[blah] action create + - update my_file[blah] + - set content to "hola mundo" (was "hello world") + ``` + +##### Handling Multiple Operations -Starting with Chef 12.4.0, accessing an LWRP class by name from the `Chef::Resource` namespace will trigger a deprecation warning message. This means that if your cookbook includes the LWRP `mycookbook/resources/myresource.rb`, you will no longer be able to extend or reference `Chef::Resource::MycookbookMyresource` in Ruby code. LWRP recipe DSL does not change: the LWRP will still be available to recipes as `mycookbook_myresource`. +If you have two separate, expensive operations to handle converge, `converge_if_changed` +can be called multiple times with multiple properties. Adding `mode` to `my_file` +demonstrates this: -You can still get the LWRP class by calling `Chef::ResourceResolver.resolve(:mycookbook_myresource)`. +```ruby +# resources/my_file.rb +property :path, String, name_property: true +property :content, String +property :mode, String -The primary aim here is clearing out the `Chef::Resource` namespace. +load_current_value do + if File.exist?(path) + content IO.read(path) + mode File.stat(path).mode + end +end -References to these classes is deprecated (and will emit a warning) in Chef 12, and will be removed in Chef 13. +action :create do + # Only change content here + converge_if_changed :content do + IO.write(path, content) + end + # Only change mode here + converge_if_changed :mode do + File.chmod(mode, path) + end +end +``` @@ -13,7 +13,7 @@ end group(:development, :test) do gem "simplecov" gem 'rack', "~> 1.5.1" - gem 'cheffish', "~> 1.2" + gem 'cheffish', "~> 1.3" gem 'ruby-shadow', :platforms => :ruby unless RUBY_PLATFORM.downcase.match(/(aix|cygwin)/) end diff --git a/MAINTAINERS.md b/MAINTAINERS.md index e3bf07573a..3c777366f8 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -19,7 +19,7 @@ a maintainer, lieutenant, or the project lead. ## Chef Core Handles the core parts of the Chef DSL, base resource and provider -infrastructure, and the Chef applications. Includes anything not covered by +infrastructure, the Chef applications and [omnibus-chef](https://github.com/chef/omnibus-chef). Includes anything not covered by another component. ### Lieutenant @@ -29,12 +29,14 @@ another component. ### Maintainers * [Bryan McLellan](https://github.com/btm) +* [Noah Kantrowitz](https://github.com/coderanger) * [Daniel DeLeo](https://github.com/danielsdeleo) * [AJ Christensen](https://github.com/fujin) * [Phil Dibowitz](https://github.com/jaymzh) * [Jay Mundrawala](https://github.com/jdmundrawala) * [Jon Cowie](https://github.com/jonlives) * [Lamont Granquist](https://github.com/lamont-granquist) +* [Claire McQuin](https://github.com/mcquin) * [Steven Murawski](https://github.com/smurawski) * [Tyler Ball](https://github.com/tyler-ball) * [Ranjib Dey](https://github.com/ranjib) @@ -79,10 +81,13 @@ The specific components of Chef related to a given platform - including (but not ## Ubuntu +### Lieutenant + +* [Ranjib Dey](https://github.com/ranjib) + ### Maintainers * [Lamont Granquist](https://github.com/lamont-granquist) -* [Ranjib Dey](https://github.com/ranjib) * [Thom May](https://github.com/thommay) ## Windows @@ -96,16 +101,21 @@ The specific components of Chef related to a given platform - including (but not * [Jay Mundrawala](https://github.com/jdmundrawala) * [Kartik Cating-Subramanian](https://github.com/ksubrama) * [Steven Murawski](https://github.com/smurawski) +* [Salim Alam](https://github.com/chefsalim) ## Solaris +### Lieutenant + +* [Thom May](https://github.com/thommay) + ### Maintainers * [Lamont Granquist](https://github.com/lamont-granquist) ## AIX -### Maintainers +### Lieutenant * [Lamont Granquist](https://github.com/lamont-granquist) @@ -119,6 +129,34 @@ The specific components of Chef related to a given platform - including (but not * [Tyler Ball](https://github.com/tyler-ball) +## Debian + +### Lieutenant + +* [Thom May](https://github.com/thommay) + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) + +## Fedora + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) + +## openSUSE + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) + +## SUSE Enterprise Linux Server + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) + ## FreeBSD ### Lieutenant @@ -130,3 +168,28 @@ The specific components of Chef related to a given platform - including (but not * [Cory Stephenson](https://github.com/Aevin1387) * [David Aronsohn](https://github.com/tbunnyman) +## OpenBSD + +### Lieutenant + +* [Joe Miller](https://github.com/joemiller) + +## Gentoo + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) + +## OmniOS + +### Maintainers + +* [Thom May](https://github.com/thommay) + +## ArchLinux + +### Maintainers + +* [Lamont Granquist](https://github.com/lamont-granquist) +* [Ryan Cragun](https://github.com/ryancragun) + diff --git a/MAINTAINERS.toml b/MAINTAINERS.toml index f949fd4542..61c75c1a30 100644 --- a/MAINTAINERS.toml +++ b/MAINTAINERS.toml @@ -26,7 +26,7 @@ a maintainer, lieutenant, or the project lead. title = "Chef Core" text = """ Handles the core parts of the Chef DSL, base resource and provider -infrastructure, and the Chef applications. Includes anything not covered by +infrastructure, the Chef applications and [omnibus-chef](https://github.com/chef/omnibus-chef). Includes anything not covered by another component. """ @@ -34,12 +34,14 @@ another component. maintainers = [ "btm", + "coderanger", "danielsdeleo", "fujin", "jaymzh", "jdmundrawala", "jonlives", "lamont-granquist", + "mcquin", "smurawski", "tyler-ball", "ranjib" @@ -95,9 +97,10 @@ The specific components of Chef related to a given platform - including (but not [Org.Components.Subsystems.Ubuntu] title = "Ubuntu" + lieutenant = "ranjib" + maintainers = [ "lamont-granquist", - "ranjib", "thommay" ] @@ -108,12 +111,15 @@ The specific components of Chef related to a given platform - including (but not maintainers = [ "jdmundrawala", "ksubrama", - "smurawski" + "smurawski", + "chefsalim" ] [Org.Components.Subsystems.Solaris] title = "Solaris" + lieutenant = "thommay" + maintainers = [ "lamont-granquist" ] @@ -121,9 +127,7 @@ The specific components of Chef related to a given platform - including (but not [Org.Components.Subsystems.AIX] title = "AIX" - maintainers = [ - "lamont-granquist" - ] + lieutenant = "lamont-granquist" [Org.Components.Subsystems."Mac OS X"] title = "Mac OS X" @@ -134,6 +138,36 @@ The specific components of Chef related to a given platform - including (but not "tyler-ball" ] + [Org.Components.Subsystems.Debian] + title = "Debian" + + lieutenant = "thommay" + + maintainers = [ + "lamont-granquist" + ] + + [Org.Components.Subsystems.Fedora] + title = "Fedora" + + maintainers = [ + "lamont-granquist" + ] + + [Org.Components.Subsystems.openSUSE] + title = "openSUSE" + + maintainers = [ + "lamont-granquist" + ] + + [Org.Components.Subsystems."SUSE Enterprise Linux"] + title = "SUSE Enterprise Linux Server" + + maintainers = [ + "lamont-granquist" + ] + [Org.Components.Subsystems.FreeBSD] title = "FreeBSD" @@ -144,6 +178,32 @@ The specific components of Chef related to a given platform - including (but not "tBunnyMan" ] + [Org.Components.Subsystems.OpenBSD] + title = "OpenBSD" + + lieutenant = "joemiller" + + [Org.Components.Subsystems.Gentoo] + title = "Gentoo" + + maintainers = [ + "lamont-granquist" + ] + + [Org.Components.Subsystems.OmniOS] + title = "OmniOS" + + maintainers = [ + "thommay" + ] + + [Org.Components.Subsystems.ArchLinux] + title = "ArchLinux" + + maintainers = [ + "lamont-granquist", + "ryancragun" + ] [people] [people.adamhjk] @@ -229,3 +289,19 @@ The specific components of Chef related to a given platform - including (but not [people.ksubrama] Name = "Kartik Cating-Subramanian" GitHub = "ksubrama" + + [people.joemiller] + Name = "Joe Miller" + GitHub = "joemiller" + + [people.coderanger] + Name = "Noah Kantrowitz" + GitHub = "coderanger" + + [people.ryancragun] + Name = "Ryan Cragun" + GitHub = "ryancragun" + + [people.chefsalim] + Name = "Salim Alam" + GitHub = "chefsalim" @@ -68,11 +68,11 @@ read the The general development process is: -1. Fork this repo and clone it to your workstation -2. Create a feature branch for your change -3. Write code and tests +1. Fork this repo and clone it to your workstation. +2. Create a feature branch for your change. +3. Write code and tests. 4. Push your feature branch to github and open a pull request against - master + master. Once your repository is set up, you can start working on the code. We do use TDD with RSpec, so you'll need to get a development environment running. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 049640d7ab..cba5b9f415 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,87 +1,76 @@ -# Chef Client Release Notes 12.4.0: +# Chef Client Release Notes 12.5.0: +* OSX 10.11 support (support for SIP and service changes) -## Knife Key Management Commands for Users and Clients +## PSCredential support for the `dsc_script` resource -`knife user` and `knife client` now have a suite of subcommands that live under -the `key` subcommand. These subcommands allow you to list, show, create, delete -and edit keys for a given user or client. They can be used to implement -key rotation with multiple expiring keys for a single actor or just -for basic key management. See `knife user key` and `knife client key` -for a full list of subcommands and their usage. - -## System Loggers - -You can now have all Chef logs sent to a logging system of your choice. - -### Syslog Logger - -Syslog can be used by adding the following line to your chef config -file: - -```ruby -log_location Chef::Log::Syslog.new("chef-client", ::Syslog::LOG_DAEMON) -``` - -THis will write to the `daemon` facility with the originator set as -`chef-client`. - -### Windows Event Logger - -The logger can be used by adding the following line to your chef config file: +The `dsc_script` resource now supports the use of the `ps_credential` +helper method. This method generates a Ruby object which can be described +as a Powershell PSCredential object. For example, if you wanted to created +a user using DSC, previously you would have had to do something like: ```ruby -log_location Chef::Log::WinEvt.new +dsc_script 'create-foo-user' do + code <<-EOH + $username = "placeholder" + $password = "#{FooBarBaz1!}" | ConvertTo-SecureString -asPlainText -Force + $cred = New-Object System.Management.Automation.PSCredential($username, $password) + User FooUser00 + { + Ensure = "Present" + UserName = 'FooUser00' + Password = $cred + } + EOH + configuration_data_script "path/to/config/data.psd1" +end ``` -This will write to the Application log with the source set as Chef. - -## RemoteFile resource supports UNC paths on Windows - -You can now use UNC paths with `remote_file` on Windows machines. For -example, you can get `Foo.tar.gz` off of `fooshare` on `foohost` using -the following resource: +This can now be replaced with ```ruby -remote_file 'C:\Foo.tar.gz' do - source "\\\\foohost\\fooshare\\Foo.tar.gz" +dsc_script 'create-foo-user' do + code <<-EOH + User FooUser00 + { + Ensure = "Present" + UserName = 'FooUser00' + Password = #{ps_credential("FooBarBaz1!")} + } + EOH + configuration_data_script "path/to/config/data.psd1" end ``` -## WindowsPackage resource supports URLs +## New `knife rehash` for faster command loading -The `windows_package` resource now allows specifying URLs for the source -attribute. For example, you could install 7zip with the following resource: +The new `knife rehash` command speeds up day-to-day knife usage by +caching information about installed plugins and available commands. +Initial testing has shown substantial improvements in `knife` startup +times for users with a large number of Gems installed and Windows +users. -```ruby -windows_package '7zip' do - source "http://www.7-zip.org/a/7z938-x64.msi" -end -``` +To use this feature, simply run `knife rehash` and continue using +`knife`. When you install or remove gems that provide knife plugins, +run `knife rehash` again to keep the cache up to date. + +## Support for `/usr/bin/yum-deprecated` in the yum provider -Internally, this is done by using a `remote_file` resource to download the -contents at the specified url. If needed, you can modify the attributes of -the `remote_file` resource using the `remote_file_attributes` attribute. -The `remote_file_attributes` accepts a hash of attributes that will be set -on the underlying remote_file. For example, the checksum of the contents can -be verified using +In Fedora 22 yum has been deprecated in favor of DNF. Unfortunately, while DNF tries to be backwards +compatible with yum, the yum provider in Chef is not compatible with DNF. Until a proper `dnf_package` +resource and associated provider is written and merged into core, 12.5.0 has been patched so that the +`yum_package` resource takes a property named `yum_binary` which can be set to point at the yum binary +to run for all its commands. The `yum_binary` will also default to `yum-deprecated` if the +`/usr/bin/yum-deprecated` command is found on the system. This means that Fedora 22 users can run +something like this early in their chef-client run: ```ruby -windows_package '7zip' do - source "http://www.7-zip.org/a/7z938-x64.msi" - remote_file_attributes { - :path => "C:\\7zip.msi", - :checksum => '7c8e873991c82ad9cfcdbdf45254ea6101e9a645e12977dcd518979e50fdedf3' - } +if File.exist?("/usr/bin/dnf") + execute "dnf install -y yum" do + not_if { File.exist?("/usr/bin/yum-deprecated") } + end end ``` -To make the transition easier from the Windows cookbook, `windows_package` also -accepts the `checksum` attribute, and the previous resource could be rewritten -as: +After which the yum-deprecated binary will exist, and the yum provider will find it and should operate +normally and successfully. -```ruby -windows_package '7zip' do - source "http://www.7-zip.org/a/7z938-x64.msi" - checksum '7c8e873991c82ad9cfcdbdf45254ea6101e9a645e12977dcd518979e50fdedf3' -end -``` @@ -20,121 +20,15 @@ VERSION = IO.read(File.expand_path("../VERSION", __FILE__)).strip require 'rubygems' -require 'rubygems/package_task' +require 'chef-config/package_task' require 'rdoc/task' require_relative 'tasks/rspec' require_relative 'tasks/external_tests' require_relative 'tasks/maintainers' -GEM_NAME = "chef" - -desc "build Gems of Chef's components" -task :package_components do - Dir.chdir("chef-config") do - sh "rake package" - end -end - -task :package => :package_components - -desc "build and install chef's components" -task :install_components => :package_components do - Dir.chdir("chef-config") do - sh "rake install" - end -end - -task :install => :install_components - -desc "clean up builds of Chef's components" -task :clobber_component_packages do - Dir.chdir("chef-config") do - sh "rake clobber_package" - end -end - -task :clobber_package => :clobber_component_packages - -desc "Update the version number for Chef's components" -task :update_components_versions do - Dir.chdir("chef-config") do - sh "rake version" - end -end - -desc "Regenerate lib/chef/version.rb from VERSION file" -task :version => :update_components_versions do - contents = <<-VERSION_RB -# Copyright:: Copyright (c) 2010-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. - -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# NOTE: This file is generated by running `rake version` in the top level of -# this repo. Do not edit this manually. Edit the VERSION file and run the rake -# task instead. -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -class Chef - CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '#{VERSION}' -end - -# -# NOTE: the Chef::Version class is defined in version_class.rb -# -# NOTE: DO NOT Use the Chef::Version class on Chef::VERSIONs. The -# Chef::Version class is for _cookbooks_ only, and cannot handle -# pre-release chef-client versions like "10.14.0.rc.2". Please -# use Rubygem's Gem::Version class instead. -# -VERSION_RB - version_rb_path = File.expand_path("../lib/chef/version.rb", __FILE__) - IO.write(version_rb_path, contents) -end - -Dir[File.expand_path("../*gemspec", __FILE__)].reverse.each do |gemspec_path| - gemspec = eval(IO.read(gemspec_path)) - Gem::PackageTask.new(gemspec).define -end - -def with_clean_env(&block) - if defined?(Bundler) - Bundler.with_clean_env(&block) - else - block.call - end -end - -desc "Build and install a chef gem" -task :install => [:package] do - with_clean_env do - sh %{gem install pkg/#{GEM_NAME}-#{VERSION}.gem --no-rdoc --no-ri} - end -end - -task :uninstall do - sh %{gem uninstall #{GEM_NAME} -x -v #{VERSION} } -end - -desc "Build it, tag it and ship it" -task :ship => [:clobber_package, :gem] do - sh("git tag #{VERSION}") - sh("git push opscode --tags") - Dir[File.expand_path("../pkg/*.gem", __FILE__)].reverse.each do |built_gem| - sh("gem push #{built_gem}") - end +ChefConfig::PackageTask.new(File.expand_path('..', __FILE__), 'Chef') do |package| + package.component_paths = ['chef-config'] + package.generate_version_class = true end task :pedant do @@ -1 +1 @@ -12.4.0.rc.2 +12.5.0.current.0 diff --git a/appveyor.yml b/appveyor.yml index 5609648cb1..06448e2be2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,7 +14,7 @@ skip_tags: true branches: only: - master - - 12-stable + - 12.4-stable cache: - C:\Ruby200\lib\ruby\gems\2.0.0 diff --git a/bin/chef-service-manager b/bin/chef-service-manager index 7cef10f506..7c031f70d4 100755 --- a/bin/chef-service-manager +++ b/bin/chef-service-manager @@ -28,10 +28,11 @@ if Chef::Platform.windows? :service_name => "chef-client", :service_display_name => "Chef Client Service", :service_description => "Runs Chef Client on regular, configurable intervals.", - :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../../../bin/chef-windows-service')) + :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../../../bin/chef-windows-service')), + :delayed_start => true, + :dependencies => ['Winmgmt'] } Chef::Application::WindowsServiceManager.new(chef_client_service).run else puts "chef-service-manager is only available on Windows platforms." end - diff --git a/chef-config/Rakefile b/chef-config/Rakefile index 10b6010de3..36e7e2572d 100644 --- a/chef-config/Rakefile +++ b/chef-config/Rakefile @@ -1,26 +1,8 @@ require 'rspec/core/rake_task' -require 'rubygems/package_task' +require 'chef-config/package_task' -VERSION = IO.read(File.expand_path("../../VERSION", __FILE__)).strip - -Dir[File.expand_path("../*gemspec", __FILE__)].reverse.each do |gemspec_path| - gemspec = eval(IO.read(gemspec_path)) - Gem::PackageTask.new(gemspec).define -end - -def with_clean_env(&block) - if defined?(Bundler) - Bundler.with_clean_env(&block) - else - block.call - end -end - -desc "Build and install a chef-config gem" -task :install => [:package] do - with_clean_env do - sh(%{gem install pkg/chef-config-#{ChefConfig::VERSION}.gem --no-rdoc --no-ri}, verbose: true) - end +ChefConfig::PackageTask.new(File.expand_path('..', __FILE__), 'ChefConfig') do |package| + package.module_path = 'chef-config' end task :default => :spec @@ -30,36 +12,3 @@ RSpec::Core::RakeTask.new(:spec) do |t| t.pattern = FileList['spec/**/*_spec.rb'] end -desc "Regenerate lib/chef/version.rb from VERSION file" -task :version do - contents = <<-VERSION_RB -# Copyright:: Copyright (c) 2010-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. - -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -# NOTE: This file is generated by running `rake version` in the top level of -# this repo. Do not edit this manually. Edit the VERSION file and run the rake -# task instead. -#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - -module ChefConfig - VERSION = '#{VERSION}' -end - -VERSION_RB - version_rb_path = File.expand_path("../lib/chef-config/version.rb", __FILE__) - IO.write(version_rb_path, contents) -end - diff --git a/chef-config/VERSION b/chef-config/VERSION new file mode 100644 index 0000000000..9a03cd310d --- /dev/null +++ b/chef-config/VERSION @@ -0,0 +1 @@ +12.5.0.current.0 diff --git a/chef-config/chef-config.gemspec b/chef-config/chef-config.gemspec index f0a569f3c1..475bd0f2d2 100644 --- a/chef-config/chef-config.gemspec +++ b/chef-config/chef-config.gemspec @@ -10,7 +10,7 @@ Gem::Specification.new do |spec| 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.homepage = "https://github.com/chef/chef" spec.license = "Apache-2.0" spec.require_paths = ["lib"] diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb index 63de8a451f..a3f06e9b23 100644 --- a/chef-config/lib/chef-config/config.rb +++ b/chef-config/lib/chef-config/config.rb @@ -128,7 +128,7 @@ module ChefConfig 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)} + chef_repo_path.uniq.map { |path| PathHelper.join(path, child_path)} end end @@ -739,6 +739,3 @@ module ChefConfig end end end - - - diff --git a/chef-config/lib/chef-config/exceptions.rb b/chef-config/lib/chef-config/exceptions.rb index f5d76d856b..1f80e505df 100644 --- a/chef-config/lib/chef-config/exceptions.rb +++ b/chef-config/lib/chef-config/exceptions.rb @@ -20,7 +20,7 @@ require 'chef-config/logger' module ChefConfig - class InvalidPath < StandardError - end + class ConfigurationError < ArgumentError; end + class InvalidPath < StandardError; end end diff --git a/chef-config/lib/chef-config/package_task.rb b/chef-config/lib/chef-config/package_task.rb new file mode 100644 index 0000000000..0aa063a2ff --- /dev/null +++ b/chef-config/lib/chef-config/package_task.rb @@ -0,0 +1,223 @@ +# +# Author:: Kartik Null Cating-Subramanian (<ksubramanian@chef.io>) +# Copyright:: Copyright (c) 2015 Chef, 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 'rake' +require 'rubygems' +require 'rubygems/package_task' + +module ChefConfig + class PackageTask < Rake::TaskLib + + # Full path to root of top-level repository. All other files (like VERSION or + # lib/<module_path>/version.rb are rooted at this path). + attr_accessor :root_path + + # Name of the top-level module/library build built. This is used to define + # the top level module which contains VERSION and MODULE_ROOT. + attr_accessor :module_name + + # Should the generated version.rb be in a class or module? Default is false (module). + attr_accessor :generate_version_class + + # Paths to the roots of any components that also support ChefPackageTask. + # If relative paths are provided, they are rooted against root_path. + attr_accessor :component_paths + + # This is the module name as it appears on the path "lib/module/". + # e.g. for module_name "ChefDK", you'd want module_path to be "chef-dk". + # The default is module_name but lower-cased. + attr_writer :module_path + + def module_path + @module_path || module_name.downcase + end + + # Path to a VERSION file with a single string that contains the package version. + # By default, this is root_path/VERSION + attr_accessor :version_file_path + + # Directory used to store package files and output that is generated. + # This has the same meaning (or lack thereof) as package_dir in + # rake/packagetask. + attr_accessor :package_dir + + # Name of git remote used to push tags during a release. Default is origin. + attr_accessor :git_remote + + def initialize(root_path=nil, module_name=nil) + init(root_path, module_name) + yield self if block_given? + define unless root_path.nil? || module_name.nil? + end + + def init(root_path, module_name) + @root_path = root_path + @module_name = module_name + @component_paths = [] + @module_path = nil + @version_file_path = 'VERSION' + @package_dir = 'pkg' + @git_remote = 'origin' + @generate_version_class = false + end + + def component_full_paths + component_paths.map { |path| File.expand_path(path, root_path)} + end + + def version_rb_path + File.expand_path("lib/#{module_path}/version.rb", root_path) + end + + def version + IO.read(File.expand_path(version_file_path, root_path)).strip + end + + def full_package_dir + File.expand_path(package_dir, root_path) + end + + def class_or_module + generate_version_class ? 'class' : 'module' + end + + def with_clean_env(&block) + if defined?(Bundler) + Bundler.with_clean_env(&block) + else + block.call + end + end + + def define + fail 'Need to provide package root and module name' if root_path.nil? || module_name.nil? + + desc 'Build Gems of component dependencies' + task :package_components do + component_full_paths.each do |component_path| + Dir.chdir(component_path) do + sh 'rake package' + end + end + end + + task :package => :package_components + + desc 'Build and install component dependencies' + task :install_components => :package_components do + component_full_paths.each do |component_path| + Dir.chdir(component_path) do + sh 'rake install' + end + end + end + + task :install => :install_components + + desc 'Clean up builds of component dependencies' + task :clobber_component_packages do + component_full_paths.each do |component_path| + Dir.chdir(component_path) do + sh 'rake clobber_package' + end + end + end + + task :clobber_package => :clobber_component_packages + + desc 'Update the version number for component dependencies' + task :update_components_versions do + component_full_paths.each do |component_path| + Dir.chdir(component_path) do + sh 'rake version' + end + end + end + + desc 'Regenerate lib/#{@module_path}/version.rb from VERSION file' + task :version => :update_components_versions do + contents = <<-VERSION_RB +# Copyright:: Copyright (c) 2010-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. + +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# NOTE: This file is generated by running `rake version` in the top level of +# this repo. Do not edit this manually. Edit the VERSION file and run the rake +# task instead. +#!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +#{class_or_module} #{module_name} + #{module_name.upcase}_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) + VERSION = '#{version}' +end + +# +# NOTE: the Chef::Version class is defined in version_class.rb +# +# NOTE: DO NOT Use the Chef::Version class on #{module_name}::VERSIONs. The +# Chef::Version class is for _cookbooks_ only, and cannot handle +# pre-release versions like "10.14.0.rc.2". Please use Rubygem's +# Gem::Version class instead. +# + VERSION_RB + IO.write(version_rb_path, contents) + end + + Dir[File.expand_path("*gemspec", root_path)].reverse.each do |gemspec_path| + gemspec = eval(IO.read(gemspec_path)) + Gem::PackageTask.new(gemspec) do |task| + task.package_dir = full_package_dir + end + end + + desc "Build and install a #{module_path} gem" + task :install => [:package] do + with_clean_env do + full_module_path = File.join(full_package_dir, module_path) + sh %{gem install #{full_module_path}-#{version}.gem --no-rdoc --no-ri} + end + end + + task :uninstall do + sh %{gem uninstall #{module_path} -x -v #{version} } + end + + desc 'Build it, tag it and ship it' + task :ship => [:clobber_package, :gem] do + sh("git tag #{version}") + sh("git push #{git_remote} --tags") + Dir[File.expand_path('*.gem', full_package_dir)].reverse.each do |built_gem| + sh("gem push #{built_gem}") + end + end + end + end + +end diff --git a/chef-config/lib/chef-config/path_helper.rb b/chef-config/lib/chef-config/path_helper.rb index acc6b76377..45f451479a 100644 --- a/chef-config/lib/chef-config/path_helper.rb +++ b/chef-config/lib/chef-config/path_helper.rb @@ -228,6 +228,37 @@ module ChefConfig joined_paths end end + + # Determine if the given path is protected by OS X System Integrity Protection. + def self.is_sip_path?(path, node) + if node['platform'] == 'mac_os_x' and Gem::Version.new(node['platform_version']) >= Gem::Version.new('10.11') + # todo: parse rootless.conf for this? + sip_paths= [ + '/System', '/bin', '/sbin', '/usr', + ] + sip_paths.each do |sip_path| + ChefConfig.logger.info("This is a SIP path, checking if it in exceptions list.") + return true if path.start_with?(sip_path) + end + false + else + false + end + end + # Determine if the given path is on the exception list for OS X System Integrity Protection. + def self.writable_sip_path?(path) + # todo: parse rootless.conf for this? + sip_exceptions = [ + '/System/Library/Caches', '/System/Library/Extensions', + '/System/Library/Speech', '/System/Library/User Template', + '/usr/libexec/cups', '/usr/local', '/usr/share/man' + ] + sip_exceptions.each do |exception_path| + return true if path.start_with?(exception_path) + end + ChefConfig.logger.error("Cannot write to a SIP Path on OS X 10.11+") + false + end end end diff --git a/chef-config/lib/chef-config/version.rb b/chef-config/lib/chef-config/version.rb index a6bf636540..9579f0638d 100644 --- a/chef-config/lib/chef-config/version.rb +++ b/chef-config/lib/chef-config/version.rb @@ -20,6 +20,15 @@ #!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! module ChefConfig - VERSION = '12.4.0.rc.2' + CHEFCONFIG_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) + VERSION = '12.5.0.current.0' end +# +# NOTE: the Chef::Version class is defined in version_class.rb +# +# NOTE: DO NOT Use the Chef::Version class on ChefConfig::VERSIONs. The +# Chef::Version class is for _cookbooks_ only, and cannot handle +# pre-release versions like "10.14.0.rc.2". Please use Rubygem's +# Gem::Version class instead. +# diff --git a/chef-config/lib/chef-config/workstation_config_loader.rb b/chef-config/lib/chef-config/workstation_config_loader.rb new file mode 100644 index 0000000000..177cd776d4 --- /dev/null +++ b/chef-config/lib/chef-config/workstation_config_loader.rb @@ -0,0 +1,179 @@ +# +# Author:: Daniel DeLeo (<dan@chef.io>) +# 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/config' +require 'chef-config/exceptions' +require 'chef-config/logger' +require 'chef-config/path_helper' +require 'chef-config/windows' + +module ChefConfig + class WorkstationConfigLoader + + # Path to a config file requested by user, (e.g., via command line option). Can be nil + attr_accessor :explicit_config_file + + # TODO: initialize this with a logger for Chef and Knife + def initialize(explicit_config_file, logger=nil) + @explicit_config_file = explicit_config_file + @chef_config_dir = nil + @config_location = nil + @logger = logger || NullLogger.new + end + + def no_config_found? + config_location.nil? + end + + def config_location + @config_location ||= (explicit_config_file || locate_local_config) + end + + def chef_config_dir + if @chef_config_dir.nil? + @chef_config_dir = false + full_path = working_directory.split(File::SEPARATOR) + (full_path.length - 1).downto(0) do |i| + candidate_directory = File.join(full_path[0..i] + [".chef"]) + if File.exist?(candidate_directory) && File.directory?(candidate_directory) + @chef_config_dir = candidate_directory + break + end + end + end + @chef_config_dir + end + + def load + # Ignore it if there's no explicit_config_file and can't find one at a + # default path. + return false if config_location.nil? + + if explicit_config_file && !path_exists?(config_location) + raise ChefConfig::ConfigurationError, "Specified config file #{config_location} does not exist" + end + + # Have to set Config.config_file b/c other config is derived from it. + Config.config_file = config_location + read_config(IO.read(config_location), config_location) + end + + # (Private API, public for test purposes) + def env + ENV + end + + # (Private API, public for test purposes) + def path_exists?(path) + Pathname.new(path).expand_path.exist? + end + + private + + def have_config?(path) + if path_exists?(path) + logger.info("Using config at #{path}") + true + else + logger.debug("Config not found at #{path}, trying next option") + false + end + end + + def locate_local_config + candidate_configs = [] + + # Look for $KNIFE_HOME/knife.rb (allow multiple knives config on same machine) + if env['KNIFE_HOME'] + candidate_configs << File.join(env['KNIFE_HOME'], 'config.rb') + candidate_configs << File.join(env['KNIFE_HOME'], 'knife.rb') + end + # Look for $PWD/knife.rb + if Dir.pwd + candidate_configs << File.join(Dir.pwd, 'config.rb') + candidate_configs << File.join(Dir.pwd, 'knife.rb') + end + # Look for $UPWARD/.chef/knife.rb + if chef_config_dir + candidate_configs << File.join(chef_config_dir, 'config.rb') + candidate_configs << File.join(chef_config_dir, 'knife.rb') + end + # Look for $HOME/.chef/knife.rb + PathHelper.home('.chef') do |dot_chef_dir| + candidate_configs << File.join(dot_chef_dir, 'config.rb') + candidate_configs << File.join(dot_chef_dir, 'knife.rb') + end + + candidate_configs.find do | candidate_config | + have_config?(candidate_config) + end + end + + def working_directory + a = if ChefConfig.windows? + env['CD'] + else + env['PWD'] + end || Dir.pwd + + a + end + + def read_config(config_content, config_file_path) + Config.from_string(config_content, config_file_path) + rescue SignalException + raise + rescue SyntaxError => e + message = "" + message << "You have invalid ruby syntax in your config file #{config_file_path}\n\n" + message << "#{e.class.name}: #{e.message}\n" + if file_line = e.message[/#{Regexp.escape(config_file_path)}:[\d]+/] + line = file_line[/:([\d]+)$/, 1].to_i + message << highlight_config_error(config_file_path, line) + end + raise ChefConfig::ConfigurationError, message + rescue Exception => e + message = "You have an error in your config file #{config_file_path}\n\n" + message << "#{e.class.name}: #{e.message}\n" + filtered_trace = e.backtrace.grep(/#{Regexp.escape(config_file_path)}/) + filtered_trace.each {|bt_line| message << " " << bt_line << "\n" } + if !filtered_trace.empty? + line_nr = filtered_trace.first[/#{Regexp.escape(config_file_path)}:([\d]+)/, 1] + message << highlight_config_error(config_file_path, line_nr.to_i) + end + raise ChefConfig::ConfigurationError, message + end + + + def highlight_config_error(file, line) + config_file_lines = [] + IO.readlines(file).each_with_index {|l, i| config_file_lines << "#{(i + 1).to_s.rjust(3)}: #{l.chomp}"} + if line == 1 + lines = config_file_lines[0..3] + else + lines = config_file_lines[Range.new(line - 2, line)] + end + "Relevant file content:\n" + lines.join("\n") + "\n" + end + + def logger + @logger + end + + end +end diff --git a/spec/unit/workstation_config_loader_spec.rb b/chef-config/spec/unit/workstation_config_loader_spec.rb index 72631f3dfa..9f24a4f11b 100644 --- a/spec/unit/workstation_config_loader_spec.rb +++ b/chef-config/spec/unit/workstation_config_loader_spec.rb @@ -18,9 +18,12 @@ require 'spec_helper' require 'tempfile' -require 'chef/workstation_config_loader' -describe Chef::WorkstationConfigLoader do +require 'chef-config/exceptions' +require 'chef-config/windows' +require 'chef-config/workstation_config_loader' + +RSpec.describe ChefConfig::WorkstationConfigLoader do let(:explicit_config_location) { nil } @@ -65,7 +68,7 @@ describe Chef::WorkstationConfigLoader do let(:home) { "/Users/example.user" } before do - allow(Chef::Util::PathHelper).to receive(:home).with('.chef').and_yield(File.join(home, '.chef')) + allow(ChefConfig::PathHelper).to receive(:home).with('.chef').and_yield(File.join(home, '.chef')) allow(config_loader).to receive(:path_exists?).with("#{home}/.chef/knife.rb").and_return(true) end @@ -88,7 +91,7 @@ describe Chef::WorkstationConfigLoader do let(:env_pwd) { "/path/to/cwd" } before do - if Chef::Platform.windows? + if ChefConfig.windows? env["CD"] = env_pwd else env["PWD"] = env_pwd @@ -224,7 +227,7 @@ describe Chef::WorkstationConfigLoader do let(:explicit_config_location) { "/nope/nope/nope/frab/jab/nab" } it "raises a configuration error" do - expect { config_loader.load }.to raise_error(Chef::Exceptions::ConfigurationError) + expect { config_loader.load }.to raise_error(ChefConfig::ConfigurationError) end end @@ -233,15 +236,20 @@ describe Chef::WorkstationConfigLoader do let(:config_content) { "" } + # We need to keep a reference to the tempfile because while #close does + # not unlink the file, the object being GC'd will. + let(:tempfile) do + Tempfile.new("Chef-WorkstationConfigLoader-rspec-test").tap do |t| + t.print(config_content) + t.close + end + end + let(:explicit_config_location) do - # could use described_class, but remove all ':' from the path if so. - t = Tempfile.new("Chef-WorkstationConfigLoader-rspec-test") - t.print(config_content) - t.close - t.path + tempfile.path end - after { File.unlink(explicit_config_location) if File.exists?(explicit_config_location) } + after { File.unlink(explicit_config_location) if File.exist?(explicit_config_location) } context "and is valid" do @@ -249,12 +257,12 @@ describe Chef::WorkstationConfigLoader do it "loads the config" do expect(config_loader.load).to be(true) - expect(Chef::Config.config_file_evaluated).to be(true) + expect(ChefConfig::Config.config_file_evaluated).to be(true) end - it "sets Chef::Config.config_file" do + it "sets ChefConfig::Config.config_file" do config_loader.load - expect(Chef::Config.config_file).to eq(explicit_config_location) + expect(ChefConfig::Config.config_file).to eq(explicit_config_location) end end @@ -263,7 +271,7 @@ describe Chef::WorkstationConfigLoader do let(:config_content) { "{{{{{:{{" } it "raises a ConfigurationError" do - expect { config_loader.load }.to raise_error(Chef::Exceptions::ConfigurationError) + expect { config_loader.load }.to raise_error(ChefConfig::ConfigurationError) end end @@ -272,7 +280,7 @@ describe Chef::WorkstationConfigLoader do let(:config_content) { ":foo\n:bar\nraise 'oops'\n:baz\n" } it "raises a ConfigurationError" do - expect { config_loader.load }.to raise_error(Chef::Exceptions::ConfigurationError) + expect { config_loader.load }.to raise_error(ChefConfig::ConfigurationError) end end diff --git a/chef-windows.gemspec b/chef-windows.gemspec index 167358c7d7..428174889f 100644 --- a/chef-windows.gemspec +++ b/chef-windows.gemspec @@ -10,7 +10,7 @@ gemspec.add_dependency "win32-eventlog", "~> 0.6.2" gemspec.add_dependency "win32-mmap", "~> 0.4.1" gemspec.add_dependency "win32-mutex", "~> 0.4.2" gemspec.add_dependency "win32-process", "~> 0.7.5" -gemspec.add_dependency "win32-service", "0.8.6" +gemspec.add_dependency "win32-service", "~> 0.8.7" gemspec.add_dependency "windows-api", "~> 0.4.4" gemspec.add_dependency "windows-pr", "~> 1.2.4" gemspec.add_dependency "wmi-lite", "~> 1.0" diff --git a/chef.gemspec b/chef.gemspec index f4f8a31207..f28cde21e7 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -20,13 +20,12 @@ Gem::Specification.new do |s| s.add_dependency "mixlib-cli", "~> 1.4" s.add_dependency "mixlib-log", "~> 1.3" s.add_dependency "mixlib-authentication", "~> 1.3" - s.add_dependency "mixlib-shellout", ">= 2.0.0.rc.0", "< 3.0" - s.add_dependency "ohai", "~> 8.0" + s.add_dependency "mixlib-shellout", "~> 2.0" + s.add_dependency "ohai", ">= 8.6.0.alpha.1", "< 9" s.add_dependency "ffi-yajl", "~> 2.2" s.add_dependency "net-ssh", "~> 2.6" s.add_dependency "net-ssh-multi", "~> 1.1" - # CHEF-3027: The knife-cloud plugins require newer features from highline, core chef should not. s.add_dependency "highline", "~> 1.6", ">= 1.6.9" s.add_dependency "erubis", "~> 2.7" s.add_dependency "diff-lcs", "~> 1.2", ">= 1.2.4" diff --git a/kitchen-tests/Gemfile b/kitchen-tests/Gemfile index 988d876417..5e1907ba1f 100644 --- a/kitchen-tests/Gemfile +++ b/kitchen-tests/Gemfile @@ -6,4 +6,5 @@ group :end_to_end do gem 'kitchen-appbundle-updater', '~> 0.0.1' gem "kitchen-vagrant", '~> 0.17.0' gem 'kitchen-ec2', github: 'test-kitchen/kitchen-ec2' + gem 'vagrant-wrapper' end diff --git a/lib/chef.rb b/lib/chef.rb index 6bce976439..1a0b802adb 100644 --- a/lib/chef.rb +++ b/lib/chef.rb @@ -31,5 +31,5 @@ require 'chef/daemon' require 'chef/run_status' require 'chef/handler' require 'chef/handler/json_file' - +require 'chef/event_dispatch/dsl' require 'chef/chef_class' diff --git a/lib/chef/api_client.rb b/lib/chef/api_client.rb index ad31fb7d7b..b7b9f7dc43 100644 --- a/lib/chef/api_client.rb +++ b/lib/chef/api_client.rb @@ -1,7 +1,7 @@ # -# Author:: Adam Jacob (<adam@chef.io>) -# Author:: Nuo Yan (<nuo@chef.io>) -# Copyright:: Copyright (c) 2008, 2015 Chef Software, Inc. +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Nuo Yan (<nuo@opscode.com>) +# Copyright:: Copyright (c) 2008 Opscode, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,18 +23,18 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' -require 'chef/exceptions' -require 'chef/mixin/api_version_request_handling' require 'chef/server_api' +# DEPRECATION NOTE +# +# This code will be removed in Chef 13 in favor of the code in Chef::ApiClientV1, +# which will be moved to this namespace. New development should occur in +# Chef::ApiClientV1 until the time before Chef 13. class Chef class ApiClient include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate - include Chef::Mixin::ApiVersionRequestHandling - - SUPPORTED_API_VERSIONS = [0,1] # Create a new Chef::ApiClient object. def initialize @@ -43,25 +43,6 @@ class Chef @private_key = nil @admin = false @validator = false - @create_key = nil - end - - def chef_rest_v0 - @chef_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) - end - - def chef_rest_v1 - @chef_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "1"}) - end - - # will default to the current version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) - def http_api - @http_api ||= Chef::REST.new(Chef::Config[:chef_server_url]) - end - - # will default to the current version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) - def self.http_api - Chef::REST.new(Chef::Config[:chef_server_url]) end # Gets or sets the client name. @@ -113,8 +94,7 @@ class Chef ) end - # Private key. The server will return it as a string. - # Set to true under API V0 to have the server regenerate the default key. + # Gets or sets the private key. # # @params [Optional String] The string representation of the private key. # @return [String] The current value. @@ -122,19 +102,7 @@ class Chef set_or_return( :private_key, arg, - :kind_of => [String, TrueClass, FalseClass] - ) - end - - # Used to ask server to generate key pair under api V1 - # - # @params [Optional True/False] Should be true or false - default is false. - # @return [True/False] The current value - def create_key(arg=nil) - set_or_return( - :create_key, - arg, - :kind_of => [ TrueClass, FalseClass ] + :kind_of => [String, FalseClass] ) end @@ -145,14 +113,13 @@ class Chef def to_hash result = { "name" => @name, + "public_key" => @public_key, "validator" => @validator, "admin" => @admin, 'json_class' => self.class.name, "chef_type" => "client" } - result["private_key"] = @private_key unless @private_key.nil? - result["public_key"] = @public_key unless @public_key.nil? - result["create_key"] = @create_key unless @create_key.nil? + result["private_key"] = @private_key if @private_key result end @@ -166,11 +133,10 @@ class Chef def self.from_hash(o) client = Chef::ApiClient.new client.name(o["name"] || o["clientname"]) + client.private_key(o["private_key"]) if o.key?("private_key") + client.public_key(o["public_key"]) client.admin(o["admin"]) client.validator(o["validator"]) - client.private_key(o["private_key"]) if o.key?("private_key") - client.public_key(o["public_key"]) if o.key?("public_key") - client.create_key(o["create_key"]) if o.key?("create_key") client end @@ -182,6 +148,10 @@ class Chef from_hash(Chef::JSONCompat.parse(j)) end + def self.http_api + Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) + end + def self.reregister(name) api_client = load(name) api_client.reregister @@ -218,11 +188,11 @@ class Chef # Save this client via the REST API, returns a hash including the private key def save begin - update + http_api.put("clients/#{name}", { :name => self.name, :admin => self.admin, :validator => self.validator}) rescue Net::HTTPServerException => e # If that fails, go ahead and try and update it if e.response.code == "404" - create + http_api.post("clients", {:name => self.name, :admin => self.admin, :validator => self.validator }) else raise e end @@ -230,95 +200,18 @@ class Chef end def reregister - # Try API V0 and if it fails due to V0 not being supported, raise the proper error message. - # reregister only supported in API V0 or lesser. - reregistered_self = chef_rest_v0.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) + reregistered_self = http_api.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) if reregistered_self.respond_to?(:[]) private_key(reregistered_self["private_key"]) else private_key(reregistered_self.private_key) end self - rescue Net::HTTPServerException => e - # if there was a 406 related to versioning, give error explaining that - # only API version 0 is supported for reregister command - if e.response.code == "406" && e.response["x-ops-server-api-version"] - version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) - min_version = version_header["min_version"] - max_version = version_header["max_version"] - error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) - raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) - else - raise e - end - end - - # Updates the client via the REST API - def update - # NOTE: API V1 dropped support for updating client keys via update (aka PUT), - # but this code never supported key updating in the first place. Since - # it was never implemented, we will simply ignore that functionality - # as it is being deprecated. - # Delete this comment after V0 support is dropped. - payload = { :name => name } - payload[:validator] = validator unless validator.nil? - - # DEPRECATION - # This field is ignored in API V1, but left for backwards-compat, - # can remove after API V0 is no longer supported. - payload[:admin] = admin unless admin.nil? - - begin - new_client = chef_rest_v1.put("clients/#{name}", payload) - rescue Net::HTTPServerException => e - # rescue API V0 if 406 and the server supports V0 - supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) - raise e unless supported_versions && supported_versions.include?(0) - new_client = chef_rest_v0.put("clients/#{name}", payload) - end - - new_client end # Create the client via the REST API def create - payload = { - :name => name, - :validator => validator, - # this field is ignored in API V1, but left for backwards-compat, - # can remove after OSC 11 support is finished? - :admin => admin - } - begin - # try API V1 - raise Chef::Exceptions::InvalidClientAttribute, "You cannot set both public_key and create_key for create." if !create_key.nil? && !public_key.nil? - - payload[:public_key] = public_key unless public_key.nil? - payload[:create_key] = create_key unless create_key.nil? - - new_client = chef_rest_v1.post("clients", payload) - - # get the private_key out of the chef_key hash if it exists - if new_client['chef_key'] - if new_client['chef_key']['private_key'] - new_client['private_key'] = new_client['chef_key']['private_key'] - end - new_client['public_key'] = new_client['chef_key']['public_key'] - new_client.delete('chef_key') - end - - rescue Net::HTTPServerException => e - # rescue API V0 if 406 and the server supports V0 - supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) - raise e unless supported_versions && supported_versions.include?(0) - - # under API V0, a key pair will always be created unless public_key is - # passed on initial POST - payload[:public_key] = public_key unless public_key.nil? - - new_client = chef_rest_v0.post("clients", payload) - end - Chef::ApiClient.from_hash(self.to_hash.merge(new_client)) + http_api.post("clients", self) end # As a string @@ -326,5 +219,14 @@ class Chef "client[#{@name}]" end + def inspect + "Chef::ApiClient name:'#{name}' admin:'#{admin.inspect}' validator:'#{validator}' " + + "public_key:'#{public_key}' private_key:'#{private_key}'" + end + + def http_api + @http_api ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) + end + end end diff --git a/lib/chef/api_client_v1.rb b/lib/chef/api_client_v1.rb new file mode 100644 index 0000000000..80f0d2517c --- /dev/null +++ b/lib/chef/api_client_v1.rb @@ -0,0 +1,325 @@ +# +# Author:: Adam Jacob (<adam@chef.io>) +# Author:: Nuo Yan (<nuo@chef.io>) +# Copyright:: Copyright (c) 2008, 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' +require 'chef/mixin/params_validate' +require 'chef/mixin/from_file' +require 'chef/mash' +require 'chef/json_compat' +require 'chef/search/query' +require 'chef/exceptions' +require 'chef/mixin/api_version_request_handling' +require 'chef/server_api' +require 'chef/api_client' + +# COMPATIBILITY NOTE +# +# This ApiClientV1 code attempts to make API V1 requests and falls back to +# API V0 requests when it fails. New development should occur here instead +# of Chef::ApiClient as this will replace that namespace when Chef 13 is released. +# +# If you need to default to API V0 behavior (i.e. you need GET client to return +# a public key, etc), please use Chef::ApiClient and update your code to support +# API V1 before you pull in Chef 13. +class Chef + class ApiClientV1 + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] + + # Create a new Chef::ApiClientV1 object. + def initialize + @name = '' + @public_key = nil + @private_key = nil + @admin = false + @validator = false + @create_key = nil + end + + def chef_rest_v0 + @chef_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0", :inflate_json_class => false}) + end + + def chef_rest_v1 + @chef_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "1", :inflate_json_class => false}) + end + + def self.http_api + Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "1", :inflate_json_class => false}) + end + + # Gets or sets the client name. + # + # @params [Optional String] The name must be alpha-numeric plus - and _. + # @return [String] The current value of the name. + def name(arg=nil) + set_or_return( + :name, + arg, + :regex => /^[\-[:alnum:]_\.]+$/ + ) + end + + # Gets or sets whether this client is an admin. + # + # @params [Optional True/False] Should be true or false - default is false. + # @return [True/False] The current value + def admin(arg=nil) + set_or_return( + :admin, + arg, + :kind_of => [ TrueClass, FalseClass ] + ) + end + + # Gets or sets the public key. + # + # @params [Optional String] The string representation of the public key. + # @return [String] The current value. + def public_key(arg=nil) + set_or_return( + :public_key, + arg, + :kind_of => String + ) + end + + # Gets or sets whether this client is a validator. + # + # @params [Boolean] whether or not the client is a validator. If + # `nil`, retrieves the already-set value. + # @return [Boolean] The current value + def validator(arg=nil) + set_or_return( + :validator, + arg, + :kind_of => [TrueClass, FalseClass] + ) + end + + # Private key. The server will return it as a string. + # Set to true under API V0 to have the server regenerate the default key. + # + # @params [Optional String] The string representation of the private key. + # @return [String] The current value. + def private_key(arg=nil) + set_or_return( + :private_key, + arg, + :kind_of => [String, TrueClass, FalseClass] + ) + end + + # Used to ask server to generate key pair under api V1 + # + # @params [Optional True/False] Should be true or false - default is false. + # @return [True/False] The current value + def create_key(arg=nil) + set_or_return( + :create_key, + arg, + :kind_of => [ TrueClass, FalseClass ] + ) + end + + # The hash representation of the object. Includes the name and public_key. + # Private key is included if available. + # + # @return [Hash] + def to_hash + result = { + "name" => @name, + "validator" => @validator, + "admin" => @admin, + "chef_type" => "client" + } + result["private_key"] = @private_key unless @private_key.nil? + result["public_key"] = @public_key unless @public_key.nil? + result["create_key"] = @create_key unless @create_key.nil? + result + end + + # The JSON representation of the object. + # + # @return [String] the JSON string. + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def self.from_hash(o) + client = Chef::ApiClientV1.new + client.name(o["name"] || o["clientname"]) + client.admin(o["admin"]) + client.validator(o["validator"]) + client.private_key(o["private_key"]) if o.key?("private_key") + client.public_key(o["public_key"]) if o.key?("public_key") + client.create_key(o["create_key"]) if o.key?("create_key") + client + end + + def self.from_json(j) + Chef::ApiClientV1.from_hash(Chef::JSONCompat.from_json(j)) + end + + def self.reregister(name) + api_client = Chef::ApiClientV1.load(name) + api_client.reregister + end + + def self.list(inflate=false) + if inflate + response = Hash.new + Chef::Search::Query.new.search(:client) do |n| + n = self.from_hash(n) if n.instance_of?(Hash) + response[n.name] = n + end + response + else + http_api.get("clients") + end + end + + # Load a client by name via the API + def self.load(name) + response = http_api.get("clients/#{name}") + Chef::ApiClientV1.from_hash(response) + end + + # Remove this client via the REST API + def destroy + chef_rest_v1.delete("clients/#{@name}") + end + + # Save this client via the REST API, returns a hash including the private key + def save + begin + update + rescue Net::HTTPServerException => e + # If that fails, go ahead and try and update it + if e.response.code == "404" + create + else + raise e + end + end + end + + def reregister + # Try API V0 and if it fails due to V0 not being supported, raise the proper error message. + # reregister only supported in API V0 or lesser. + reregistered_self = chef_rest_v0.put("clients/#{name}", { :name => name, :admin => admin, :validator => validator, :private_key => true }) + if reregistered_self.respond_to?(:[]) + private_key(reregistered_self["private_key"]) + else + private_key(reregistered_self.private_key) + end + self + rescue Net::HTTPServerException => e + # if there was a 406 related to versioning, give error explaining that + # only API version 0 is supported for reregister command + if e.response.code == "406" && e.response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) + min_version = version_header["min_version"] + max_version = version_header["max_version"] + error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) + raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) + else + raise e + end + end + + # Updates the client via the REST API + def update + # NOTE: API V1 dropped support for updating client keys via update (aka PUT), + # but this code never supported key updating in the first place. Since + # it was never implemented, we will simply ignore that functionality + # as it is being deprecated. + # Delete this comment after V0 support is dropped. + payload = { :name => name } + payload[:validator] = validator unless validator.nil? + + # DEPRECATION + # This field is ignored in API V1, but left for backwards-compat, + # can remove after API V0 is no longer supported. + payload[:admin] = admin unless admin.nil? + + begin + new_client = chef_rest_v1.put("clients/#{name}", payload) + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + new_client = chef_rest_v0.put("clients/#{name}", payload) + end + + Chef::ApiClientV1.from_hash(new_client) + end + + # Create the client via the REST API + def create + payload = { + :name => name, + :validator => validator, + # this field is ignored in API V1, but left for backwards-compat, + # can remove after OSC 11 support is finished? + :admin => admin + } + begin + # try API V1 + raise Chef::Exceptions::InvalidClientAttribute, "You cannot set both public_key and create_key for create." if !create_key.nil? && !public_key.nil? + + payload[:public_key] = public_key unless public_key.nil? + payload[:create_key] = create_key unless create_key.nil? + + new_client = chef_rest_v1.post("clients", payload) + + # get the private_key out of the chef_key hash if it exists + if new_client['chef_key'] + if new_client['chef_key']['private_key'] + new_client['private_key'] = new_client['chef_key']['private_key'] + end + new_client['public_key'] = new_client['chef_key']['public_key'] + new_client.delete('chef_key') + end + + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + + # under API V0, a key pair will always be created unless public_key is + # passed on initial POST + payload[:public_key] = public_key unless public_key.nil? + + new_client = chef_rest_v0.post("clients", payload) + end + Chef::ApiClientV1.from_hash(self.to_hash.merge(new_client)) + end + + # As a string + def to_s + "client[#{@name}]" + end + + end +end diff --git a/lib/chef/application.rb b/lib/chef/application.rb index 0563822ede..970544c068 100644 --- a/lib/chef/application.rb +++ b/lib/chef/application.rb @@ -382,7 +382,7 @@ class Chef def emit_warnings if Chef::Config[:chef_gem_compile_time] - Chef::Log.deprecation "setting chef_gem_compile_time to true is deprecated" + Chef.log_deprecation "setting chef_gem_compile_time to true is deprecated" end end diff --git a/lib/chef/application/apply.rb b/lib/chef/application/apply.rb index e9768b218c..243b441119 100644 --- a/lib/chef/application/apply.rb +++ b/lib/chef/application/apply.rb @@ -49,6 +49,24 @@ class Chef::Application::Apply < Chef::Application :description => "Load attributes from a JSON file or URL", :proc => nil + option :force_logger, + :long => "--force-logger", + :description => "Use logger output instead of formatter output", + :boolean => true, + :default => false + + option :force_formatter, + :long => "--force-formatter", + :description => "Use formatter output instead of logger output", + :boolean => true, + :default => false + + option :formatter, + :short => "-F FORMATTER", + :long => "--format FORMATTER", + :description => "output format to use", + :proc => lambda { |format| Chef::Config.add_formatter(format) } + option :log_level, :short => "-l LEVEL", :long => "--log_level LEVEL", diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index 409680b553..73eda81343 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -449,9 +449,9 @@ class Chef::Application::Client < Chef::Application end def audit_mode_settings_explanation - "\n* To enable audit mode after converge, use command line option `--audit-mode enabled` or set `:audit_mode = :enabled` in your config file." + - "\n* To disable audit mode, use command line option `--audit-mode disabled` or set `:audit_mode = :disabled` in your config file." + - "\n* To only run audit mode, use command line option `--audit-mode audit-only` or set `:audit_mode = :audit_only` in your config file." + + "\n* To enable audit mode after converge, use command line option `--audit-mode enabled` or set `audit_mode :enabled` in your config file." + + "\n* To disable audit mode, use command line option `--audit-mode disabled` or set `audit_mode :disabled` in your config file." + + "\n* To only run audit mode, use command line option `--audit-mode audit-only` or set `audit_mode :audit_only` in your config file." + "\nAudit mode is disabled by default." end diff --git a/lib/chef/application/solo.rb b/lib/chef/application/solo.rb index dd09d65b42..5bb2a1ceb0 100644 --- a/lib/chef/application/solo.rb +++ b/lib/chef/application/solo.rb @@ -214,7 +214,7 @@ class Chef::Application::Solo < Chef::Application FileUtils.mkdir_p(recipes_path) tarball_path = File.join(recipes_path, 'recipes.tgz') fetch_recipe_tarball(Chef::Config[:recipe_url], tarball_path) - Chef::Mixin::Command.run_command(:command => "tar zxvf #{tarball_path} -C #{recipes_path}") + Mixlib::ShellOut.new("tar zxvf #{tarball_path} -C #{recipes_path}").run_command end # json_attribs shuld be fetched after recipe_url tarball is unpacked. diff --git a/lib/chef/application/windows_service_manager.rb b/lib/chef/application/windows_service_manager.rb index de8ed657c2..44526c1720 100644 --- a/lib/chef/application/windows_service_manager.rb +++ b/lib/chef/application/windows_service_manager.rb @@ -78,7 +78,7 @@ class Chef raise ArgumentError, "Service definition is not provided" if service_options.nil? - required_options = [:service_name, :service_display_name, :service_name, :service_description, :service_file_path] + required_options = [:service_name, :service_display_name, :service_description, :service_file_path] required_options.each do |req_option| if !service_options.has_key?(req_option) @@ -92,6 +92,8 @@ class Chef @service_file_path = service_options[:service_file_path] @service_start_name = service_options[:run_as_user] @password = service_options[:run_as_password] + @delayed_start = service_options[:delayed_start] + @dependencies = service_options[:dependencies] end def run(params = ARGV) @@ -113,17 +115,22 @@ class Chef cmd = "\"#{ruby}\" \"#{@service_file_path}\" #{opts}".gsub(File::SEPARATOR, File::ALT_SEPARATOR) ::Win32::Service.new( - :service_name => @service_name, - :display_name => @service_display_name, - :description => @service_description, - # Prior to 0.8.5, win32-service creates interactive services by default, - # and we don't want that, so we need to override the service type. - :service_type => ::Win32::Service::SERVICE_WIN32_OWN_PROCESS, - :start_type => ::Win32::Service::SERVICE_AUTO_START, - :binary_path_name => cmd, - :service_start_name => @service_start_name, - :password => @password, - ) + :service_name => @service_name, + :display_name => @service_display_name, + :description => @service_description, + # Prior to 0.8.5, win32-service creates interactive services by default, + # and we don't want that, so we need to override the service type. + :service_type => ::Win32::Service::SERVICE_WIN32_OWN_PROCESS, + :start_type => ::Win32::Service::SERVICE_AUTO_START, + :binary_path_name => cmd, + :service_start_name => @service_start_name, + :password => @password, + :dependencies => @dependencies + ) + ::Win32::Service.configure( + :service_name => @service_name, + :delayed_start => @delayed_start + ) unless @delayed_start.nil? puts "Service '#{@service_name}' has successfully been installed." end when 'status' diff --git a/lib/chef/chef_class.rb b/lib/chef/chef_class.rb index f1dd797c04..c2cb9e2b24 100644 --- a/lib/chef/chef_class.rb +++ b/lib/chef/chef_class.rb @@ -28,6 +28,8 @@ require 'chef/platform/provider_priority_map' require 'chef/platform/resource_priority_map' +require 'chef/platform/provider_handler_map' +require 'chef/platform/resource_handler_map' class Chef class << self @@ -50,7 +52,14 @@ class Chef # attr_reader :run_context + # Register an event handler with user specified block # + # @return[Chef::EventDispatch::Base] handler object + def event_handler(&block) + dsl = Chef::EventDispatch::DSL.new('Chef client DSL') + dsl.instance_eval(&block) + end + # Get the array of providers associated with a resource_name for the current node # # @param resource_name [Symbol] name of the resource as a symbol @@ -58,7 +67,7 @@ class Chef # @return [Array<Class>] Priority Array of Provider Classes to use for the resource_name on the node # def get_provider_priority_array(resource_name) - result = provider_priority_map.get_priority_array(node, resource_name) + result = provider_priority_map.get_priority_array(node, resource_name.to_sym) result = result.dup if result result end @@ -71,7 +80,7 @@ class Chef # @return [Array<Class>] Priority Array of Resource Classes to use for the resource_name on the node # def get_resource_priority_array(resource_name) - result = resource_priority_map.get_priority_array(node, resource_name) + result = resource_priority_map.get_priority_array(node, resource_name.to_sym) result = result.dup if result result end @@ -86,7 +95,7 @@ class Chef # @return [Array<Class>] Modified Priority Array of Provider Classes to use for the resource_name on the node # def set_provider_priority_array(resource_name, priority_array, *filter, &block) - result = provider_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = provider_priority_map.set_priority_array(resource_name.to_sym, priority_array, *filter, &block) result = result.dup if result result end @@ -101,7 +110,7 @@ class Chef # @return [Array<Class>] Modified Priority Array of Resource Classes to use for the resource_name on the node # def set_resource_priority_array(resource_name, priority_array, *filter, &block) - result = resource_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = resource_priority_map.set_priority_array(resource_name.to_sym, priority_array, *filter, &block) result = result.dup if result result end @@ -160,19 +169,48 @@ class Chef @node = nil @provider_priority_map = nil @resource_priority_map = nil + @provider_handler_map = nil + @resource_handler_map = nil end # @api private def provider_priority_map - @provider_priority_map ||= begin - # these slurp in the resource+provider world, so be exceedingly lazy about requiring them - Chef::Platform::ProviderPriorityMap.instance - end + # these slurp in the resource+provider world, so be exceedingly lazy about requiring them + @provider_priority_map ||= Chef::Platform::ProviderPriorityMap.instance end # @api private def resource_priority_map - @resource_priority_map ||= begin - Chef::Platform::ResourcePriorityMap.instance + @resource_priority_map ||= Chef::Platform::ResourcePriorityMap.instance + end + # @api private + def provider_handler_map + @provider_handler_map ||= Chef::Platform::ProviderHandlerMap.instance + end + # @api private + def resource_handler_map + @resource_handler_map ||= Chef::Platform::ResourceHandlerMap.instance + end + + # + # Emit a deprecation message. + # + # @param message The message to send. + # @param location The location. Defaults to the caller who called you (since + # generally the person who triggered the check is the one that needs to be + # fixed). + # + # @example + # Chef.deprecation("Deprecated!") + # + # @api private this will likely be removed in favor of an as-yet unwritten + # `Chef.log` + def log_deprecation(message, location=caller(2..2)[0]) + # `run_context.events` is the primary deprecation target if we're in a + # run. If we are not yet in a run, print to `Chef::Log`. + if run_context && run_context.events + run_context.events.deprecation(message, location) + else + Chef::Log.deprecation(message, location) end end end diff --git a/lib/chef/chef_fs/config.rb b/lib/chef/chef_fs/config.rb index 6666a3deee..40cbb36530 100644 --- a/lib/chef/chef_fs/config.rb +++ b/lib/chef/chef_fs/config.rb @@ -111,7 +111,7 @@ class Chef # def initialize(chef_config = Chef::Config, cwd = Dir.pwd, options = {}, ui = nil) @chef_config = chef_config - @cwd = cwd + @cwd = File.expand_path(cwd) @cookbook_version = options[:cookbook_version] if @chef_config[:repo_mode] == 'everything' && is_hosted? && !ui.nil? @@ -166,34 +166,37 @@ class Chef # server_path('/home/jkeiser/chef_repo/cookbooks/blah') == '/cookbooks/blah' # server_path('/home/*/chef_repo/cookbooks/blah') == nil # - # If there are multiple paths (cookbooks, roles, data bags, etc. can all - # have separate paths), and cwd+the path reaches into one of them, we will - # return a path relative to that. Otherwise we will return a path to - # chef_repo. + # If there are multiple different, manually specified paths to object locations + # (cookbooks, roles, data bags, etc. can all have separate paths), and cwd+the + # path reaches into one of them, we will return a path relative to the first + # one to match it. Otherwise we expect the path provided to be to the chef + # repo path itself. Paths that are not available on the server are not supported. # # Globs are allowed as well, but globs outside server paths are NOT # (presently) supported. See above examples. TODO support that. # # If the path does not reach into ANY specified directory, nil is returned. def server_path(file_path) - pwd = File.expand_path(Dir.pwd) - absolute_pwd = Chef::ChefFS::PathUtils.realest_path(File.expand_path(file_path, pwd)) + target_path = Chef::ChefFS::PathUtils.realest_path(file_path, @cwd) # Check all object paths (cookbooks_dir, data_bags_dir, etc.) + # These are either manually specified by the user or autogenerated relative + # to chef_repo_path. object_paths.each_pair do |name, paths| paths.each do |path| - realest_path = Chef::ChefFS::PathUtils.realest_path(path) - if PathUtils.descendant_of?(absolute_pwd, realest_path) - relative_path = Chef::ChefFS::PathUtils::relative_to(absolute_pwd, realest_path) - return relative_path == '.' ? "/#{name}" : "/#{name}/#{relative_path}" + object_abs_path = Chef::ChefFS::PathUtils.realest_path(path, @cwd) + if relative_path = PathUtils.descendant_path(target_path, object_abs_path) + return Chef::ChefFS::PathUtils.join("/#{name}", relative_path) end end end # Check chef_repo_path Array(@chef_config[:chef_repo_path]).flatten.each do |chef_repo_path| - realest_chef_repo_path = Chef::ChefFS::PathUtils.realest_path(chef_repo_path) - if absolute_pwd == realest_chef_repo_path + # We're using realest_path here but we really don't need to - we can just expand the + # path and use realpath because a repo_path if provided *must* exist. + realest_chef_repo_path = Chef::ChefFS::PathUtils.realest_path(chef_repo_path, @cwd) + if Chef::ChefFS::PathUtils.os_path_eq?(target_path, realest_chef_repo_path) return '/' end end @@ -201,15 +204,10 @@ class Chef nil end - # The current directory, relative to server root + # The current directory, relative to server root. This is a case-sensitive server path. + # It only exists if the current directory is a child of one of the recognized object_paths below. def base_path - @base_path ||= begin - if @chef_config[:chef_repo_path] - server_path(File.expand_path(@cwd)) - else - nil - end - end + @base_path ||= server_path(@cwd) end # Print the given server path, relative to the current directory @@ -217,10 +215,10 @@ class Chef server_path = entry.path if base_path && server_path[0,base_path.length] == base_path if server_path == base_path - return "." - elsif server_path[base_path.length,1] == "/" + return '.' + elsif server_path[base_path.length,1] == '/' return server_path[base_path.length + 1, server_path.length - base_path.length - 1] - elsif base_path == "/" && server_path[0,1] == "/" + elsif base_path == '/' && server_path[0,1] == '/' return server_path[1, server_path.length - 1] end end diff --git a/lib/chef/chef_fs/file_pattern.rb b/lib/chef/chef_fs/file_pattern.rb index 134d22cbd5..b2351dac68 100644 --- a/lib/chef/chef_fs/file_pattern.rb +++ b/lib/chef/chef_fs/file_pattern.rb @@ -72,7 +72,7 @@ class Chef def could_match_children?(path) return false if path == '' # Empty string is not a path - argument_is_absolute = !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + argument_is_absolute = Chef::ChefFS::PathUtils::is_absolute?(path) return false if is_absolute != argument_is_absolute path = path[1,path.length-1] if argument_is_absolute @@ -111,7 +111,7 @@ class Chef # # This method assumes +could_match_children?(path)+ is +true+. def exact_child_name_under(path) - path = path[1,path.length-1] if !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + path = path[1,path.length-1] if Chef::ChefFS::PathUtils::is_absolute?(path) dirs_in_path = Chef::ChefFS::PathUtils::split(path).length return nil if exact_parts.length <= dirs_in_path return exact_parts[dirs_in_path] @@ -149,7 +149,7 @@ class Chef # abc/*/def.match?('abc/foo/def') == true # abc/*/def.match?('abc/foo') == false def match?(path) - argument_is_absolute = !!(path =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + argument_is_absolute = Chef::ChefFS::PathUtils::is_absolute?(path) return false if is_absolute != argument_is_absolute path = path[1,path.length-1] if argument_is_absolute !!regexp.match(path) @@ -160,17 +160,6 @@ class Chef pattern end - # Given a relative file pattern and a directory, makes a new file pattern - # starting with the directory. - # - # FilePattern.relative_to('/usr/local', 'bin/*grok') == FilePattern.new('/usr/local/bin/*grok') - # - # BUG: this does not support patterns starting with <tt>..</tt> - def self.relative_to(dir, pattern) - return FilePattern.new(pattern) if pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/ - FilePattern.new(Chef::ChefFS::PathUtils::join(dir, pattern)) - end - private def regexp @@ -195,7 +184,7 @@ class Chef def calculate if !@regexp - @is_absolute = !!(@pattern =~ /^#{Chef::ChefFS::PathUtils::regexp_path_separator}/) + @is_absolute = Chef::ChefFS::PathUtils::is_absolute?(@pattern) full_regexp_parts = [] normalized_parts = [] diff --git a/lib/chef/chef_fs/file_system/acl_dir.rb b/lib/chef/chef_fs/file_system/acl_dir.rb index c2354d478d..9f68d7cda7 100644 --- a/lib/chef/chef_fs/file_system/acl_dir.rb +++ b/lib/chef/chef_fs/file_system/acl_dir.rb @@ -28,10 +28,9 @@ class Chef parent.parent.child(name).api_path end - def child(name) + def make_child_entry(name, exists = nil) result = @children.select { |child| child.name == name }.first if @children - result ||= can_have_child?(name, false) ? - AclEntry.new(name, self) : NonexistentFSObject.new(name, self) + result || AclEntry.new(name, self, exists) end def can_have_child?(name, is_dir) @@ -42,7 +41,7 @@ class Chef if @children.nil? # Grab the ACTUAL children (/nodes, /containers, etc.) and get their names names = parent.parent.child(name).children.map { |child| child.dir? ? "#{child.name}.json" : child.name } - @children = names.map { |name| AclEntry.new(name, self, true) } + @children = names.map { |name| make_child_entry(name, true) } end @children end diff --git a/lib/chef/chef_fs/file_system/acls_dir.rb b/lib/chef/chef_fs/file_system/acls_dir.rb index 938bf73fb2..a8c63726b7 100644 --- a/lib/chef/chef_fs/file_system/acls_dir.rb +++ b/lib/chef/chef_fs/file_system/acls_dir.rb @@ -40,8 +40,12 @@ class Chef parent.api_path end + def make_child_entry(name) + children.select { |child| child.name == name }.first + end + def can_have_child?(name, is_dir) - is_dir ? ENTITY_TYPES.include(name) : name == 'organization.json' + is_dir ? ENTITY_TYPES.include?(name) : name == 'organization.json' end def children diff --git a/lib/chef/chef_fs/file_system/base_fs_dir.rb b/lib/chef/chef_fs/file_system/base_fs_dir.rb index 8cc277facc..47e33f961a 100644 --- a/lib/chef/chef_fs/file_system/base_fs_dir.rb +++ b/lib/chef/chef_fs/file_system/base_fs_dir.rb @@ -31,11 +31,6 @@ class Chef true end - # Override child(name) to provide a child object by name without the network read - def child(name) - children.select { |child| child.name == name }.first || NonexistentFSObject.new(name, self) - end - def can_have_child?(name, is_dir) true end diff --git a/lib/chef/chef_fs/file_system/base_fs_object.rb b/lib/chef/chef_fs/file_system/base_fs_object.rb index 43e6a513d7..916ab8297d 100644 --- a/lib/chef/chef_fs/file_system/base_fs_object.rb +++ b/lib/chef/chef_fs/file_system/base_fs_object.rb @@ -95,7 +95,10 @@ class Chef # directly perform a network request to retrieve the y.json data bag. No # network request was necessary to retrieve def child(name) - NonexistentFSObject.new(name, self) + if can_have_child?(name, true) || can_have_child?(name, false) + result = make_child_entry(name) + end + result || NonexistentFSObject.new(name, self) end # Override children to report your *actual* list of children as an array. @@ -171,7 +174,7 @@ class Chef # Important directory attributes: name, parent, path, root # Overridable attributes: dir?, child(name), path_for_printing - # Abstract: read, write, delete, children, can_have_child?, create_child, compare_to + # Abstract: read, write, delete, children, can_have_child?, create_child, compare_to, make_child_entry end # class BaseFsObject end end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb index a7f1d733b1..4391bdbfcd 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_dir.rb @@ -58,14 +58,7 @@ class Chef end def children - begin - Dir.entries(file_path).sort. - select { |child_name| can_have_child?(child_name, File.directory?(File.join(file_path, child_name))) }. - map { |child_name| make_child(child_name) }. - select { |entry| !(entry.dir? && entry.children.size == 0) } - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end + super.select { |entry| !(entry.dir? && entry.children.size == 0 ) } end def can_have_child?(name, is_dir) @@ -99,7 +92,7 @@ class Chef protected - def make_child(child_name) + def make_child_entry(child_name) segment_info = CookbookDir::COOKBOOK_SEGMENT_INFO[child_name.to_sym] || {} ChefRepositoryFileSystemCookbookEntry.new(child_name, self, nil, segment_info[:ruby_only], segment_info[:recursive]) end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb index 66709ccf68..914412f839 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbook_entry.rb @@ -34,14 +34,7 @@ class Chef attr_reader :recursive def children - begin - Dir.entries(file_path).sort. - select { |child_name| can_have_child?(child_name, File.directory?(File.join(file_path, child_name))) }. - map { |child_name| make_child(child_name) }. - select { |entry| !(entry.dir? && entry.children.size == 0) } - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end + super.select { |entry| !(entry.dir? && entry.children.size == 0 ) } end def can_have_child?(name, is_dir) @@ -78,7 +71,7 @@ class Chef protected - def make_child(child_name) + def make_child_entry(child_name) ChefRepositoryFileSystemCookbookEntry.new(child_name, self, nil, ruby_only, recursive) end end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb index 7c60b51114..5b495666c3 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_cookbooks_dir.rb @@ -37,21 +37,14 @@ class Chef attr_reader :chefignore def children - begin - Dir.entries(file_path).sort. - select { |child_name| can_have_child?(child_name, File.directory?(File.join(file_path, child_name))) }. - map { |child_name| make_child(child_name) }. - select do |entry| - # empty cookbooks and cookbook directories are ignored - if !entry.can_upload? - Chef::Log.warn("Cookbook '#{entry.name}' is empty or entirely chefignored at #{entry.path_for_printing}") - false - else - true - end - end - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) + super.select do |entry| + # empty cookbooks and cookbook directories are ignored + if !entry.can_upload? + Chef::Log.warn("Cookbook '#{entry.name}' is empty or entirely chefignored at #{entry.path_for_printing}") + false + else + true + end end end @@ -61,7 +54,7 @@ class Chef def write_cookbook(cookbook_path, cookbook_version_json, from_fs) cookbook_name = File.basename(cookbook_path) - child = make_child(cookbook_name) + child = make_child_entry(cookbook_name) # Use the copy/diff algorithm to copy it down so we don't destroy # chefignored data. This is terribly un-thread-safe. @@ -80,7 +73,7 @@ class Chef protected - def make_child(child_name) + def make_child_entry(child_name) ChefRepositoryFileSystemCookbookDir.new(child_name, self) end end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb index 0b14750744..39172e7ab9 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_entry.rb @@ -70,20 +70,9 @@ class Chef Chef::JSONCompat.to_json_pretty(object) end - def children - # Except cookbooks and data bag dirs, all things must be json files - begin - Dir.entries(file_path).sort. - select { |child_name| can_have_child?(child_name, File.directory?(File.join(file_path, child_name))) }. - map { |child_name| make_child(child_name) } - rescue Errno::ENOENT - raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) - end - end - protected - def make_child(child_name) + def make_child_entry(child_name) ChefRepositoryFileSystemEntry.new(child_name, self) end end diff --git a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb index d03baf91fe..267fe30456 100644 --- a/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_repository_file_system_root_dir.rb @@ -68,13 +68,13 @@ class Chef attr_reader :child_paths attr_reader :versioned_cookbooks - CHILDREN = %w(invitations.json members.json org.json) + CHILDREN = %w(org.json invitations.json members.json) def children @children ||= begin - result = child_paths.keys.sort.map { |name| make_child_entry(name) }.select { |child| !child.nil? } - result += root_dir.children.select { |c| CHILDREN.include?(c.name) } if root_dir - result.sort_by { |c| c.name } + result = child_paths.keys.sort.map { |name| make_child_entry(name) } + result += CHILDREN.map { |name| make_child_entry(name) } + result.select { |c| c && c.exists? }.sort_by { |c| c.name } end end @@ -149,19 +149,23 @@ class Chef # cookbooks from all of them when you list or grab them). # def make_child_entry(name) - paths = child_paths[name].select do |path| - File.exists?(path) + if CHILDREN.include?(name) + return nil if !root_dir + return root_dir.child(name) end + + paths = (child_paths[name] || []).select { |path| File.exists?(path) } if paths.size == 0 - return nil + return NonexistentFSObject.new(name, self) end - if name == 'cookbooks' + case name + when 'cookbooks' dirs = paths.map { |path| ChefRepositoryFileSystemCookbooksDir.new(name, self, path) } - elsif name == 'data_bags' + when 'data_bags' dirs = paths.map { |path| ChefRepositoryFileSystemDataBagsDir.new(name, self, path) } - elsif name == 'policies' + when 'policies' dirs = paths.map { |path| ChefRepositoryFileSystemPoliciesDir.new(name, self, path) } - elsif name == 'acls' + when 'acls' dirs = paths.map { |path| ChefRepositoryFileSystemAclsDir.new(name, self, path) } else data_handler = case name diff --git a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb index 370308ee0a..e3ffd644ad 100644 --- a/lib/chef/chef_fs/file_system/chef_server_root_dir.rb +++ b/lib/chef/chef_fs/file_system/chef_server_root_dir.rb @@ -90,11 +90,11 @@ class Chef end def rest - Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :raw_output => true) + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :raw_output => true, :api_version => "0") end def get_json(path) - Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key).get(path) + Chef::ServerAPI.new(chef_server_url, :client_name => chef_username, :signing_key_filename => chef_private_key, :api_version => "0").get(path) end def chef_rest @@ -110,7 +110,8 @@ class Chef end def can_have_child?(name, is_dir) - is_dir && children.any? { |child| child.name == name } + result = children.select { |child| child.name == name }.first + result && !!result.dir? == !!is_dir end def org @@ -124,6 +125,10 @@ class Chef end end + def make_child_entry(name) + children.select { |child| child.name == name }.first + end + def children @children ||= begin result = [ diff --git a/lib/chef/chef_fs/file_system/cookbook_dir.rb b/lib/chef/chef_fs/file_system/cookbook_dir.rb index 03652dc376..c0f0390e98 100644 --- a/lib/chef/chef_fs/file_system/cookbook_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbook_dir.rb @@ -16,6 +16,7 @@ # limitations under the License. # +require 'chef/chef_fs/command_line' require 'chef/chef_fs/file_system/rest_list_dir' require 'chef/chef_fs/file_system/cookbook_subdir' require 'chef/chef_fs/file_system/cookbook_file' @@ -71,16 +72,15 @@ class Chef "#{parent.api_path}/#{cookbook_name}/#{version || "_latest"}" end - def child(name) + def make_child_entry(name) # Since we're ignoring the rules and doing a network request here, # we need to make sure we don't rethrow the exception. (child(name) # is not supposed to fail.) begin - result = children.select { |child| child.name == name }.first - return result if result + children.select { |child| child.name == name }.first rescue Chef::ChefFS::FileSystem::NotFoundError + nil end - return NonexistentFSObject.new(name, self) end def can_have_child?(name, is_dir) diff --git a/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb index d6246f1e60..560ceb4886 100644 --- a/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbooks_acl_dir.rb @@ -31,7 +31,7 @@ class Chef def children if @children.nil? names = parent.parent.child(name).children.map { |child| "#{child.cookbook_name}.json" } - @children = names.uniq.map { |name| AclEntry.new(name, self, true) } + @children = names.uniq.map { |name| make_child_entry(name, true) } end @children end diff --git a/lib/chef/chef_fs/file_system/cookbooks_dir.rb b/lib/chef/chef_fs/file_system/cookbooks_dir.rb index 27bedd3827..6f49c28996 100644 --- a/lib/chef/chef_fs/file_system/cookbooks_dir.rb +++ b/lib/chef/chef_fs/file_system/cookbooks_dir.rb @@ -36,17 +36,9 @@ class Chef super("cookbooks", parent) end - def child(name) - if @children - result = self.children.select { |child| child.name == name }.first - if result - result - else - NonexistentFSObject.new(name, self) - end - else - CookbookDir.new(name, self) - end + def make_child_entry(name) + result = @children.select { |child| child.name == name }.first if @children + result || CookbookDir.new(name, self) end def children diff --git a/lib/chef/chef_fs/file_system/data_bags_dir.rb b/lib/chef/chef_fs/file_system/data_bags_dir.rb index 6d0685d3b7..1cb61bbd1a 100644 --- a/lib/chef/chef_fs/file_system/data_bags_dir.rb +++ b/lib/chef/chef_fs/file_system/data_bags_dir.rb @@ -27,16 +27,14 @@ class Chef super("data_bags", parent, "data") end - def child(name) + def make_child_entry(name, exists = false) result = @children.select { |child| child.name == name }.first if @children - result || DataBagDir.new(name, self) + result || DataBagDir.new(name, self, exists) end def children begin - @children ||= root.get_json(api_path).keys.sort.map do |entry| - DataBagDir.new(entry, self, true) - end + @children ||= root.get_json(api_path).keys.sort.map { |entry| make_child_entry(entry, true) } rescue Timeout::Error => e raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e), "Timeout getting children: #{e}" rescue Net::HTTPServerException => e diff --git a/lib/chef/chef_fs/file_system/environments_dir.rb b/lib/chef/chef_fs/file_system/environments_dir.rb index 559dd6af86..3aee3ee5af 100644 --- a/lib/chef/chef_fs/file_system/environments_dir.rb +++ b/lib/chef/chef_fs/file_system/environments_dir.rb @@ -30,7 +30,7 @@ class Chef super("environments", parent, nil, Chef::ChefFS::DataHandler::EnvironmentDataHandler.new) end - def _make_child_entry(name, exists = nil) + def make_child_entry(name, exists = nil) if name == '_default.json' DefaultEnvironmentEntry.new(name, self, exists) else diff --git a/lib/chef/chef_fs/file_system/file_system_entry.rb b/lib/chef/chef_fs/file_system/file_system_entry.rb index 1af7e618de..8611aa2e0f 100644 --- a/lib/chef/chef_fs/file_system/file_system_entry.rb +++ b/lib/chef/chef_fs/file_system/file_system_entry.rb @@ -40,15 +40,18 @@ class Chef end def children + # Except cookbooks and data bag dirs, all things must be json files begin - Dir.entries(file_path).sort.select { |entry| entry != '.' && entry != '..' }.map { |entry| make_child(entry) } + Dir.entries(file_path).sort. + map { |child_name| make_child_entry(child_name) }. + select { |child| child && can_have_child?(child.name, child.dir?) } rescue Errno::ENOENT raise Chef::ChefFS::FileSystem::NotFoundError.new(self, $!) end end def create_child(child_name, file_contents=nil) - child = make_child(child_name) + child = make_child_entry(child_name) if child.exists? raise Chef::ChefFS::FileSystem::AlreadyExistsError.new(:create_child, child) end @@ -80,7 +83,7 @@ class Chef end def exists? - File.exists?(file_path) + File.exists?(file_path) && parent.can_have_child?(name, dir?) end def read @@ -99,7 +102,7 @@ class Chef protected - def make_child(child_name) + def make_child_entry(child_name) FileSystemEntry.new(child_name, self) end end diff --git a/lib/chef/chef_fs/file_system/memory_dir.rb b/lib/chef/chef_fs/file_system/memory_dir.rb index a7eda3c654..260a91693c 100644 --- a/lib/chef/chef_fs/file_system/memory_dir.rb +++ b/lib/chef/chef_fs/file_system/memory_dir.rb @@ -1,5 +1,4 @@ require 'chef/chef_fs/file_system/base_fs_dir' -require 'chef/chef_fs/file_system/nonexistent_fs_object' require 'chef/chef_fs/file_system/memory_file' class Chef @@ -13,8 +12,8 @@ class Chef attr_reader :children - def child(name) - @children.select { |child| child.name == name }.first || Chef::ChefFS::FileSystem::NonexistentFSObject.new(name, self) + def make_child_entry(name) + @children.select { |child| child.name == name }.first end def add_child(child) diff --git a/lib/chef/chef_fs/file_system/multiplexed_dir.rb b/lib/chef/chef_fs/file_system/multiplexed_dir.rb index 06d4af705d..70b827f85f 100644 --- a/lib/chef/chef_fs/file_system/multiplexed_dir.rb +++ b/lib/chef/chef_fs/file_system/multiplexed_dir.rb @@ -35,6 +35,21 @@ class Chef end end + def make_child_entry(name) + result = nil + multiplexed_dirs.each do |dir| + child_entry = dir.child(name) + if child_entry.exists? + if result + Chef::Log.warn("Child with name '#{child_entry.name}' found in multiple directories: #{result.parent.path_for_printing} and #{child_entry.parent.path_for_printing}") + else + result = child_entry + end + end + end + result + end + def can_have_child?(name, is_dir) write_dir.can_have_child?(name, is_dir) end diff --git a/lib/chef/chef_fs/file_system/nodes_dir.rb b/lib/chef/chef_fs/file_system/nodes_dir.rb index c3c48377cd..2610b06a82 100644 --- a/lib/chef/chef_fs/file_system/nodes_dir.rb +++ b/lib/chef/chef_fs/file_system/nodes_dir.rb @@ -33,7 +33,7 @@ class Chef def children begin @children ||= root.get_json(env_api_path).keys.sort.map do |key| - _make_child_entry("#{key}.json", true) + make_child_entry("#{key}.json", true) end rescue Timeout::Error => e raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e), "Timeout retrieving children: #{e}" diff --git a/lib/chef/chef_fs/file_system/rest_list_dir.rb b/lib/chef/chef_fs/file_system/rest_list_dir.rb index 672fa444f1..0ac735a2c4 100644 --- a/lib/chef/chef_fs/file_system/rest_list_dir.rb +++ b/lib/chef/chef_fs/file_system/rest_list_dir.rb @@ -33,12 +33,6 @@ class Chef attr_reader :api_path attr_reader :data_handler - def child(name) - result = @children.select { |child| child.name == name }.first if @children - result ||= can_have_child?(name, false) ? - _make_child_entry(name) : NonexistentFSObject.new(name, self) - end - def can_have_child?(name, is_dir) name =~ /\.json$/ && !is_dir end @@ -46,7 +40,7 @@ class Chef def children begin @children ||= root.get_json(api_path).keys.sort.map do |key| - _make_child_entry("#{key}.json", true) + make_child_entry("#{key}.json", true) end rescue Timeout::Error => e raise Chef::ChefFS::FileSystem::OperationFailedError.new(:children, self, e), "Timeout retrieving children: #{e}" @@ -66,7 +60,7 @@ class Chef raise Chef::ChefFS::FileSystem::OperationFailedError.new(:create_child, self, e), "Parse error reading JSON creating child '#{name}': #{e}" end - result = _make_child_entry(name, true) + result = make_child_entry(name, true) if data_handler object = data_handler.normalize_for_post(object, result) @@ -106,7 +100,8 @@ class Chef parent.rest end - def _make_child_entry(name, exists = nil) + def make_child_entry(name, exists = nil) + @children.select { |child| child.name == name }.first if @children RestListEntry.new(name, self, exists) end end diff --git a/lib/chef/chef_fs/knife.rb b/lib/chef/chef_fs/knife.rb index 86872dab71..9101e455f8 100644 --- a/lib/chef/chef_fs/knife.rb +++ b/lib/chef/chef_fs/knife.rb @@ -17,6 +17,7 @@ # require 'chef/knife' +require 'pathname' class Chef module ChefFS @@ -63,7 +64,7 @@ class Chef # --chef-repo-path forcibly overrides all other paths if config[:chef_repo_path] Chef::Config[:chef_repo_path] = config[:chef_repo_path] - %w(acl client cookbook container data_bag environment group node role user).each do |variable_name| + Chef::ChefFS::Config::INFLECTIONS.each_value do |variable_name| Chef::Config.delete("#{variable_name}_path".to_sym) end end @@ -98,14 +99,41 @@ class Chef end def pattern_arg_from(arg) - # TODO support absolute file paths and not just patterns? Too much? - # Could be super useful in a world with multiple repo paths - if !@chef_fs_config.base_path && !Chef::ChefFS::PathUtils.is_absolute?(arg) - # Check if chef repo path is specified to give a better error message - ui.error("Attempt to use relative path '#{arg}' when current directory is outside the repository path") + inferred_path = nil + if Chef::ChefFS::PathUtils.is_absolute?(arg) + # We should be able to use this as-is - but the user might have incorrectly provided + # us with a path that is based off of the OS root path instead of the Chef-FS root. + # Do a quick and dirty sanity check. + if possible_server_path = @chef_fs_config.server_path(arg) + ui.warn("The absolute path provided is suspicious: #{arg}") + ui.warn("If you wish to refer to a file location, please provide a path that is rooted at the chef-repo.") + ui.warn("Consider writing '#{possible_server_path}' instead of '#{arg}'") + end + # Use the original path because we can't be sure. + inferred_path = arg + elsif arg[0,1] == '~' + # Let's be nice and fix it if possible - but warn the user. + ui.warn("A path relative to a user home directory has been provided: #{arg}") + ui.warn("Paths provided need to be rooted at the chef-repo being considered or be relative paths.") + inferred_path = @chef_fs_config.server_path(arg) + ui.warn("Using '#{inferred_path}' as the path instead of '#{arg}'.") + elsif Pathname.new(arg).absolute? + # It is definitely a system absolute path (such as C:\ or \\foo\bar) but it cannot be + # interpreted as a Chef-FS absolute path. Again attempt to be nice but warn the user. + ui.warn("An absolute file system path that isn't a server path was provided: #{arg}") + ui.warn("Paths provided need to be rooted at the chef-repo being considered or be relative paths.") + inferred_path = @chef_fs_config.server_path(arg) + ui.warn("Using '#{inferred_path}' as the path instead of '#{arg}'.") + elsif @chef_fs_config.base_path.nil? + # These are all relative paths. We can't resolve and root paths unless we are in the + # chef repo. + ui.error("Attempt to use relative path '#{arg}' when current directory is outside the repository path.") + ui.error("Current working directory is '#{@chef_fs_config.cwd}'.") exit(1) + else + inferred_path = Chef::ChefFS::PathUtils::join(@chef_fs_config.base_path, arg) end - Chef::ChefFS::FilePattern.relative_to(@chef_fs_config.base_path, arg) + Chef::ChefFS::FilePattern.new(inferred_path) end def format_path(entry) diff --git a/lib/chef/chef_fs/path_utils.rb b/lib/chef/chef_fs/path_utils.rb index 9ef75ce2e5..595f966378 100644 --- a/lib/chef/chef_fs/path_utils.rb +++ b/lib/chef/chef_fs/path_utils.rb @@ -23,31 +23,31 @@ class Chef module ChefFS class PathUtils - # If you are in 'source', this is what you would have to type to reach 'dest' - # relative_to('/a/b/c/d/e', '/a/b/x/y') == '../../c/d/e' - # relative_to('/a/b', '/a/b') == '.' - def self.relative_to(dest, source) - # Skip past the common parts - source_parts = Chef::ChefFS::PathUtils.split(source) - dest_parts = Chef::ChefFS::PathUtils.split(dest) - i = 0 - until i >= source_parts.length || i >= dest_parts.length || source_parts[i] != dest_parts[i] - i+=1 - end - # dot-dot up from 'source' to the common ancestor, then - # descend to 'dest' from the common ancestor - result = Chef::ChefFS::PathUtils.join(*(['..']*(source_parts.length-i) + dest_parts[i,dest.length-i])) - result == '' ? '.' : result - end + # A Chef-FS path is a path in a chef-repository that can be used to address + # both files on a local file-system as well as objects on a chef server. + # These paths are stricter than file-system paths allowed on various OSes. + # Absolute Chef-FS paths begin with "/" (on windows, "\" is acceptable as well). + # "/" is used as the path element separator (on windows, "\" is acceptable as well). + # No directory/path element may contain a literal "\" character. Any such characters + # encountered are either dealt with as separators (on windows) or as escape + # characters (on POSIX systems). Relative Chef-FS paths may use ".." or "." but + # may never use these to back-out of the root of a Chef-FS path. Any such extraneous + # ".."s are ignored. + # Chef-FS paths are case sensitive (since the paths on the server are). + # On OSes with case insensitive paths, you may be unable to locally deal with two + # objects whose server paths only differ by case. OTOH, the case of path segments + # that are outside the Chef-FS root (such as when looking at a file-system absolute + # path to discover the Chef-FS root path) are handled in accordance to the rules + # of the local file-system and OS. def self.join(*parts) return "" if parts.length == 0 # Determine if it started with a slash absolute = parts[0].length == 0 || parts[0].length > 0 && parts[0] =~ /^#{regexp_path_separator}/ # Remove leading and trailing slashes from each part so that the join will work (and the slash at the end will go away) - parts = parts.map { |part| part.gsub(/^\/|\/$/, "") } + parts = parts.map { |part| part.gsub(/^#{regexp_path_separator}+|#{regexp_path_separator}+$/, '') } # Don't join empty bits - result = parts.select { |part| part != "" }.join("/") + result = parts.select { |part| part != '' }.join('/') # Put the / back on absolute ? "/#{result}" : result end @@ -60,36 +60,67 @@ class Chef Chef::ChefFS::windows? ? '[\/\\\\]' : '/' end + # Given a server path, determines if it is absolute. + def self.is_absolute?(path) + !!(path =~ /^#{regexp_path_separator}/) + end # Given a path which may only be partly real (i.e. /x/y/z when only /x exists, # or /x/y/*/blah when /x/y/z/blah exists), call File.realpath on the biggest - # part that actually exists. + # part that actually exists. The paths operated on here are not Chef-FS paths. + # These are OS paths that may contain symlinks but may not also fully exist. # # If /x is a symlink to /blarghle, and has no subdirectories, then: # PathUtils.realest_path('/x/y/z') == '/blarghle/y/z' # PathUtils.realest_path('/x/*/z') == '/blarghle/*/z' # PathUtils.realest_path('/*/y/z') == '/*/y/z' - def self.realest_path(path) - path = Pathname.new(path) - begin - path.realpath.to_s - rescue Errno::ENOENT - dirname = path.dirname - if dirname - PathUtils.join(realest_path(dirname), path.basename.to_s) - else - path.to_s + # + # TODO: Move this to wherever util/path_helper is these days. + def self.realest_path(path, cwd = Dir.pwd) + path = File.expand_path(path, cwd) + parent_path = File.dirname(path) + suffix = [] + + # File.dirname happens to return the path as its own dirname if you're + # at the root (such as at \\foo\bar, C:\ or /) + until parent_path == path do + # This can occur if a path such as "C:" is given. Ruby gives the parent as "C:." + # for reasons only it knows. + raise ArgumentError "Invalid path segment #{path}" if parent_path.length > path.length + begin + path = File.realpath(path) + break + rescue Errno::ENOENT + suffix << File.basename(path) + path = parent_path + parent_path = File.dirname(path) end end + File.join(path, *suffix.reverse) end - def self.descendant_of?(path, ancestor) - path[0,ancestor.length] == ancestor && - (ancestor.length == path.length || path[ancestor.length,1] =~ /#{PathUtils.regexp_path_separator}/) + # Compares two path fragments according to the case-sentitivity of the host platform. + def self.os_path_eq?(left, right) + Chef::ChefFS::windows? ? left.casecmp(right) == 0 : left == right end - def self.is_absolute?(path) - path =~ /^#{regexp_path_separator}/ + # Given two general OS-dependent file paths, determines the relative path of the + # child with respect to the ancestor. Both child and ancestor must exist and be + # fully resolved - this is strictly a lexical comparison. No trailing slashes + # and other shenanigans are allowed. + # + # TODO: Move this to util/path_helper. + def self.descendant_path(path, ancestor) + candidate_fragment = path[0, ancestor.length] + return nil unless PathUtils.os_path_eq?(candidate_fragment, ancestor) + if ancestor.length == path.length + '' + elsif path[ancestor.length,1] =~ /#{PathUtils.regexp_path_separator}/ + path[ancestor.length+1..-1] + else + nil + end end + end end end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 86e92585e3..621ce3d489 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -3,7 +3,7 @@ # Author:: Christopher Walters (<cw@opscode.com>) # Author:: Christopher Brown (<cb@opscode.com>) # Author:: Tim Hinderliter (<tim@opscode.com>) -# Copyright:: Copyright (c) 2008-2011 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -302,19 +302,24 @@ class Chef @run_status = nil run_context = nil runlock.release - GC.start end # Raise audit, converge, and other errors here so that we exit # with the proper exit status code and everything gets raised # as a RunFailedWrappingError if run_error || converge_error || audit_error - error = if run_error == converge_error - Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) - else - Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) - end - error.fill_backtrace + error = if Chef::Config[:audit_mode] == :disabled + run_error || converge_error + else + e = if run_error == converge_error + Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) + else + Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) + end + e.fill_backtrace + e + end + Chef::Application.debug_stacktrace(error) raise error end diff --git a/lib/chef/config.rb b/lib/chef/config.rb index 9beb18b53e..6382af14c2 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -28,9 +28,21 @@ require 'chef-config/logger' ChefConfig.logger = Chef::Log require 'chef-config/config' - require 'chef/platform/query_helpers' +# Ohai::Config defines its own log_level and log_location. When loaded, it will +# override the default ChefConfig::Config values. We save them here before +# loading ohai/config so that we can override them again inside Chef::Config. +# +# REMOVEME once these configurables are removed from the top level of Ohai. +LOG_LEVEL = ChefConfig::Config[:log_level] unless defined? LOG_LEVEL +LOG_LOCATION = ChefConfig::Config[:log_location] unless defined? LOG_LOCATION + +# Load the ohai config into the chef config. We can't have an empty ohai +# configuration context because `ohai.plugins_path << some_path` won't work, +# and providing default ohai config values here isn't DRY. +require 'ohai/config' + class Chef Config = ChefConfig::Config @@ -49,5 +61,24 @@ class Chef evt_loggers end + # Override the default values that were set by Ohai. + # + # REMOVEME once these configurables are removed from the top level of Ohai. + default :log_level, LOG_LEVEL + default :log_location, LOG_LOCATION + + # Ohai::Config[:log_level] is deprecated and warns when set. Unfortunately, + # there is no way to distinguish between setting log_level and setting + # Ohai::Config[:log_level]. Since log_level and log_location are used by + # chef-client and other tools (e.g., knife), we will mute the warnings here + # by redefining the config_attr_writer to not warn for these options. + # + # REMOVEME once the warnings for these configurables are removed from Ohai. + [ :log_level, :log_location ].each do |option| + config_attr_writer option do |value| + value + end + end + end end diff --git a/lib/chef/constants.rb b/lib/chef/constants.rb new file mode 100644 index 0000000000..d39ce4c68d --- /dev/null +++ b/lib/chef/constants.rb @@ -0,0 +1,27 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 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. + +class Chef + NOT_PASSED = Object.new + def NOT_PASSED.to_s + "NOT_PASSED" + end + def NOT_PASSED.inspect + to_s + end + NOT_PASSED.freeze +end diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb index 01a98fda39..9822920a7d 100644 --- a/lib/chef/cookbook/metadata.rb +++ b/lib/chef/cookbook/metadata.rb @@ -54,12 +54,13 @@ class Chef VERSION = 'version'.freeze SOURCE_URL = 'source_url'.freeze ISSUES_URL = 'issues_url'.freeze + PRIVACY = 'privacy'.freeze COMPARISON_FIELDS = [ :name, :description, :long_description, :maintainer, :maintainer_email, :license, :platforms, :dependencies, :recommendations, :suggestions, :conflicting, :providing, :replacing, :attributes, :groupings, :recipes, :version, - :source_url, :issues_url ] + :source_url, :issues_url, :privacy ] VERSION_CONSTRAINTS = {:depends => DEPENDENCIES, :recommends => RECOMMENDATIONS, @@ -116,6 +117,7 @@ class Chef @version = Version.new("0.0.0") @source_url = '' @issues_url = '' + @privacy = false @errors = [] end @@ -454,7 +456,8 @@ class Chef :recipes => { :kind_of => [ Array ], :default => [] }, :default => { :kind_of => [ String, Array, Hash, Symbol, Numeric, TrueClass, FalseClass ] }, :source_url => { :kind_of => String }, - :issues_url => { :kind_of => String } + :issues_url => { :kind_of => String }, + :privacy => { :kind_of => [ TrueClass, FalseClass ] } } ) options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil? @@ -498,7 +501,8 @@ class Chef RECIPES => self.recipes, VERSION => self.version, SOURCE_URL => self.source_url, - ISSUES_URL => self.issues_url + ISSUES_URL => self.issues_url, + PRIVACY => self.privacy } end @@ -532,6 +536,7 @@ class Chef @version = o[VERSION] if o.has_key?(VERSION) @source_url = o[SOURCE_URL] if o.has_key?(SOURCE_URL) @issues_url = o[ISSUES_URL] if o.has_key?(ISSUES_URL) + @privacy = o[PRIVACY] if o.has_key?(PRIVACY) self end @@ -590,6 +595,23 @@ class Chef ) end + # + # Sets the cookbook's privacy flag, or returns it. + # + # === Parameters + # privacy<TrueClass,FalseClass>:: Whether this cookbook is private or not + # + # === Returns + # privacy<TrueClass,FalseClass>:: Whether this cookbook is private or not + # + def privacy(arg=nil) + set_or_return( + :privacy, + arg, + :kind_of => [ TrueClass, FalseClass ] + ) + end + private def run_validation diff --git a/lib/chef/cookbook/synchronizer.rb b/lib/chef/cookbook/synchronizer.rb index 1b96d0510b..fc8e739d73 100644 --- a/lib/chef/cookbook/synchronizer.rb +++ b/lib/chef/cookbook/synchronizer.rb @@ -131,7 +131,7 @@ class Chef files_remaining_by_cookbook[file.cookbook] -= 1 if files_remaining_by_cookbook[file.cookbook] == 0 - @events.synchronized_cookbook(file.cookbook.name) + @events.synchronized_cookbook(file.cookbook.name, file.cookbook) end end diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb index 8d302eeec2..bff3146572 100644 --- a/lib/chef/cookbook_version.rb +++ b/lib/chef/cookbook_version.rb @@ -51,12 +51,12 @@ class Chef attr_accessor :metadata_filenames def status=(new_status) - Chef::Log.deprecation("Deprecated method `status' called from #{caller(1).first}. This method will be removed") + Chef.log_deprecation("Deprecated method `status' called. This method will be removed.", caller(1..1)) @status = new_status end def status - Chef::Log.deprecation("Deprecated method `status' called from #{caller(1).first}. This method will be removed") + Chef.log_deprecation("Deprecated method `status' called. This method will be removed.", caller(1..1)) @status end @@ -480,7 +480,7 @@ class Chef # @deprecated This method was used by the Ruby Chef Server and is no longer # needed. There is no replacement. def generate_manifest_with_urls(&url_generator) - Chef::Log.deprecation("Deprecated method #generate_manifest_with_urls called from #{caller(1).first}") + Chef.log_deprecation("Deprecated method #generate_manifest_with_urls.", caller(1..1)) rendered_manifest = manifest.dup COOKBOOK_SEGMENTS.each do |segment| diff --git a/lib/chef/delayed_evaluator.rb b/lib/chef/delayed_evaluator.rb new file mode 100644 index 0000000000..9f18a53445 --- /dev/null +++ b/lib/chef/delayed_evaluator.rb @@ -0,0 +1,21 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 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. + +class Chef + class DelayedEvaluator < Proc + end +end diff --git a/lib/chef/deprecation/mixin/template.rb b/lib/chef/deprecation/mixin/template.rb index 36d18ad90d..58a661c4bd 100644 --- a/lib/chef/deprecation/mixin/template.rb +++ b/lib/chef/deprecation/mixin/template.rb @@ -25,7 +25,7 @@ class Chef # == Deprecation::Provider::Mixin::Template # This module contains the deprecated functions of # Chef::Mixin::Template. These functions are refactored to different - # components. They are frozen and will be removed in Chef 12. + # components. They are frozen and will be removed in Chef 13. # module Template @@ -46,4 +46,3 @@ class Chef end end end - diff --git a/lib/chef/deprecation/provider/cookbook_file.rb b/lib/chef/deprecation/provider/cookbook_file.rb index dfbf4a39a4..92f5ce3623 100644 --- a/lib/chef/deprecation/provider/cookbook_file.rb +++ b/lib/chef/deprecation/provider/cookbook_file.rb @@ -24,7 +24,7 @@ class Chef # == Deprecation::Provider::CookbookFile # This module contains the deprecated functions of # Chef::Provider::CookbookFile. These functions are refactored to - # different components. They are frozen and will be removed in Chef 12. + # different components. They are frozen and will be removed in Chef 13. # module CookbookFile diff --git a/lib/chef/deprecation/provider/file.rb b/lib/chef/deprecation/provider/file.rb index 125f31fe10..31038ab3d8 100644 --- a/lib/chef/deprecation/provider/file.rb +++ b/lib/chef/deprecation/provider/file.rb @@ -25,7 +25,7 @@ class Chef # == Deprecation::Provider::File # This module contains the deprecated functions of # Chef::Provider::File. These functions are refactored to different - # components. They are frozen and will be removed in Chef 12. + # components. They are frozen and will be removed in Chef 13. # module File diff --git a/lib/chef/deprecation/provider/remote_file.rb b/lib/chef/deprecation/provider/remote_file.rb index 4452de67cd..c06a5cc695 100644 --- a/lib/chef/deprecation/provider/remote_file.rb +++ b/lib/chef/deprecation/provider/remote_file.rb @@ -23,7 +23,7 @@ class Chef # == Deprecation::Provider::RemoteFile # This module contains the deprecated functions of # Chef::Provider::RemoteFile. These functions are refactored to different - # components. They are frozen and will be removed in Chef 12. + # components. They are frozen and will be removed in Chef 13. # module RemoteFile @@ -83,4 +83,3 @@ class Chef end end end - diff --git a/lib/chef/deprecation/provider/template.rb b/lib/chef/deprecation/provider/template.rb index d7a228e97a..34e5f54b7e 100644 --- a/lib/chef/deprecation/provider/template.rb +++ b/lib/chef/deprecation/provider/template.rb @@ -25,7 +25,7 @@ class Chef # == Deprecation::Provider::Template # This module contains the deprecated functions of # Chef::Provider::Template. These functions are refactored to different - # components. They are frozen and will be removed in Chef 12. + # components. They are frozen and will be removed in Chef 13. # module Template diff --git a/lib/chef/deprecation/warnings.rb b/lib/chef/deprecation/warnings.rb index 34f468ff53..376629710e 100644 --- a/lib/chef/deprecation/warnings.rb +++ b/lib/chef/deprecation/warnings.rb @@ -25,10 +25,9 @@ class Chef m = instance_method(name) define_method(name) do |*args| message = [] - message << "Method '#{name}' of '#{self.class}' is deprecated. It will be removed in Chef 12." - message << "Please update your cookbooks accordingly. Accessed from:" - caller[0..3].each {|l| message << l} - Chef::Log.deprecation message + message << "Method '#{name}' of '#{self.class}' is deprecated. It will be removed in Chef 13." + message << "Please update your cookbooks accordingly." + Chef.log_deprecation(message, caller(0..3)) super(*args) end end diff --git a/lib/chef/dsl/reboot_pending.rb b/lib/chef/dsl/reboot_pending.rb index 7af67e94a5..c577118dd4 100644 --- a/lib/chef/dsl/reboot_pending.rb +++ b/lib/chef/dsl/reboot_pending.rb @@ -45,7 +45,7 @@ class Chef registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') || # Vista + Server 2008 and newer may have reboots pending from CBS - registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired') || + registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') || # The mere existence of the UpdateExeVolatile key should indicate a pending restart for certain updates # http://support.microsoft.com/kb/832475 diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index d69f0a8f11..26c0ec6768 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -19,12 +19,10 @@ require 'chef/mixin/convert_to_class_name' require 'chef/exceptions' -require 'chef/resource_builder' require 'chef/mixin/shell_out' require 'chef/mixin/powershell_out' require 'chef/dsl/resources' require 'chef/dsl/definitions' -require 'chef/resource' class Chef module DSL @@ -122,9 +120,9 @@ class Chef def describe_self_for_error if respond_to?(:name) - %Q[`#{self.class.name} "#{name}"'] + %Q[`#{self.class} "#{name}"'] elsif respond_to?(:recipe_name) - %Q[`#{self.class.name} "#{recipe_name}"'] + %Q[`#{self.class} "#{recipe_name}"'] else to_s end @@ -142,8 +140,7 @@ class Chef # method_missing manually. Not a fan. Not. A. Fan. # if respond_to?(method_symbol) - Chef::Log.deprecation("Calling method_missing(#{method_symbol.inspect}) directly is deprecated in Chef 12 and will be removed in Chef 13.") - Chef::Log.deprecation("Use public_send() or send() instead.") + Chef.log_deprecation("Calling method_missing(#{method_symbol.inspect}) directly is deprecated in Chef 12 and will be removed in Chef 13. Use public_send() or send() instead.") return send(method_symbol, *args, &block) end @@ -152,7 +149,7 @@ class Chef # never called. DEPRECATED. # if run_context.definitions.has_key?(method_symbol.to_sym) - Chef::Log.deprecation("Definition #{method_symbol} (#{run_context.definitions[method_symbol.to_sym]}) was added to the run_context without calling Chef::DSL::Definitions.add_definition(#{method_symbol.to_sym.inspect}). This will become required in Chef 13.") + Chef.log_deprecation("Definition #{method_symbol} (#{run_context.definitions[method_symbol.to_sym]}) was added to the run_context without calling Chef::DSL::Definitions.add_definition(#{method_symbol.to_sym.inspect}). This will become required in Chef 13.") Chef::DSL::Definitions.add_definition(method_symbol) return send(method_symbol, *args, &block) end @@ -176,10 +173,32 @@ class Chef raise NameError, "No resource, method, or local variable named `#{method_symbol}' for #{describe_self_for_error}" end end + + module FullDSL + require 'chef/dsl/data_query' + require 'chef/dsl/platform_introspection' + require 'chef/dsl/include_recipe' + require 'chef/dsl/registry_helper' + require 'chef/dsl/reboot_pending' + require 'chef/dsl/audit' + require 'chef/dsl/powershell' + include Chef::DSL::DataQuery + include Chef::DSL::PlatformIntrospection + include Chef::DSL::IncludeRecipe + include Chef::DSL::Recipe + include Chef::DSL::RegistryHelper + include Chef::DSL::RebootPending + include Chef::DSL::Audit + include Chef::DSL::Powershell + end end end end +# Avoid circular references for things that are only used in instance methods +require 'chef/resource_builder' +require 'chef/resource' + # **DEPRECATED** # This used to be part of chef/mixin/recipe_definition_dsl_core. Load the file to activate the deprecation code. require 'chef/mixin/recipe_definition_dsl_core' diff --git a/lib/chef/dsl/resources.rb b/lib/chef/dsl/resources.rb index 1ce12ed0a0..49588ed516 100644 --- a/lib/chef/dsl/resources.rb +++ b/lib/chef/dsl/resources.rb @@ -10,14 +10,16 @@ class Chef def self.add_resource_dsl(dsl_name) begin module_eval(<<-EOM, __FILE__, __LINE__+1) - def #{dsl_name}(name=nil, created_at=nil, &block) - declare_resource(#{dsl_name.inspect}, name, created_at || caller[0], &block) + def #{dsl_name}(*args, &block) + Chef.log_deprecation("Cannot create resource #{dsl_name} with more than one argument. All arguments except the name (\#{args[0].inspect}) will be ignored. This will cause an error in Chef 13. Arguments: \#{args}") if args.size > 1 + declare_resource(#{dsl_name.inspect}, args[0], caller[0], &block) end EOM rescue SyntaxError # Handle the case where dsl_name has spaces, etc. - define_method(dsl_name.to_sym) do |name=nil, created_at=nil, &block| - declare_resource(dsl_name, name, created_at || caller[0], &block) + define_method(dsl_name.to_sym) do |*args, &block| + Chef.log_deprecation("Cannot create resource #{dsl_name} with more than one argument. All arguments except the name (#{args[0].inspect}) will be ignored. This will cause an error in Chef 13. Arguments: #{args}") if args.size > 1 + declare_resource(dsl_name, args[0], caller[0], &block) end end end diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index 73fe25ec13..1c9a58be23 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -47,14 +47,19 @@ class Chef def ohai_completed(node) end - # Already have a client key, assuming this node has registered. + # Announce that we're not going to register the client. Generally because + # we already have the private key, or because we're deliberately not using + # a key. def skipping_registration(node_name, config) end - # About to attempt to register as +node_name+ + # About to attempt to create a private key registered to the server with + # client +node_name+. def registration_start(node_name, config) end + # Successfully created the private key and registered this client with the + # server. def registration_completed end @@ -118,8 +123,8 @@ class Chef def cookbook_sync_start(cookbook_count) end - # Called when cookbook +cookbook_name+ has been sync'd - def synchronized_cookbook(cookbook_name) + # Called when cookbook +cookbook+ has been sync'd + def synchronized_cookbook(cookbook_name, cookbook) end # Called when an individual file in a cookbook has been updated @@ -269,26 +274,37 @@ class Chef # def notifications_resolved # end + # + # Resource events and ordering: + # + # 1. Start the action + # - resource_action_start + # 2. Check the guard + # - resource_skipped: (goto 7) if only_if/not_if say to skip + # 3. Load the current resource + # - resource_current_state_loaded + # - resource_current_state_load_bypassed (if not why-run safe) + # 4. Check if why-run safe + # - resource_bypassed: (goto 7) if not why-run safe + # 5. During processing: + # - resource_update_applied: For each actual change (many per action) + # 6. Processing complete status: + # - resource_failed if the resource threw an exception while running + # - resource_failed_retriable: (goto 3) if resource failed and will be retried + # - resource_updated if the resource was updated (resource_update_applied will have been called) + # - resource_up_to_date if the resource was up to date (no resource_update_applied) + # 7. Processing complete: + # - resource_completed + # + # Called before action is executed on a resource. def resource_action_start(resource, action, notification_type=nil, notifier=nil) end - # Called when a resource fails, but will retry. - def resource_failed_retriable(resource, action, retry_count, exception) - end - - # Called when a resource fails and will not be retried. - def resource_failed(resource, action, exception) - end - # Called when a resource action has been skipped b/c of a conditional def resource_skipped(resource, action, conditional) end - # Called when a resource action has been completed - def resource_completed(resource) - end - # Called after #load_current_resource has run. def resource_current_state_loaded(resource, action, current_resource) end @@ -302,21 +318,33 @@ class Chef def resource_bypassed(resource, action, current_resource) end - # Called when a resource has no converge actions, e.g., it was already correct. - def resource_up_to_date(resource, action) - end - # Called when a change has been made to a resource. May be called multiple # times per resource, e.g., a file may have its content updated, and then # its permissions updated. def resource_update_applied(resource, action, update) end + # Called when a resource fails, but will retry. + def resource_failed_retriable(resource, action, retry_count, exception) + end + + # Called when a resource fails and will not be retried. + def resource_failed(resource, action, exception) + end + # Called after a resource has been completely converged, but only if # modifications were made. def resource_updated(resource, action) end + # Called when a resource has no converge actions, e.g., it was already correct. + def resource_up_to_date(resource, action) + end + + # Called when a resource action has been completed + def resource_completed(resource) + end + # A stream has opened. def stream_opened(stream, options = {}) end @@ -352,8 +380,9 @@ class Chef def whyrun_assumption(action, resource, message) end - ## TODO: deprecation warning. this way we can queue them up and present - # them all at once. + # Emit a message about something being deprecated. + def deprecation(message, location=caller(2..2)[0]) + end # An uncategorized message. This supports the case that a user needs to # pass output that doesn't fit into one of the callbacks above. Note that diff --git a/lib/chef/event_dispatch/dispatcher.rb b/lib/chef/event_dispatch/dispatcher.rb index 370f8c51b4..966a3f32ec 100644 --- a/lib/chef/event_dispatch/dispatcher.rb +++ b/lib/chef/event_dispatch/dispatcher.rb @@ -25,18 +25,30 @@ class Chef # define the forwarding in one go: # - # Define a method that will be forwarded to all - def self.def_forwarding_method(method_name) - define_method(method_name) do |*args| - @subscribers.each { |s| s.send(method_name, *args) } + def call_subscribers(method_name, *args) + @subscribers.each do |s| + # Skip new/unsupported event names. + next if !s.respond_to?(method_name) + mth = s.method(method_name) + # Trim arguments to match what the subscriber expects to allow + # adding new arguments without breaking compat. + args = args.take(mth.arity) if mth.arity < args.size && mth.arity >= 0 + mth.call(*args) end end (Base.instance_methods - Object.instance_methods).each do |method_name| - def_forwarding_method(method_name) + class_eval <<-EOM + def #{method_name}(*args) + call_subscribers(#{method_name.inspect}, *args) + end + EOM end + # Special case deprecation, since it needs to know its caller + def deprecation(message, location=caller(2..2)[0]) + call_subscribers(:deprecation, message, location) + end end end end - diff --git a/lib/chef/event_dispatch/dsl.rb b/lib/chef/event_dispatch/dsl.rb new file mode 100644 index 0000000000..c6f21c9b45 --- /dev/null +++ b/lib/chef/event_dispatch/dsl.rb @@ -0,0 +1,64 @@ +# +# Author:: Ranjib Dey (<ranjib@linux.com>) +# Copyright:: Copyright (c) 2015 Ranjib Dey +# 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/event_dispatch/base' +require 'chef/exceptions' +require 'chef/config' + +class Chef + module EventDispatch + class DSL + attr_reader :handler + + def initialize(name) + klass = Class.new(Chef::EventDispatch::Base) do + attr_reader :name + end + @handler = klass.new + @handler.instance_variable_set(:@name, name) + + # Use event.register API to add anonymous handler if Chef.run_context + # and associated event dispatcher is set, else fallback to + # Chef::Config[:hanlder] + if Chef.run_context && Chef.run_context.events + Chef::Log.debug("Registering handler '#{name}' using events api") + Chef.run_context.events.register(handler) + else + Chef::Log.debug("Registering handler '#{name}' using global config") + Chef::Config[:event_handlers] << handler + end + end + + # Adds a new event handler derived from base handler + # with user defined block against a chef event + # + # @return [Chef::EventDispatch::Base] a base handler object + def on(event_type, &block) + validate!(event_type) + handler.define_singleton_method(event_type) do |*args| + instance_exec(*args, &block) + end + end + + private + def validate!(event_type) + all_event_types = (Chef::EventDispatch::Base.instance_methods - Object.instance_methods) + raise Chef::Exceptions::InvalidEventType, "Invalid event type: #{event_type}" unless all_event_types.include?(event_type) + end + end + end +end diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index dd0bac3cf9..e3649c068b 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -17,12 +17,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'chef-config/exceptions' + class Chef # == Chef::Exceptions # Chef's custom exceptions are all contained within the Chef::Exceptions # namespace. class Exceptions + ConfigurationError = ChefConfig::ConfigurationError + # Backcompat with Chef::ShellOut code: require 'mixlib/shellout/exceptions' @@ -68,7 +72,6 @@ class Chef class DuplicateRole < RuntimeError; end class ValidationFailed < ArgumentError; end class InvalidPrivateKey < ArgumentError; end - class ConfigurationError < ArgumentError; end class MissingKeyAttribute < ArgumentError; end class KeyCommandInputError < ArgumentError; end class InvalidKeyArgument < ArgumentError; end @@ -97,7 +100,14 @@ class Chef class ConflictingMembersInGroup < ArgumentError; end class InvalidResourceReference < RuntimeError; end class ResourceNotFound < RuntimeError; end + class ProviderNotFound < RuntimeError; end + NoProviderAvailable = ProviderNotFound class VerificationNotFound < RuntimeError; end + class InvalidEventType < ArgumentError; end + class MultipleIdentityError < RuntimeError; end + # Used in Resource::ActionProvider#load_current_resource to denote that + # the resource doesn't actually exist (for example, the file does not exist) + class CurrentValueDoesNotExist < RuntimeError; end # Can't find a Resource of this type that is valid on this platform. class NoSuchResourceType < NameError @@ -119,6 +129,23 @@ class Chef class EnclosingDirectoryDoesNotExist < ArgumentError; end # Errors originating from calls to the Win32 API class Win32APIError < RuntimeError; end + + class Win32NetAPIError < Win32APIError + attr_reader :msg, :error_code + def initialize(msg, error_code) + @msg = msg + @error_code = error_code + + formatted_message = "" + formatted_message << "---- Begin Win32 API output ----\n" + formatted_message << "Net Api Error Code: #{error_code}\n" + formatted_message << "Net Api Error Message: #{msg}\n" + formatted_message << "---- End Win32 API output ----\n" + + super(formatted_message) + end + end + # Thrown when Win32 API layer binds to non-existent Win32 function. Occurs # when older versions of Windows don't support newer Win32 API functions. class Win32APIFunctionNotImplemented < NotImplementedError; end @@ -218,8 +245,6 @@ class Chef class ChildConvergeError < RuntimeError; end - class NoProviderAvailable < RuntimeError; end - class DeprecatedFeatureError < RuntimeError; def initalize(message) super("#{message} (raising error due to treat_deprecation_warnings_as_errors being set)") diff --git a/lib/chef/formatters/base.rb b/lib/chef/formatters/base.rb index c901068aa0..d3756ef00c 100644 --- a/lib/chef/formatters/base.rb +++ b/lib/chef/formatters/base.rb @@ -212,6 +212,9 @@ class Chef file_load_failed(path, exception) end + def deprecation(message, location=caller(2..2)[0]) + Chef::Log.deprecation("#{message} at #{location}") + end end diff --git a/lib/chef/formatters/doc.rb b/lib/chef/formatters/doc.rb index e76a940c38..614cc44e6d 100644 --- a/lib/chef/formatters/doc.rb +++ b/lib/chef/formatters/doc.rb @@ -22,6 +22,7 @@ class Chef @failed_audits = 0 @start_time = Time.now @end_time = @start_time + @skipped_resources = 0 end def elapsed_time @@ -33,7 +34,7 @@ class Chef end def total_resources - @up_to_date_resources + @updated_resources + @up_to_date_resources + @updated_resources + @skipped_resources end def total_audits @@ -42,6 +43,26 @@ class Chef def run_completed(node) @end_time = Time.now + # Print out deprecations. + if !deprecations.empty? + puts_line "" + puts_line "Deprecated features used!" + deprecations.each do |message, locations| + if locations.size == 1 + puts_line " #{message} at #{locations.size} location:" + else + puts_line " #{message} at #{locations.size} locations:" + end + locations.each do |location| + prefix = " - " + Array(location).each do |line| + puts_line "#{prefix}#{line}" + prefix = " " + end + end + end + puts_line "" + end if Chef::Config[:why_run] puts_line "Chef Client finished, #{@updated_resources}/#{total_resources} resources would have been updated" else @@ -132,9 +153,9 @@ class Chef indent end - # Called when cookbook +cookbook_name+ has been sync'd - def synchronized_cookbook(cookbook_name) - puts_line "- #{cookbook_name}" + # Called when cookbook +cookbook+ has been sync'd + def synchronized_cookbook(cookbook_name, cookbook) + puts_line "- #{cookbook.name} (#{cookbook.version})" end # Called when an individual file in a cookbook has been updated @@ -236,6 +257,7 @@ class Chef # Called when a resource action has been skipped b/c of a conditional def resource_skipped(resource, action, conditional) + @skipped_resources += 1 # TODO: more info about conditional puts " (skipped due to #{conditional.short_description})", :stream => resource unindent @@ -334,6 +356,16 @@ class Chef end end + def deprecation(message, location=caller(2..2)[0]) + if Chef::Config[:treat_deprecation_warnings_as_errors] + super + end + + # Save deprecations to the screen until the end + deprecations[message] ||= Set.new + deprecations[message] << location + end + def indent indent_by(2) end @@ -341,6 +373,12 @@ class Chef def unindent indent_by(-2) end + + protected + + def deprecations + @deprecations ||= {} + end end end end diff --git a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb index d64d5e7b01..fe418ed485 100644 --- a/lib/chef/formatters/error_inspectors/compile_error_inspector.rb +++ b/lib/chef/formatters/error_inspectors/compile_error_inspector.rb @@ -44,6 +44,18 @@ class Chef error_description.section("Cookbook Trace:", traceback) error_description.section("Relevant File Content:", context) end + + if exception_message_modifying_frozen? + msg = <<-MESSAGE + Chef calls the freeze method on certain ruby objects to prevent + pollution across multiple instances. Specifically, resource + properties have frozen default values to avoid modifying the + property for all instances of a resource. Try modifying the + particular instance variable or using an instance accessor instead. + MESSAGE + + error_description.section("Additional information:", msg.gsub(/^ {6}/, '')) + end end def context @@ -111,6 +123,10 @@ class Chef end end + def exception_message_modifying_frozen? + exception.message.include?("can't modify frozen") + end + end end diff --git a/lib/chef/formatters/minimal.rb b/lib/chef/formatters/minimal.rb index a189cc67eb..3862951f76 100644 --- a/lib/chef/formatters/minimal.rb +++ b/lib/chef/formatters/minimal.rb @@ -109,8 +109,8 @@ class Chef puts "Synchronizing cookbooks" end - # Called when cookbook +cookbook_name+ has been sync'd - def synchronized_cookbook(cookbook_name) + # Called when cookbook +cookbook+ has been sync'd + def synchronized_cookbook(cookbook_name, cookbook) print "." end diff --git a/lib/chef/guard_interpreter/resource_guard_interpreter.rb b/lib/chef/guard_interpreter/resource_guard_interpreter.rb index d4b386a15a..8cff3bc032 100644 --- a/lib/chef/guard_interpreter/resource_guard_interpreter.rb +++ b/lib/chef/guard_interpreter/resource_guard_interpreter.rb @@ -68,7 +68,10 @@ class Chef run_action = action || @resource.action begin - @resource.run_action(run_action) + # Coerce to an array to be safe. This could happen with a legacy + # resource or something overriding the default_action code in a + # subclass. + Array(run_action).each {|action_to_run| @resource.run_action(action_to_run) } resource_updated = @resource.updated rescue Mixlib::ShellOut::ShellCommandFailed resource_updated = nil diff --git a/lib/chef/http/http_request.rb b/lib/chef/http/http_request.rb index 7582f4458f..1baf5724ae 100644 --- a/lib/chef/http/http_request.rb +++ b/lib/chef/http/http_request.rb @@ -40,7 +40,7 @@ class Chef engine = defined?(RUBY_ENGINE) ? RUBY_ENGINE : "ruby" - UA_COMMON = "/#{::Chef::VERSION} (#{engine}-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}; ohai-#{Ohai::VERSION}; #{RUBY_PLATFORM}; +http://opscode.com)" + UA_COMMON = "/#{::Chef::VERSION} (#{engine}-#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}; ohai-#{Ohai::VERSION}; #{RUBY_PLATFORM}; +https://chef.io)" DEFAULT_UA = "Chef Client" << UA_COMMON USER_AGENT = "User-Agent".freeze diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb index 4a93697a1b..46e968827e 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -87,6 +87,7 @@ class Chef def self.inherited(subclass) unless subclass.unnamed? subcommands[subclass.snake_case_name] = subclass + subcommand_files[subclass.snake_case_name] += [caller[0].split(/:\d+/).first] end end @@ -121,17 +122,29 @@ class Chef end def self.subcommand_loader - @subcommand_loader ||= Knife::SubcommandLoader.new(chef_config_dir) + @subcommand_loader ||= Chef::Knife::SubcommandLoader.for_config(chef_config_dir) end def self.load_commands @commands_loaded ||= subcommand_loader.load_commands end + def self.guess_category(args) + subcommand_loader.guess_category(args) + end + + def self.subcommand_class_from(args) + subcommand_loader.command_class_from(args) || subcommand_not_found!(args) + end + def self.subcommands @@subcommands ||= {} end + def self.subcommand_files + @@subcommand_files ||= Hash.new([]) + end + def self.subcommands_by_category unless @subcommands_by_category @subcommands_by_category = Hash.new { |hash, key| hash[key] = [] } @@ -142,30 +155,6 @@ class Chef @subcommands_by_category end - # Print the list of subcommands knife knows about. If +preferred_category+ - # is given, only subcommands in that category are shown - def self.list_commands(preferred_category=nil) - load_commands - - category_desc = preferred_category ? preferred_category + " " : '' - msg "Available #{category_desc}subcommands: (for details, knife SUB-COMMAND --help)\n\n" - - if preferred_category && subcommands_by_category.key?(preferred_category) - commands_to_show = {preferred_category => subcommands_by_category[preferred_category]} - else - commands_to_show = subcommands_by_category - end - - commands_to_show.sort.each do |category, commands| - next if category =~ /deprecated/i - msg "** #{category.upcase} COMMANDS **" - commands.sort.each do |command| - msg subcommands[command].banner if subcommands[command] - end - msg - end - end - # Shared with subclasses @@chef_config_dir = nil @@ -206,7 +195,6 @@ class Chef Chef::Log.level(:debug) end - load_commands subcommand_class = subcommand_class_from(args) subcommand_class.options = options.merge!(subcommand_class.options) subcommand_class.load_deps @@ -215,34 +203,6 @@ class Chef instance.run_with_pretty_exceptions end - def self.guess_category(args) - category_words = args.select {|arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ } - category_words.map! {|w| w.split('-')}.flatten! - matching_category = nil - while (!matching_category) && (!category_words.empty?) - candidate_category = category_words.join(' ') - matching_category = candidate_category if subcommands_by_category.key?(candidate_category) - matching_category || category_words.pop - end - matching_category - end - - def self.subcommand_class_from(args) - command_words = args.select {|arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ } - - subcommand_class = nil - - while ( !subcommand_class ) && ( !command_words.empty? ) - snake_case_class_name = command_words.join("_") - unless subcommand_class = subcommands[snake_case_class_name] - command_words.pop - end - end - # see if we got the command as e.g., knife node-list - subcommand_class ||= subcommands[args.first.gsub('-', '_')] - subcommand_class || subcommand_not_found!(args) - end - def self.dependency_loaders @dependency_loaders ||= [] end @@ -265,7 +225,13 @@ class Chef # Error out and print usage. probably because the arguments given by the # user could not be resolved to a subcommand. def self.subcommand_not_found!(args) - ui.fatal("Cannot find sub command for: '#{args.join(' ')}'") + ui.fatal("Cannot find subcommand for: '#{args.join(' ')}'") + + # Mention rehash when the subcommands cache(plugin_manifest.json) is used + if subcommand_loader.is_a?(Chef::Knife::SubcommandLoader::HashedCommandLoader) || + subcommand_loader.is_a?(Chef::Knife::SubcommandLoader::CustomManifestLoader) + ui.info("If this is a recently installed plugin, please run 'knife rehash' to update the subcommands cache.") + end if category_commands = guess_category(args) list_commands(category_commands) @@ -280,6 +246,20 @@ class Chef exit 10 end + def self.list_commands(preferred_category=nil) + category_desc = preferred_category ? preferred_category + " " : '' + msg "Available #{category_desc}subcommands: (for details, knife SUB-COMMAND --help)\n\n" + subcommand_loader.list_commands(preferred_category).sort.each do |category, commands| + next if category =~ /deprecated/i + msg "** #{category.upcase} COMMANDS **" + commands.sort.each do |command| + subcommand_loader.load_command(command) + msg subcommands[command].banner if subcommands[command] + end + msg + end + end + def self.reset_config_path! @@chef_config_dir = nil end diff --git a/lib/chef/knife/bootstrap.rb b/lib/chef/knife/bootstrap.rb index 5b29591fcc..f173b6b909 100644 --- a/lib/chef/knife/bootstrap.rb +++ b/lib/chef/knife/bootstrap.rb @@ -143,6 +143,12 @@ class Chef :proc => lambda { |o| o.split(/[\s,]+/) }, :default => [] + option :tags, + :long => "--tags TAGS", + :description => "Comma separated list of tags to apply to the node", + :proc => lambda { |o| o.split(/[\s,]+/) }, + :default => [] + option :first_boot_attributes, :short => "-j JSON_ATTRIBS", :long => "--json-attributes", diff --git a/lib/chef/knife/bootstrap/chef_vault_handler.rb b/lib/chef/knife/bootstrap/chef_vault_handler.rb index 749f61e6da..f658957499 100644 --- a/lib/chef/knife/bootstrap/chef_vault_handler.rb +++ b/lib/chef/knife/bootstrap/chef_vault_handler.rb @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # +require 'chef/knife/bootstrap' class Chef class Knife diff --git a/lib/chef/knife/bootstrap/client_builder.rb b/lib/chef/knife/bootstrap/client_builder.rb index b9c1d98bec..59b0cabd49 100644 --- a/lib/chef/knife/bootstrap/client_builder.rb +++ b/lib/chef/knife/bootstrap/client_builder.rb @@ -20,6 +20,7 @@ require 'chef/node' require 'chef/rest' require 'chef/api_client/registration' require 'chef/api_client' +require 'chef/knife/bootstrap' require 'tmpdir' class Chef @@ -140,6 +141,9 @@ class Chef node.run_list(normalized_run_list) node.normal_attrs = first_boot_attributes if first_boot_attributes node.environment(environment) if environment + (knife_config[:tags] || []).each do |tag| + node.tags << tag + end node end end diff --git a/lib/chef/knife/bootstrap/templates/archlinux-gems.erb b/lib/chef/knife/bootstrap/templates/archlinux-gems.erb deleted file mode 100644 index 55d2c0cc12..0000000000 --- a/lib/chef/knife/bootstrap/templates/archlinux-gems.erb +++ /dev/null @@ -1,76 +0,0 @@ -bash -c ' -<%= "export http_proxy=\"#{knife_config[:bootstrap_proxy]}\"" if knife_config[:bootstrap_proxy] -%> - -if [ ! -f /usr/bin/chef-client ]; then - pacman -Syy - pacman -S --noconfirm ruby ntp base-devel - ntpdate -u pool.ntp.org - gem install ohai --no-user-install --no-document --verbose - gem install chef --no-user-install --no-document --verbose <%= Chef::VERSION %> -fi - -mkdir -p /etc/chef - -<% if validation_key -%> -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem -<% end -%> - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<% unless trusted_certs.empty? -%> -mkdir -p /etc/chef/trusted_certs -<%= trusted_certs %> -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= Chef::JSONCompat.to_json(hash) %> -EOP -<% end -%> -<% end -%> - -<% if client_pem -%> -cat > /etc/chef/client.pem <<'EOP' -<%= ::File.read(::File.expand_path(client_pem)) %> -EOP -chmod 0600 /etc/chef/client.pem -<% end -%> - -cat > /etc/chef/client.rb <<'EOP' -log_level :info -log_location STDOUT -chef_server_url "<%= @chef_config[:chef_server_url] %>" -validation_client_name "<%= @chef_config[:validation_client_name] %>" -<% if @config[:chef_node_name] -%> -node_name "<%= @config[:chef_node_name] %>" -<% else -%> -# Using default node name (fqdn) -<% end -%> -# ArchLinux follows the Filesystem Hierarchy Standard -file_cache_path "/var/cache/chef" -file_backup_path "/var/lib/chef/backup" -pid_file "/var/run/chef/client.pid" -cache_options({ :path => "/var/cache/chef/checksums", :skip_expires => true}) -<% if knife_config[:bootstrap_proxy] %> -http_proxy "<%= knife_config[:bootstrap_proxy] %>" -https_proxy "<%= knife_config[:bootstrap_proxy] %>" -<% end -%> -EOP - -cat > /etc/chef/first-boot.json <<'EOP' -<%= Chef::JSONCompat.to_json(first_boot) %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/templates/chef-aix.erb b/lib/chef/knife/bootstrap/templates/chef-aix.erb deleted file mode 100644 index 45fbba7b48..0000000000 --- a/lib/chef/knife/bootstrap/templates/chef-aix.erb +++ /dev/null @@ -1,72 +0,0 @@ -ksh -c ' - -function exists { - if type $1 >/dev/null 2>&1 - then - return 0 - else - return 1 - fi -} - -if ! exists /usr/bin/chef-client; then - <% if @chef_config[:aix_package] -%> - # Read the download URL/location from knife.rb with option aix_package - rm -rf /tmp/chef_installer # ensure there no older pkg - echo "<%= @chef_config[:aix_package] %>" - perl -e '\''use LWP::Simple; getprint($ARGV[0]);'\'' <%= @chef_config[:aix_package] %> > /tmp/chef_installer - installp -aYF -d /tmp/chef_installer chef - <% else -%> - echo ":aix_package location is not set in knife.rb" - exit - <% end -%> -fi - -mkdir -p /etc/chef - -<% if client_pem -%> -cat > /etc/chef/client.pem <<'EOP' -<%= ::File.read(::File.expand_path(client_pem)) %> -EOP -chmod 0600 /etc/chef/client.pem -<% end -%> - -<% if validation_key -%> -cat > /etc/chef/validation.pem <<'EOP' -<%= validation_key %> -EOP -chmod 0600 /etc/chef/validation.pem -<% end -%> - -<% if encrypted_data_bag_secret -%> -cat > /etc/chef/encrypted_data_bag_secret <<'EOP' -<%= encrypted_data_bag_secret %> -EOP -chmod 0600 /etc/chef/encrypted_data_bag_secret -<% end -%> - -<% unless trusted_certs.empty? -%> -mkdir -p /etc/chef/trusted_certs -<%= trusted_certs %> -<% end -%> - -<%# Generate Ohai Hints -%> -<% unless @chef_config[:knife][:hints].nil? || @chef_config[:knife][:hints].empty? -%> -mkdir -p /etc/chef/ohai/hints - -<% @chef_config[:knife][:hints].each do |name, hash| -%> -cat > /etc/chef/ohai/hints/<%= name %>.json <<'EOP' -<%= Chef::JSONCompat.to_json(hash) %> -EOP -<% end -%> -<% end -%> - -cat > /etc/chef/client.rb <<'EOP' -<%= config_content %> -EOP - -cat > /etc/chef/first-boot.json <<'EOP' -<%= Chef::JSONCompat.to_json(first_boot) %> -EOP - -<%= start_chef %>' diff --git a/lib/chef/knife/bootstrap/templates/chef-full.erb b/lib/chef/knife/bootstrap/templates/chef-full.erb index 335b1f181c..575aec0f50 100644 --- a/lib/chef/knife/bootstrap/templates/chef-full.erb +++ b/lib/chef/knife/bootstrap/templates/chef-full.erb @@ -12,7 +12,7 @@ tmp_dir="$tmp/install.sh.$$" (umask 077 && mkdir $tmp_dir) || exit 1 exists() { - if command -v $1 &>/dev/null + if command -v $1 >/dev/null 2>&1 then return 0 else @@ -166,12 +166,12 @@ do_download() { <%= knife_config[:bootstrap_install_command] %> <% else %> install_sh="<%= knife_config[:bootstrap_url] ? knife_config[:bootstrap_url] : "https://www.opscode.com/chef/install.sh" %>" - if ! exists /usr/bin/chef-client; then + if test -f /usr/bin/chef-client; then + echo "-----> Existing Chef installation detected" + else echo "-----> Installing Chef Omnibus (<%= latest_current_chef_version_string %>)" do_download ${install_sh} $tmp_dir/install.sh sh $tmp_dir/install.sh -P chef <%= latest_current_chef_version_string %> - else - echo "-----> Existing Chef installation detected" fi <% end %> @@ -226,6 +226,6 @@ cat > /etc/chef/first-boot.json <<EOP <%= Chef::JSONCompat.to_json(first_boot) %> EOP -echo "Starting first Chef Client run..." +echo "Starting the first Chef Client run..." <%= start_chef %>' diff --git a/lib/chef/knife/client_bulk_delete.rb b/lib/chef/knife/client_bulk_delete.rb index f2be772759..b439e6f995 100644 --- a/lib/chef/knife/client_bulk_delete.rb +++ b/lib/chef/knife/client_bulk_delete.rb @@ -23,7 +23,7 @@ class Chef class ClientBulkDelete < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -39,7 +39,7 @@ class Chef ui.fatal("You must supply a regular expression to match the results against") exit 42 end - all_clients = Chef::ApiClient.list(true) + all_clients = Chef::ApiClientV1.list(true) matcher = /#{name_args[0]}/ clients_to_delete = {} diff --git a/lib/chef/knife/client_create.rb b/lib/chef/knife/client_create.rb index 570c1ee950..fa9a1a7e32 100644 --- a/lib/chef/knife/client_create.rb +++ b/lib/chef/knife/client_create.rb @@ -23,7 +23,7 @@ class Chef class ClientCreate < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -57,12 +57,12 @@ class Chef banner "knife client create CLIENTNAME (options)" def client - @client_field ||= Chef::ApiClient.new + @client_field ||= Chef::ApiClientV1.new end def create_client(client) # should not be using save :( bad behavior - client.save + Chef::ApiClientV1.from_hash(client).save end def run @@ -93,7 +93,7 @@ class Chef output = edit_data(client) final_client = create_client(output) - ui.info("Created #{output}") + ui.info("Created #{final_client}") # output private_key if one if final_client.private_key diff --git a/lib/chef/knife/client_delete.rb b/lib/chef/knife/client_delete.rb index d7d302ee1d..a49c0867a8 100644 --- a/lib/chef/knife/client_delete.rb +++ b/lib/chef/knife/client_delete.rb @@ -23,7 +23,7 @@ class Chef class ClientDelete < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -43,8 +43,8 @@ class Chef exit 1 end - delete_object(Chef::ApiClient, @client_name, 'client') { - object = Chef::ApiClient.load(@client_name) + delete_object(Chef::ApiClientV1, @client_name, 'client') { + object = Chef::ApiClientV1.load(@client_name) if object.validator unless config[:delete_validators] ui.fatal("You must specify --delete-validators to delete the validator client #{@client_name}") diff --git a/lib/chef/knife/client_edit.rb b/lib/chef/knife/client_edit.rb index c81bce902a..5dcd8f212b 100644 --- a/lib/chef/knife/client_edit.rb +++ b/lib/chef/knife/client_edit.rb @@ -23,7 +23,7 @@ class Chef class ClientEdit < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -38,7 +38,15 @@ class Chef exit 1 end - edit_object(Chef::ApiClient, @client_name) + original_data = Chef::ApiClientV1.load(@client_name).to_hash + edited_client = edit_data(original_data) + if original_data != edited_client + client = Chef::ApiClientV1.from_hash(edited_client) + client.save + ui.msg("Saved #{client}.") + else + ui.msg("Client unchanged, not saving.") + end end end end diff --git a/lib/chef/knife/client_list.rb b/lib/chef/knife/client_list.rb index da0bf12dc3..d8a3698b6a 100644 --- a/lib/chef/knife/client_list.rb +++ b/lib/chef/knife/client_list.rb @@ -23,7 +23,7 @@ class Chef class ClientList < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -35,7 +35,7 @@ class Chef :description => "Show corresponding URIs" def run - output(format_list_for_display(Chef::ApiClient.list)) + output(format_list_for_display(Chef::ApiClientV1.list)) end end end diff --git a/lib/chef/knife/client_reregister.rb b/lib/chef/knife/client_reregister.rb index 666fd09fd2..b94761e718 100644 --- a/lib/chef/knife/client_reregister.rb +++ b/lib/chef/knife/client_reregister.rb @@ -23,7 +23,7 @@ class Chef class ClientReregister < Knife deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -43,7 +43,7 @@ class Chef exit 1 end - client = Chef::ApiClient.reregister(@client_name) + client = Chef::ApiClientV1.reregister(@client_name) Chef::Log.debug("Updated client data: #{client.inspect}") key = client.private_key if config[:file] diff --git a/lib/chef/knife/client_show.rb b/lib/chef/knife/client_show.rb index 822848fdc2..bdac3f9758 100644 --- a/lib/chef/knife/client_show.rb +++ b/lib/chef/knife/client_show.rb @@ -25,7 +25,7 @@ class Chef include Knife::Core::MultiAttributeReturnOption deps do - require 'chef/api_client' + require 'chef/api_client_v1' require 'chef/json_compat' end @@ -40,7 +40,7 @@ class Chef exit 1 end - client = Chef::ApiClient.load(@client_name) + client = Chef::ApiClientV1.load(@client_name) output(format_for_display(client)) end diff --git a/lib/chef/knife/core/bootstrap_context.rb b/lib/chef/knife/core/bootstrap_context.rb index 7197653489..867b6fe366 100644 --- a/lib/chef/knife/core/bootstrap_context.rb +++ b/lib/chef/knife/core/bootstrap_context.rb @@ -163,11 +163,14 @@ CONFIG end def first_boot - (@config[:first_boot_attributes] || {}).merge(:run_list => @run_list) + (@config[:first_boot_attributes] || {}).tap do |attributes| + attributes.merge!(:run_list => @run_list) + attributes.merge!(:tags => @config[:tags]) if @config[:tags] && !@config[:tags].empty? + end end private - + # Returns a string for copying the trusted certificates on the workstation to the system being bootstrapped # This string should contain both the commands necessary to both create the files, as well as their content def trusted_certs_content diff --git a/lib/chef/knife/core/custom_manifest_loader.rb b/lib/chef/knife/core/custom_manifest_loader.rb new file mode 100644 index 0000000000..c19e749f32 --- /dev/null +++ b/lib/chef/knife/core/custom_manifest_loader.rb @@ -0,0 +1,69 @@ +# 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/version' +class Chef + class Knife + class SubcommandLoader + + # + # Load a subcommand from a user-supplied + # manifest file + # + class CustomManifestLoader < Chef::Knife::SubcommandLoader + attr_accessor :manifest + def initialize(chef_config_dir, plugin_manifest) + super(chef_config_dir) + @manifest = plugin_manifest + end + + # If the user has created a ~/.chef/plugin_manifest.json file, we'll use + # that instead of inspecting the on-system gems to find the plugins. The + # file format is expected to look like: + # + # { "plugins": { + # "knife-ec2": { + # "paths": [ + # "/home/alice/.rubymanagerthing/gems/knife-ec2-x.y.z/lib/chef/knife/ec2_server_create.rb", + # "/home/alice/.rubymanagerthing/gems/knife-ec2-x.y.z/lib/chef/knife/ec2_server_delete.rb" + # ] + # } + # } + # } + # + # Extraneous content in this file is ignored. This is intentional so that we + # can adapt the file format for potential behavior changes to knife in + # the future. + def find_subcommands_via_manifest + # Format of subcommand_files is "relative_path" (something you can + # Kernel.require()) => full_path. The relative path isn't used + # currently, so we just map full_path => full_path. + subcommand_files = {} + manifest["plugins"].each do |plugin_name, plugin_manifest| + plugin_manifest["paths"].each do |cmd_path| + subcommand_files[cmd_path] = cmd_path + end + end + subcommand_files.merge(find_subcommands_via_dirglob) + end + + def subcommand_files + subcommand_files ||= (find_subcommands_via_manifest.values + site_subcommands).flatten.uniq + end + end + end + end +end diff --git a/lib/chef/knife/core/gem_glob_loader.rb b/lib/chef/knife/core/gem_glob_loader.rb new file mode 100644 index 0000000000..d09131aacb --- /dev/null +++ b/lib/chef/knife/core/gem_glob_loader.rb @@ -0,0 +1,138 @@ +# Author:: Christopher Brown (<cb@opscode.com>) +# Author:: Daniel DeLeo (<dan@opscode.com>) +# Copyright:: Copyright (c) 2009-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/version' +require 'chef/util/path_helper' +class Chef + class Knife + class SubcommandLoader + class GemGlobLoader < Chef::Knife::SubcommandLoader + MATCHES_CHEF_GEM = %r{/chef-[\d]+\.[\d]+\.[\d]+}.freeze + MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze + + def subcommand_files + @subcommand_files ||= (gem_and_builtin_subcommands.values + site_subcommands).flatten.uniq + end + + # Returns a Hash of paths to knife commands built-in to chef, or installed via gem. + # If rubygems is not installed, falls back to globbing the knife directory. + # The Hash is of the form {"relative/path" => "/absolute/path"} + #-- + # Note: the "right" way to load the plugins is to require the relative path, i.e., + # require 'chef/knife/command' + # but we're getting frustrated by bugs at every turn, and it's slow besides. So + # subcommand loader has been modified to load the plugins by using Kernel.load + # with the absolute path. + def gem_and_builtin_subcommands + require 'rubygems' + find_subcommands_via_rubygems + rescue LoadError + find_subcommands_via_dirglob + end + + def find_subcommands_via_dirglob + # The "require paths" of the core knife subcommands bundled with chef + files = Dir[File.join(Chef::Util::PathHelper.escape_glob(File.expand_path('../../../knife', __FILE__)), '*.rb')] + subcommand_files = {} + files.each do |knife_file| + rel_path = knife_file[/#{CHEF_ROOT}#{Regexp.escape(File::SEPARATOR)}(.*)\.rb/,1] + subcommand_files[rel_path] = knife_file + end + subcommand_files + end + + def find_subcommands_via_rubygems + files = find_files_latest_gems 'chef/knife/*.rb' + subcommand_files = {} + files.each do |file| + rel_path = file[/(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 1] + + # When not installed as a gem (ChefDK/appbundler in particular), AND + # a different version of Chef is installed via gems, `files` will + # include some files from the 'other' Chef install. If this contains + # a knife command that doesn't exist in this version of Chef, we will + # get a LoadError later when we try to require it. + next if from_different_chef_version?(file) + + subcommand_files[rel_path] = file + end + + subcommand_files.merge(find_subcommands_via_dirglob) + end + + private + + def find_files_latest_gems(glob, check_load_path=true) + files = [] + + if check_load_path + files = $LOAD_PATH.map { |load_path| + Dir["#{File.expand_path glob, Chef::Util::PathHelper.escape_glob(load_path)}#{Gem.suffix_pattern}"] + }.flatten.select { |file| File.file? file.untaint } + end + + gem_files = latest_gem_specs.map do |spec| + # Gem::Specification#matches_for_glob wasn't added until RubyGems 1.8 + if spec.respond_to? :matches_for_glob + spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}") + else + check_spec_for_glob(spec, glob) + end + end.flatten + + files.concat gem_files + files.uniq! if check_load_path + + return files + end + + def latest_gem_specs + @latest_gem_specs ||= if Gem::Specification.respond_to? :latest_specs + Gem::Specification.latest_specs(true) # find prerelease gems + else + Gem.source_index.latest_specs(true) + end + end + + def check_spec_for_glob(spec, glob) + dirs = if spec.require_paths.size > 1 then + "{#{spec.require_paths.join(',')}}" + else + spec.require_paths.first + end + + glob = File.join(Chef::Util::PathHelper.escape_glob(spec.full_gem_path, dirs), glob) + + Dir[glob].map { |f| f.untaint } + end + + def from_different_chef_version?(path) + matches_any_chef_gem?(path) && !matches_this_chef_gem?(path) + end + + def matches_any_chef_gem?(path) + path =~ MATCHES_CHEF_GEM + end + + def matches_this_chef_gem?(path) + path =~ MATCHES_THIS_CHEF_GEM + end + end + end + end +end diff --git a/lib/chef/knife/core/hashed_command_loader.rb b/lib/chef/knife/core/hashed_command_loader.rb new file mode 100644 index 0000000000..6eb3635726 --- /dev/null +++ b/lib/chef/knife/core/hashed_command_loader.rb @@ -0,0 +1,80 @@ +# Author:: Steven Danna (<steve@chef.io>) +# 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/version' +class Chef + class Knife + class SubcommandLoader + # + # Load a subcommand from a pre-computed path + # for the given command. + # + class HashedCommandLoader < Chef::Knife::SubcommandLoader + KEY = '_autogenerated_command_paths' + + attr_accessor :manifest + def initialize(chef_config_dir, plugin_manifest) + super(chef_config_dir) + @manifest = plugin_manifest + end + + def guess_category(args) + category_words = positional_arguments(args) + category_words.map! { |w| w.split('-') }.flatten! + find_longest_key(manifest[KEY]["plugins_by_category"], category_words, ' ') + end + + def list_commands(pref_category=nil) + if pref_category || manifest[KEY]["plugins_by_category"].key?(pref_category) + { pref_category => manifest[KEY]["plugins_by_category"][pref_category] } + else + manifest[KEY]["plugins_by_category"] + end + end + + def subcommand_files + manifest[KEY]["plugins_paths"].values.flatten + end + + def load_command(args) + paths = manifest[KEY]["plugins_paths"][subcommand_for_args(args)] + if paths.nil? || paths.empty? || (! paths.is_a? Array) + false + else + paths.each do |sc| + if File.exists?(sc) + Kernel.load sc + else + Chef::Log.error "The file #{sc} is missing for subcommand '#{subcommand_for_args(args)}'. Please rehash to update the subcommands cache." + return false + end + end + true + end + end + + def subcommand_for_args(args) + if manifest[KEY]["plugins_paths"].key?(args) + args + else + find_longest_key(manifest[KEY]["plugins_paths"], args, "_") + end + end + end + end + end +end diff --git a/lib/chef/knife/core/object_loader.rb b/lib/chef/knife/core/object_loader.rb index 698b09ac84..97ca381471 100644 --- a/lib/chef/knife/core/object_loader.rb +++ b/lib/chef/knife/core/object_loader.rb @@ -18,6 +18,7 @@ require 'ffi_yajl' require 'chef/util/path_helper' +require 'chef/data_bag_item' class Chef class Knife diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb index a8705c724f..808e053c40 100644 --- a/lib/chef/knife/core/subcommand_loader.rb +++ b/lib/chef/knife/core/subcommand_loader.rb @@ -18,105 +18,120 @@ require 'chef/version' require 'chef/util/path_helper' +require 'chef/knife/core/gem_glob_loader' +require 'chef/knife/core/hashed_command_loader' +require 'chef/knife/core/custom_manifest_loader' + class Chef class Knife + # + # Public Methods of a Subcommand Loader + # + # load_commands - loads all available subcommands + # load_command(args) - loads subcommands for the given args + # list_commands(args) - lists all available subcommands, + # optionally filtering by category + # subcommand_files - returns an array of all subcommand files + # that could be loaded + # commnad_class_from(args) - returns the subcommand class for the + # user-requested command + # class SubcommandLoader - - MATCHES_CHEF_GEM = %r{/chef-[\d]+\.[\d]+\.[\d]+}.freeze - MATCHES_THIS_CHEF_GEM = %r{/chef-#{Chef::VERSION}(-\w+)?(-\w+)?/}.freeze - attr_reader :chef_config_dir attr_reader :env - def initialize(chef_config_dir, env=nil) + # A small factory method. Eventually, this is the only place + # where SubcommandLoader should know about its subclasses, but + # to maintain backwards compatibility many of the instance + # methods in this base class contain default implementations + # of the functions sub classes should otherwise provide + # or directly instantiate the appropriate subclass + def self.for_config(chef_config_dir) + if autogenerated_manifest? + Chef::Log.debug("Using autogenerated hashed command manifest #{plugin_manifest_path}") + Knife::SubcommandLoader::HashedCommandLoader.new(chef_config_dir, plugin_manifest) + elsif custom_manifest? + Chef.log_deprecation("Using custom manifest #{plugin_manifest_path} is deprecated. Please use a `knife rehash` autogenerated manifest instead.") + Knife::SubcommandLoader::CustomManifestLoader.new(chef_config_dir, plugin_manifest) + else + Knife::SubcommandLoader::GemGlobLoader.new(chef_config_dir) + end + end + + def self.plugin_manifest? + plugin_manifest_path && File.exist?(plugin_manifest_path) + end + + def self.autogenerated_manifest? + plugin_manifest? && plugin_manifest.key?(HashedCommandLoader::KEY) + end + + def self.custom_manifest? + plugin_manifest? && plugin_manifest.key?("plugins") + end + + def self.plugin_manifest + Chef::JSONCompat.from_json(File.read(plugin_manifest_path)) + end + + def self.plugin_manifest_path + Chef::Util::PathHelper.home('.chef', 'plugin_manifest.json') + end + + def initialize(chef_config_dir, env = nil) @chef_config_dir = chef_config_dir - @forced_activate = {} # Deprecated and un-used instance variable. @env = env unless env.nil? - Chef::Log.deprecation("The env argument to Chef::Knife::SubcommandLoader is deprecated. If you are using env to inject/mock HOME, consider mocking Chef::Util::PathHelper.home instead.") + Chef.log_deprecation("The env argument to Chef::Knife::SubcommandLoader is deprecated. If you are using env to inject/mock HOME, consider mocking Chef::Util::PathHelper.home instead.") end end # Load all the sub-commands def load_commands + return true if @loaded subcommand_files.each { |subcommand| Kernel.load subcommand } - true + @loaded = true end - # Returns an Array of paths to knife commands located in chef_config_dir/plugins/knife/ - # and ~/.chef/plugins/knife/ - def site_subcommands - user_specific_files = [] - - if chef_config_dir - user_specific_files.concat Dir.glob(File.expand_path("plugins/knife/*.rb", Chef::Util::PathHelper.escape_glob(chef_config_dir))) - end - - # finally search ~/.chef/plugins/knife/*.rb - Chef::Util::PathHelper.home('.chef', 'plugins', 'knife') do |p| - user_specific_files.concat Dir.glob(File.join(Chef::Util::PathHelper.escape_glob(p), '*.rb')) - end + def force_load + @loaded=false + load_commands + end - user_specific_files + def load_command(_command_args) + load_commands end - # Returns a Hash of paths to knife commands built-in to chef, or installed via gem. - # If rubygems is not installed, falls back to globbing the knife directory. - # The Hash is of the form {"relative/path" => "/absolute/path"} - #-- - # Note: the "right" way to load the plugins is to require the relative path, i.e., - # require 'chef/knife/command' - # but we're getting frustrated by bugs at every turn, and it's slow besides. So - # subcommand loader has been modified to load the plugins by using Kernel.load - # with the absolute path. - def gem_and_builtin_subcommands - if have_plugin_manifest? - find_subcommands_via_manifest + def list_commands(pref_cat = nil) + load_commands + if pref_cat && Chef::Knife.subcommands_by_category.key?(pref_cat) + { pref_cat => Chef::Knife.subcommands_by_category[pref_cat] } else - # search all gems for chef/knife/*.rb - require 'rubygems' - find_subcommands_via_rubygems + Chef::Knife.subcommands_by_category end - rescue LoadError - find_subcommands_via_dirglob end - def subcommand_files - @subcommand_files ||= (gem_and_builtin_subcommands.values + site_subcommands).flatten.uniq + def command_class_from(args) + cmd_words = positional_arguments(args) + load_command(cmd_words) + result = Chef::Knife.subcommands[find_longest_key(Chef::Knife.subcommands, + cmd_words, '_')] + result || Chef::Knife.subcommands[args.first.gsub('-', '_')] end - # If the user has created a ~/.chef/plugin_manifest.json file, we'll use - # that instead of inspecting the on-system gems to find the plugins. The - # file format is expected to look like: - # - # { "plugins": { - # "knife-ec2": { - # "paths": [ - # "/home/alice/.rubymanagerthing/gems/knife-ec2-x.y.z/lib/chef/knife/ec2_server_create.rb", - # "/home/alice/.rubymanagerthing/gems/knife-ec2-x.y.z/lib/chef/knife/ec2_server_delete.rb" - # ] - # } - # } - # } - # - # Extraneous content in this file is ignored. This intentional so that we - # can adapt the file format for potential behavior changes to knife in - # the future. - def find_subcommands_via_manifest - # Format of subcommand_files is "relative_path" (something you can - # Kernel.require()) => full_path. The relative path isn't used - # currently, so we just map full_path => full_path. - subcommand_files = {} - plugin_manifest["plugins"].each do |plugin_name, plugin_manifest| - plugin_manifest["paths"].each do |cmd_path| - subcommand_files[cmd_path] = cmd_path - end - end - subcommand_files.merge(find_subcommands_via_dirglob) + def guess_category(args) + category_words = positional_arguments(args) + category_words.map! { |w| w.split('-') }.flatten! + find_longest_key(Chef::Knife.subcommands_by_category, + category_words, ' ') end + + # + # This is shared between the custom_manifest_loader and the gem_glob_loader + # def find_subcommands_via_dirglob # The "require paths" of the core knife subcommands bundled with chef files = Dir[File.join(Chef::Util::PathHelper.escape_glob(File.expand_path('../../../knife', __FILE__)), '*.rb')] @@ -128,95 +143,65 @@ class Chef subcommand_files end - def find_subcommands_via_rubygems - files = find_files_latest_gems 'chef/knife/*.rb' - subcommand_files = {} - files.each do |file| - rel_path = file[/(#{Regexp.escape File.join('chef', 'knife', '')}.*)\.rb/, 1] - - # When not installed as a gem (ChefDK/appbundler in particular), AND - # a different version of Chef is installed via gems, `files` will - # include some files from the 'other' Chef install. If this contains - # a knife command that doesn't exist in this version of Chef, we will - # get a LoadError later when we try to require it. - next if from_different_chef_version?(file) - - subcommand_files[rel_path] = file - end - - subcommand_files.merge(find_subcommands_via_dirglob) - end - - def have_plugin_manifest? - plugin_manifest_path && File.exist?(plugin_manifest_path) - end - - def plugin_manifest - Chef::JSONCompat.from_json(File.read(plugin_manifest_path)) - end - - def plugin_manifest_path - Chef::Util::PathHelper.home('.chef', 'plugin_manifest.json') + # + # Subclassses should define this themselves. Eventually, this will raise a + # NotImplemented error, but for now, we mimic the behavior the user was likely + # to get in the past. + # + def subcommand_files + Chef.log_deprecation "Using Chef::Knife::SubcommandLoader directly is deprecated. +Please use Chef::Knife::SubcommandLoader.for_config(chef_config_dir, env)" + @subcommand_files ||= if Chef::Knife::SubcommandLoader.plugin_manifest? + Chef::Knife::SubcommandLoader::CustomManifestLoader.new(chef_config_dir, env).subcommand_files + else + Chef::Knife::SubcommandLoader::GemGlobLoader.new(chef_config_dir, env).subcommand_files + end end - private - - def find_files_latest_gems(glob, check_load_path=true) - files = [] - - if check_load_path - files = $LOAD_PATH.map { |load_path| - Dir["#{File.expand_path glob, Chef::Util::PathHelper.escape_glob(load_path)}#{Gem.suffix_pattern}"] - }.flatten.select { |file| File.file? file.untaint } - end - - gem_files = latest_gem_specs.map do |spec| - # Gem::Specification#matches_for_glob wasn't added until RubyGems 1.8 - if spec.respond_to? :matches_for_glob - spec.matches_for_glob("#{glob}#{Gem.suffix_pattern}") + # + # Utility function for finding an element in a hash given an array + # of words and a separator. We find the the longest key in the + # hash composed of the given words joined by the separator. + # + def find_longest_key(hash, words, sep = '_') + match = nil + until match || words.empty? + candidate = words.join(sep) + if hash.key?(candidate) + match = candidate else - check_spec_for_glob(spec, glob) + words.pop end - end.flatten - - files.concat gem_files - files.uniq! if check_load_path - - return files - end - - def latest_gem_specs - @latest_gem_specs ||= if Gem::Specification.respond_to? :latest_specs - Gem::Specification.latest_specs(true) # find prerelease gems - else - Gem.source_index.latest_specs(true) end + match end - def check_spec_for_glob(spec, glob) - dirs = if spec.require_paths.size > 1 then - "{#{spec.require_paths.join(',')}}" - else - spec.require_paths.first - end - - glob = File.join(Chef::Util::PathHelper.escape_glob(spec.full_gem_path, dirs), glob) - - Dir[glob].map { |f| f.untaint } + # + # The positional arguments from the argument list provided by the + # users. Used to search for subcommands and categories. + # + # @return [Array<String>] + # + def positional_arguments(args) + args.select { |arg| arg =~ /^(([[:alnum:]])[[:alnum:]\_\-]+)$/ } end - def from_different_chef_version?(path) - matches_any_chef_gem?(path) && !matches_this_chef_gem?(path) - end + # Returns an Array of paths to knife commands located in + # chef_config_dir/plugins/knife/ and ~/.chef/plugins/knife/ + def site_subcommands + user_specific_files = [] - def matches_any_chef_gem?(path) - path =~ MATCHES_CHEF_GEM - end + if chef_config_dir + user_specific_files.concat Dir.glob(File.expand_path("plugins/knife/*.rb", Chef::Util::PathHelper.escape_glob(chef_config_dir))) + end - def matches_this_chef_gem?(path) - path =~ MATCHES_THIS_CHEF_GEM - end + # finally search ~/.chef/plugins/knife/*.rb + Chef::Util::PathHelper.home('.chef', 'plugins', 'knife') do |p| + user_specific_files.concat Dir.glob(File.join(Chef::Util::PathHelper.escape_glob(p), '*.rb')) + end + user_specific_files + end end end end diff --git a/lib/chef/knife/node_run_list_remove.rb b/lib/chef/knife/node_run_list_remove.rb index 4b8953a264..ef03c176b8 100644 --- a/lib/chef/knife/node_run_list_remove.rb +++ b/lib/chef/knife/node_run_list_remove.rb @@ -42,7 +42,18 @@ class Chef entries = @name_args[1].split(',').map { |e| e.strip } end - entries.each { |e| node.run_list.remove(e) } + # iterate over the list of things to remove, + # warning if one of them was not found + entries.each do |e| + if node.run_list.find { |rli| e == rli.to_s } + node.run_list.remove(e) + else + ui.warn "#{e} is not in the run list" + unless e =~ /^(recipe|role)\[/ + ui.warn '(did you forget recipe[] or role[] around it?)' + end + end + end node.save diff --git a/lib/chef/knife/null.rb b/lib/chef/knife/null.rb new file mode 100644 index 0000000000..0b5058e8ea --- /dev/null +++ b/lib/chef/knife/null.rb @@ -0,0 +1,10 @@ +class Chef + class Knife + class Null < Chef::Knife + banner "knife null" + + def run + end + end + end +end diff --git a/lib/chef/knife/osc_user_create.rb b/lib/chef/knife/osc_user_create.rb index c368296040..6c3415473f 100644 --- a/lib/chef/knife/osc_user_create.rb +++ b/lib/chef/knife/osc_user_create.rb @@ -27,7 +27,7 @@ class Chef class OscUserCreate < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -69,7 +69,7 @@ class Chef exit 1 end - user = Chef::OscUser.new + user = Chef::User.new user.name(@user_name) user.admin(config[:admin]) user.password config[:user_password] @@ -79,7 +79,7 @@ class Chef end output = edit_data(user) - user = Chef::OscUser.from_hash(output).create + user = Chef::User.from_hash(output).create ui.info("Created #{user}") if user.private_key diff --git a/lib/chef/knife/osc_user_delete.rb b/lib/chef/knife/osc_user_delete.rb index d6fbd4a6a9..5cd4f10413 100644 --- a/lib/chef/knife/osc_user_delete.rb +++ b/lib/chef/knife/osc_user_delete.rb @@ -28,7 +28,7 @@ class Chef class OscUserDelete < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -43,7 +43,7 @@ class Chef exit 1 end - delete_object(Chef::OscUser, @user_name) + delete_object(Chef::User, @user_name) end end diff --git a/lib/chef/knife/osc_user_edit.rb b/lib/chef/knife/osc_user_edit.rb index 4c38674d08..526475db05 100644 --- a/lib/chef/knife/osc_user_edit.rb +++ b/lib/chef/knife/osc_user_edit.rb @@ -28,7 +28,7 @@ class Chef class OscUserEdit < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -43,10 +43,10 @@ class Chef exit 1 end - original_user = Chef::OscUser.load(@user_name).to_hash + original_user = Chef::User.load(@user_name).to_hash edited_user = edit_data(original_user) if original_user != edited_user - user = Chef::OscUser.from_hash(edited_user) + user = Chef::User.from_hash(edited_user) user.update ui.msg("Saved #{user}.") else diff --git a/lib/chef/knife/osc_user_list.rb b/lib/chef/knife/osc_user_list.rb index 92f049cd19..84fca31899 100644 --- a/lib/chef/knife/osc_user_list.rb +++ b/lib/chef/knife/osc_user_list.rb @@ -28,7 +28,7 @@ class Chef class OscUserList < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -40,7 +40,7 @@ class Chef :description => "Show corresponding URIs" def run - output(format_list_for_display(Chef::OscUser.list)) + output(format_list_for_display(Chef::User.list)) end end end diff --git a/lib/chef/knife/osc_user_reregister.rb b/lib/chef/knife/osc_user_reregister.rb index a71e0aa677..163b286fe0 100644 --- a/lib/chef/knife/osc_user_reregister.rb +++ b/lib/chef/knife/osc_user_reregister.rb @@ -28,7 +28,7 @@ class Chef class OscUserReregister < Knife deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -48,7 +48,7 @@ class Chef exit 1 end - user = Chef::OscUser.load(@user_name).reregister + user = Chef::User.load(@user_name).reregister Chef::Log.debug("Updated user data: #{user.inspect}") key = user.private_key if config[:file] diff --git a/lib/chef/knife/osc_user_show.rb b/lib/chef/knife/osc_user_show.rb index 6a41ddae88..cb3a77585a 100644 --- a/lib/chef/knife/osc_user_show.rb +++ b/lib/chef/knife/osc_user_show.rb @@ -30,7 +30,7 @@ class Chef include Knife::Core::MultiAttributeReturnOption deps do - require 'chef/osc_user' + require 'chef/user' require 'chef/json_compat' end @@ -45,7 +45,7 @@ class Chef exit 1 end - user = Chef::OscUser.load(@user_name) + user = Chef::User.load(@user_name) output(format_for_display(user)) end diff --git a/lib/chef/knife/rehash.rb b/lib/chef/knife/rehash.rb new file mode 100644 index 0000000000..6f1fd91911 --- /dev/null +++ b/lib/chef/knife/rehash.rb @@ -0,0 +1,62 @@ +# +# Author:: Steven Danna <steve@chef.io> +# 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/knife' +require 'chef/knife/core/subcommand_loader' + +class Chef + class Knife + class Rehash < Chef::Knife + banner "knife rehash" + + def run + if ! Chef::Knife::SubcommandLoader.autogenerated_manifest? + ui.msg "Using knife-rehash will speed up knife's load time by caching the location of subcommands on disk." + ui.msg "However, you will need to update the cache by running `knife rehash` anytime you install a new knife plugin." + else + reload_plugins + end + write_hash(generate_hash) + end + + def reload_plugins + Chef::Knife::SubcommandLoader::GemGlobLoader.new(@@chef_config_dir).load_commands + end + + def generate_hash + output = if Chef::Knife::SubcommandLoader.plugin_manifest? + Chef::Knife::SubcommandLoader.plugin_manifest + else + { Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY => {}} + end + output[Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY]['plugins_paths'] = Chef::Knife.subcommand_files + output[Chef::Knife::SubcommandLoader::HashedCommandLoader::KEY]['plugins_by_category'] = Chef::Knife.subcommands_by_category + output + end + + def write_hash(data) + plugin_manifest_dir = File.expand_path('..', Chef::Knife::SubcommandLoader.plugin_manifest_path) + FileUtils.mkdir_p(plugin_manifest_dir) unless File.directory?(plugin_manifest_dir) + File.open(Chef::Knife::SubcommandLoader.plugin_manifest_path, 'w') do |f| + f.write(Chef::JSONCompat.to_json_pretty(data)) + ui.msg "Knife subcommands are cached in #{Chef::Knife::SubcommandLoader.plugin_manifest_path}. Delete this file to disable the caching." + end + end + end + end +end diff --git a/lib/chef/knife/ssl_check.rb b/lib/chef/knife/ssl_check.rb index c5fe4fc1aa..d71eacfc7e 100644 --- a/lib/chef/knife/ssl_check.rb +++ b/lib/chef/knife/ssl_check.rb @@ -73,11 +73,12 @@ class Chef exit 1 end - def verify_peer_socket @verify_peer_socket ||= begin tcp_connection = TCPSocket.new(host, port) - OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context) + ssl_client = OpenSSL::SSL::SSLSocket.new(tcp_connection, verify_peer_ssl_context) + ssl_client.hostname = host + ssl_client end end diff --git a/lib/chef/knife/user_create.rb b/lib/chef/knife/user_create.rb index e73f6be8b6..995573cd03 100644 --- a/lib/chef/knife/user_create.rb +++ b/lib/chef/knife/user_create.rb @@ -27,7 +27,7 @@ class Chef attr_accessor :user_field deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -61,11 +61,11 @@ class Chef banner "knife user create USERNAME DISPLAY_NAME FIRST_NAME LAST_NAME EMAIL PASSWORD (options)" def user - @user_field ||= Chef::User.new + @user_field ||= Chef::UserV1.new end def create_user_from_hash(hash) - Chef::User.from_hash(hash).create + Chef::UserV1.from_hash(hash).create end def osc_11_warning diff --git a/lib/chef/knife/user_delete.rb b/lib/chef/knife/user_delete.rb index 803be6b90c..828cd51588 100644 --- a/lib/chef/knife/user_delete.rb +++ b/lib/chef/knife/user_delete.rb @@ -23,7 +23,7 @@ class Chef class UserDelete < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -55,7 +55,7 @@ EOF if Kernel.block_given? object = block.call else - object = Chef::User.load(user_name) + object = Chef::UserV1.load(user_name) object.destroy end @@ -77,10 +77,10 @@ EOF # Below is modification of Chef::Knife.delete_object to detect OSC 11 server. # When OSC 11 is deprecated, simply delete all this and go back to: # - # delete_object(Chef::User, @user_name) + # delete_object(Chef::UserV1, @user_name) # # Also delete our override of delete_object above - object = Chef::User.load(@user_name) + object = Chef::UserV1.load(@user_name) # OSC 11 case if object.username.nil? diff --git a/lib/chef/knife/user_edit.rb b/lib/chef/knife/user_edit.rb index dd2fc02743..c3a4326ee8 100644 --- a/lib/chef/knife/user_edit.rb +++ b/lib/chef/knife/user_edit.rb @@ -23,7 +23,7 @@ class Chef class UserEdit < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -56,8 +56,7 @@ EOF exit 1 end - original_user = Chef::User.load(@user_name).to_hash - + original_user = Chef::UserV1.load(@user_name).to_hash # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. # @@ -69,11 +68,11 @@ EOF else # EC / CS 12 user create edited_user = edit_data(original_user) if original_user != edited_user - user = Chef::User.from_hash(edited_user) + user = Chef::UserV1.from_hash(edited_user) user.update ui.msg("Saved #{user}.") else - ui.msg("User unchaged, not saving.") + ui.msg("User unchanged, not saving.") end end diff --git a/lib/chef/knife/user_list.rb b/lib/chef/knife/user_list.rb index 7ae43dadc9..6a130392b9 100644 --- a/lib/chef/knife/user_list.rb +++ b/lib/chef/knife/user_list.rb @@ -25,7 +25,7 @@ class Chef class UserList < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -37,7 +37,7 @@ class Chef :description => "Show corresponding URIs" def run - output(format_list_for_display(Chef::User.list)) + output(format_list_for_display(Chef::UserV1.list)) end end diff --git a/lib/chef/knife/user_reregister.rb b/lib/chef/knife/user_reregister.rb index eab2245025..09fd1cd2d6 100644 --- a/lib/chef/knife/user_reregister.rb +++ b/lib/chef/knife/user_reregister.rb @@ -23,7 +23,7 @@ class Chef class UserReregister < Knife deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -61,7 +61,7 @@ EOF exit 1 end - user = Chef::User.load(@user_name) + user = Chef::UserV1.load(@user_name) # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. diff --git a/lib/chef/knife/user_show.rb b/lib/chef/knife/user_show.rb index f5e81e9972..3a2443471a 100644 --- a/lib/chef/knife/user_show.rb +++ b/lib/chef/knife/user_show.rb @@ -25,7 +25,7 @@ class Chef include Knife::Core::MultiAttributeReturnOption deps do - require 'chef/user' + require 'chef/user_v1' require 'chef/json_compat' end @@ -58,7 +58,7 @@ EOF exit 1 end - user = Chef::User.load(@user_name) + user = Chef::UserV1.load(@user_name) # DEPRECATION NOTE # Remove this if statement and corrosponding code post OSC 11 support. diff --git a/lib/chef/log.rb b/lib/chef/log.rb index 9b27778a40..bf846c2072 100644 --- a/lib/chef/log.rb +++ b/lib/chef/log.rb @@ -37,7 +37,11 @@ class Chef end end - def self.deprecation(msg=nil, &block) + def self.deprecation(msg=nil, location=caller(2..2)[0], &block) + if msg + msg << " at #{Array(location).join("\n")}" + msg = msg.join("") if msg.respond_to?(:join) + end if Chef::Config[:treat_deprecation_warnings_as_errors] error(msg, &block) raise Chef::Exceptions::DeprecatedFeatureError.new(msg) diff --git a/lib/chef/mixin/deprecation.rb b/lib/chef/mixin/deprecation.rb index a3eacf75cb..562af541bd 100644 --- a/lib/chef/mixin/deprecation.rb +++ b/lib/chef/mixin/deprecation.rb @@ -102,20 +102,20 @@ class Chef def deprecated_attr_reader(name, alternative, level=:warn) define_method(name) do - Chef::Log.deprecation("#{self.class}.#{name} is deprecated. Support will be removed in a future release.") - Chef::Log.deprecation(alternative) - Chef::Log.deprecation("Called from:") - caller[0..3].each {|c| Chef::Log.deprecation(c)} + Chef.log_deprecation("#{self.class}.#{name} is deprecated. Support will be removed in a future release.") + Chef.log_deprecation(alternative) + Chef.log_deprecation("Called from:") + caller[0..3].each {|c| Chef.log_deprecation(c)} instance_variable_get("@#{name}") end end def deprecated_attr_writer(name, alternative, level=:warn) define_method("#{name}=") do |value| - Chef::Log.deprecation("Writing to #{self.class}.#{name} with #{name}= is deprecated. Support will be removed in a future release.") - Chef::Log.deprecation(alternative) - Chef::Log.deprecation("Called from:") - caller[0..3].each {|c| Chef::Log.deprecation(c)} + Chef.log_deprecation("Writing to #{self.class}.#{name} with #{name}= is deprecated. Support will be removed in a future release.") + Chef.log_deprecation(alternative) + Chef.log_deprecation("Called from:") + caller[0..3].each {|c| Chef.log_deprecation(c)} instance_variable_set("@#{name}", value) end end diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb index 78d72dc801..e3c7657b1b 100644 --- a/lib/chef/mixin/params_validate.rb +++ b/lib/chef/mixin/params_validate.rb @@ -15,9 +15,11 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'chef/constants' +require 'chef/property' +require 'chef/delayed_evaluator' + class Chef - class DelayedEvaluator < Proc - end module Mixin module ParamsValidate @@ -32,20 +34,55 @@ class Chef # Would raise an exception if the value of :one above is not a kind_of? string. Valid # map options are: # - # :default:: Sets the default value for this parameter. - # :callbacks:: Takes a hash of Procs, which should return true if the argument is valid. - # The key will be inserted into the error message if the Proc does not return true: - # "Option #{key}'s value #{value} #{message}!" - # :kind_of:: Ensure that the value is a kind_of?(Whatever). If passed an array, it will ensure - # that the value is one of those types. - # :respond_to:: Ensure that the value has a given method. Takes one method name or an array of - # method names. - # :required:: Raise an exception if this parameter is missing. Valid values are true or false, - # by default, options are not required. - # :regex:: Match the value of the parameter against a regular expression. - # :equal_to:: Match the value of the parameter with ==. An array means it can be equal to any - # of the values. + # @param opts [Hash<Symbol,Object>] Validation opts. + # @option opts [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`opts[:is].any? { |v| v === value }`). (See #_pv_is.) + # @option opts [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`opts[:is].any? { |v| v == value }`) (See #_pv_equal_to.) + # @option opts [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # (See #_pv_regex) + # @option opts [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. (See + # #_pv_kind_of.) + # @option opts [Hash<String,Proc>] :callbacks A hash of + # messages -> procs, all of which match the value. The proc must + # return a truthy or falsey value (true means it matches). (See + # #_pv_callbacks.) + # @option opts [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. (See + # #_pv_respond_to.) + # @option opts [Symbol,Array<Symbol>] :cannot_be A property, + # or a list of properties, that the value cannot have (such as `:nil` or + # `:empty`). The method with a questionmark at the end is called on the + # value (e.g. `value.empty?`). If the value does not have this method, + # it is considered valid (i.e. if you don't respond to `empty?` we + # assume you are not empty). (See #_pv_cannot_be.) + # @option opts [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # (See #_pv_coerce.) (See #_pv_coerce.) + # @option opts [Boolean] :required `true` if this property + # must be present and not `nil`; `false` otherwise. This is checked + # after the resource is fully initialized. (See #_pv_required.) + # @option opts [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. (See + # #_pv_name_property.) + # @option opts [Boolean] :name_attribute Same as `name_property`. + # @option opts [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties). (See #_pv_default.) + # def validate(opts, map) + map = map.validation_options if map.is_a?(Property) + #-- # validate works by taking the keys in the validation map, assuming it's a hash, and # looking for _pv_:symbol as methods. Assuming it find them, it calls the right @@ -65,7 +102,7 @@ class Chef true when Hash validation.each do |check, carg| - check_method = "_pv_#{check.to_s}" + check_method = "_pv_#{check}" if self.respond_to?(check_method, true) self.send(check_method, opts, key, carg) else @@ -81,162 +118,352 @@ class Chef DelayedEvaluator.new(&block) end - def set_or_return(symbol, arg, validation) - iv_symbol = "@#{symbol.to_s}".to_sym - if arg == nil && self.instance_variable_defined?(iv_symbol) == true - ivar = self.instance_variable_get(iv_symbol) - if(ivar.is_a?(DelayedEvaluator)) - validate({ symbol => ivar.call }, { symbol => validation })[symbol] - else - ivar - end - else - if(arg.is_a?(DelayedEvaluator)) - val = arg - else - val = validate({ symbol => arg }, { symbol => validation })[symbol] + def set_or_return(symbol, value, validation) + property = SetOrReturnProperty.new(name: symbol, **validation) + property.call(self, value) + end - # Handle the case where the "default" was a DelayedEvaluator. In - # this case, the block yields an optional parameter of +self+, - # which is the equivalent of "new_resource" - if val.is_a?(DelayedEvaluator) - val = val.call(self) - end - end - self.instance_variable_set(iv_symbol, val) + private + + def explicitly_allows_nil?(key, validation) + validation.has_key?(:is) && _pv_is({ key => nil }, key, validation[:is], raise_error: false) + end + + # Return the value of a parameter, or nil if it doesn't exist. + def _pv_opts_lookup(opts, key) + if opts.has_key?(key.to_s) + opts[key.to_s] + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] + else + nil end end - private + # Raise an exception if the parameter is not found. + def _pv_required(opts, key, is_required=true, explicitly_allows_nil=false) + if is_required + return true if opts.has_key?(key.to_s) && (explicitly_allows_nil || !opts[key.to_s].nil?) + return true if opts.has_key?(key.to_sym) && (explicitly_allows_nil || !opts[key.to_sym].nil?) + raise Exceptions::ValidationFailed, "Required argument #{key.inspect} is missing!" + end + true + end - # Return the value of a parameter, or nil if it doesn't exist. - def _pv_opts_lookup(opts, key) - if opts.has_key?(key.to_s) - opts[key.to_s] - elsif opts.has_key?(key.to_sym) - opts[key.to_sym] - else - nil + # + # List of things values must be equal to. + # + # Uses Ruby's `==` to evaluate (equal_to == value). At least one must + # match for the value to be valid. + # + # `nil` passes this validation automatically. + # + # @return [Array,nil] List of things values must be equal to, or nil if + # equal_to is unspecified. + # + def _pv_equal_to(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + to_be = Array(to_be) + to_be.each do |tb| + return true if value == tb end + raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." end + end - # Raise an exception if the parameter is not found. - def _pv_required(opts, key, is_required=true) - if is_required - if (opts.has_key?(key.to_s) && !opts[key.to_s].nil?) || - (opts.has_key?(key.to_sym) && !opts[key.to_sym].nil?) - true - else - raise Exceptions::ValidationFailed, "Required argument #{key} is missing!" - end + # + # List of things values must be instances of. + # + # Uses value.kind_of?(kind_of) to evaluate. At least one must match for + # the value to be valid. + # + # `nil` automatically passes this validation. + # + def _pv_kind_of(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + to_be = Array(to_be) + to_be.each do |tb| + return true if value.kind_of?(tb) end + raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." end + end - def _pv_equal_to(opts, key, to_be) - value = _pv_opts_lookup(opts, key) - unless value.nil? - passes = false - Array(to_be).each do |tb| - passes = true if value == tb - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." + # + # List of method names values must respond to. + # + # Uses value.respond_to?(respond_to) to evaluate. At least one must match + # for the value to be valid. + # + def _pv_respond_to(opts, key, method_name_list) + value = _pv_opts_lookup(opts, key) + unless value.nil? + Array(method_name_list).each do |method_name| + unless value.respond_to?(method_name) + raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" end end end + end - # Raise an exception if the parameter is not a kind_of?(to_be) - def _pv_kind_of(opts, key, to_be) - value = _pv_opts_lookup(opts, key) - unless value.nil? - passes = false - Array(to_be).each do |tb| - passes = true if value.kind_of?(tb) - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." + # + # List of things that must not be true about the value. + # + # Calls `value.<thing>?` All responses must be false for the value to be + # valid. + # Values which do not respond to <thing>? are considered valid (because if + # a value doesn't respond to `:readable?`, then it probably isn't + # readable.) + # + # @example + # ```ruby + # property :x, cannot_be: [ :nil, :empty ] + # x [ 1, 2 ] #=> valid + # x 1 #=> valid + # x [] #=> invalid + # x nil #=> invalid + # ``` + # + def _pv_cannot_be(opts, key, predicate_method_base_name) + value = _pv_opts_lookup(opts, key) + if !value.nil? + Array(predicate_method_base_name).each do |method_name| + predicate_method = :"#{method_name}?" + + if value.respond_to?(predicate_method) + if value.send(predicate_method) + raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" + end end end end + end - # Raise an exception if the parameter does not respond to a given set of methods. - def _pv_respond_to(opts, key, method_name_list) - value = _pv_opts_lookup(opts, key) - unless value.nil? - Array(method_name_list).each do |method_name| - unless value.respond_to?(method_name) - raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" - end - end + # + # The default value for a property. + # + # When the property is not assigned, this will be used. + # + # If this is a lazy value, it will either be passed the resource as a value, + # or if the lazy proc does not take parameters, it will be run in the + # context of the instance with instance_eval. + # + # @example + # ```ruby + # property :x, default: 10 + # ``` + # + # @example + # ```ruby + # property :x + # property :y, default: lazy { x+2 } + # ``` + # + # @example + # ```ruby + # property :x + # property :y, default: lazy { |r| r.x+2 } + # ``` + # + def _pv_default(opts, key, default_value) + value = _pv_opts_lookup(opts, key) + if value.nil? + default_value = default_value.freeze if !default_value.is_a?(DelayedEvaluator) + opts[key] = default_value + end + end + + # + # List of regexes values that must match. + # + # Uses regex.match() to evaluate. At least one must match for the value to + # be valid. + # + # `nil` passes regex validation automatically. + # + # @example + # ```ruby + # property :x, regex: [ /abc/, /xyz/ ] + # ``` + # + def _pv_regex(opts, key, regex) + value = _pv_opts_lookup(opts, key) + if !value.nil? + Array(regex).flatten.each do |r| + return true if r.match(value.to_s) end + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" end + end - # Assert that parameter returns false when passed a predicate method. - # For example, :cannot_be => :blank will raise a Exceptions::ValidationFailed - # error value.blank? returns a 'truthy' (not nil or false) value. - # - # Note, this will *PASS* if the object doesn't respond to the method. - # So, to make sure a value is not nil and not blank, you need to do - # both :cannot_be => :blank *and* :cannot_be => :nil (or :required => true) - def _pv_cannot_be(opts, key, predicate_method_base_name) - value = _pv_opts_lookup(opts, key) - predicate_method = (predicate_method_base_name.to_s + "?").to_sym - - if value.respond_to?(predicate_method) - if value.send(predicate_method) - raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" + # + # List of procs we pass the value to. + # + # All procs must return true for the value to be valid. If any procs do + # not return true, the key will be used for the message: `"Property x's + # value :y <message>"`. + # + # @example + # ```ruby + # property :x, callbacks: { "is bigger than 10" => proc { |v| v <= 10 }, "is not awesome" => proc { |v| !v.awesome }} + # ``` + # + def _pv_callbacks(opts, key, callbacks) + raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) + value = _pv_opts_lookup(opts, key) + if !value.nil? + callbacks.each do |message, zeproc| + unless zeproc.call(value) + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" end end end + end - # Assign a default value to a parameter. - def _pv_default(opts, key, default_value) - value = _pv_opts_lookup(opts, key) - if value == nil - opts[key] = default_value + # + # Allows a parameter to default to the value of the resource name. + # + # @example + # ```ruby + # property :x, name_property: true + # ``` + # + def _pv_name_property(opts, key, is_name_property=true) + if is_name_property + if opts[key].nil? + opts[key] = self.instance_variable_get(:"@name") end end + end + alias :_pv_name_attribute :_pv_name_property - # Check a parameter against a regular expression. - def _pv_regex(opts, key, regex) - value = _pv_opts_lookup(opts, key) - if value != nil - passes = false - [ regex ].flatten.each do |r| - if value != nil - if r.match(value.to_s) - passes = true - end - end - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" - end + # + # List of valid things values can be. + # + # Uses Ruby's `===` to evaluate (is === value). At least one must match + # for the value to be valid. + # + # If a proc is passed, it is instance_eval'd in the resource, passed the + # value, and must return a truthy or falsey value. + # + # @example Class + # ```ruby + # property :x, String + # x 'valid' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # + # @example Value + # ```ruby + # property :x, [ :a, :b, :c, nil ] + # x :a #=> valid + # x nil #=> valid + # ``` + # + # @example Regex + # ```ruby + # property :x, /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + # @example Proc + # ```ruby + # property :x, proc { |x| x > y } + # property :y, default: 2 + # x 3 #=> valid + # x 1 #=> invalid + # ``` + # + # @example Property + # ```ruby + # type = Property.new(is: String) + # property :x, type + # x 'foo' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # ``` + # + # @example RSpec Matcher + # ```ruby + # include RSpec::Matchers + # property :x, a_string_matching /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + def _pv_is(opts, key, to_be, raise_error: true) + return true if !opts.has_key?(key.to_s) && !opts.has_key?(key.to_sym) + value = _pv_opts_lookup(opts, key) + to_be = [ to_be ].flatten(1) + to_be.each do |tb| + case tb + when Proc + return true if instance_exec(value, &tb) + when Property + validate(opts, { key => tb.validation_options }) + return true + else + return true if tb === value end end - # Check a parameter against a hash of proc's. - def _pv_callbacks(opts, key, callbacks) - raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) - value = _pv_opts_lookup(opts, key) - if value != nil - callbacks.each do |message, zeproc| - if zeproc.call(value) != true - raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" - end - end + if raise_error + raise Exceptions::ValidationFailed, "Option #{key} must be one of: #{to_be.join(", ")}! You passed #{value.inspect}." + else + false + end + end + + # + # Method to mess with a value before it is validated and stored. + # + # Allows you to transform values into a canonical form that is easy to + # work with. + # + # This is passed the value to transform, and is run in the context of the + # instance (so it has access to other resource properties). It must return + # the value that will be stored in the instance. + # + # @example + # ```ruby + # property :x, Integer, coerce: { |v| v.to_i } + # ``` + # + def _pv_coerce(opts, key, coercer) + if opts.has_key?(key.to_s) + opts[key.to_s] = instance_exec(opts[key], &coercer) + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] = instance_exec(opts[key], &coercer) + end + end + + # Used by #set_or_return to avoid emitting a deprecation warning for + # "value nil" and to keep default stickiness working exactly the same + # @api private + class SetOrReturnProperty < Chef::Property + def get(resource) + value = super + # All values are sticky, frozen or not + if !is_set?(resource) + set_value(resource, value) end + value end - # Allow a parameter to default to @name - def _pv_name_attribute(opts, key, is_name_attribute=true) - if is_name_attribute - if opts[key] == nil - opts[key] = self.instance_variable_get("@name") - end + def call(resource, value=NOT_PASSED) + # setting to nil does a get + if value.nil? && !explicitly_accepts_nil?(resource) + get(resource) + else + super end end + end end end end - diff --git a/lib/chef/mixin/template.rb b/lib/chef/mixin/template.rb index 9b35bbcc33..db9a2f6d91 100644 --- a/lib/chef/mixin/template.rb +++ b/lib/chef/mixin/template.rb @@ -44,6 +44,52 @@ class Chef attr_reader :_extension_modules + # + # Helpers for adding context of which resource is rendering the template (CHEF-5012) + # + + # name of the cookbook containing the template resource, e.g.: + # test + # + # @return [String] cookbook name + attr_reader :cookbook_name + + # name of the recipe containing the template resource, e.g.: + # default + # + # @return [String] recipe name + attr_reader :recipe_name + + # string representation of the line in the recipe containing the template resource, e.g.: + # /Users/lamont/solo/cookbooks/test/recipes/default.rb:2:in `from_file' + # + # @return [String] recipe line + attr_reader :recipe_line_string + + # path to the recipe containing the template resource, e.g.: + # /Users/lamont/solo/cookbooks/test/recipes/default.rb + # + # @return [String] recipe path + attr_reader :recipe_path + + # line in the recipe containing the template reosurce, e.g.: + # 2 + # + # @return [String] recipe line + attr_reader :recipe_line + + # name of the template source itself, e.g.: + # foo.erb + # + # @return [String] template name + attr_reader :template_name + + # path to the template source itself, e.g.: + # /Users/lamont/solo/cookbooks/test/templates/default/foo.erb + # + # @return [String] template path + attr_reader :template_path + def initialize(variables) super @_extension_modules = [] @@ -62,6 +108,7 @@ class Chef "include a node variable if you plan to use it." end + # # Takes the name of the partial, plus a hash of options. Returns a # string that contains the result of the evaluation of the partial. diff --git a/lib/chef/mixin/wide_string.rb b/lib/chef/mixin/wide_string.rb new file mode 100644 index 0000000000..0c32b76365 --- /dev/null +++ b/lib/chef/mixin/wide_string.rb @@ -0,0 +1,72 @@ +# +# Author:: Jay Mundrawala(<jdm@chef.io>) +# Copyright:: Copyright 2015 Chef Software +# 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. +# + +class Chef + module Mixin + module WideString + + def wstring(str) + if str.nil? || str.encoding == Encoding::UTF_16LE + str + else + utf8_to_wide(str) + end + end + + def utf8_to_wide(ustring) + # ensure it is actually UTF-8 + # Ruby likes to mark binary data as ASCII-8BIT + ustring = (ustring + "").force_encoding('UTF-8') if ustring.respond_to?(:force_encoding) && ustring.encoding.name != "UTF-8" + + # ensure we have the double-null termination Windows Wide likes + ustring = ustring + "\000\000" if ustring.length == 0 or ustring[-1].chr != "\000" + + # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode + ustring = begin + if ustring.respond_to?(:encode) + ustring.encode('UTF-16LE') + else + require 'iconv' + Iconv.conv("UTF-16LE", "UTF-8", ustring) + end + end + ustring + end + + def wide_to_utf8(wstring) + # ensure it is actually UTF-16LE + # Ruby likes to mark binary data as ASCII-8BIT + wstring = wstring.force_encoding('UTF-16LE') if wstring.respond_to?(:force_encoding) + + # encode it all as UTF-8 + wstring = begin + if wstring.respond_to?(:encode) + wstring.encode('UTF-8') + else + require 'iconv' + Iconv.conv("UTF-8", "UTF-16LE", wstring) + end + end + # remove trailing CRLF and NULL characters + wstring.strip! + wstring + end + + end + end +end diff --git a/lib/chef/mixin/windows_architecture_helper.rb b/lib/chef/mixin/windows_architecture_helper.rb index c5f3e1bd79..744001f8a2 100644 --- a/lib/chef/mixin/windows_architecture_helper.rb +++ b/lib/chef/mixin/windows_architecture_helper.rb @@ -19,19 +19,13 @@ require 'chef/exceptions' require 'chef/platform/query_helpers' -require 'win32/api' if Chef::Platform.windows? -require 'chef/win32/api/process' if Chef::Platform.windows? -require 'chef/win32/api/error' if Chef::Platform.windows? +require 'chef/win32/process' if Chef::Platform.windows? +require 'chef/win32/system' if Chef::Platform.windows? class Chef module Mixin module WindowsArchitectureHelper - if Chef::Platform.windows? - include Chef::ReservedNames::Win32::API::Process - include Chef::ReservedNames::Win32::API::Error - end - def node_windows_architecture(node) node[:kernel][:machine].to_sym end @@ -42,6 +36,16 @@ class Chef is_i386_process_on_x86_64_windows? end + def forced_32bit_override_required?(node, desired_architecture) + desired_architecture == :i386 && + node_windows_architecture(node) == :x86_64 && + !is_i386_process_on_x86_64_windows? + end + + def wow64_directory + Chef::ReservedNames::Win32::System.get_system_wow64_directory + end + def with_os_architecture(node, architecture: nil) node ||= begin os_arch = ENV['PROCESSOR_ARCHITEW6432'] || @@ -88,49 +92,21 @@ class Chef def is_i386_process_on_x86_64_windows? if Chef::Platform.windows? - is_64_bit_process_result = FFI::MemoryPointer.new(:int) - - # The return value of IsWow64Process is nonzero value if the API call succeeds. - # The result data are returned in the last parameter, not the return value. - call_succeeded = IsWow64Process(GetCurrentProcess(), is_64_bit_process_result) - - # The result is nonzero if IsWow64Process's calling process, in the case here - # this process, is running under WOW64, i.e. the result is nonzero if this - # process is 32-bit (aka :i386). - result = (call_succeeded != 0) && (is_64_bit_process_result.get_int(0) != 0) + Chef::ReservedNames::Win32::Process.is_wow64_process else false end end def disable_wow64_file_redirection( node ) - original_redirection_state = ['0'].pack('P') - if ( ( node_windows_architecture(node) == :x86_64) && ::Chef::Platform.windows?) - win32_wow_64_disable_wow_64_fs_redirection = - ::Win32::API.new('Wow64DisableWow64FsRedirection', 'P', 'L', 'kernel32') - - succeeded = win32_wow_64_disable_wow_64_fs_redirection.call(original_redirection_state) - - if succeeded == 0 - raise Win32APIError "Failed to disable Wow64 file redirection" - end - + Chef::ReservedNames::Win32::System.wow64_disable_wow64_fs_redirection end - - original_redirection_state end def restore_wow64_file_redirection( node, original_redirection_state ) if ( (node_windows_architecture(node) == :x86_64) && ::Chef::Platform.windows?) - win32_wow_64_revert_wow_64_fs_redirection = - ::Win32::API.new('Wow64RevertWow64FsRedirection', 'P', 'L', 'kernel32') - - succeeded = win32_wow_64_revert_wow_64_fs_redirection.call(original_redirection_state) - - if succeeded == 0 - raise Win32APIError "Failed to revert Wow64 file redirection" - end + Chef::ReservedNames::Win32::System.wow64_revert_wow64_fs_redirection(original_redirection_state) end end diff --git a/lib/chef/mixin/windows_env_helper.rb b/lib/chef/mixin/windows_env_helper.rb index a126801a28..cd12b4254a 100644 --- a/lib/chef/mixin/windows_env_helper.rb +++ b/lib/chef/mixin/windows_env_helper.rb @@ -18,6 +18,7 @@ require 'chef/exceptions' +require 'chef/mixin/wide_string' require 'chef/platform/query_helpers' require 'chef/win32/error' if Chef::Platform.windows? require 'chef/win32/api/system' if Chef::Platform.windows? @@ -26,6 +27,8 @@ require 'chef/win32/api/unicode' if Chef::Platform.windows? class Chef module Mixin module WindowsEnvHelper + include Chef::Mixin::WideString + if Chef::Platform.windows? include Chef::ReservedNames::Win32::API::System end @@ -45,7 +48,7 @@ class Chef Chef::ReservedNames::Win32::Error.raise! end if ( SendMessageTimeoutW(HWND_BROADCAST, WM_SETTINGCHANGE, 0, FFI::MemoryPointer.from_string( - Chef::ReservedNames::Win32::Unicode.utf8_to_wide('Environment') + utf8_to_wide('Environment') ).address, flags, 5000, nil) == 0 ) Chef::ReservedNames::Win32::Error.raise! end diff --git a/lib/chef/node.rb b/lib/chef/node.rb index d5078371c5..22c7d5bd8e 100644 --- a/lib/chef/node.rb +++ b/lib/chef/node.rb @@ -315,6 +315,7 @@ class Chef # Consumes the combined run_list and other attributes in +attrs+ def consume_attributes(attrs) normal_attrs_to_merge = consume_run_list(attrs) + normal_attrs_to_merge = consume_chef_environment(normal_attrs_to_merge) Chef::Log.debug("Applying attributes from json file") self.normal_attrs = Chef::Mixin::DeepMerge.merge(normal_attrs,normal_attrs_to_merge) self.tags # make sure they're defined @@ -347,6 +348,24 @@ class Chef attrs end + # chef_environment when set in -j JSON will take precedence over + # -E ENVIRONMENT. Ideally, IMO, the order of precedence should be (lowest to + # highest): + # config_file + # -j JSON + # -E ENVIRONMENT + # so that users could reuse their JSON and override the chef_environment + # configured within it with -E ENVIRONMENT. Because command line options are + # merged with Chef::Config there is currently no way to distinguish between + # an environment set via config from an environment set via command line. + def consume_chef_environment(attrs) + attrs = attrs ? attrs.dup : {} + if env = attrs.delete("chef_environment") + chef_environment(env) + end + attrs + end + # Clear defaults and overrides, so that any deleted attributes # between runs are still gone. def reset_defaults_and_overrides diff --git a/lib/chef/node_map.rb b/lib/chef/node_map.rb index f547018a38..751f9576f6 100644 --- a/lib/chef/node_map.rb +++ b/lib/chef/node_map.rb @@ -20,13 +20,6 @@ class Chef class NodeMap # - # Create a new NodeMap - # - def initialize - @map = {} - end - - # # Set a key/value pair on the map with a filter. The filter must be true # when applied to the node in order to retrieve the value. # @@ -38,30 +31,34 @@ class Chef # # @return [NodeMap] Returns self for possible chaining # - def set(key, value, platform: nil, platform_version: nil, platform_family: nil, os: nil, on_platform: nil, on_platforms: nil, canonical: nil, &block) - Chef::Log.deprecation "The on_platform option to node_map has been deprecated" if on_platform - Chef::Log.deprecation "The on_platforms option to node_map has been deprecated" if on_platforms + def set(key, value, platform: nil, platform_version: nil, platform_family: nil, os: nil, on_platform: nil, on_platforms: nil, canonical: nil, override: nil, &block) + Chef.log_deprecation("The on_platform option to node_map has been deprecated") if on_platform + Chef.log_deprecation("The on_platforms option to node_map has been deprecated") if on_platforms platform ||= on_platform || on_platforms - filters = { platform: platform, platform_version: platform_version, platform_family: platform_family, os: os } - new_matcher = { filters: filters, block: block, value: value, canonical: canonical } - @map[key] ||= [] - # Decide where to insert the matcher; the new value is preferred over - # anything more specific (see `priority_of`) and is preferred over older - # values of the same specificity. (So all other things being equal, - # newest wins.) + filters = {} + filters[:platform] = platform if platform + filters[:platform_version] = platform_version if platform_version + filters[:platform_family] = platform_family if platform_family + filters[:os] = os if os + new_matcher = { value: value, filters: filters } + new_matcher[:block] = block if block + new_matcher[:canonical] = canonical if canonical + new_matcher[:override] = override if override + + # The map is sorted in order of preference already; we just need to find + # our place in it (just before the first value with the same preference level). insert_at = nil - @map[key].each_with_index do |matcher, index| - if specificity(new_matcher) >= specificity(matcher) - insert_at = index - break - end + map[key] ||= [] + map[key].each_with_index do |matcher,index| + cmp = compare_matchers(key, new_matcher, matcher) + insert_at ||= index if cmp && cmp <= 0 end if insert_at - @map[key].insert(insert_at, new_matcher) + map[key].insert(insert_at, new_matcher) else - @map[key] << new_matcher + map[key] << new_matcher end - self + map end # @@ -95,8 +92,8 @@ class Chef # def list(node, key, canonical: nil) raise ArgumentError, "first argument must be a Chef::Node" unless node.is_a?(Chef::Node) || node.nil? - return [] unless @map.has_key?(key) - @map[key].select do |matcher| + return [] unless map.has_key?(key) + map[key].select do |matcher| node_matches?(node, matcher) && canonical_matches?(canonical, matcher) end.map { |matcher| matcher[:value] } end @@ -105,41 +102,18 @@ class Chef # @return remaining # @api private def delete_canonical(key, value) - remaining = @map[key] + remaining = map[key] if remaining remaining.delete_if { |matcher| matcher[:canonical] && Array(matcher[:value]) == Array(value) } if remaining.empty? - @map.delete(key) + map.delete(key) remaining = nil end end remaining end - private - - # - # Gives a value for "how specific" the matcher is. - # Things which specify more specific filters get a higher number - # (platform_version > platform > platform_family > os); things - # with a block have higher specificity than similar things without - # a block. - # - def specificity(matcher) - if matcher[:filters][:platform_version] - specificity = 8 - elsif matcher[:filters][:platform] - specificity = 6 - elsif matcher[:filters][:platform_family] - specificity = 4 - elsif matcher[:filters][:os] - specificity = 2 - else - specificity = 0 - end - specificity += 1 if matcher[:block] - specificity - end + protected # # Succeeds if: @@ -197,5 +171,52 @@ class Chef return true if canonical.nil? !!canonical == !!matcher[:canonical] end + + def compare_matchers(key, new_matcher, matcher) + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:block] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform_version] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform_family] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:os] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:override] } + return cmp if cmp != 0 + # If all things are identical, return 0 + 0 + end + + def compare_matcher_properties(new_matcher, matcher) + a = yield(new_matcher) + b = yield(matcher) + + # Check for blcacklists ('!windows'). Those always come *after* positive + # whitelists. + a_negated = Array(a).any? { |f| f.is_a?(String) && f.start_with?('!') } + b_negated = Array(b).any? { |f| f.is_a?(String) && f.start_with?('!') } + if a_negated != b_negated + return 1 if a_negated + return -1 if b_negated + end + + # We treat false / true and nil / not-nil with the same comparison + a = nil if a == false + b = nil if b == false + cmp = a <=> b + # This is the case where one is non-nil, and one is nil. The one that is + # nil is "greater" (i.e. it should come last). + if cmp.nil? + return 1 if a.nil? + return -1 if b.nil? + end + cmp + end + + def map + @map ||= {} + end end end diff --git a/lib/chef/osc_user.rb b/lib/chef/osc_user.rb deleted file mode 100644 index 52bfd11108..0000000000 --- a/lib/chef/osc_user.rb +++ /dev/null @@ -1,194 +0,0 @@ -# -# Author:: Steven Danna (steve@opscode.com) -# Copyright:: Copyright 2012 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 'chef/config' -require 'chef/mixin/params_validate' -require 'chef/mixin/from_file' -require 'chef/mash' -require 'chef/json_compat' -require 'chef/search/query' - -# TODO -# DEPRECATION NOTE -# This class was previously Chef::User. It is the code to support the User object -# corrosponding to the Open Source Chef Server 11 and only still exists to support -# users still on OSC 11. -# -# Chef::User now supports Chef Server 12. -# -# New development should occur in Chef::User. -# This file and corrosponding osc_user knife files -# should be removed once client support for Open Source Chef Server 11 expires. -class Chef - class OscUser - - include Chef::Mixin::FromFile - include Chef::Mixin::ParamsValidate - - def initialize - @name = '' - @public_key = nil - @private_key = nil - @password = nil - @admin = false - end - - def name(arg=nil) - set_or_return(:name, arg, - :regex => /^[a-z0-9\-_]+$/) - end - - def admin(arg=nil) - set_or_return(:admin, - arg, :kind_of => [TrueClass, FalseClass]) - end - - def public_key(arg=nil) - set_or_return(:public_key, - arg, :kind_of => String) - end - - def private_key(arg=nil) - set_or_return(:private_key, - arg, :kind_of => String) - end - - def password(arg=nil) - set_or_return(:password, - arg, :kind_of => String) - end - - def to_hash - result = { - "name" => @name, - "public_key" => @public_key, - "admin" => @admin - } - result["private_key"] = @private_key if @private_key - result["password"] = @password if @password - result - end - - def to_json(*a) - Chef::JSONCompat.to_json(to_hash, *a) - end - - def destroy - Chef::REST.new(Chef::Config[:chef_server_url]).delete_rest("users/#{@name}") - end - - def create - payload = {:name => self.name, :admin => self.admin, :password => self.password } - payload[:public_key] = public_key if public_key - new_user =Chef::REST.new(Chef::Config[:chef_server_url]).post_rest("users", payload) - Chef::OscUser.from_hash(self.to_hash.merge(new_user)) - end - - def update(new_key=false) - payload = {:name => name, :admin => admin} - payload[:private_key] = new_key if new_key - payload[:password] = password if password - updated_user = Chef::REST.new(Chef::Config[:chef_server_url]).put_rest("users/#{name}", payload) - Chef::OscUser.from_hash(self.to_hash.merge(updated_user)) - end - - def save(new_key=false) - begin - create - rescue Net::HTTPServerException => e - if e.response.code == "409" - update(new_key) - else - raise e - end - end - end - - def reregister - r = Chef::REST.new(Chef::Config[:chef_server_url]) - reregistered_self = r.put_rest("users/#{name}", { :name => name, :admin => admin, :private_key => true }) - private_key(reregistered_self["private_key"]) - self - end - - def to_s - "user[#{@name}]" - end - - def inspect - "Chef::OscUser name:'#{name}' admin:'#{admin.inspect}'" + - "public_key:'#{public_key}' private_key:#{private_key}" - end - - # Class Methods - - def self.from_hash(user_hash) - user = Chef::OscUser.new - user.name user_hash['name'] - user.private_key user_hash['private_key'] if user_hash.key?('private_key') - user.password user_hash['password'] if user_hash.key?('password') - user.public_key user_hash['public_key'] - user.admin user_hash['admin'] - user - end - - def self.from_json(json) - Chef::OscUser.from_hash(Chef::JSONCompat.from_json(json)) - end - - class << self - alias_method :json_create, :from_json - end - - def self.list(inflate=false) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest('users') - users = if response.is_a?(Array) - transform_ohc_list_response(response) # OHC/OPC - else - response # OSC - end - if inflate - users.inject({}) do |user_map, (name, _url)| - user_map[name] = Chef::OscUser.load(name) - user_map - end - else - users - end - end - - def self.load(name) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get_rest("users/#{name}") - Chef::OscUser.from_hash(response) - end - - # Gross. Transforms an API response in the form of: - # [ { "user" => { "username" => USERNAME }}, ...] - # into the form - # { "USERNAME" => "URI" } - def self.transform_ohc_list_response(response) - new_response = Hash.new - response.each do |u| - name = u['user']['username'] - new_response[name] = Chef::Config[:chef_server_url] + "/users/#{name}" - end - new_response - end - - private_class_method :transform_ohc_list_response - end -end diff --git a/lib/chef/platform/handler_map.rb b/lib/chef/platform/handler_map.rb new file mode 100644 index 0000000000..a9551a344b --- /dev/null +++ b/lib/chef/platform/handler_map.rb @@ -0,0 +1,40 @@ +# +# Author:: John Keiser (<jkeiser@chef.io>) +# Copyright:: Copyright (c) 2015 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 'chef/node_map' + +class Chef + class Platform + class HandlerMap < Chef::NodeMap + # + # "provides" lines with identical filters sort by class name (ascending). + # + def compare_matchers(key, new_matcher, matcher) + cmp = super + if cmp == 0 + # Sort by class name (ascending) as well, if all other properties + # are exactly equal + if new_matcher[:value].is_a?(Class) && !new_matcher[:override] + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:value].name } + end + end + cmp + end + end + end +end diff --git a/lib/chef/platform/priority_map.rb b/lib/chef/platform/priority_map.rb new file mode 100644 index 0000000000..0b050deb59 --- /dev/null +++ b/lib/chef/platform/priority_map.rb @@ -0,0 +1,41 @@ +# +# Author:: John Keiser (<jkeiser@chef.io>) +# Copyright:: Copyright (c) 2015 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 'chef/node_map' + +class Chef + class Platform + class PriorityMap < Chef::NodeMap + def priority(resource_name, priority_array, *filter) + set_priority_array(resource_name.to_sym, priority_array, *filter) + end + + # @api private + def get_priority_array(node, key) + get(node, key) + end + + # @api private + def set_priority_array(key, priority_array, *filter, &block) + priority_array = Array(priority_array) + set(key, priority_array, *filter, &block) + priority_array + end + end + end +end diff --git a/lib/chef/mixin/wstring.rb b/lib/chef/platform/provider_handler_map.rb index bb6fdf4884..4549d7994e 100644 --- a/lib/chef/mixin/wstring.rb +++ b/lib/chef/platform/provider_handler_map.rb @@ -1,6 +1,6 @@ # -# Author:: Jay Mundrawala(<jdm@chef.io>) -# Copyright:: Copyright 2015 Chef Software +# Author:: John Keiser (<jkeiser@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,16 +16,14 @@ # limitations under the License. # +require 'singleton' +require 'chef/platform/handler_map' + class Chef - module Mixin - module WideString - def wstring(str) - if str.nil? || str.encoding == Encoding::UTF_16LE - str - else - str.to_wstring - end - end + class Platform + # @api private + class ProviderHandlerMap < Chef::Platform::HandlerMap + include Singleton end end end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index e3a894c8ac..9b511f0237 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -176,7 +176,7 @@ class Chef platform_provider(platform, version, resource_type) || resource_matching_provider(platform, version, resource_type) - raise ArgumentError, "Cannot find a provider for #{resource_type} on #{platform} version #{version}" if provider_klass.nil? + raise Chef::Exceptions::ProviderNotFound, "Cannot find a provider for #{resource_type} on #{platform} version #{version}" if provider_klass.nil? provider_klass end @@ -197,16 +197,16 @@ class Chef def resource_matching_provider(platform, version, resource_type) if resource_type.kind_of?(Chef::Resource) - class_name = resource_type.class.to_s.split('::').last + class_name = resource_type.class.name ? resource_type.class.name.split('::').last : + convert_to_class_name(resource_type.resource_name.to_s) - begin - result = Chef::Provider.const_get(class_name) + if Chef::Provider.const_defined?(class_name) Chef::Log.warn("Class Chef::Provider::#{class_name} does not declare 'provides #{convert_to_snake_case(class_name).to_sym.inspect}'.") - Chef::Log.warn("This will no longer work in Chef 13: you must use 'provides' to provide DSL.") - rescue NameError + Chef::Log.warn("This will no longer work in Chef 13: you must use 'provides' to use the resource's DSL.") + return Chef::Provider.const_get(class_name) end end - result + nil end end diff --git a/lib/chef/platform/provider_priority_map.rb b/lib/chef/platform/provider_priority_map.rb index 9d703c9178..5599c74c2d 100644 --- a/lib/chef/platform/provider_priority_map.rb +++ b/lib/chef/platform/provider_priority_map.rb @@ -1,29 +1,11 @@ require 'singleton' +require 'chef/platform/priority_map' class Chef class Platform - class ProviderPriorityMap + # @api private + class ProviderPriorityMap < Chef::Platform::PriorityMap include Singleton - - def get_priority_array(node, resource_name) - priority_map.get(node, resource_name.to_sym) - end - - def set_priority_array(resource_name, priority_array, *filter, &block) - priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) - end - - # @api private - def list_handlers(node, resource_name) - priority_map.list(node, resource_name.to_sym).flatten(1).uniq - end - - private - - def priority_map - require 'chef/node_map' - @priority_map ||= Chef::NodeMap.new - end end end end diff --git a/lib/chef/platform/query_helpers.rb b/lib/chef/platform/query_helpers.rb index b3948eac21..e64189fbd6 100644 --- a/lib/chef/platform/query_helpers.rb +++ b/lib/chef/platform/query_helpers.rb @@ -25,13 +25,10 @@ class Chef end def windows_server_2003? + # WMI startup shouldn't be performed unless we're on Windows. return false unless windows? require 'wmi-lite/wmi' - # CHEF-4888: Work around ruby #2618, expected to be fixed in Ruby 2.1.0 - # https://github.com/ruby/ruby/commit/588504b20f5cc880ad51827b93e571e32446e5db - # https://github.com/ruby/ruby/commit/27ed294c7134c0de582007af3c915a635a6506cd - wmi = WmiLite::Wmi.new host = wmi.first_of('Win32_OperatingSystem') is_server_2003 = (host['version'] && host['version'].start_with?("5.2")) diff --git a/lib/chef/platform/rebooter.rb b/lib/chef/platform/rebooter.rb index b46f0e394c..b78ac38f0c 100644 --- a/lib/chef/platform/rebooter.rb +++ b/lib/chef/platform/rebooter.rb @@ -32,7 +32,7 @@ class Chef cmd = if Chef::Platform.windows? # should this do /f as well? do we then need a minimum delay to let apps quit? - "shutdown /r /t #{reboot_info[:delay_mins]} /c \"#{reboot_info[:reason]}\"" + "shutdown /r /t #{reboot_info[:delay_mins]*60} /c \"#{reboot_info[:reason]}\"" else # probably Linux-only. "shutdown -r +#{reboot_info[:delay_mins]} \"#{reboot_info[:reason]}\"" diff --git a/lib/chef/platform/resource_handler_map.rb b/lib/chef/platform/resource_handler_map.rb new file mode 100644 index 0000000000..27a7bb1342 --- /dev/null +++ b/lib/chef/platform/resource_handler_map.rb @@ -0,0 +1,29 @@ +# +# Author:: John Keiser (<jkeiser@chef.io>) +# 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 'singleton' +require 'chef/platform/handler_map' + +class Chef + class Platform + # @api private + class ResourceHandlerMap < Chef::Platform::HandlerMap + include Singleton + end + end +end diff --git a/lib/chef/platform/resource_priority_map.rb b/lib/chef/platform/resource_priority_map.rb index fb08debc53..5cc86fd2e7 100644 --- a/lib/chef/platform/resource_priority_map.rb +++ b/lib/chef/platform/resource_priority_map.rb @@ -1,34 +1,11 @@ require 'singleton' +require 'chef/platform/priority_map' class Chef class Platform - class ResourcePriorityMap + # @api private + class ResourcePriorityMap < Chef::Platform::PriorityMap include Singleton - - def get_priority_array(node, resource_name, canonical: nil) - priority_map.get(node, resource_name.to_sym, canonical: canonical) - end - - def set_priority_array(resource_name, priority_array, *filter, &block) - priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) - end - - # @api private - def delete_canonical(resource_name, resource_class) - priority_map.delete_canonical(resource_name, resource_class) - end - - # @api private - def list_handlers(*args) - priority_map.list(*args).flatten(1).uniq - end - - private - - def priority_map - require 'chef/node_map' - @priority_map ||= Chef::NodeMap.new - end end end end diff --git a/lib/chef/property.rb b/lib/chef/property.rb new file mode 100644 index 0000000000..09198d90f1 --- /dev/null +++ b/lib/chef/property.rb @@ -0,0 +1,536 @@ +# +# Author:: John Keiser <jkeiser@chef.io> +# Copyright:: Copyright (c) 2015 John Keiser. +# 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/exceptions' +require 'chef/delayed_evaluator' + +class Chef + # + # Type and validation information for a property on a resource. + # + # A property named "x" manipulates the "@x" instance variable on a + # resource. The *presence* of the variable (`instance_variable_defined?(@x)`) + # tells whether the variable is defined; it may have any actual value, + # constrained only by validation. + # + # Properties may have validation, defaults, and coercion, and have full + # support for lazy values. + # + # @see Chef::Resource.property + # @see Chef::DelayedEvaluator + # + class Property + # + # Create a reusable property type that can be used in multiple properties + # in different resources. + # + # @param options [Hash<Symbol,Object>] Validation options. See Chef::Resource.property for + # the list of options. + # + # @example + # Property.derive(default: 'hi') + # + def self.derive(**options) + new(**options) + end + + # + # Create a new property. + # + # @param options [Hash<Symbol,Object>] Property options, including + # control options here, as well as validation options (see + # Chef::Mixin::ParamsValidate#validate for a description of validation + # options). + # @option options [Symbol] :name The name of this property. + # @option options [Class] :declared_in The class this property comes from. + # @option options [Symbol] :instance_variable_name The instance variable + # tied to this property. Must include a leading `@`. Defaults to `@<name>`. + # `nil` means the property is opaque and not tied to a specific instance + # variable. + # @option options [Boolean] :desired_state `true` if this property is part of desired + # state. Defaults to `true`. + # @option options [Boolean] :identity `true` if this property is part of object + # identity. Defaults to `false`. + # @option options [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. + # @option options [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties) and cached. If not, the value will be frozen with Object#freeze + # to prevent users from modifying it in an instance. + # @option options [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # @option options [Boolean] :required `true` if this property + # must be present; `false` otherwise. This is checked after the resource + # is fully initialized. + # + def initialize(**options) + options.each { |k,v| options[k.to_sym] = v if k.is_a?(String) } + options[:name_property] = options.delete(:name_attribute) if options.has_key?(:name_attribute) && !options.has_key?(:name_property) + @options = options + + options[:name] = options[:name].to_sym if options[:name] + options[:instance_variable_name] = options[:instance_variable_name].to_sym if options[:instance_variable_name] + end + + # + # The name of this property. + # + # @return [String] + # + def name + options[:name] + end + + # + # The class this property was defined in. + # + # @return [Class] + # + def declared_in + options[:declared_in] + end + + # + # The instance variable associated with this property. + # + # Defaults to `@<name>` + # + # @return [Symbol] + # + def instance_variable_name + if options.has_key?(:instance_variable_name) + options[:instance_variable_name] + elsif name + :"@#{name}" + end + end + + # + # The raw default value for this resource. + # + # Does not coerce or validate the default. Does not evaluate lazy values. + # + # Defaults to `lazy { name }` if name_property is true; otherwise defaults to + # `nil` + # + def default + return options[:default] if options.has_key?(:default) + return Chef::DelayedEvaluator.new { name } if name_property? + nil + end + + # + # Whether this is part of the resource's natural identity or not. + # + # @return [Boolean] + # + def identity? + options[:identity] + end + + # + # Whether this is part of desired state or not. + # + # Defaults to true. + # + # @return [Boolean] + # + def desired_state? + return true if !options.has_key?(:desired_state) + options[:desired_state] + end + + # + # Whether this is name_property or not. + # + # @return [Boolean] + # + def name_property? + options[:name_property] + end + + # + # Whether this property has a default value. + # + # @return [Boolean] + # + def has_default? + options.has_key?(:default) || name_property? + end + + # + # Whether this property is required or not. + # + # @return [Boolean] + # + def required? + options[:required] + end + + # + # Validation options. (See Chef::Mixin::ParamsValidate#validate.) + # + # @return [Hash<Symbol,Object>] + # + def validation_options + @validation_options ||= options.reject { |k,v| + [:declared_in,:name,:instance_variable_name,:desired_state,:identity,:default,:name_property,:coerce,:required].include?(k) + } + end + + # + # Handle the property being called. + # + # The base implementation does the property get-or-set: + # + # ```ruby + # resource.myprop # get + # resource.myprop value # set + # ``` + # + # Subclasses may implement this with any arguments they want, as long as + # the corresponding DSL calls it correctly. + # + # @param resource [Chef::Resource] The resource to get the property from. + # @param value The value to set (or NOT_PASSED if it is a get). + # + # @return The current value of the property. If it is a `set`, lazy values + # will be returned without running, validating or coercing. If it is a + # `get`, the non-lazy, coerced, validated value will always be returned. + # + def call(resource, value=NOT_PASSED) + if value == NOT_PASSED + return get(resource) + end + + # myprop nil is sometimes a get (backcompat) + if value.nil? && !explicitly_accepts_nil?(resource) + # If you say "my_property nil" and the property explicitly accepts + # nil values, we consider this a get. + Chef.log_deprecation("#{name} nil currently does not overwrite the value of #{name}. This will change in Chef 13, and the value will be set to nil instead. Please change your code to explicitly accept nil using \"property :#{name}, [MyType, nil]\", or stop setting this value to nil.") + return get(resource) + end + + # Anything else (myprop value) is a set + set(resource, value) + end + + # + # Get the property value from the resource, handling lazy values, + # defaults, and validation. + # + # - If the property's value is lazy, it is evaluated, coerced and validated. + # - If the property has no value, and is required, raises ValidationFailed. + # - If the property has no value, but has a lazy default, it is evaluated, + # coerced and validated. If the evaluated value is frozen, the resulting + # - If the property has no value, but has a default, the default value + # will be returned and frozen. If the default value is lazy, it will be + # evaluated, coerced and validated, and the result stored in the property. + # - If the property has no value, but is name_property, `resource.name` + # is retrieved, coerced, validated and stored in the property. + # - Otherwise, `nil` is returned. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + # @return The value of the property. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property, or if the value is required and not set. + # + def get(resource) + if is_set?(resource) + value = get_value(resource) + if value.is_a?(DelayedEvaluator) + value = exec_in_resource(resource, value) + value = coerce(resource, value) + validate(resource, value) + end + value + + else + if has_default? + value = default + if value.is_a?(DelayedEvaluator) + value = exec_in_resource(resource, value) + end + + value = coerce(resource, value) + + # We don't validate defaults + + # If the value is mutable (non-frozen), we set it on the instance + # so that people can mutate it. (All constant default values are + # frozen.) + if !value.frozen? && !value.nil? + set_value(resource, value) + end + + value + + elsif required? + raise Chef::Exceptions::ValidationFailed, "#{name} is required" + end + end + end + + # + # Set the value of this property in the given resource. + # + # Non-lazy values are coerced and validated before being set. Coercion + # and validation of lazy values is delayed until they are first retrieved. + # + # @param resource [Chef::Resource] The resource to set this property in. + # @param value The value to set. + # + # @return The value that was set, after coercion (if lazy, still returns + # the lazy value) + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def set(resource, value) + unless value.is_a?(DelayedEvaluator) + value = coerce(resource, value) + validate(resource, value) + end + set_value(resource, value) + end + + # + # Find out whether this property has been set. + # + # This will be true if: + # - The user explicitly set the value + # - The property has a default, and the value was retrieved. + # + # From this point of view, it is worth looking at this as "what does the + # user think this value should be." In order words, if the user grabbed + # the value, even if it was a default, they probably based calculations on + # it. If they based calculations on it and the value changes, the rest of + # the world gets inconsistent. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + # @return [Boolean] + # + def is_set?(resource) + value_is_set?(resource) + end + + # + # Reset the value of this property so that is_set? will return false and the + # default will be returned in the future. + # + # @param resource [Chef::Resource] The resource to get the property from. + # + def reset(resource) + reset_value(resource) + end + + # + # Coerce an input value into canonical form for the property. + # + # After coercion, the value is suitable for storage in the resource. + # You must validate values after coercion, however. + # + # Does no special handling for lazy values. + # + # @param resource [Chef::Resource] The resource we're coercing against + # (to provide context for the coerce). + # @param value The value to coerce. + # + # @return The coerced value. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def coerce(resource, value) + if options.has_key?(:coerce) + value = exec_in_resource(resource, options[:coerce], value) + end + value + end + + # + # Validate a value. + # + # Calls Chef::Mixin::ParamsValidate#validate with #validation_options as + # options. + # + # @param resource [Chef::Resource] The resource we're validating against + # (to provide context for the validate). + # @param value The value to validate. + # + # @raise Chef::Exceptions::ValidationFailed If the value is invalid for + # this property. + # + def validate(resource, value) + resource.validate({ name => value }, { name => validation_options }) + end + + # + # Derive a new Property that is just like this one, except with some added or + # changed options. + # + # @param options [Hash<Symbol,Object>] List of options that would be passed + # to #initialize. + # + # @return [Property] The new property type. + # + def derive(**modified_options) + Property.new(**options.merge(**modified_options)) + end + + # + # Emit the DSL for this property into the resource class (`declared_in`). + # + # Creates a getter and setter for the property. + # + def emit_dsl + # We don't create the getter/setter if it's a custom property; we will + # be using the existing getter/setter to manipulate it instead. + return if !instance_variable_name + + # We prefer this form because the property name won't show up in the + # stack trace if you use `define_method`. + declared_in.class_eval <<-EOM, __FILE__, __LINE__+1 + def #{name}(value=NOT_PASSED) + self.class.properties[#{name.inspect}].call(self, value) + end + def #{name}=(value) + self.class.properties[#{name.inspect}].set(self, value) + end + EOM + rescue SyntaxError + # If the name is not a valid ruby name, we use define_method. + resource_class.define_method(name) do |value=NOT_PASSED| + self.class.properties[name].call(self, value) + end + resource_class.define_method("#{name}=") do |value| + self.class.properties[name].set(self, value) + end + end + + protected + + # + # The options this Property will use for get/set behavior and validation. + # + # @see #initialize for a list of valid options. + # + attr_reader :options + + # + # Find out whether this type accepts nil explicitly. + # + # A type accepts nil explicitly if "is" allows nil, it validates as nil, *and* is not simply + # an empty type. + # + # These examples accept nil explicitly: + # ```ruby + # property :a, [ String, nil ] + # property :a, [ String, NilClass ] + # property :a, [ String, proc { |v| v.nil? } ] + # ``` + # + # This does not (because the "is" doesn't exist or doesn't have nil): + # + # ```ruby + # property :x, String + # ``` + # + # These do not, even though nil would validate fine (because they do not + # have "is"): + # + # ```ruby + # property :a + # property :a, equal_to: [ 1, 2, 3, nil ] + # property :a, kind_of: [ String, NilClass ] + # property :a, respond_to: [ ] + # property :a, callbacks: { "a" => proc { |v| v.nil? } } + # ``` + # + # @param resource [Chef::Resource] The resource we're coercing against + # (to provide context for the coerce). + # + # @return [Boolean] Whether this value explicitly accepts nil. + # + # @api private + def explicitly_accepts_nil?(resource) + options.has_key?(:is) && resource.send(:_pv_is, { name => nil }, name, options[:is], raise_error: false) + end + + def get_value(resource) + if instance_variable_name + resource.instance_variable_get(instance_variable_name) + else + resource.send(name) + end + end + + def set_value(resource, value) + if instance_variable_name + resource.instance_variable_set(instance_variable_name, value) + else + resource.send(name, value) + end + end + + def value_is_set?(resource) + if instance_variable_name + resource.instance_variable_defined?(instance_variable_name) + else + true + end + end + + def reset_value(resource) + if instance_variable_name + if value_is_set?(resource) + resource.remove_instance_variable(instance_variable_name) + end + else + raise ArgumentError, "Property #{name} has no instance variable defined and cannot be reset" + end + end + + def exec_in_resource(resource, proc, *args) + if resource + if proc.arity > args.size + value = proc.call(resource, *args) + else + value = resource.instance_exec(*args, &proc) + end + else + value = proc.call + end + + if value.is_a?(DelayedEvaluator) + value = coerce(resource, value) + validate(resource, value) + end + value + end + end +end diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index e50e374804..3138704a55 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -26,6 +26,7 @@ require 'chef/mixin/powershell_out' require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' +require 'forwardable' class Chef class Provider @@ -65,6 +66,7 @@ class Chef @recipe_name = nil @cookbook_name = nil + self.class.include_resource_dsl_module(new_resource) end def whyrun_mode? @@ -119,11 +121,11 @@ class Chef check_resource_semantics! # user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode - if !whyrun_mode? || whyrun_supported? + if whyrun_mode? && !whyrun_supported? + events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource) + else load_current_resource events.resource_current_state_loaded(@new_resource, @action, @current_resource) - elsif whyrun_mode? && !whyrun_supported? - events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource) end define_resource_requirements @@ -136,9 +138,7 @@ class Chef # we can't execute the action. # in non-whyrun mode, this will still cause the action to be # executed normally. - if whyrun_supported? && !requirements.action_blocked?(@action) - send("action_#{@action}") - elsif whyrun_mode? + if whyrun_mode? && (!whyrun_supported? || requirements.action_blocked?(@action)) events.resource_bypassed(@new_resource, @action, self) else send("action_#{@action}") @@ -175,14 +175,221 @@ class Chef converge_actions.add_action(descriptions, &block) end + # + # Handle patchy convergence safely. + # + # - Does *not* call the block if the current_resource's properties match + # the properties the user specified on the resource. + # - Calls the block if current_resource does not exist + # - Calls the block if the user has specified any properties in the resource + # whose values are *different* from current_resource. + # - Does *not* call the block if why-run is enabled (just prints out text). + # - Prints out automatic green text saying what properties have changed. + # + # @param properties An optional list of property names (symbols). If not + # specified, `new_resource.class.state_properties` will be used. + # @param converge_block The block to do the converging in. + # + # @return [Boolean] whether the block was executed. + # + def converge_if_changed(*properties, &converge_block) + if !converge_block + raise ArgumentError, "converge_if_changed must be passed a block!" + end + + properties = new_resource.class.state_properties.map { |p| p.name } if properties.empty? + properties = properties.map { |p| p.to_sym } + if current_resource + # Collect the list of modified properties + specified_properties = properties.select { |property| new_resource.property_is_set?(property) } + modified = specified_properties.select { |p| new_resource.send(p) != current_resource.send(p) } + if modified.empty? + Chef::Log.debug("Skipping update of #{new_resource.to_s}: has not changed any of the specified properties #{specified_properties.map { |p| "#{p}=#{new_resource.send(p).inspect}" }.join(", ")}.") + return false + end + + # Print the pretty green text and run the block + property_size = modified.map { |p| p.size }.max + modified = modified.map { |p| " set #{p.to_s.ljust(property_size)} to #{new_resource.send(p).inspect} (was #{current_resource.send(p).inspect})" } + converge_by([ "update #{current_resource.identity}" ] + modified, &converge_block) + + else + # The resource doesn't exist. Mark that we are *creating* this, and + # write down any properties we are setting. + property_size = properties.map { |p| p.size }.max + created = [] + properties.each do |property| + if new_resource.property_is_set?(property) + created << " set #{property.to_s.ljust(property_size)} to #{new_resource.send(property).inspect}" + else + created << " set #{property.to_s.ljust(property_size)} to #{new_resource.send(property).inspect} (default value)" + end + end + + converge_by([ "create #{new_resource.identity}" ] + created, &converge_block) + end + true + end + def self.provides(short_name, opts={}, &block) - Chef.set_provider_priority_array(short_name, self, opts, &block) + Chef.provider_handler_map.set(short_name, self, opts, &block) end def self.provides?(node, resource) Chef::ProviderResolver.new(node, resource, :nothing).provided_by?(self) end + # + # Include attributes, public and protected methods from this Resource in + # the provider. + # + # If this is set to true, delegate methods are included in the provider so + # that you can call (for example) `attrname` and it will call + # `new_resource.attrname`. + # + # The actual include does not happen until the first time the Provider + # is instantiated (so that we don't have to worry about load order issues). + # + # @param include_resource_dsl [Boolean] Whether to include resource DSL or + # not (defaults to `false`). + # + def self.include_resource_dsl(include_resource_dsl) + @include_resource_dsl = include_resource_dsl + end + + # Create the resource DSL module that forwards resource methods to new_resource + # + # @api private + def self.include_resource_dsl_module(resource) + if @include_resource_dsl && !defined?(@included_resource_dsl_module) + provider_class = self + @included_resource_dsl_module = Module.new do + extend Forwardable + define_singleton_method(:to_s) { "#{resource_class} forwarder module" } + define_singleton_method(:inspect) { to_s } + # Add a delegator for each explicit property that will get the *current* value + # of the property by default instead of the *actual* value. + resource.class.properties.each do |name, property| + class_eval(<<-EOM, __FILE__, __LINE__) + def #{name}(*args, &block) + # If no arguments were passed, we process "get" by defaulting + # the value to current_resource, not new_resource. This helps + # avoid issues where resources accidentally overwrite perfectly + # valid stuff with default values. + if args.empty? && !block + if !new_resource.property_is_set?(__method__) && current_resource + return current_resource.public_send(__method__) + end + end + new_resource.public_send(__method__, *args, &block) + end + EOM + end + dsl_methods = + resource.class.public_instance_methods + + resource.class.protected_instance_methods - + provider_class.instance_methods - + resource.class.properties.keys + def_delegators(:new_resource, *dsl_methods) + end + include @included_resource_dsl_module + end + end + + # Enables inline evaluation of resources in provider actions. + # + # Without this option, any resources declared inside the Provider are added + # to the resource collection after the current position at the time the + # action is executed. Because they are added to the primary resource + # collection for the chef run, they can notify other resources outside + # the Provider, and potentially be notified by resources outside the Provider + # (but this is complicated by the fact that they don't exist until the + # provider executes). In this mode, it is impossible to correctly set the + # updated_by_last_action flag on the parent Provider resource, since it + # executes and returns before its component resources are run. + # + # With this option enabled, each action creates a temporary run_context + # with its own resource collection, evaluates the action's code in that + # context, and then converges the resources created. If any resources + # were updated, then this provider's new_resource will be marked updated. + # + # In this mode, resources created within the Provider cannot interact with + # external resources via notifies, though notifications to other + # resources within the Provider will work. Delayed notifications are executed + # at the conclusion of the provider's action, *not* at the end of the + # main chef run. + # + # This mode of evaluation is experimental, but is believed to be a better + # set of tradeoffs than the append-after mode, so it will likely become + # the default in a future major release of Chef. + # + def self.use_inline_resources + extend InlineResources::ClassMethods + include InlineResources + end + + # Chef::Provider::InlineResources + # Implementation of inline resource convergence for providers. See + # Provider.use_inline_resources for a longer explanation. + # + # This code is restricted to a module so that it can be selectively + # applied to providers on an opt-in basis. + # + # @api private + module InlineResources + + # Our run context is a child of the main run context; that gives us a + # whole new resource collection and notification set. + def initialize(resource, run_context) + super(resource, run_context.create_child) + end + + # Class methods for InlineResources. Overrides the `action` DSL method + # with one that enables inline resource convergence. + # + # @api private + module ClassMethods + # Defines an action method on the provider, running the block to + # compile the resources, converging them, and then checking if any + # were updated (and updating new-resource if so) + def action(name, &block) + # We first try to create the method using "def method_name", which is + # preferred because it actually shows up in stack traces. If that + # fails, we try define_method. + begin + class_eval <<-EOM, __FILE__, __LINE__+1 + def action_#{name} + return_value = compile_action_#{name} + Chef::Runner.new(run_context).converge + return_value + ensure + if run_context.resource_collection.any? {|r| r.updated? } + new_resource.updated_by_last_action(true) + end + end + EOM + rescue SyntaxError + define_method("action_#{name}") do + begin + return_value = send("compile_action_#{name}") + Chef::Runner.new(run_context).converge + return_value + ensure + if run_context.resource_collection.any? {|r| r.updated? } + new_resource.updated_by_last_action(true) + end + end + end + end + # We put the action in its own method so that super() works. + define_method("compile_action_#{name}", &block) + end + end + + require 'chef/dsl/recipe' + include Chef::DSL::Recipe::FullDSL + end + protected def converge_actions @@ -200,19 +407,21 @@ class Chef # manipulating notifies. converge_by ("evaluate block and run any associated actions") do - saved_run_context = @run_context - @run_context = @run_context.dup - @run_context.resource_collection = Chef::ResourceCollection.new - instance_eval(&block) - Chef::Runner.new(@run_context).converge - @run_context = saved_run_context + saved_run_context = run_context + begin + @run_context = run_context.create_child + instance_eval(&block) + Chef::Runner.new(run_context).converge + ensure + @run_context = saved_run_context + end end end module DeprecatedLWRPClass def const_missing(class_name) if deprecated_constants[class_name.to_sym] - Chef::Log.deprecation("Using an LWRP provider by its name (#{class_name}) directly is no longer supported in Chef 12 and will be removed. Use Chef::ProviderResolver.new(node, resource, action) instead.") + Chef.log_deprecation("Using an LWRP provider by its name (#{class_name}) directly is no longer supported in Chef 12 and will be removed. Use Chef::ProviderResolver.new(node, resource, action) instead.") deprecated_constants[class_name.to_sym] else raise NameError, "uninitialized constant Chef::Provider::#{class_name}" diff --git a/lib/chef/provider/batch.rb b/lib/chef/provider/batch.rb index b6b386e5a8..5f0134443d 100644 --- a/lib/chef/provider/batch.rb +++ b/lib/chef/provider/batch.rb @@ -28,6 +28,14 @@ class Chef super(new_resource, run_context, '.bat') end + def command + basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + + interpreter_path = Chef::Util::PathHelper.join(basepath, interpreter) + + "\"#{interpreter_path}\" #{flags} \"#{script_file.path}\"" + end + def flags @new_resource.flags.nil? ? '/c' : new_resource.flags + ' /c' end diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb index 19e7c01ab1..77a0410593 100644 --- a/lib/chef/provider/deploy.rb +++ b/lib/chef/provider/deploy.rb @@ -201,7 +201,7 @@ class Chef converge_by("execute migration command #{@new_resource.migration_command}") do Chef::Log.info "#{@new_resource} migrating #{@new_resource.user} with environment #{env_info}" - run_command(run_options(:command => @new_resource.migration_command, :cwd=>release_path, :log_level => :info)) + shell_out!(@new_resource.migration_command,run_options(:cwd=>release_path, :log_level => :info)) end end end @@ -221,7 +221,7 @@ class Chef else converge_by("restart app using command #{@new_resource.restart_command}") do Chef::Log.info("#{@new_resource} restarting app") - run_command(run_options(:command => @new_resource.restart_command, :cwd => @new_resource.current_path)) + shell_out!(@new_resource.restart_command,run_options(:cwd=>@new_resource.current_path)) end end end @@ -373,11 +373,9 @@ class Chef end def gem_resource_collection_runner - gems_collection = Chef::ResourceCollection.new - gem_packages.each { |rbgem| gems_collection.insert(rbgem) } - gems_run_context = run_context.dup - gems_run_context.resource_collection = gems_collection - Chef::Runner.new(gems_run_context) + child_context = run_context.create_child + gem_packages.each { |rbgem| child_context.resource_collection.insert(rbgem) } + Chef::Runner.new(child_context) end def gem_packages diff --git a/lib/chef/provider/directory.rb b/lib/chef/provider/directory.rb index 4d5423d0e8..8892d3a73d 100644 --- a/lib/chef/provider/directory.rb +++ b/lib/chef/provider/directory.rb @@ -64,7 +64,13 @@ class Chef is_parent_writable = lambda do |base_dir| base_dir = ::File.dirname(base_dir) if ::File.exists?(base_dir) - Chef::FileAccessControl.writable?(base_dir) + if Chef::FileAccessControl.writable?(base_dir) + true + elsif Chef::Util::PathHelper.is_sip_path?(base_dir, node) + Chef::Util::PathHelper.writable_sip_path?(base_dir) + else + false + end else is_parent_writable.call(base_dir) end @@ -74,7 +80,13 @@ class Chef # in why run mode & parent directory does not exist no permissions check is required # If not in why run, permissions must be valid and we rely on prior assertion that dir exists if !whyrun_mode? || ::File.exists?(parent_directory) - Chef::FileAccessControl.writable?(parent_directory) + if Chef::FileAccessControl.writable?(parent_directory) + true + elsif Chef::Util::PathHelper.is_sip_path?(parent_directory, node) + Chef::Util::PathHelper.writable_sip_path?(@new_resource.path) + else + false + end else true end diff --git a/lib/chef/provider/dsc_resource.rb b/lib/chef/provider/dsc_resource.rb index 5fa84a21e9..379369ba6e 100644 --- a/lib/chef/provider/dsc_resource.rb +++ b/lib/chef/provider/dsc_resource.rb @@ -53,7 +53,7 @@ class Chef requirements.assert(:run) do |a| a.assertion { supports_dsc_invoke_resource? } err = ["You must have Powershell version >= 5.0.10018.0 to use dsc_resource."] - a.failure_message Chef::Exceptions::NoProviderAvailable, + a.failure_message Chef::Exceptions::ProviderNotFound, err a.whyrun err + ["Assuming a previous resource installs Powershell 5.0.10018.0 or higher."] a.block_action! @@ -63,7 +63,7 @@ class Chef meta_configuration['RefreshMode'] == 'Disabled' } err = ["The LCM must have its RefreshMode set to Disabled. "] - a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ') + a.failure_message Chef::Exceptions::ProviderNotFound, err.join(' ') a.whyrun err + ["Assuming a previous resource sets the RefreshMode."] a.block_action! end diff --git a/lib/chef/provider/dsc_script.rb b/lib/chef/provider/dsc_script.rb index a75e68a475..b2432132b7 100644 --- a/lib/chef/provider/dsc_script.rb +++ b/lib/chef/provider/dsc_script.rb @@ -70,7 +70,7 @@ class Chef "Powershell 4.0 or higher was not detected on your system and is required to use the dsc_script resource.", ] a.assertion { supports_dsc? } - a.failure_message Chef::Exceptions::NoProviderAvailable, err.join(' ') + a.failure_message Chef::Exceptions::ProviderNotFound, err.join(' ') a.whyrun err + ["Assuming a previous resource installs Powershell 4.0 or higher."] a.block_action! end diff --git a/lib/chef/provider/group/pw.rb b/lib/chef/provider/group/pw.rb index f877ed2424..5b5c8136f1 100644 --- a/lib/chef/provider/group/pw.rb +++ b/lib/chef/provider/group/pw.rb @@ -109,7 +109,7 @@ class Chef else # Append is not set so we're resetting the membership of # the group to the given members. - members_to_be_added = @new_resource.members + members_to_be_added = @new_resource.members.dup @current_resource.members.each do |member| # No need to re-add a member if it's present in the new # list of members diff --git a/lib/chef/provider/ifconfig.rb b/lib/chef/provider/ifconfig.rb index 468e1ec639..7869917307 100644 --- a/lib/chef/provider/ifconfig.rb +++ b/lib/chef/provider/ifconfig.rb @@ -194,7 +194,7 @@ class Chef private def add_command - command = "ifconfig #{@new_resource.device} #{@new_resource.name}" + command = "ifconfig #{@new_resource.device} #{@new_resource.target}" command << " netmask #{@new_resource.mask}" if @new_resource.mask command << " metric #{@new_resource.metric}" if @new_resource.metric command << " mtu #{@new_resource.mtu}" if @new_resource.mtu @@ -202,7 +202,7 @@ class Chef end def enable_command - command = "ifconfig #{@new_resource.device} #{@new_resource.name}" + command = "ifconfig #{@new_resource.device} #{@new_resource.target}" command << " netmask #{@new_resource.mask}" if @new_resource.mask command << " metric #{@new_resource.metric}" if @new_resource.metric command << " mtu #{@new_resource.mtu}" if @new_resource.mtu diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index b5efbb284d..a96c382a01 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -28,52 +28,10 @@ class Chef # Base class from which LWRP providers inherit. class LWRPBase < Provider - # Chef::Provider::LWRPBase::InlineResources - # Implementation of inline resource convergence for LWRP providers. See - # Provider::LWRPBase.use_inline_resources for a longer explanation. - # - # This code is restricted to a module so that it can be selectively - # applied to providers on an opt-in basis. - module InlineResources - - # Class methods for InlineResources. Overrides the `action` DSL method - # with one that enables inline resource convergence. - module ClassMethods - # Defines an action method on the provider, using - # recipe_eval_with_update_check to execute the given block. - def action(name, &block) - define_method("action_#{name}") do - recipe_eval_with_update_check(&block) - end - end - end - - # Executes the given block in a temporary run_context with its own - # resource collection. After the block is executed, any resources - # declared inside are converged, and if any are updated, the - # new_resource will be marked updated. - def recipe_eval_with_update_check(&block) - saved_run_context = @run_context - temp_run_context = @run_context.dup - @run_context = temp_run_context - @run_context.resource_collection = Chef::ResourceCollection.new - - return_value = instance_eval(&block) - Chef::Runner.new(@run_context).converge - return_value - ensure - @run_context = saved_run_context - if temp_run_context.resource_collection.any? {|r| r.updated? } - new_resource.updated_by_last_action(true) - end - end - - end - include Chef::DSL::Recipe # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore. - # They are not included by its replacment, Chef::DSL::Recipe, but + # They are not included by its replacement, Chef::DSL::Recipe, but # they may be used in existing LWRPs. include Chef::DSL::PlatformIntrospection include Chef::DSL::DataQuery @@ -122,38 +80,6 @@ class Chef provider_class end - # Enables inline evaluation of resources in provider actions. - # - # Without this option, any resources declared inside the LWRP are added - # to the resource collection after the current position at the time the - # action is executed. Because they are added to the primary resource - # collection for the chef run, they can notify other resources outside - # the LWRP, and potentially be notified by resources outside the LWRP - # (but this is complicated by the fact that they don't exist until the - # provider executes). In this mode, it is impossible to correctly set the - # updated_by_last_action flag on the parent LWRP resource, since it - # executes and returns before its component resources are run. - # - # With this option enabled, each action creates a temporary run_context - # with its own resource collection, evaluates the action's code in that - # context, and then converges the resources created. If any resources - # were updated, then this provider's new_resource will be marked updated. - # - # In this mode, resources created within the LWRP cannot interact with - # external resources via notifies, though notifications to other - # resources within the LWRP will work. Delayed notifications are executed - # at the conclusion of the provider's action, *not* at the end of the - # main chef run. - # - # This mode of evaluation is experimental, but is believed to be a better - # set of tradeoffs than the append-after mode, so it will likely become - # the default in a future major release of Chef. - # - def use_inline_resources - extend InlineResources::ClassMethods - include InlineResources - end - # DSL for defining a provider's actions. def action(name, &block) define_method("action_#{name}") do diff --git a/lib/chef/provider/mount.rb b/lib/chef/provider/mount.rb index 2039e9ae51..6bdfd5b867 100644 --- a/lib/chef/provider/mount.rb +++ b/lib/chef/provider/mount.rb @@ -42,13 +42,17 @@ class Chef end def action_mount - unless current_resource.mounted + if current_resource.mounted + if mount_options_unchanged? + Chef::Log.debug("#{new_resource} is already mounted") + else + action_remount + end + else converge_by("mount #{current_resource.device} to #{current_resource.mount_point}") do mount_fs Chef::Log.info("#{new_resource} mounted") end - else - Chef::Log.debug("#{new_resource} is already mounted") end end diff --git a/lib/chef/provider/mount/aix.rb b/lib/chef/provider/mount/aix.rb index 4ad7b24c15..510dfde46d 100644 --- a/lib/chef/provider/mount/aix.rb +++ b/lib/chef/provider/mount/aix.rb @@ -32,7 +32,7 @@ class Chef @new_resource.options.clear end if @new_resource.fstype == "auto" - @new_resource.fstype = nil + @new_resource.send(:clear_fstype) end end diff --git a/lib/chef/provider/package.rb b/lib/chef/provider/package.rb index 9d534ec414..880104bff7 100644 --- a/lib/chef/provider/package.rb +++ b/lib/chef/provider/package.rb @@ -142,7 +142,7 @@ class Chef def action_remove if removing_package? description = @new_resource.version ? "version #{@new_resource.version} of " : "" - converge_by("remove #{description} package #{@current_resource.package_name}") do + converge_by("remove #{description}package #{@current_resource.package_name}") do remove_package(@current_resource.package_name, @new_resource.version) Chef::Log.info("#{@new_resource} removed") end @@ -491,37 +491,6 @@ class Chef end end - # Set provider priority - require 'chef/chef_class' - require 'chef/provider/package/dpkg' - require 'chef/provider/package/homebrew' - require 'chef/provider/package/macports' - require 'chef/provider/package/apt' - require 'chef/provider/package/yum' - require 'chef/provider/package/zypper' - require 'chef/provider/package/portage' - require 'chef/provider/package/pacman' - require 'chef/provider/package/ips' - require 'chef/provider/package/solaris' - require 'chef/provider/package/smartos' - require 'chef/provider/package/aix' - require 'chef/provider/package/paludis' - - Chef.set_provider_priority_array :package, [ Homebrew, Macports ], os: "darwin" - - Chef.set_provider_priority_array :package, Apt, platform_family: "debian" - Chef.set_provider_priority_array :package, Yum, platform_family: %w(rhel fedora) - Chef.set_provider_priority_array :package, Zypper, platform_family: "suse" - Chef.set_provider_priority_array :package, Portage, platform: "gentoo" - Chef.set_provider_priority_array :package, Pacman, platform: "arch" - Chef.set_provider_priority_array :package, Ips, platform: %w(openindiana opensolaris omnios solaris2) - Chef.set_provider_priority_array :package, Solaris, platform: "nexentacore" - Chef.set_provider_priority_array :package, Solaris, platform: "solaris2", platform_version: '< 5.11' - - Chef.set_provider_priority_array :package, SmartOS, platform: "smartos" - Chef.set_provider_priority_array :package, Aix, platform: "aix" - Chef.set_provider_priority_array :package, Paludis, platform: "exherbo" - private def shell_out_with_timeout(*command_args) diff --git a/lib/chef/provider/package/aix.rb b/lib/chef/provider/package/aix.rb index b97db9d061..5165f4b4ea 100644 --- a/lib/chef/provider/package/aix.rb +++ b/lib/chef/provider/package/aix.rb @@ -26,6 +26,7 @@ class Chef class Package class Aix < Chef::Provider::Package + provides :package, os: "aix" provides :bff_package, os: "aix" include Chef::Mixin::GetSourceFromPackage diff --git a/lib/chef/provider/package/apt.rb b/lib/chef/provider/package/apt.rb index bd6ed283bf..e109c9966a 100644 --- a/lib/chef/provider/package/apt.rb +++ b/lib/chef/provider/package/apt.rb @@ -25,6 +25,7 @@ class Chef class Package class Apt < Chef::Provider::Package + provides :package, platform_family: "debian" provides :apt_package, os: "linux" # return [Hash] mapping of package name to Boolean value diff --git a/lib/chef/provider/package/dpkg.rb b/lib/chef/provider/package/dpkg.rb index a262f1ab1a..67e9b903c6 100644 --- a/lib/chef/provider/package/dpkg.rb +++ b/lib/chef/provider/package/dpkg.rb @@ -25,8 +25,6 @@ class Chef class Provider class Package class Dpkg < Chef::Provider::Package - # http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version - DPKG_INFO = /([a-z\d\-\+\.]+)\t([\w\d.~:-]+)/ DPKG_INSTALLED = /^Status: install ok installed/ DPKG_VERSION = /^Version: (.+)$/ @@ -54,26 +52,22 @@ class Chef @source_exists = true @current_resource = Chef::Resource::Package.new(@new_resource.name) @current_resource.package_name(@new_resource.package_name) - @new_resource.version(nil) if @new_resource.source @source_exists = ::File.exists?(@new_resource.source) if @source_exists # Get information from the package if supplied Chef::Log.debug("#{@new_resource} checking dpkg status") - - shell_out_with_timeout("dpkg-deb -W #{@new_resource.source}").stdout.each_line do |line| - if pkginfo = DPKG_INFO.match(line) - @current_resource.package_name(pkginfo[1]) - @new_resource.version(pkginfo[2]) - @candidate_version = pkginfo[2] - end + status = shell_out_with_timeout("dpkg-deb -W #{@new_resource.source}") + pkginfo = status.stdout.split("\t") + unless pkginfo.empty? + @current_resource.package_name(pkginfo[0]) + @candidate_version = pkginfo[1].strip end else # Source provided but not valid means we can't safely do further processing return end - end # Check to see if it is installed diff --git a/lib/chef/provider/package/homebrew.rb b/lib/chef/provider/package/homebrew.rb index beede1c916..e5c45f0a62 100644 --- a/lib/chef/provider/package/homebrew.rb +++ b/lib/chef/provider/package/homebrew.rb @@ -26,6 +26,7 @@ class Chef class Package class Homebrew < Chef::Provider::Package + provides :package, os: "darwin", override: true provides :homebrew_package include Chef::Mixin::HomebrewUser diff --git a/lib/chef/provider/package/ips.rb b/lib/chef/provider/package/ips.rb index 4d7f4a3583..96c2e711d4 100644 --- a/lib/chef/provider/package/ips.rb +++ b/lib/chef/provider/package/ips.rb @@ -27,6 +27,7 @@ class Chef class Package class Ips < Chef::Provider::Package + provides :package, platform: %w(openindiana opensolaris omnios solaris2) provides :ips_package, os: "solaris2" attr_accessor :virtual diff --git a/lib/chef/provider/package/macports.rb b/lib/chef/provider/package/macports.rb index e945211540..c7ea71ac8c 100644 --- a/lib/chef/provider/package/macports.rb +++ b/lib/chef/provider/package/macports.rb @@ -3,6 +3,7 @@ class Chef class Package class Macports < Chef::Provider::Package + provides :package, os: "darwin" provides :macports_package def load_current_resource diff --git a/lib/chef/provider/package/openbsd.rb b/lib/chef/provider/package/openbsd.rb index f231101390..83fc09c8ae 100644 --- a/lib/chef/provider/package/openbsd.rb +++ b/lib/chef/provider/package/openbsd.rb @@ -31,6 +31,7 @@ class Chef class Openbsd < Chef::Provider::Package provides :package, os: "openbsd" + provides :openbsd_package include Chef::Mixin::ShellOut include Chef::Mixin::GetSourceFromPackage diff --git a/lib/chef/provider/package/pacman.rb b/lib/chef/provider/package/pacman.rb index bf03e54656..01e3a9cc01 100644 --- a/lib/chef/provider/package/pacman.rb +++ b/lib/chef/provider/package/pacman.rb @@ -25,6 +25,7 @@ class Chef class Package class Pacman < Chef::Provider::Package + provides :package, platform: "arch" provides :pacman_package, os: "linux" def load_current_resource diff --git a/lib/chef/provider/package/paludis.rb b/lib/chef/provider/package/paludis.rb index 407e0d0110..2d6302515b 100644 --- a/lib/chef/provider/package/paludis.rb +++ b/lib/chef/provider/package/paludis.rb @@ -24,6 +24,7 @@ class Chef class Package class Paludis < Chef::Provider::Package + provides :package, platform: "exherbo" provides :paludis_package, os: "linux" def load_current_resource diff --git a/lib/chef/provider/package/portage.rb b/lib/chef/provider/package/portage.rb index 4ba0160bb0..95782a6774 100644 --- a/lib/chef/provider/package/portage.rb +++ b/lib/chef/provider/package/portage.rb @@ -25,6 +25,8 @@ class Chef class Provider class Package class Portage < Chef::Provider::Package + + provides :package, platform: "gentoo" provides :portage_package PACKAGE_NAME_PATTERN = %r{(?:([^/]+)/)?([^/]+)} diff --git a/lib/chef/provider/package/rpm.rb b/lib/chef/provider/package/rpm.rb index 21c39752d1..c5d52a8384 100644 --- a/lib/chef/provider/package/rpm.rb +++ b/lib/chef/provider/package/rpm.rb @@ -61,7 +61,7 @@ class Chef Chef::Log.debug("#{@new_resource} checking rpm status") shell_out_with_timeout!("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@new_resource.source}").stdout.each_line do |line| case line - when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ + when /^([\w\d+_.-]+)\s([\w\d~_.-]+)$/ @current_resource.package_name($1) @new_resource.version($2) @candidate_version = $2 @@ -78,7 +78,7 @@ class Chef @rpm_status = shell_out_with_timeout("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{@current_resource.package_name}") @rpm_status.stdout.each_line do |line| case line - when /^([\w\d+_.-]+)\s([\w\d_.-]+)$/ + when /^([\w\d+_.-]+)\s([\w\d~_.-]+)$/ Chef::Log.debug("#{@new_resource} current version is #{$2}") @current_resource.version($2) end diff --git a/lib/chef/provider/package/rubygems.rb b/lib/chef/provider/package/rubygems.rb index b5f7dbdd80..729f755b2a 100644 --- a/lib/chef/provider/package/rubygems.rb +++ b/lib/chef/provider/package/rubygems.rb @@ -394,7 +394,7 @@ class Chef end def is_omnibus? - if RbConfig::CONFIG['bindir'] =~ %r!/opt/(opscode|chef)/embedded/bin! + if RbConfig::CONFIG['bindir'] =~ %r!/(opscode|chef|chefdk)/embedded/bin! Chef::Log.debug("#{@new_resource} detected omnibus installation in #{RbConfig::CONFIG['bindir']}") # Omnibus installs to a static path because of linking on unix, find it. true diff --git a/lib/chef/provider/package/smartos.rb b/lib/chef/provider/package/smartos.rb index 0d5b801c96..71b8a9b9e1 100644 --- a/lib/chef/provider/package/smartos.rb +++ b/lib/chef/provider/package/smartos.rb @@ -29,6 +29,7 @@ class Chef class SmartOS < Chef::Provider::Package attr_accessor :is_virtual_package + provides :package, platform: "smartos" provides :smartos_package, os: "solaris2", platform_family: "smartos" def load_current_resource diff --git a/lib/chef/provider/package/solaris.rb b/lib/chef/provider/package/solaris.rb index 9b10403344..e62f37d27b 100644 --- a/lib/chef/provider/package/solaris.rb +++ b/lib/chef/provider/package/solaris.rb @@ -27,6 +27,8 @@ class Chef include Chef::Mixin::GetSourceFromPackage + provides :package, platform: "nexentacore" + provides :package, platform: "solaris2", platform_version: '< 5.11' provides :solaris_package, os: "solaris2" # def initialize(*args) diff --git a/lib/chef/provider/package/yum.rb b/lib/chef/provider/package/yum.rb index 85c2ba683c..81454380a3 100644 --- a/lib/chef/provider/package/yum.rb +++ b/lib/chef/provider/package/yum.rb @@ -1,6 +1,6 @@ # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ class Chef class Package class Yum < Chef::Provider::Package + provides :package, platform_family: %w(rhel fedora) provides :yum_package, os: "linux" class RPMUtils @@ -650,6 +651,8 @@ class Chef include Chef::Mixin::ShellOut include Singleton + attr_accessor :yum_binary + def initialize @rpmdb = RPMDb.new @@ -780,7 +783,7 @@ class Chef end def python_bin - yum_executable = which("yum") + yum_executable = which(yum_binary) if yum_executable && shabang?(yum_executable) extract_interpreter(yum_executable) else @@ -979,6 +982,15 @@ class Chef super @yum = YumCache.instance + @yum.yum_binary = yum_binary + end + + def yum_binary + @yum_binary ||= + begin + yum_binary = new_resource.yum_binary if new_resource.is_a?(Chef::Resource::YumPackage) + yum_binary ||= ::File.exist?("/usr/bin/yum-deprecated") ? "yum-deprecated" : "yum" + end end # Extra attributes @@ -1025,6 +1037,7 @@ class Chef end def yum_command(command) + command = "#{yum_binary} #{command}" Chef::Log.debug("#{@new_resource}: yum command: \"#{command}\"") status = shell_out_with_timeout(command, {:timeout => Chef::Config[:yum_timeout]}) @@ -1232,7 +1245,7 @@ class Chef end pkg_string = pkg_string_bits.join(' ') Chef::Log.info("#{@new_resource} #{log_method} #{repos.join(' ')}") - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{pkg_string}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} #{method} #{pkg_string}") else raise Chef::Exceptions::Package, "Version #{version} of #{name} not found. Did you specify both version " + "and release? (version-release, e.g. 1.84-10.fc6)" @@ -1241,7 +1254,7 @@ class Chef def install_package(name, version) if @new_resource.source - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} localinstall #{@new_resource.source}") else install_remote_package(name, version) end @@ -1289,7 +1302,7 @@ class Chef "#{n}#{yum_arch(a)}" end.join(' ') end - yum_command("yum -d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") + yum_command("-d0 -e0 -y#{expand_options(@new_resource.options)} remove #{remove_str}") if flush_cache[:after] @yum.reload diff --git a/lib/chef/provider/package/zypper.rb b/lib/chef/provider/package/zypper.rb index c2a3ac4ba8..ac42304ffb 100644 --- a/lib/chef/provider/package/zypper.rb +++ b/lib/chef/provider/package/zypper.rb @@ -29,6 +29,7 @@ class Chef class Package class Zypper < Chef::Provider::Package + provides :package, platform_family: "suse" provides :zypper_package, os: "linux" def load_current_resource diff --git a/lib/chef/provider/powershell_script.rb b/lib/chef/provider/powershell_script.rb index ed44dee6ae..b876b6d8ee 100644 --- a/lib/chef/provider/powershell_script.rb +++ b/lib/chef/provider/powershell_script.rb @@ -30,8 +30,17 @@ class Chef end def action_run - valid_syntax = validate_script_syntax! - super if valid_syntax + validate_script_syntax! + super + end + + def command + basepath = is_forced_32bit ? wow64_directory : run_context.node.kernel.os_info.system_directory + + # Powershell.exe is always in "v1.0" folder (for backwards compatibility) + interpreter_path = Chef::Util::PathHelper.join(basepath, "WindowsPowerShell", "v1.0", interpreter) + + "\"#{interpreter_path}\" #{flags} \"#{script_file.path}\"" end def flags @@ -62,30 +71,46 @@ class Chef def validate_script_syntax! interpreter_arguments = default_interpreter_flags.join(' ') Tempfile.open(['chef_powershell_script-user-code', '.ps1']) do | user_script_file | - user_script_file.puts("{#{@new_resource.code}}") - user_script_file.close + # Wrap the user's code in a PowerShell script block so that + # it isn't executed. However, syntactically invalid script + # in that block will still trigger a syntax error which is + # exactly what we want here -- verify the syntax without + # actually running the script. + user_code_wrapped_in_powershell_script_block = <<-EOH +{ + #{@new_resource.code} +} +EOH + user_script_file.puts user_code_wrapped_in_powershell_script_block + # A .close or explicit .flush required to ensure the file is + # written to the file system at this point, which is required since + # the intent is to execute the code just written to it. + user_script_file.close validation_command = "\"#{interpreter}\" #{interpreter_arguments} -Command #{user_script_file.path}" - # For consistency with other script resources, allow even syntax errors - # to be suppressed if the returns attribute would have suppressed it - # at converge. - valid_returns = [0] - specified_returns = @new_resource.returns.is_a?(Integer) ? - [@new_resource.returns] : - @new_resource.returns - valid_returns.concat([1]) if specified_returns.include?(1) - - result = shell_out!(validation_command, {returns: valid_returns}) - result.exitstatus == 0 + # Note that other script providers like bash allow syntax errors + # to be suppressed by setting 'returns' to a value that the + # interpreter would return as a status code in the syntax + # error case. We explicitly don't do this here -- syntax + # errors will not be suppressed, since doing so could make + # it harder for users to detect / debug invalid scripts. + + # Therefore, the only return value for a syntactically valid + # script is 0. If an exception is raised by shellout, this + # means a non-zero return and thus a syntactically invalid script. + + with_os_architecture(node, architecture: new_resource.architecture) do + shell_out!(validation_command, {returns: [0]}) + end end end def default_interpreter_flags - # 'Bypass' is preferable since it doesn't require user input confirmation - # for files such as PowerShell modules downloaded from the - # Internet. However, 'Bypass' is not supported prior to - # PowerShell 3.0, so the fallback is 'Unrestricted' + # Execution policy 'Bypass' is preferable since it doesn't require + # user input confirmation for files such as PowerShell modules + # downloaded from the Internet. However, 'Bypass' is not supported + # prior to PowerShell 3.0, so the fallback is 'Unrestricted' execution_policy = Chef::Platform.supports_powershell_execution_bypass?(run_context.node) ? 'Bypass' : 'Unrestricted' [ diff --git a/lib/chef/provider/registry_key.rb b/lib/chef/provider/registry_key.rb index cd62f7c56f..948fa6c63f 100644 --- a/lib/chef/provider/registry_key.rb +++ b/lib/chef/provider/registry_key.rb @@ -64,7 +64,7 @@ class Chef def values_to_hash(values) if values - @name_hash = Hash[values.map { |val| [val[:name], val] }] + @name_hash = Hash[values.map { |val| [val[:name].downcase, val] }] else @name_hash = {} end @@ -100,8 +100,8 @@ class Chef end end @new_resource.unscrubbed_values.each do |value| - if @name_hash.has_key?(value[:name]) - current_value = @name_hash[value[:name]] + if @name_hash.has_key?(value[:name].downcase) + current_value = @name_hash[value[:name].downcase] unless current_value[:type] == value[:type] && current_value[:data] == value[:data] converge_by("set value #{value}") do registry.set_value(@new_resource.key, value) @@ -122,7 +122,7 @@ class Chef end end @new_resource.unscrubbed_values.each do |value| - unless @name_hash.has_key?(value[:name]) + unless @name_hash.has_key?(value[:name].downcase) converge_by("create value #{value}") do registry.set_value(@new_resource.key, value) end @@ -133,7 +133,7 @@ class Chef def action_delete if registry.key_exists?(@new_resource.key) @new_resource.unscrubbed_values.each do |value| - if @name_hash.has_key?(value[:name]) + if @name_hash.has_key?(value[:name].downcase) converge_by("delete value #{value}") do registry.delete_value(@new_resource.key, value) end diff --git a/lib/chef/provider/remote_directory.rb b/lib/chef/provider/remote_directory.rb index eaccce46cf..85ceb5cdae 100644 --- a/lib/chef/provider/remote_directory.rb +++ b/lib/chef/provider/remote_directory.rb @@ -67,7 +67,7 @@ class Chef ::File::FNM_DOTMATCH) # Remove current directory and previous directory - files.reject! do |name| + files = files.reject do |name| basename = Pathname.new(name).basename().to_s ['.', '..'].include?(basename) end diff --git a/lib/chef/provider/service.rb b/lib/chef/provider/service.rb index 9c523b5e66..e7bb2a76d7 100644 --- a/lib/chef/provider/service.rb +++ b/lib/chef/provider/service.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -25,6 +25,10 @@ class Chef include Chef::Mixin::Command + def supports + @supports ||= new_resource.supports.dup + end + def initialize(new_resource, run_context) super @enabled = nil @@ -34,6 +38,12 @@ class Chef true end + def load_current_resource + supports[:status] = false if supports[:status].nil? + supports[:reload] = false if supports[:reload].nil? + supports[:restart] = false if supports[:restart].nil? + end + def load_new_resource_state # If the user didn't specify a change in enabled state, # it will be the same as the old resource @@ -50,7 +60,7 @@ class Chef def define_resource_requirements requirements.assert(:reload) do |a| - a.assertion { @new_resource.supports[:reload] || @new_resource.reload_command } + a.assertion { supports[:reload] || @new_resource.reload_command } a.failure_message Chef::Exceptions::UnsupportedAction, "#{self.to_s} does not support :reload" # if a service is not declared to support reload, that won't # typically change during the course of a run - so no whyrun @@ -188,29 +198,11 @@ class Chef require 'chef/provider/service/upstart' require 'chef/provider/service/debian' require 'chef/provider/service/invokercd' - require 'chef/provider/service/freebsd' - require 'chef/provider/service/openbsd' - require 'chef/provider/service/solaris' - require 'chef/provider/service/macosx' - - def self.os(os, *providers) - Chef.set_provider_priority_array(:service, providers, os: os) - end - def self.platform_family(platform_family, *providers) - Chef.set_provider_priority_array(:service, providers, platform_family: platform_family) - end - - os %w(freebsd netbsd), Freebsd - os %w(openbsd), Openbsd - os %w(solaris2), Solaris - os %w(darwin), Macosx - os %w(linux), Systemd, Insserv, Redhat - - platform_family %w(arch), Systemd, Arch - platform_family %w(gentoo), Systemd, Gentoo - platform_family %w(debian), Systemd, Upstart, Insserv, Debian, Invokercd - platform_family %w(rhel fedora suse), Systemd, Insserv, Redhat + Chef.set_provider_priority_array :service, [ Systemd, Arch ], platform_family: 'arch' + Chef.set_provider_priority_array :service, [ Systemd, Gentoo ], platform_family: 'gentoo' + Chef.set_provider_priority_array :service, [ Systemd, Upstart, Insserv, Debian, Invokercd ], platform_family: 'debian' + Chef.set_provider_priority_array :service, [ Systemd, Insserv, Redhat ], platform_family: %w(rhel fedora suse) end end end diff --git a/lib/chef/provider/service/aix.rb b/lib/chef/provider/service/aix.rb index 09ed4bbf01..0c95ce2c8e 100644 --- a/lib/chef/provider/service/aix.rb +++ b/lib/chef/provider/service/aix.rb @@ -116,7 +116,7 @@ class Chef end def is_resource_group? - so = shell_out!("lssrc -g #{@new_resource.service_name}") + so = shell_out("lssrc -g #{@new_resource.service_name}") if so.exitstatus == 0 Chef::Log.debug("#{@new_resource.service_name} is a group") @is_resource_group = true diff --git a/lib/chef/provider/service/debian.rb b/lib/chef/provider/service/debian.rb index 01505924cb..7d23e4ac77 100644 --- a/lib/chef/provider/service/debian.rb +++ b/lib/chef/provider/service/debian.rb @@ -22,15 +22,13 @@ class Chef class Provider class Service class Debian < Chef::Provider::Service::Init + provides :service, platform_family: 'debian' do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:debian) + end + UPDATE_RC_D_ENABLED_MATCHES = /\/rc[\dS].d\/S|not installed/i UPDATE_RC_D_PRIORITIES = /\/rc([\dS]).d\/([SK])(\d\d)/i - provides :service, platform_family: "debian" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:debian) - end - def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:initd) end diff --git a/lib/chef/provider/service/freebsd.rb b/lib/chef/provider/service/freebsd.rb index 6c78f86fe0..78ca0be235 100644 --- a/lib/chef/provider/service/freebsd.rb +++ b/lib/chef/provider/service/freebsd.rb @@ -99,7 +99,7 @@ class Chef def restart_service if new_resource.restart_command super - elsif new_resource.supports[:restart] + elsif supports[:restart] shell_out_with_systems_locale!("#{init_command} fastrestart") else stop_service diff --git a/lib/chef/provider/service/gentoo.rb b/lib/chef/provider/service/gentoo.rb index 3dab920f06..903c55af7a 100644 --- a/lib/chef/provider/service/gentoo.rb +++ b/lib/chef/provider/service/gentoo.rb @@ -1,7 +1,7 @@ # # Author:: Lee Jensen (<ljensen@engineyard.com>) # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,9 +26,9 @@ class Chef::Provider::Service::Gentoo < Chef::Provider::Service::Init provides :service, platform_family: "gentoo" def load_current_resource + supports[:status] = true if supports[:status].nil? + supports[:restart] = true if supports[:restart].nil? - @new_resource.supports[:status] = true - @new_resource.supports[:restart] = true @found_script = false super diff --git a/lib/chef/provider/service/init.rb b/lib/chef/provider/service/init.rb index 355e98a0eb..8fe5b0281f 100644 --- a/lib/chef/provider/service/init.rb +++ b/lib/chef/provider/service/init.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -72,7 +72,7 @@ class Chef def restart_service if @new_resource.restart_command super - elsif @new_resource.supports[:restart] + elsif supports[:restart] shell_out_with_systems_locale!("#{default_init_command} restart") else stop_service @@ -84,7 +84,7 @@ class Chef def reload_service if @new_resource.reload_command super - elsif @new_resource.supports[:reload] + elsif supports[:reload] shell_out_with_systems_locale!("#{default_init_command} reload") end end diff --git a/lib/chef/provider/service/insserv.rb b/lib/chef/provider/service/insserv.rb index 31965a4bc6..dd01f9ab87 100644 --- a/lib/chef/provider/service/insserv.rb +++ b/lib/chef/provider/service/insserv.rb @@ -24,10 +24,8 @@ class Chef class Service class Insserv < Chef::Provider::Service::Init - provides :service, os: "linux" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:insserv) + provides :service, platform_family: %w(debian rhel fedora suse) do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:insserv) end def self.supports?(resource, action) diff --git a/lib/chef/provider/service/invokercd.rb b/lib/chef/provider/service/invokercd.rb index 5ff24e0dbb..2b045e0e60 100644 --- a/lib/chef/provider/service/invokercd.rb +++ b/lib/chef/provider/service/invokercd.rb @@ -23,10 +23,8 @@ class Chef class Service class Invokercd < Chef::Provider::Service::Init - provides :service, platform_family: "debian" - - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:invokercd) + provides :service, platform_family: 'debian', override: true do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:invokercd) end def self.supports?(resource, action) diff --git a/lib/chef/provider/service/macosx.rb b/lib/chef/provider/service/macosx.rb index 7324822eff..0a8fca4262 100644 --- a/lib/chef/provider/service/macosx.rb +++ b/lib/chef/provider/service/macosx.rb @@ -42,6 +42,10 @@ class Chef PLIST_DIRS = gather_plist_dirs + def this_version_or_newer?(this_version) + Gem::Version.new(node['platform_version']) >= Gem::Version.new(this_version) + end + def load_current_resource @current_resource = Chef::Resource::MacosxService.new(@new_resource.name) @current_resource.service_name(@new_resource.service_name) @@ -56,7 +60,7 @@ class Chef @console_user = Etc.getlogin Chef::Log.debug("#{new_resource} console_user: '#{@console_user}'") cmd = "su " - param = !node['platform_version'].include?('10.10') ? '-l ' : '' + param = this_version_or_newer?('10.10') ? '' : '-l ' @base_user_cmd = cmd + param + "#{@console_user} -c" # Default LauchAgent session should be Aqua @session_type = 'Aqua' if @session_type.nil? diff --git a/lib/chef/provider/service/openbsd.rb b/lib/chef/provider/service/openbsd.rb index d509ee10ff..36c9e8141e 100644 --- a/lib/chef/provider/service/openbsd.rb +++ b/lib/chef/provider/service/openbsd.rb @@ -26,7 +26,7 @@ class Chef class Service class Openbsd < Chef::Provider::Service::Init - provides :service, os: [ "openbsd" ] + provides :service, os: "openbsd" include Chef::Mixin::ShellOut @@ -40,11 +40,12 @@ class Chef @rc_conf = ::File.read(RC_CONF_PATH) rescue '' @rc_conf_local = ::File.read(RC_CONF_LOCAL_PATH) rescue '' @init_command = ::File.exist?(rcd_script_path) ? rcd_script_path : nil - new_resource.supports[:status] = true new_resource.status_command("#{default_init_command} check") end def load_current_resource + supports[:status] = true if supports[:status].nil? + @current_resource = Chef::Resource::Service.new(new_resource.name) current_resource.service_name(new_resource.service_name) diff --git a/lib/chef/provider/service/redhat.rb b/lib/chef/provider/service/redhat.rb index 850953125e..33a9778715 100644 --- a/lib/chef/provider/service/redhat.rb +++ b/lib/chef/provider/service/redhat.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -23,24 +23,32 @@ class Chef class Service class Redhat < Chef::Provider::Service::Init - CHKCONFIG_ON = /\d:on/ - CHKCONFIG_MISSING = /No such/ - - provides :service, platform_family: [ "rhel", "fedora", "suse" ] + # @api private + attr_accessor :service_missing + # @api private + attr_accessor :current_run_levels - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:redhat) + provides :service, platform_family: %w(rhel fedora suse) do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:redhat) end + CHKCONFIG_ON = /\d:on/ + CHKCONFIG_MISSING = /No such/ + def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:initd) end def initialize(new_resource, run_context) super - @init_command = "/sbin/service #{@new_resource.service_name}" - @new_resource.supports[:status] = true + @init_command = "/sbin/service #{new_resource.service_name}" @service_missing = false + @current_run_levels = [] + end + + # @api private + def run_levels + new_resource.run_levels end def define_resource_requirements @@ -49,34 +57,60 @@ class Chef requirements.assert(:all_actions) do |a| chkconfig_file = "/sbin/chkconfig" a.assertion { ::File.exists? chkconfig_file } - a.failure_message Chef::Exceptions::Service, "#{chkconfig_file} does not exist!" + a.failure_message Chef::Exceptions::Service, "#{chkconfig_file} dbleoes not exist!" end requirements.assert(:start, :enable, :reload, :restart) do |a| a.assertion { !@service_missing } - a.failure_message Chef::Exceptions::Service, "#{@new_resource}: unable to locate the init.d script!" + a.failure_message Chef::Exceptions::Service, "#{new_resource}: unable to locate the init.d script!" a.whyrun "Assuming service would be disabled. The init script is not presently installed." end end def load_current_resource + supports[:status] = true if supports[:status].nil? + super if ::File.exists?("/sbin/chkconfig") - chkconfig = shell_out!("/sbin/chkconfig --list #{@current_resource.service_name}", :returns => [0,1]) - @current_resource.enabled(!!(chkconfig.stdout =~ CHKCONFIG_ON)) + chkconfig = shell_out!("/sbin/chkconfig --list #{current_resource.service_name}", :returns => [0,1]) + unless run_levels.nil? or run_levels.empty? + all_levels_match = true + chkconfig.stdout.split(/\s+/)[1..-1].each do |level| + index = level.split(':').first + status = level.split(':').last + if level =~ CHKCONFIG_ON + @current_run_levels << index.to_i + all_levels_match = false unless run_levels.include?(index.to_i) + else + all_levels_match = false if run_levels.include?(index.to_i) + end + end + current_resource.enabled(all_levels_match) + else + current_resource.enabled(!!(chkconfig.stdout =~ CHKCONFIG_ON)) + end @service_missing = !!(chkconfig.stderr =~ CHKCONFIG_MISSING) end - @current_resource + current_resource + end + + # @api private + def levels + (run_levels.nil? or run_levels.empty?) ? "" : "--level #{run_levels.join('')} " end def enable_service() - shell_out! "/sbin/chkconfig #{@new_resource.service_name} on" + unless run_levels.nil? or run_levels.empty? + disable_levels = current_run_levels - run_levels + shell_out! "/sbin/chkconfig --level #{disable_levels.join('')} #{new_resource.service_name} off" unless disable_levels.empty? + end + shell_out! "/sbin/chkconfig #{levels}#{new_resource.service_name} on" end def disable_service() - shell_out! "/sbin/chkconfig #{@new_resource.service_name} off" + shell_out! "/sbin/chkconfig #{levels}#{new_resource.service_name} off" end end end diff --git a/lib/chef/provider/service/simple.rb b/lib/chef/provider/service/simple.rb index ee403ee163..d295513b42 100644 --- a/lib/chef/provider/service/simple.rb +++ b/lib/chef/provider/service/simple.rb @@ -76,7 +76,7 @@ class Chef end requirements.assert(:all_actions) do |a| - a.assertion { @new_resource.status_command or @new_resource.supports[:status] or + a.assertion { @new_resource.status_command or supports[:status] or (!ps_cmd.nil? and !ps_cmd.empty?) } a.failure_message Chef::Exceptions::Service, "#{@new_resource} could not determine how to inspect the process table, please set this node's 'command.ps' attribute" end @@ -127,7 +127,7 @@ class Chef nil end - elsif @new_resource.supports[:status] + elsif supports[:status] Chef::Log.debug("#{@new_resource} supports status, running") begin if shell_out("#{default_init_command} status").exitstatus == 0 diff --git a/lib/chef/provider/service/systemd.rb b/lib/chef/provider/service/systemd.rb index 9085ffde2e..d41f6248c2 100644 --- a/lib/chef/provider/service/systemd.rb +++ b/lib/chef/provider/service/systemd.rb @@ -24,14 +24,12 @@ class Chef::Provider::Service::Systemd < Chef::Provider::Service::Simple include Chef::Mixin::Which - provides :service, os: "linux" + provides :service, os: "linux" do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:systemd) + end attr_accessor :status_check_success - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:systemd) - end - def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:systemd) end diff --git a/lib/chef/provider/service/upstart.rb b/lib/chef/provider/service/upstart.rb index 8d4aa41035..c08a5f8636 100644 --- a/lib/chef/provider/service/upstart.rb +++ b/lib/chef/provider/service/upstart.rb @@ -25,14 +25,13 @@ class Chef class Provider class Service class Upstart < Chef::Provider::Service::Simple - UPSTART_STATE_FORMAT = /\w+ \(?(\w+)\)?[\/ ](\w+)/ - - provides :service, os: "linux" - def self.provides?(node, resource) - super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) + provides :service, platform_family: 'debian', override: true do |node| + Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) end + UPSTART_STATE_FORMAT = /\w+ \(?(\w+)\)?[\/ ](\w+)/ + def self.supports?(resource, action) Chef::Platform::ServiceHelpers.config_for_service(resource.service_name).include?(:upstart) end @@ -107,7 +106,7 @@ class Chef Chef::Log.debug("#{@new_resource} you have specified a status command, running..") begin - if shell_out!(@new_resource.status_command) == 0 + if shell_out!(@new_resource.status_command).exitstatus == 0 @current_resource.running true end rescue diff --git a/lib/chef/provider/template/content.rb b/lib/chef/provider/template/content.rb index 7fc680ec85..a231bd509e 100644 --- a/lib/chef/provider/template/content.rb +++ b/lib/chef/provider/template/content.rb @@ -39,6 +39,16 @@ class Chef context = TemplateContext.new(@new_resource.variables) context[:node] = @run_context.node context[:template_finder] = template_finder + + # helper variables + context[:cookbook_name] = @new_resource.cookbook_name unless context.keys.include?(:coookbook_name) + context[:recipe_name] = @new_resource.recipe_name unless context.keys.include?(:recipe_name) + context[:recipe_line_string] = @new_resource.source_line unless context.keys.include?(:recipe_line_string) + context[:recipe_path] = @new_resource.source_line_file unless context.keys.include?(:recipe_path) + context[:recipe_line] = @new_resource.source_line_number unless context.keys.include?(:recipe_line) + context[:template_name] = @new_resource.source unless context.keys.include?(:template_name) + context[:template_path] = template_location unless context.keys.include?(:template_path) + context._extend_modules(@new_resource.helper_modules) output = context.render_template(template_location) diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb index ad92a72a0a..244b11db98 100644 --- a/lib/chef/provider/user.rb +++ b/lib/chef/provider/user.rb @@ -23,8 +23,6 @@ require 'etc' class Chef class Provider class User < Chef::Provider - provides :user - include Chef::Mixin::Command attr_accessor :user_exists, :locked diff --git a/lib/chef/provider/windows_script.rb b/lib/chef/provider/windows_script.rb index e600bb2837..62b49bd833 100644 --- a/lib/chef/provider/windows_script.rb +++ b/lib/chef/provider/windows_script.rb @@ -23,6 +23,8 @@ class Chef class Provider class WindowsScript < Chef::Provider::Script + attr_reader :is_forced_32bit + protected include Chef::Mixin::WindowsArchitectureHelper @@ -36,11 +38,7 @@ class Chef @is_wow64 = wow64_architecture_override_required?(run_context.node, target_architecture) - # if the user wants to run the script 32 bit && we are on a 64bit windows system && we are running a 64bit ruby ==> fail - if ( target_architecture == :i386 ) && node_windows_architecture(run_context.node) == :x86_64 && !is_i386_process_on_x86_64_windows? - raise Chef::Exceptions::Win32ArchitectureIncorrect, - "Support for the i386 architecture from a 64-bit Ruby runtime is not yet implemented" - end + @is_forced_32bit = forced_32bit_override_required?(run_context.node, target_architecture) end public diff --git a/lib/chef/provider_resolver.rb b/lib/chef/provider_resolver.rb index 5bfee343d1..82a24fc078 100644 --- a/lib/chef/provider_resolver.rb +++ b/lib/chef/provider_resolver.rb @@ -17,7 +17,7 @@ # require 'chef/exceptions' -require 'chef/platform/provider_priority_map' +require 'chef/platform/priority_map' class Chef # @@ -62,12 +62,47 @@ class Chef maybe_chef_platform_lookup(resource) end + # Does NOT call provides? on the resource (it is assumed this is being + # called *from* provides?). def provided_by?(provider_class) - prioritized_handlers.include?(provider_class) + potential_handlers.include?(provider_class) + end + + def enabled_handlers + @enabled_handlers ||= potential_handlers.select { |handler| !overrode_provides?(handler) || handler.provides?(node, resource) } + end + + # TODO deprecate this and allow actions to be passed as a filter to + # `provides` so we don't have to have two separate things. + # @api private + def supported_handlers + enabled_handlers.select { |handler| handler.supports?(resource, action) } end private + def potential_handlers + handler_map.list(node, resource.resource_name).uniq + end + + # The list of handlers, with any in the priority_map moved to the front + def prioritized_handlers + @prioritized_handlers ||= begin + supported_handlers = self.supported_handlers + if supported_handlers.empty? + # if none of the providers specifically support the resource, we still need to pick one of the providers that are + # enabled on the node to handle the why-run use case. FIXME we should only do this in why-run mode then. + Chef::Log.debug "No providers responded true to `supports?` for action #{action} on resource #{resource}, falling back to enabled handlers so we can return something anyway." + supported_handlers = enabled_handlers + end + + prioritized = priority_map.list(node, resource.resource_name).flatten(1) + prioritized &= supported_handlers # Filter the priority map by the actual enabled handlers + prioritized |= supported_handlers # Bring back any handlers that aren't in the priority map, at the *end* (ordered set) + prioritized + end + end + # if resource.provider is set, just return one of those objects def maybe_explicit_provider(resource) return nil unless resource.provider @@ -78,27 +113,7 @@ class Chef def maybe_dynamic_provider_resolution(resource, action) Chef::Log.debug "Providers for generic #{resource.resource_name} resource enabled on node include: #{enabled_handlers}" - # Get all the handlers in the priority bucket - handlers = prioritized_handlers - - # Narrow it down to handlers that return `true` to `provides?` - # TODO deprecate this and don't bother calling--the fact that they said - # `provides` should be enough. But we need to do it right now because - # some classes implement additional handling. - enabled_handlers = prioritized_handlers.select { |handler| handler.provides?(node, resource) } - - # Narrow it down to handlers that return `true` to `supports?` - # TODO deprecate this and allow actions to be passed as a filter to - # `provides` so we don't have to have two separate things. - supported_handlers = enabled_handlers.select { |handler| handler.supports?(resource, action) } - if supported_handlers.empty? - # if none of the providers specifically support the resource, we still need to pick one of the providers that are - # enabled on the node to handle the why-run use case. FIXME we should only do this in why-run mode then. - Chef::Log.debug "No providers responded true to `supports?` for action #{action} on resource #{resource}, falling back to enabled handlers so we can return something anyway." - handler = enabled_handlers.first - else - handler = supported_handlers.first - end + handler = prioritized_handlers.first if handler Chef::Log.debug "Provider for action #{action} on resource #{resource} is #{handler}" @@ -114,13 +129,16 @@ class Chef Chef::Platform.find_provider_for_node(node, resource) end - def provider_priority_map - Chef::Platform::ProviderPriorityMap.instance + def priority_map + Chef.provider_priority_map end - def prioritized_handlers - @prioritized_handlers ||= - provider_priority_map.list_handlers(node, resource.resource_name).flatten(1).uniq + def handler_map + Chef.provider_handler_map + end + + def overrode_provides?(handler) + handler.method(:provides?).owner != Chef::Provider.method(:provides?).owner end module Deprecated @@ -129,33 +147,21 @@ class Chef @providers ||= Chef::Provider.descendants end - # this cut looks at if the provider can handle the resource type on the node def enabled_handlers - @enabled_handlers ||= - providers.select do |klass| - # NB: this is different from resource_resolver which must pass a resource_name - # FIXME: deprecate this and normalize on passing resource_name here - klass.provides?(node, resource) - end.sort {|a,b| a.to_s <=> b.to_s } - end - - # this cut looks at if the provider can handle the specific resource and action - def supported_handlers - @supported_handlers ||= - enabled_handlers.select do |klass| - klass.supports?(resource, action) - end - end - - # If there are no providers for a DSL, we search through the - def prioritized_handlers - @prioritized_handlers ||= super || begin - result = providers.select { |handler| handler.provides?(node, resource) }.sort_by(:name) - if !result.empty? - Chef::Log.deprecation("#{resource.resource_name.to_sym} is marked as providing DSL #{method_symbol}, but provides #{resource.resource_name.to_sym.inspect} was never called!") - Chef::Log.deprecation("In Chef 13, this will break: you must call provides to mark the names you provide, even if you also override provides? yourself.") + @enabled_handlers ||= begin + handlers = super + if handlers.empty? + # Look through all providers, and find ones that return true to provides. + # Don't bother with ones that don't override provides?, since they + # would have been in enabled_handlers already if that were so. (It's a + # perf concern otherwise.) + handlers = providers.select { |handler| overrode_provides?(handler) && handler.provides?(node, resource) } + handlers.each do |handler| + Chef.log_deprecation("#{handler}.provides? returned true when asked if it provides DSL #{resource.resource_name}, but provides #{resource.resource_name.inspect} was never called!") + Chef.log_deprecation("In Chef 13, this will break: you must call provides to mark the names you provide, even if you also override provides? yourself.") + end end - result + handlers end end end diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index b4d37c2d61..262560f754 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -36,14 +36,7 @@ class Chef # A Recipe object is the context in which Chef recipes are evaluated. class Recipe - include Chef::DSL::DataQuery - include Chef::DSL::PlatformIntrospection - include Chef::DSL::IncludeRecipe - include Chef::DSL::Recipe - include Chef::DSL::RegistryHelper - include Chef::DSL::RebootPending - include Chef::DSL::Audit - include Chef::DSL::Powershell + include Chef::DSL::Recipe::FullDSL include Chef::Mixin::FromFile include Chef::Mixin::Deprecation diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 7fe8a52d95..ee75dec3b9 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -1,7 +1,8 @@ # # Author:: Adam Jacob (<adam@opscode.com>) # Author:: Christopher Walters (<cw@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Author:: John Keiser (<jkeiser@chef.io) +# Copyright:: Copyright (c) 2008-2015 Chef, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,6 +18,7 @@ # limitations under the License. # +require 'chef/exceptions' require 'chef/mixin/params_validate' require 'chef/dsl/platform_introspection' require 'chef/dsl/data_query' @@ -27,6 +29,7 @@ require 'chef/mixin/convert_to_class_name' require 'chef/guard_interpreter/resource_guard_interpreter' require 'chef/resource/conditional' require 'chef/resource/conditional_action_not_nothing' +require 'chef/resource/action_provider' require 'chef/resource_collection' require 'chef/node_map' require 'chef/node' @@ -58,8 +61,6 @@ class Chef include Chef::Mixin::ShellOut include Chef::Mixin::PowershellOut - NULL_ARG = Object.new - # # The node the current Chef run is using. # @@ -103,7 +104,7 @@ class Chef # @param run_context The context of the Chef run. Corresponds to #run_context. # def initialize(name, run_context=nil) - name(name) + name(name) unless name.nil? @run_context = run_context @noop = nil @before = nil @@ -132,37 +133,27 @@ class Chef end # - # The name of this particular resource. - # - # This special resource attribute is set automatically from the declaration - # of the resource, e.g. + # The list of properties defined on this resource. # - # execute 'Vitruvius' do - # command 'ls' - # end + # Everything defined with `property` is in this list. # - # Will set the name to "Vitruvius". - # - # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`. - # - # This is also used for resource notifications and subscribes in the same manner. - # - # This will coerce any object into a string via #to_s. Arrays are a special case - # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more - # awkward `package[["foo", "bar"]]` that #to_s would produce. + # @param include_superclass [Boolean] `true` to include properties defined + # on superclasses; `false` or `nil` to return the list of properties + # directly on this class. # - # @param name [Object] The name to set, typically a String or Array - # @return [String] The name of this Resource. + # @return [Hash<Symbol,Property>] The list of property names and types. # - def name(name=nil) - if !name.nil? - if name.is_a?(Array) - @name = name.join(', ') + def self.properties(include_superclass=true) + @properties ||= {} + if include_superclass + if superclass.respond_to?(:properties) + superclass.properties.merge(@properties) else - @name = name.to_s + @properties.dup end + else + @properties end - @name end # @@ -171,27 +162,24 @@ class Chef # @param arg [Array[Symbol], Symbol] A list of actions (e.g. `:create`) # @return [Array[Symbol]] the list of actions. # - attr_accessor :action def action(arg=nil) if arg - if arg.is_a?(Array) - arg = arg.map { |a| a.to_sym } - else - arg = arg.to_sym - end - Array(arg).each do |action| + arg = Array(arg).map(&:to_sym) + arg.each do |action| validate( { action: action }, { action: { kind_of: Symbol, equal_to: allowed_actions } } ) end - self.action = arg + @action = arg else - # Pull the action from the class if it's not set - @action || self.class.default_action + @action end end + # Alias for normal assigment syntax. + alias_method :action=, :action + # # Sets up a notification that will run a particular action on another resource # if and when *this* resource is updated by an action. @@ -480,13 +468,21 @@ class Chef # # Get the value of the state attributes in this resource as a hash. # + # Does not include properties that are not set (unless they are identity + # properties). + # # @return [Hash{Symbol => Object}] A Hash of attribute => value for the # Resource class's `state_attrs`. + # def state_for_resource_reporter - self.class.state_attrs.inject({}) do |state_attrs, attr_name| - state_attrs[attr_name] = send(attr_name) - state_attrs + state = {} + state_properties = self.class.state_properties + state_properties.each do |property| + if property.identity? || property.is_set?(self) + state[property.name] = send(property.name) + end end + state end # @@ -499,17 +495,22 @@ class Chef alias_method :state, :state_for_resource_reporter # - # The value of the identity attribute, if declared. Falls back to #name if - # no identity attribute is declared. + # The value of the identity of this resource. + # + # - If there are no identity properties on the resource, `name` is returned. + # - If there is exactly one identity property on the resource, it is returned. + # - If there are more than one, they are returned in a hash. # - # @return The value of the identity attribute. + # @return [Object,Hash<Symbol,Object>] The identity of this resource. # def identity - if identity_attr = self.class.identity_attr - send(identity_attr) - else - name + result = {} + identity_properties = self.class.identity_properties + identity_properties.each do |property| + result[property.name] = send(property.name) end + return result.values.first if identity_properties.size == 1 + result end # @@ -531,9 +532,7 @@ class Chef # # Equivalent to #ignore_failure. # - def epic_fail(arg=nil) - ignore_failure(arg) - end + alias :epic_fail :ignore_failure # # Make this resource into an exact (shallow) copy of the other resource. @@ -688,66 +687,393 @@ class Chef # # The provider class for this resource. # + # If `action :x do ... end` has been declared on this resource or its + # superclasses, this will return the `action_provider_class`. + # # If this is not set, `provider_for_action` will dynamically determine the # provider. # # @param arg [String, Symbol, Class] Sets the provider class for this resource. # If passed a String or Symbol, e.g. `:file` or `"file"`, looks up the # provider based on the name. + # # @return The provider class for this resource. # + # @see Chef::Resource.action_provider_class + # def provider(arg=nil) klass = if arg.kind_of?(String) || arg.kind_of?(Symbol) lookup_provider_constant(arg) else arg end - set_or_return(:provider, klass, kind_of: [ Class ]) + set_or_return(:provider, klass, kind_of: [ Class ]) || + self.class.action_provider_class end def provider=(arg) provider(arg) end - # Set or return the list of "state attributes" implemented by the Resource - # subclass. State attributes are attributes that describe the desired state - # of the system, such as file permissions or ownership. In general, state - # attributes are attributes that could be populated by examining the state - # of the system (e.g., File.stat can tell you the permissions on an - # existing file). Contrarily, attributes that are not "state attributes" - # usually modify the way Chef itself behaves, for example by providing - # additional options for a package manager to use when installing a - # package. + # + # Create a property on this resource class. + # + # If a superclass has this property, or if this property has already been + # defined by this resource, this will *override* the previous value. + # + # @param name [Symbol] The name of the property. + # @param type [Object,Array<Object>] The type(s) of this property. + # If present, this is prepended to the `is` validation option. + # @param options [Hash<Symbol,Object>] Validation options. + # @option options [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`options[:is].any? { |v| v === value }`). + # @option options [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`options[:is].any? { |v| v == value }`) + # @option options [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # @option options [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. + # @option options [Hash<String,Proc>] :callbacks A hash of + # messages -> procs, all of which match the value. The proc must + # return a truthy or falsey value (true means it matches). + # @option options [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. + # @option options [Symbol,Array<Symbol>] :cannot_be A property, + # or a list of properties, that the value cannot have (such as `:nil` or + # `:empty`). The method with a questionmark at the end is called on the + # value (e.g. `value.empty?`). If the value does not have this method, + # it is considered valid (i.e. if you don't respond to `empty?` we + # assume you are not empty). + # @option options [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # @option options [Boolean] :required `true` if this property + # must be present; `false` otherwise. This is checked after the resource + # is fully initialized. + # @option options [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. + # @option options [Boolean] :name_attribute Same as `name_property`. + # @option options [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties). + # @option options [Boolean] :desired_state `true` if this property is + # part of desired state. Defaults to `true`. + # @option options [Boolean] :identity `true` if this property + # is part of object identity. Defaults to `false`. + # + # @example Bare property + # property :x + # + # @example With just a type + # property :x, String + # + # @example With just options + # property :x, default: 'hi' + # + # @example With type and options + # property :x, String, default: 'hi' + # + def self.property(name, type=NOT_PASSED, **options) + name = name.to_sym + + options[:instance_variable_name] = :"@#{name}" if !options.has_key?(:instance_variable_name) + options.merge!(name: name, declared_in: self) + + if type == NOT_PASSED + # If a type is not passed, the property derives from the + # superclass property (if any) + if properties.has_key?(name) + property = properties[name].derive(**options) + else + property = property_type(**options) + end + + # If a Property is specified, derive a new one from that. + elsif type.is_a?(Property) || (type.is_a?(Class) && type <= Property) + property = type.derive(**options) + + # If a primitive type was passed, combine it with "is" + else + if options[:is] + options[:is] = ([ type ] + [ options[:is] ]).flatten(1) + else + options[:is] = type + end + property = property_type(**options) + end + + if !options[:default].frozen? && (options[:default].is_a?(Array) || options[:default].is_a?(Hash)) + Chef.log_deprecation("Property #{self}.#{name} has an array or hash default (#{options[:default]}). This means that if one resource modifies or appends to it, all other resources of the same type will also see the changes. Either freeze the constant with `.freeze` to prevent appending, or use lazy { #{options[:default].inspect} }.") + end + + local_properties = properties(false) + local_properties[name] = property + + property.emit_dsl + end + + # + # Create a reusable property type that can be used in multiple properties + # in different resources. + # + # @param options [Hash<Symbol,Object>] Validation options. see #property for + # the list of options. + # + # @example + # property_type(default: 'hi') + # + def self.property_type(**options) + Property.derive(**options) + end + + # + # The name of this particular resource. + # + # This special resource attribute is set automatically from the declaration + # of the resource, e.g. + # + # execute 'Vitruvius' do + # command 'ls' + # end + # + # Will set the name to "Vitruvius". + # + # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`. + # + # This is also used for resource notifications and subscribes in the same manner. + # + # This will coerce any object into a string via #to_s. Arrays are a special case + # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more + # awkward `package[["foo", "bar"]]` that #to_s would produce. + # + # @param name [Object] The name to set, typically a String or Array + # @return [String] The name of this Resource. + # + property :name, String, coerce: proc { |v| v.is_a?(Array) ? v.join(', ') : v.to_s }, desired_state: false + + # + # Whether this property has been set (or whether it has a default that has + # been retrieved). + # + # @param name [Symbol] The name of the property. + # @return [Boolean] `true` if the property has been set. + # + def property_is_set?(name) + property = self.class.properties[name.to_sym] + raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property + property.is_set?(self) + end + + # + # Clear this property as if it had never been set. It will thereafter return + # the default. + # been retrieved). + # + # @param name [Symbol] The name of the property. + # + def reset_property(name) + property = self.class.properties[name.to_sym] + raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property + property.reset(self) + end + + # + # Create a lazy value for assignment to a default value. + # + # @param block The block to run when the value is retrieved. + # + # @return [Chef::DelayedEvaluator] The lazy value + # + def self.lazy(&block) + DelayedEvaluator.new(&block) + end + + # + # Get or set the list of desired state properties for this resource. + # + # State properties are properties that describe the desired state + # of the system, such as file permissions or ownership. + # In general, state properties are properties that could be populated by + # examining the state of the system (e.g., File.stat can tell you the + # permissions on an existing file). Contrarily, properties that are not + # "state properties" usually modify the way Chef itself behaves, for example + # by providing additional options for a package manager to use when + # installing a package. # # This list is used by the Chef client auditing system to extract # information from resources to describe changes made to the system. - def self.state_attrs(*attr_names) - @state_attrs ||= [] - @state_attrs = attr_names unless attr_names.empty? + # + # This method is unnecessary when declaring properties with `property`; + # properties are added to state_properties by default, and can be turned off + # with `desired_state: false`. + # + # ```ruby + # property :x # part of desired state + # property :y, desired_state: false # not part of desired state + # ``` + # + # @param names [Array<Symbol>] A list of property names to set as desired + # state. + # + # @return [Array<Property>] All properties in desired state. + # + def self.state_properties(*names) + if !names.empty? + names = names.map { |name| name.to_sym }.uniq - # Return *all* state_attrs that this class has, including inherited ones - if superclass.respond_to?(:state_attrs) - superclass.state_attrs + @state_attrs - else - @state_attrs + local_properties = properties(false) + # Add new properties to the list. + names.each do |name| + property = properties[name] + if !property + self.property name, instance_variable_name: false, desired_state: true + elsif !property.desired_state? + self.property name, desired_state: true + end + end + + # If state_attrs *excludes* something which is currently desired state, + # mark it as desired_state: false. + local_properties.each do |name,property| + if property.desired_state? && !names.include?(name) + self.property name, desired_state: false + end + end end + + properties.values.select { |property| property.desired_state? } + end + + # + # Set or return the list of "state properties" implemented by the Resource + # subclass. + # + # Equivalent to calling #state_properties and getting `state_properties.keys`. + # + # @deprecated Use state_properties.keys instead. Note that when you declare + # properties with `property`: properties are added to state_properties by + # default, and can be turned off with `desired_state: false` + # + # ```ruby + # property :x # part of desired state + # property :y, desired_state: false # not part of desired state + # ``` + # + # @param names [Array<Symbol>] A list of property names to set as desired + # state. + # + # @return [Array<Symbol>] All property names with desired state. + # + def self.state_attrs(*names) + state_properties(*names).map { |property| property.name } end - # Set or return the "identity attribute" for this resource class. This is - # generally going to be the "name attribute" for this resource. In other - # words, the resource type plus this attribute uniquely identify a given - # bit of state that chef manages. For a File resource, this would be the - # path, for a package resource, it will be the package name. This will show - # up in chef-client's audit records as a searchable field. - def self.identity_attr(attr_name=nil) - @identity_attr ||= nil - @identity_attr = attr_name if attr_name + # + # Set the identity of this resource to a particular set of properties. + # + # This drives #identity, which returns data that uniquely refers to a given + # resource on the given node (in such a way that it can be correlated + # across Chef runs). + # + # This method is unnecessary when declaring properties with `property`; + # properties can be added to identity during declaration with + # `identity: true`. + # + # ```ruby + # property :x, identity: true # part of identity + # property :y # not part of identity + # ``` + # + # If no properties are marked as identity, "name" is considered the identity. + # + # @param names [Array<Symbol>] A list of property names to set as the identity. + # + # @return [Array<Property>] All identity properties. + # + def self.identity_properties(*names) + if !names.empty? + names = names.map { |name| name.to_sym } - # If this class doesn't have an identity attr, we'll defer to the superclass: - if @identity_attr || !superclass.respond_to?(:identity_attr) - @identity_attr - else - superclass.identity_attr + # Add or change properties that are not part of the identity. + names.each do |name| + property = properties[name] + if !property + self.property name, instance_variable_name: false, identity: true + elsif !property.identity? + self.property name, identity: true + end + end + + # If identity_properties *excludes* something which is currently part of + # the identity, mark it as identity: false. + properties.each do |name,property| + if property.identity? && !names.include?(name) + self.property name, identity: false + end + end end + + result = properties.values.select { |property| property.identity? } + result = [ properties[:name] ] if result.empty? + result + end + + # + # Set the identity of this resource to a particular property. + # + # This drives #identity, which returns data that uniquely refers to a given + # resource on the given node (in such a way that it can be correlated + # across Chef runs). + # + # This method is unnecessary when declaring properties with `property`; + # properties can be added to identity during declaration with + # `identity: true`. + # + # ```ruby + # property :x, identity: true # part of identity + # property :y # not part of identity + # ``` + # + # @param name [Symbol] A list of property names to set as the identity. + # + # @return [Symbol] The identity property if there is only one; or `nil` if + # there are more than one. + # + # @raise [ArgumentError] If no arguments are passed and the resource has + # more than one identity property. + # + def self.identity_property(name=nil) + result = identity_properties(*Array(name)) + if result.size > 1 + raise Chef::Exceptions::MultipleIdentityError, "identity_property cannot be called on an object with more than one identity property (#{result.map { |r| r.name }.join(", ")})." + end + result.first + end + + # + # Set a property as the "identity attribute" for this resource. + # + # Identical to calling #identity_property.first.key. + # + # @param name [Symbol] The name of the property to set. + # + # @return [Symbol] + # + # @deprecated `identity_property` should be used instead. + # + # @raise [ArgumentError] If no arguments are passed and the resource has + # more than one identity property. + # + def self.identity_attr(name=nil) + property = identity_property(name) + return nil if !property + property.name end # @@ -773,8 +1099,8 @@ class Chef # have. # attr_accessor :allowed_actions - def allowed_actions(value=NULL_ARG) - if value != NULL_ARG + def allowed_actions(value=NOT_PASSED) + if value != NOT_PASSED self.allowed_actions = value end @allowed_actions @@ -885,7 +1211,7 @@ class Chef # @deprecated Use resource_name instead. # def self.dsl_name - Chef::Log.deprecation "Resource.dsl_name is deprecated and will be removed in Chef 13. Use resource_name instead." + Chef.log_deprecation "Resource.dsl_name is deprecated and will be removed in Chef 13. Use resource_name instead." if name name = self.name.split('::')[-1] convert_to_snake_case(name) @@ -908,29 +1234,23 @@ class Chef # # @return [Symbol] The name of this resource type (e.g. `:execute`). # - def self.resource_name(name=NULL_ARG) + def self.resource_name(name=NOT_PASSED) # Setter - if name != NULL_ARG + if name != NOT_PASSED remove_canonical_dsl # Set the resource_name and call provides if name name = name.to_sym # If our class is not already providing this name, provide it. - if !Chef::ResourceResolver.list(name).include?(self) + if !Chef::ResourceResolver.includes_handler?(name, self) provides name, canonical: true end @resource_name = name else @resource_name = nil end - else - # set resource_name automatically if it's not set - if !instance_variable_defined?(:@resource_name) && self.name - resource_name convert_to_snake_case(self.name.split('::')[-1]) - end end - @resource_name end def self.resource_name=(name) @@ -938,6 +1258,19 @@ class Chef end # + # Use the class name as the resource name. + # + # Munges the last part of the class name from camel case to snake case, + # and sets the resource_name to that: + # + # A::B::BlahDBlah -> blah_d_blah + # + def self.use_automatic_resource_name + automatic_name = convert_to_snake_case(self.name.split('::')[-1]) + resource_name automatic_name + end + + # # The module where Chef should look for providers for this resource. # The provider for `MyResource` will be looked up using # `provider_base::MyResource`. Defaults to `Chef::Provider`. @@ -955,7 +1288,7 @@ class Chef # def self.provider_base(arg=nil) if arg - Chef::Log.deprecation("Resource.provider_base is deprecated and will be removed in Chef 13. Use provides on the provider, or provider on the resource, instead.") + Chef.log_deprecation("Resource.provider_base is deprecated and will be removed in Chef 13. Use provides on the provider, or provider on the resource, instead.") end @provider_base ||= arg || Chef::Provider end @@ -965,7 +1298,7 @@ class Chef # # @param actions [Array<Symbol>] The list of actions to add to allowed_actions. # - # @return [Arrau<Symbol>] The list of actions, as symbols. + # @return [Array<Symbol>] The list of actions, as symbols. # def self.allowed_actions(*actions) @allowed_actions ||= @@ -974,10 +1307,10 @@ class Chef else [ :nothing ] end - @allowed_actions |= actions + @allowed_actions |= actions.flatten end def self.allowed_actions=(value) - @allowed_actions = value + @allowed_actions = value.uniq end # @@ -986,22 +1319,17 @@ class Chef # Setting default_action will automatially add the action to # allowed_actions, if it isn't already there. # - # Defaults to :nothing. + # Defaults to [:nothing]. # # @param action_name [Symbol,Array<Symbol>] The default action (or series # of actions) to use. # - # @return [Symbol,Array<Symbol>] The default actions for the resource. + # @return [Array<Symbol>] The default actions for the resource. # - def self.default_action(action_name=NULL_ARG) - unless action_name.equal?(NULL_ARG) - if action_name.is_a?(Array) - @default_action = action_name.map { |arg| arg.to_sym } - else - @default_action = action_name.to_sym - end - - self.allowed_actions |= Array(@default_action) + def self.default_action(action_name=NOT_PASSED) + unless action_name.equal?(NOT_PASSED) + @default_action = Array(action_name).map(&:to_sym) + self.allowed_actions |= @default_action end if @default_action @@ -1009,11 +1337,132 @@ class Chef elsif superclass.respond_to?(:default_action) superclass.default_action else - :nothing + [:nothing] end end def self.default_action=(action_name) - default_action(action_name) + default_action action_name + end + + # + # Define an action on this resource. + # + # The action is defined as a *recipe* block that will be compiled and then + # converged when the action is taken (when Resource is converged). The recipe + # has access to the resource's attributes and methods, as well as the Chef + # recipe DSL. + # + # Resources in the action recipe may notify and subscribe to other resources + # within the action recipe, but cannot notify or subscribe to resources + # in the main Chef run. + # + # Resource actions are *inheritable*: if resource A defines `action :create` + # and B is a subclass of A, B gets all of A's actions. Additionally, + # resource B can define `action :create` and call `super()` to invoke A's + # action code. + # + # The first action defined (besides `:nothing`) will become the default + # action for the resource. + # + # @param name [Symbol] The action name to define. + # @param recipe_block The recipe to run when the action is taken. This block + # takes no parameters, and will be evaluated in a new context containing: + # + # - The resource's public and protected methods (including attributes) + # - The Chef Recipe DSL (file, etc.) + # - super() referring to the parent version of the action (if any) + # + # @return The Action class implementing the action + # + def self.action(action, &recipe_block) + action = action.to_sym + new_action_provider_class.action(action, &recipe_block) + self.allowed_actions += [ action ] + default_action action if Array(default_action) == [:nothing] + end + + # + # Define a method to load up this resource's properties with the current + # actual values. + # + # @param load_block The block to load. Will be run in the context of a newly + # created resource with its identity values filled in. + # + def self.load_current_value(&load_block) + define_method(:load_current_value!, &load_block) + end + + # + # Call this in `load_current_value` to indicate that the value does not + # exist and that `current_resource` should therefore be `nil`. + # + # @raise Chef::Exceptions::CurrentValueDoesNotExist + # + def current_value_does_not_exist! + raise Chef::Exceptions::CurrentValueDoesNotExist + end + + # + # Get the current actual value of this resource. + # + # This does not cache--a new value will be returned each time. + # + # @return A new copy of the resource, with values filled in from the actual + # current value. + # + def current_resource + provider = provider_for_action(Array(action).first) + if provider.whyrun_mode? && !provider.whyrun_supported? + raise "Cannot retrieve #{self.class.current_resource} in why-run mode: #{provider} does not support why-run" + end + provider.load_current_resource + provider.current_resource + end + + # + # The action provider class is an automatic `Provider` created to handle + # actions declared by `action :x do ... end`. + # + # This class will be returned by `resource.provider` if `resource.provider` + # is not set. `provider_for_action` will also use this instead of calling + # out to `Chef::ProviderResolver`. + # + # If the user has not declared actions on this class or its superclasses + # using `action :x do ... end`, then there is no need for this class and + # `action_provider_class` will be `nil`. + # + # @api private + # + def self.action_provider_class + @action_provider_class || + # If the superclass needed one, then we need one as well. + if superclass.respond_to?(:action_provider_class) && superclass.action_provider_class + new_action_provider_class + end + end + + # + # Ensure the action provider class actually gets created. This is called + # when the user does `action :x do ... end`. + # + # @api private + def self.new_action_provider_class + return @action_provider_class if @action_provider_class + + if superclass.respond_to?(:action_provider_class) + base_provider = superclass.action_provider_class + end + base_provider ||= Chef::Provider + + resource_class = self + @action_provider_class = Class.new(base_provider) do + include ActionProvider + define_singleton_method(:to_s) { "#{resource_class} action provider" } + def self.inspect + to_s + end + end + @action_provider_class end # @@ -1087,7 +1536,7 @@ class Chef class << self # back-compat - # NOTE: that we do not support unregistering classes as descendents like + # NOTE: that we do not support unregistering classes as descendants like # we used to for LWRP unloading because that was horrible and removed in # Chef-12. # @deprecated @@ -1110,8 +1559,13 @@ class Chef end def self.inherited(child) super - @sorted_descendants = nil - child.resource_name + @@sorted_descendants = nil + # set resource_name automatically if it's not set + if child.name && !child.resource_name + if child.name =~ /^Chef::Resource::(\w+)$/ + child.resource_name(convert_to_snake_case($1)) + end + end end @@ -1143,13 +1597,13 @@ class Chef remove_canonical_dsl end - result = Chef.set_resource_priority_array(name, self, options, &block) + result = Chef.resource_handler_map.set(name, self, options, &block) Chef::DSL::Resources.add_resource_dsl(name) result end - def self.provides?(node, resource) - Chef::ResourceResolver.resolve(resource, node: node).provided_by?(self) + def self.provides?(node, resource_name) + Chef::ResourceResolver.new(node, resource_name).provided_by?(self) end # Helper for #notifies @@ -1173,16 +1627,31 @@ class Chef run_context.delayed_notifications(self) end + def source_line_file + if source_line + source_line.match(/(.*):(\d+):?.*$/).to_a[1] + else + nil + end + end + + def source_line_number + if source_line + source_line.match(/(.*):(\d+):?.*$/).to_a[2] + else + nil + end + end + def defined_at # The following regexp should match these two sourceline formats: # /some/path/to/file.rb:80:in `wombat_tears' # C:/some/path/to/file.rb:80 in 1`wombat_tears' # extracting the path to the source file and the line number. - (file, line_no) = source_line.match(/(.*):(\d+):?.*$/).to_a[1,2] if source_line if cookbook_name && recipe_name && source_line - "#{cookbook_name}::#{recipe_name} line #{line_no}" + "#{cookbook_name}::#{recipe_name} line #{source_line_number}" elsif source_line - "#{file} line #{line_no}" + "#{source_line_file} line #{source_line_number}" else "dynamically defined" end @@ -1208,7 +1677,8 @@ class Chef end def provider_for_action(action) - provider = Chef::ProviderResolver.new(node, self, action).resolve.new(self, run_context) + provider_class = Chef::ProviderResolver.new(node, self, action).resolve + provider = provider_class.new(self, run_context) provider.action = action provider end @@ -1306,57 +1776,11 @@ class Chef Chef::Resource.send(:remove_const, class_name) end - # In order to generate deprecation warnings when you use Chef::Resource::MyLwrp, - # we make a special subclass (identical in nearly all respects) of the - # actual LWRP. When you say any of these, a deprecation warning will be - # generated: - # - # - Chef::Resource::MyLwrp.new(...) - # - resource.is_a?(Chef::Resource::MyLwrp) - # - resource.kind_of?(Chef::Resource::MyLwrp) - # - case resource - # when Chef::Resource::MyLwrp - # end - # - resource_subclass = class_eval <<-EOM, __FILE__, __LINE__+1 - class Chef::Resource::#{class_name} < resource_class - resource_name nil # we do not actually provide anything - def initialize(*args, &block) - Chef::Log.deprecation("Using an LWRP by its name (#{class_name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") - super - end - def self.resource_name(*args) - if args.empty? - @resource_name ||= superclass.resource_name - else - super - end - end - self - end - EOM - # Make case, is_a and kind_of work with the new subclass, for backcompat. - # Any subclass of Chef::Resource::ResourceClass is already a subclass of resource_class - # Any subclass of resource_class is considered a subclass of Chef::Resource::ResourceClass - resource_class.class_eval do - define_method(:is_a?) do |other| - other.is_a?(Module) && other === self - end - define_method(:kind_of?) do |other| - other.is_a?(Module) && other === self - end + if !Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Resource.const_set(class_name, resource_class) + deprecated_constants[class_name.to_sym] = resource_class end - resource_subclass.class_eval do - define_singleton_method(:===) do |other| - Chef::Log.deprecation("Using an LWRP by its name (#{class_name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") - # resource_subclass is a superclass of all resource_class descendants. - if self == resource_subclass && other.class <= resource_class - return true - end - super(other) - end - end - deprecated_constants[class_name.to_sym] = resource_subclass + end def self.deprecated_constants @@ -1380,7 +1804,7 @@ class Chef def self.remove_canonical_dsl if @resource_name - remaining = Chef.resource_priority_map.delete_canonical(@resource_name, self) + remaining = Chef.resource_handler_map.delete_canonical(@resource_name, self) if !remaining Chef::DSL::Resources.remove_resource_dsl(@resource_name) end diff --git a/lib/chef/resource/action_provider.rb b/lib/chef/resource/action_provider.rb new file mode 100644 index 0000000000..d71b54ef4d --- /dev/null +++ b/lib/chef/resource/action_provider.rb @@ -0,0 +1,69 @@ +# +# Author:: John Keiser (<jkeiser@chef.io) +# Copyright:: Copyright (c) 2015 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 'chef/exceptions' + +class Chef + class Resource + module ActionProvider + # + # If load_current_value! is defined on the resource, use that. + # + def load_current_resource + if new_resource.respond_to?(:load_current_value!) + # dup the resource and then reset desired-state properties. + current_resource = new_resource.dup + + # We clear desired state in the copy, because it is supposed to be actual state. + # We keep identity properties and non-desired-state, which are assumed to be + # "control" values like `recurse: true` + current_resource.class.properties.each do |name,property| + if property.desired_state? && !property.identity? && !property.name_property? + property.reset(current_resource) + end + end + + # Call the actual load_current_value! method. If it raises + # CurrentValueDoesNotExist, set current_resource to `nil`. + begin + # If the user specifies load_current_value do |desired_resource|, we + # pass in the desired resource as well as the current one. + if current_resource.method(:load_current_value!).arity > 0 + current_resource.load_current_value!(new_resource) + else + current_resource.load_current_value! + end + rescue Chef::Exceptions::CurrentValueDoesNotExist + current_resource = nil + end + end + + @current_resource = current_resource + end + + def self.included(other) + other.extend(ClassMethods) + other.use_inline_resources + other.include_resource_dsl true + end + + module ClassMethods + end + end + end +end diff --git a/lib/chef/resource/chef_gem.rb b/lib/chef/resource/chef_gem.rb index 0c2fdfa819..7e9d21ebd2 100644 --- a/lib/chef/resource/chef_gem.rb +++ b/lib/chef/resource/chef_gem.rb @@ -50,9 +50,9 @@ class Chef # Chef::Resource.run_action: Caveat: this skips Chef::Runner.run_action, where notifications are handled # Action could be an array of symbols, but probably won't (think install + enable for a package) if compile_time.nil? - Chef::Log.deprecation "#{self} chef_gem compile_time installation is deprecated" - Chef::Log.deprecation "#{self} Please set `compile_time false` on the resource to use the new behavior." - Chef::Log.deprecation "#{self} or set `compile_time true` on the resource if compile_time behavior is required." + Chef.log_deprecation "#{self} chef_gem compile_time installation is deprecated" + Chef.log_deprecation "#{self} Please set `compile_time false` on the resource to use the new behavior." + Chef.log_deprecation "#{self} or set `compile_time true` on the resource if compile_time behavior is required." end if compile_time || compile_time.nil? diff --git a/lib/chef/resource/deploy.rb b/lib/chef/resource/deploy.rb index 3e5255bced..5df46fff60 100644 --- a/lib/chef/resource/deploy.rb +++ b/lib/chef/resource/deploy.rb @@ -27,6 +27,7 @@ # migration_command "rake db:migrate" # environment "RAILS_ENV" => "production", "OTHER_ENV" => "foo" # shallow_clone true +# depth 1 # action :deploy # or :rollback # restart_command "touch tmp/restart.txt" # git_ssh_wrapper "wrap-ssh4git.sh" @@ -74,6 +75,7 @@ class Chef @remote = "origin" @enable_submodules = false @shallow_clone = false + @depth = nil @scm_provider = Chef::Provider::Git @svn_force_export = false @additional_remotes = Hash[] @@ -97,8 +99,12 @@ class Chef @current_path ||= @deploy_to + "/current" end - def depth - @shallow_clone ? "5" : nil + def depth(arg=@shallow_clone ? 5 : nil) + set_or_return( + :depth, + arg, + :kind_of => [ Integer ] + ) end # note: deploy_to is your application "meta-root." diff --git a/lib/chef/resource/dsc_script.rb b/lib/chef/resource/dsc_script.rb index 2fcf183375..c3602fa60e 100644 --- a/lib/chef/resource/dsc_script.rb +++ b/lib/chef/resource/dsc_script.rb @@ -17,12 +17,14 @@ # require 'chef/exceptions' +require 'chef/dsl/powershell' class Chef class Resource class DscScript < Chef::Resource + include Chef::DSL::Powershell - provides :dsc_script, platform: "windows" + provides :dsc_script, os: "windows" default_action :run diff --git a/lib/chef/resource/file/verification.rb b/lib/chef/resource/file/verification.rb index f1ca0f1883..9b0788fad3 100644 --- a/lib/chef/resource/file/verification.rb +++ b/lib/chef/resource/file/verification.rb @@ -106,7 +106,14 @@ class Chef # We reuse Chef::GuardInterpreter in order to support # the same set of options that the not_if/only_if blocks do def verify_command(path, opts) - command = @command % {:file => path} + # First implementation interpolated `file`; docs & RFC claim `path` + # is interpolated. Until `file` can be deprecated, interpolate both. + Chef.log_deprecation( + '%{file} is deprecated in verify command and will not be '\ + 'supported in Chef 13. Please use %{path} instead.', + caller(2..2)[0] + ) if @command.include?('%{file}') + command = @command % {:file => path, :path => path} interpreter = Chef::GuardInterpreter.for_resource(@parent_resource, command, @command_opts) interpreter.evaluate end diff --git a/lib/chef/resource/ips_package.rb b/lib/chef/resource/ips_package.rb index 8d720dd411..2bf8e1dba8 100644 --- a/lib/chef/resource/ips_package.rb +++ b/lib/chef/resource/ips_package.rb @@ -23,6 +23,7 @@ class Chef class Resource class IpsPackage < ::Chef::Resource::Package + provides :package, os: "solaris2" provides :ips_package, os: "solaris2" allowed_actions :install, :remove, :upgrade diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index c486233020..443e0ed819 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -74,19 +74,14 @@ class Chef resource_class end - # Define an attribute on this resource, including optional validation - # parameters. - def attribute(attr_name, validation_opts={}) - define_method(attr_name) do |arg=nil| - set_or_return(attr_name.to_sym, arg, validation_opts) - end - end + alias :attribute :property # Adds +action_names+ to the list of valid actions for this resource. # Does not include superclass's action list when appending. def actions(*action_names) + action_names = action_names.flatten if !action_names.empty? && !@allowed_actions - self.allowed_actions = action_names + self.allowed_actions = ([ :nothing ] + action_names).uniq else allowed_actions(*action_names) end diff --git a/lib/chef/resource/macports_package.rb b/lib/chef/resource/macports_package.rb index 937839b6e1..5843016897 100644 --- a/lib/chef/resource/macports_package.rb +++ b/lib/chef/resource/macports_package.rb @@ -16,10 +16,11 @@ # limitations under the License. # +require 'chef/resource/package' + class Chef class Resource class MacportsPackage < Chef::Resource::Package - provides :package, os: "darwin" end end end diff --git a/lib/chef/resource/mount.rb b/lib/chef/resource/mount.rb index 79986d127f..a5da0ba329 100644 --- a/lib/chef/resource/mount.rb +++ b/lib/chef/resource/mount.rb @@ -174,6 +174,14 @@ class Chef ) end + private + + # Used by the AIX provider to set fstype to nil. + # TODO use property to make nil a valid value for fstype + def clear_fstype + @fstype = nil + end + end end end diff --git a/lib/chef/resource/openbsd_package.rb b/lib/chef/resource/openbsd_package.rb index f91fdb37e0..9ae8813d69 100644 --- a/lib/chef/resource/openbsd_package.rb +++ b/lib/chef/resource/openbsd_package.rb @@ -29,17 +29,6 @@ class Chef include Chef::Mixin::ShellOut provides :package, os: "openbsd" - - def after_created - assign_provider - end - - private - - def assign_provider - @provider = Chef::Provider::Package::Openbsd - end - end end end diff --git a/lib/chef/resource/package.rb b/lib/chef/resource/package.rb index 1c6da75678..5be1c34b89 100644 --- a/lib/chef/resource/package.rb +++ b/lib/chef/resource/package.rb @@ -100,8 +100,3 @@ class Chef end end end - -require 'chef/chef_class' -require 'chef/resource/homebrew_package' - -Chef.set_resource_priority_array :package, Chef::Resource::HomebrewPackage, os: "darwin" diff --git a/lib/chef/resource/registry_key.rb b/lib/chef/resource/registry_key.rb index 4ed0d4a4e0..d2e5c4b94c 100644 --- a/lib/chef/resource/registry_key.rb +++ b/lib/chef/resource/registry_key.rb @@ -93,7 +93,7 @@ class Chef raise ArgumentError, "Bad key #{key} in RegistryKey values hash" unless [:name,:type,:data].include?(key) end raise ArgumentError, "Type of name => #{v[:name]} should be string" unless v[:name].is_a?(String) - raise Argument Error "Type of type => #{v[:name]} should be symbol" unless v[:type].is_a?(Symbol) + raise ArgumentError, "Type of type => #{v[:type]} should be symbol" unless v[:type].is_a?(Symbol) end @unscrubbed_values = @values elsif self.instance_variable_defined?(:@values) diff --git a/lib/chef/resource/service.rb b/lib/chef/resource/service.rb index aa59b543be..6d1b81f9cb 100644 --- a/lib/chef/resource/service.rb +++ b/lib/chef/resource/service.rb @@ -1,7 +1,7 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) # Author:: Tyler Cloke (<tyler@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,7 +44,8 @@ class Chef @init_command = nil @priority = nil @timeout = nil - @supports = { :restart => false, :reload => false, :status => false } + @run_levels = nil + @supports = { :restart => nil, :reload => nil, :status => nil } end def service_name(arg=nil) @@ -174,6 +175,13 @@ class Chef ) end + def run_levels(arg=nil) + set_or_return( + :run_levels, + arg, + :kind_of => [ Array ] ) + end + def supports(args={}) if args.is_a? Array args.each { |arg| @supports[arg] = true } diff --git a/lib/chef/resource/solaris_package.rb b/lib/chef/resource/solaris_package.rb index 2dc72d5c47..a98fb8b4fa 100644 --- a/lib/chef/resource/solaris_package.rb +++ b/lib/chef/resource/solaris_package.rb @@ -24,10 +24,7 @@ class Chef class Resource class SolarisPackage < Chef::Resource::Package provides :package, os: "solaris2", platform_family: "nexentacore" - provides :package, os: "solaris2", platform_family: "solaris2" do |node| - # on >= Solaris 11 we default to IPS packages instead - node[:platform_version].to_f <= 5.10 - end + provides :package, os: "solaris2", platform_family: "solaris2", platform_version: "<= 5.10" end end end diff --git a/lib/chef/resource/yum_package.rb b/lib/chef/resource/yum_package.rb index 4d54f6051f..50ba13ce65 100644 --- a/lib/chef/resource/yum_package.rb +++ b/lib/chef/resource/yum_package.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -28,6 +28,7 @@ class Chef super @flush_cache = { :before => false, :after => false } @allow_downgrade = false + @yum_binary = nil end # Install a specific arch @@ -57,6 +58,14 @@ class Chef ) end + def yum_binary(arg=nil) + set_or_return( + :yum_binary, + arg, + :kind_of => [ String ] + ) + end + end end end diff --git a/lib/chef/resource_resolver.rb b/lib/chef/resource_resolver.rb index 31b39f7e24..67cf134c62 100644 --- a/lib/chef/resource_resolver.rb +++ b/lib/chef/resource_resolver.rb @@ -18,6 +18,7 @@ require 'chef/exceptions' require 'chef/platform/resource_priority_map' +require 'chef/mixin/convert_to_class_name' class Chef class ResourceResolver @@ -55,7 +56,7 @@ class Chef attr_reader :resource_name # @api private def resource - Chef::Log.deprecation("Chef::ResourceResolver.resource deprecated. Use resource_name instead.") + Chef.log_deprecation("Chef::ResourceResolver.resource deprecated. Use resource_name instead.") resource_name end # @api private @@ -104,52 +105,80 @@ class Chef # # Whether this DSL is provided by the given resource_class. # + # Does NOT call provides? on the resource (it is assumed this is being + # called *from* provides?). + # # @api private def provided_by?(resource_class) - !prioritized_handlers.include?(resource_class) + potential_handlers.include?(resource_class) + end + + # + # Whether the given handler attempts to provide the resource class at all. + # + # @api private + def self.includes_handler?(resource_name, resource_class) + handler_map.list(nil, resource_name).include?(resource_class) end protected + def self.priority_map + Chef.resource_priority_map + end + + def self.handler_map + Chef.resource_handler_map + end + def priority_map - Chef::Platform::ResourcePriorityMap.instance + Chef.resource_priority_map + end + + def handler_map + Chef.resource_handler_map + end + + # @api private + def potential_handlers + handler_map.list(node, resource_name, canonical: canonical).uniq + end + + def enabled_handlers + potential_handlers.select { |handler| !overrode_provides?(handler) || handler.provides?(node, resource_name) } end def prioritized_handlers - @prioritized_handlers ||= - priority_map.list_handlers(node, resource_name, canonical: canonical) + @prioritized_handlers ||= begin + enabled_handlers = self.enabled_handlers + + prioritized = priority_map.list(node, resource_name, canonical: canonical).flatten(1) + prioritized &= enabled_handlers # Filter the priority map by the actual enabled handlers + prioritized |= enabled_handlers # Bring back any handlers that aren't in the priority map, at the *end* (ordered set) + prioritized + end + end + + def overrode_provides?(handler) + handler.method(:provides?).owner != Chef::Resource.method(:provides?).owner end module Deprecated # return a deterministically sorted list of Chef::Resource subclasses - # @deprecated Now prioritized_handlers does its own work (more efficiently) def resources Chef::Resource.sorted_descendants end - # A list of all handlers - # @deprecated Now prioritized_handlers does its own work def enabled_handlers - Chef::Log.deprecation("enabled_handlers is deprecated. If you are implementing a ResourceResolver, use provided_handlers. If you are not, use Chef::ResourceResolver.list(#{resource_name.inspect}, node: <node>)") - resources.select { |klass| klass.provides?(node, resource_name) } - end - - protected - - # A list of all handlers for the given DSL. If there are no handlers in - # the map, we still check all descendants of Chef::Resource for backwards - # compatibility purposes. - def prioritized_handlers - @prioritized_handlers ||= super || - resources.select do |klass| - # Don't bother calling provides? unless it's overridden. We already - # know prioritized_handlers - if klass.method(:provides?).owner != Chef::Resource && klass.provides?(node, resource_name) - Chef::Log.deprecation("Resources #{provided.join(", ")} are marked as providing DSL #{resource_name}, but provides #{resource_name.inspect} was never called!") - Chef::Log.deprecation("In Chef 13, this will break: you must call provides to mark the names you provide, even if you also override provides? yourself.") - true - end + handlers = super + if handlers.empty? + handlers = resources.select { |handler| overrode_provides?(handler) && handler.provides?(node, resource_name) } + handlers.each do |handler| + Chef.log_deprecation("#{handler}.provides? returned true when asked if it provides DSL #{resource_name}, but provides #{resource_name.inspect} was never called!") + Chef.log_deprecation("In Chef 13, this will break: you must call provides to mark the names you provide, even if you also override provides? yourself.") end + end + handlers end end prepend Deprecated diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 44b05f0cc0..0c8d3d1a48 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -25,118 +25,223 @@ require 'chef/log' require 'chef/recipe' require 'chef/run_context/cookbook_compiler' require 'chef/event_dispatch/events_output_stream' +require 'forwardable' class Chef # == Chef::RunContext # Value object that loads and tracks the context of a Chef run class RunContext + # + # Global state + # - # Chef::Node object for this run + # + # The node for this run + # + # @return [Chef::Node] + # attr_reader :node - # Chef::CookbookCollection for this run + # + # The set of cookbooks involved in this run + # + # @return [Chef::CookbookCollection] + # attr_reader :cookbook_collection + # # Resource Definitions for this run. Populated when the files in # +definitions/+ are evaluated (this is triggered by #load). + # + # @return [Array[Chef::ResourceDefinition]] + # attr_reader :definitions - ### - # These need to be settable so deploy can run a resource_collection - # independent of any cookbooks via +recipe_eval+ + # + # Event dispatcher for this run. + # + # @return [Chef::EventDispatch::Dispatcher] + # + attr_reader :events + + # + # Hash of factoids for a reboot request. + # + # @return [Hash] + # + attr_accessor :reboot_info + + # + # Scoped state + # - # The Chef::ResourceCollection for this run. Populated by evaluating - # recipes, which is triggered by #load. (See also: CookbookCompiler) - attr_accessor :resource_collection + # + # The parent run context. + # + # @return [Chef::RunContext] The parent run context, or `nil` if this is the + # root context. + # + attr_reader :parent_run_context + # + # The collection of resources intended to be converged (and able to be + # notified). + # + # @return [Chef::ResourceCollection] + # + # @see CookbookCompiler + # + attr_reader :resource_collection + + # # The list of control groups to execute during the audit phase - attr_accessor :audits + # + attr_reader :audits + + # + # Notification handling + # + # # A Hash containing the immediate notifications triggered by resources # during the converge phase of the chef run. - attr_accessor :immediate_notification_collection + # + # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from + # <notifying resource name> => <list of notifications it sent> + # + attr_reader :immediate_notification_collection + # # A Hash containing the delayed (end of run) notifications triggered by # resources during the converge phase of the chef run. - attr_accessor :delayed_notification_collection - - # Event dispatcher for this run. - attr_reader :events - - # Hash of factoids for a reboot request. - attr_reader :reboot_info + # + # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from + # <notifying resource name> => <list of notifications it sent> + # + attr_reader :delayed_notification_collection # Creates a new Chef::RunContext object and populates its fields. This object gets # used by the Chef Server to generate a fully compiled recipe list for a node. # - # === Returns - # object<Chef::RunContext>:: Duh. :) + # @param node [Chef::Node] The node to run against. + # @param cookbook_collection [Chef::CookbookCollection] The cookbooks + # involved in this run. + # @param events [EventDispatch::Dispatcher] The event dispatcher for this + # run. + # def initialize(node, cookbook_collection, events) @node = node @cookbook_collection = cookbook_collection - @resource_collection = Chef::ResourceCollection.new - @audits = {} - @immediate_notification_collection = Hash.new {|h,k| h[k] = []} - @delayed_notification_collection = Hash.new {|h,k| h[k] = []} - @definitions = Hash.new - @loaded_recipes = {} - @loaded_attributes = {} @events = events - @reboot_info = {} - @node.run_context = self - @node.set_cookbook_attribute + node.run_context = self + node.set_cookbook_attribute + + @definitions = Hash.new + @loaded_recipes_hash = {} + @loaded_attributes_hash = {} + @reboot_info = {} @cookbook_compiler = nil + + initialize_child_state end - # Triggers the compile phase of the chef run. Implemented by - # Chef::RunContext::CookbookCompiler + # + # Triggers the compile phase of the chef run. + # + # @param run_list_expansion [Chef::RunList::RunListExpansion] The run list. + # @see Chef::RunContext::CookbookCompiler + # def load(run_list_expansion) @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events) - @cookbook_compiler.compile + cookbook_compiler.compile end - # Adds an immediate notification to the - # +immediate_notification_collection+. The notification should be a - # Chef::Resource::Notification or duck type. + # + # Initialize state that applies to both Chef::RunContext and Chef::ChildRunContext + # + def initialize_child_state + @audits = {} + @resource_collection = Chef::ResourceCollection.new + @immediate_notification_collection = Hash.new {|h,k| h[k] = []} + @delayed_notification_collection = Hash.new {|h,k| h[k] = []} + end + + # + # Adds an immediate notification to the +immediate_notification_collection+. + # + # @param [Chef::Resource::Notification] The notification to add. + # def notifies_immediately(notification) nr = notification.notifying_resource if nr.instance_of?(Chef::Resource) - @immediate_notification_collection[nr.name] << notification + immediate_notification_collection[nr.name] << notification else - @immediate_notification_collection[nr.declared_key] << notification + immediate_notification_collection[nr.declared_key] << notification end end - # Adds a delayed notification to the +delayed_notification_collection+. The - # notification should be a Chef::Resource::Notification or duck type. + # + # Adds a delayed notification to the +delayed_notification_collection+. + # + # @param [Chef::Resource::Notification] The notification to add. + # def notifies_delayed(notification) nr = notification.notifying_resource if nr.instance_of?(Chef::Resource) - @delayed_notification_collection[nr.name] << notification + delayed_notification_collection[nr.name] << notification else - @delayed_notification_collection[nr.declared_key] << notification + delayed_notification_collection[nr.declared_key] << notification end end + # + # Get the list of immediate notifications sent by the given resource. + # + # TODO seriously, this is actually wrong. resource.name is not unique, + # you need the type as well. + # + # @return [Array[Notification]] + # def immediate_notifications(resource) if resource.instance_of?(Chef::Resource) - return @immediate_notification_collection[resource.name] + return immediate_notification_collection[resource.name] else - return @immediate_notification_collection[resource.declared_key] + return immediate_notification_collection[resource.declared_key] end end + # + # Get the list of delayed (end of run) notifications sent by the given + # resource. + # + # TODO seriously, this is actually wrong. resource.name is not unique, + # you need the type as well. + # + # @return [Array[Notification]] + # def delayed_notifications(resource) if resource.instance_of?(Chef::Resource) - return @delayed_notification_collection[resource.name] + return delayed_notification_collection[resource.name] else - return @delayed_notification_collection[resource.declared_key] + return delayed_notification_collection[resource.declared_key] end end + # + # Cookbook and recipe loading + # + + # # Evaluates the recipes +recipe_names+. Used by DSL::IncludeRecipe + # + # @param recipe_names [Array[String]] The list of recipe names (e.g. + # 'my_cookbook' or 'my_cookbook::my_resource'). + # @param current_cookbook The cookbook we are currently running in. + # + # @see DSL::IncludeRecipe#include_recipe + # def include_recipe(*recipe_names, current_cookbook: nil) result_recipes = Array.new recipe_names.flatten.each do |recipe_name| @@ -147,7 +252,21 @@ class Chef result_recipes end + # # Evaluates the recipe +recipe_name+. Used by DSL::IncludeRecipe + # + # TODO I am sort of confused why we have both this and include_recipe ... + # I don't see anything different beyond accepting and returning an + # array of recipes. + # + # @param recipe_names [Array[String]] The recipe name (e.g 'my_cookbook' or + # 'my_cookbook::my_resource'). + # @param current_cookbook The cookbook we are currently running in. + # + # @return A truthy value if the load occurred; `false` if already loaded. + # + # @see DSL::IncludeRecipe#load_recipe + # def load_recipe(recipe_name, current_cookbook: nil) Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe") @@ -175,6 +294,15 @@ ERROR_MESSAGE end end + # + # Load the given recipe from a filename. + # + # @param recipe_file [String] The recipe filename. + # + # @return [Chef::Recipe] The loaded recipe. + # + # @raise [Chef::Exceptions::RecipeNotFound] If the file does not exist. + # def load_recipe_file(recipe_file) if !File.exist?(recipe_file) raise Chef::Exceptions::RecipeNotFound, "could not find recipe file #{recipe_file}" @@ -186,8 +314,19 @@ ERROR_MESSAGE recipe end - # Looks up an attribute file given the +cookbook_name+ and - # +attr_file_name+. Used by DSL::IncludeAttribute + # + # Look up an attribute filename. + # + # @param cookbook_name [String] The cookbook name of the attribute file. + # @param attr_file_name [String] The attribute file's name (not path). + # + # @return [String] The filename. + # + # @see DSL::IncludeAttribute#include_attribute + # + # @raise [Chef::Exceptions::CookbookNotFound] If the cookbook could not be found. + # @raise [Chef::Exceptions::AttributeNotFound] If the attribute file could not be found. + # def resolve_attribute(cookbook_name, attr_file_name) cookbook = cookbook_collection[cookbook_name] raise Chef::Exceptions::CookbookNotFound, "could not find cookbook #{cookbook_name} while loading attribute #{name}" unless cookbook @@ -198,76 +337,152 @@ ERROR_MESSAGE attribute_filename end - # An Array of all recipes that have been loaded. This is stored internally - # as a Hash, so ordering is predictable. # - # Recipe names are given in fully qualified form, e.g., the recipe "nginx" - # will be given as "nginx::default" + # A list of all recipes that have been loaded. + # + # This is stored internally as a Hash, so ordering is predictable. + # + # TODO is the above statement true in a 1.9+ ruby world? Is it relevant? + # + # @return [Array[String]] A list of recipes in fully qualified form, e.g. + # the recipe "nginx" will be given as "nginx::default". + # + # @see #loaded_recipe? To determine if a particular recipe has been loaded. # - # To determine if a particular recipe has been loaded, use #loaded_recipe? def loaded_recipes - @loaded_recipes.keys + loaded_recipes_hash.keys end - # An Array of all attributes files that have been loaded. Stored internally - # using a Hash, so order is predictable. # - # Attribute file names are given in fully qualified form, e.g., - # "nginx::default" instead of "nginx". + # A list of all attributes files that have been loaded. + # + # Stored internally using a Hash, so order is predictable. + # + # TODO is the above statement true in a 1.9+ ruby world? Is it relevant? + # + # @return [Array[String]] A list of attribute file names in fully qualified + # form, e.g. the "nginx" will be given as "nginx::default". + # def loaded_attributes - @loaded_attributes.keys + loaded_attributes_hash.keys end + # + # Find out if a given recipe has been loaded. + # + # @param cookbook [String] Cookbook name. + # @param recipe [String] Recipe name. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_fully_qualified_recipe?(cookbook, recipe) - @loaded_recipes.has_key?("#{cookbook}::#{recipe}") + loaded_recipes_hash.has_key?("#{cookbook}::#{recipe}") end - # Returns true if +recipe+ has been loaded, false otherwise. Default recipe - # names are expanded, so `loaded_recipe?("nginx")` and - # `loaded_recipe?("nginx::default")` are valid and give identical results. + # + # Find out if a given recipe has been loaded. + # + # @param recipe [String] Recipe name. "nginx" and "nginx::default" yield + # the same results. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_recipe?(recipe) cookbook, recipe_name = Chef::Recipe.parse_recipe_name(recipe) loaded_fully_qualified_recipe?(cookbook, recipe_name) end + # + # Mark a given recipe as having been loaded. + # + # @param cookbook [String] Cookbook name. + # @param recipe [String] Recipe name. + # + def loaded_recipe(cookbook, recipe) + loaded_recipes_hash["#{cookbook}::#{recipe}"] = true + end + + # + # Find out if a given attribute file has been loaded. + # + # @param cookbook [String] Cookbook name. + # @param attribute_file [String] Attribute file name. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_fully_qualified_attribute?(cookbook, attribute_file) - @loaded_attributes.has_key?("#{cookbook}::#{attribute_file}") + loaded_attributes_hash.has_key?("#{cookbook}::#{attribute_file}") end + # + # Mark a given attribute file as having been loaded. + # + # @param cookbook [String] Cookbook name. + # @param attribute_file [String] Attribute file name. + # def loaded_attribute(cookbook, attribute_file) - @loaded_attributes["#{cookbook}::#{attribute_file}"] = true + loaded_attributes_hash["#{cookbook}::#{attribute_file}"] = true end ## # Cookbook File Introspection + # + # Find out if the cookbook has the given template. + # + # @param cookbook [String] Cookbook name. + # @param template_name [String] Template name. + # + # @return [Boolean] `true` if the template is in the cookbook, `false` + # otherwise. + # @see Chef::CookbookVersion#has_template_for_node? + # def has_template_in_cookbook?(cookbook, template_name) cookbook = cookbook_collection[cookbook] cookbook.has_template_for_node?(node, template_name) end + # + # Find out if the cookbook has the given file. + # + # @param cookbook [String] Cookbook name. + # @param cb_file_name [String] File name. + # + # @return [Boolean] `true` if the file is in the cookbook, `false` + # otherwise. + # @see Chef::CookbookVersion#has_cookbook_file_for_node? + # def has_cookbook_file_in_cookbook?(cookbook, cb_file_name) cookbook = cookbook_collection[cookbook] cookbook.has_cookbook_file_for_node?(node, cb_file_name) end - # Delegates to CookbookCompiler#unreachable_cookbook? - # Used to raise an error when attempting to load a recipe belonging to a - # cookbook that is not in the dependency graph. See also: CHEF-4367 + # + # Find out whether the given cookbook is in the cookbook dependency graph. + # + # @param cookbook_name [String] Cookbook name. + # + # @return [Boolean] `true` if the cookbook is reachable, `false` otherwise. + # + # @see Chef::CookbookCompiler#unreachable_cookbook? def unreachable_cookbook?(cookbook_name) - @cookbook_compiler.unreachable_cookbook?(cookbook_name) + cookbook_compiler.unreachable_cookbook?(cookbook_name) end + # # Open a stream object that can be printed into and will dispatch to events # - # == Arguments - # options is a hash with these possible options: - # - name: a string that identifies the stream to the user. Preferably short. + # @param name [String] The name of the stream. + # @param options [Hash] Other options for the stream. + # + # @return [EventDispatch::EventsOutputStream] The created stream. + # + # @yield If a block is passed, it will be run and the stream will be closed + # afterwards. + # @yieldparam stream [EventDispatch::EventsOutputStream] The created stream. # - # Pass a block and the stream will be yielded to it, and close on its own - # at the end of the block. - def open_stream(options = {}) - stream = EventDispatch::EventsOutputStream.new(events, options) + def open_stream(name: nil, **options) + stream = EventDispatch::EventsOutputStream.new(events, name: name, **options) if block_given? begin yield stream @@ -280,31 +495,137 @@ ERROR_MESSAGE end # there are options for how to handle multiple calls to these functions: - # 1. first call always wins (never change @reboot_info once set). - # 2. last call always wins (happily change @reboot_info whenever). + # 1. first call always wins (never change reboot_info once set). + # 2. last call always wins (happily change reboot_info whenever). # 3. raise an exception on the first conflict. # 4. disable reboot after this run if anyone ever calls :cancel. # 5. raise an exception on any second call. # 6. ? def request_reboot(reboot_info) - Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to #{reboot_info.inspect}" + Chef::Log::info "Changing reboot status from #{self.reboot_info.inspect} to #{reboot_info.inspect}" @reboot_info = reboot_info end def cancel_reboot - Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to {}" + Chef::Log::info "Changing reboot status from #{reboot_info.inspect} to {}" @reboot_info = {} end def reboot_requested? - @reboot_info.size > 0 + reboot_info.size > 0 end - private + # + # Create a child RunContext. + # + def create_child + ChildRunContext.new(self) + end - def loaded_recipe(cookbook, recipe) - @loaded_recipes["#{cookbook}::#{recipe}"] = true + protected + + attr_reader :cookbook_compiler + attr_reader :loaded_attributes_hash + attr_reader :loaded_recipes_hash + + module Deprecated + ### + # These need to be settable so deploy can run a resource_collection + # independent of any cookbooks via +recipe_eval+ + def resource_collection=(value) + Chef.log_deprecation("Setting run_context.resource_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + @resource_collection = value + end + + def audits=(value) + Chef.log_deprecation("Setting run_context.audits will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + @audits = value + end + + def immediate_notification_collection=(value) + Chef.log_deprecation("Setting run_context.immediate_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + @immediate_notification_collection = value + end + + def delayed_notification_collection=(value) + Chef.log_deprecation("Setting run_context.delayed_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + @delayed_notification_collection = value + end end + prepend Deprecated + + + # + # A child run context. Delegates all root context calls to its parent. + # + # @api private + # + class ChildRunContext < RunContext + extend Forwardable + def_delegators :parent_run_context, *%w( + cancel_reboot + config + cookbook_collection + cookbook_compiler + definitions + events + has_cookbook_file_in_cookbook? + has_template_in_cookbook? + load + loaded_attribute + loaded_attributes + loaded_attributes_hash + loaded_fully_qualified_attribute? + loaded_fully_qualified_recipe? + loaded_recipe + loaded_recipe? + loaded_recipes + loaded_recipes_hash + node + open_stream + reboot_info + reboot_info= + reboot_requested? + request_reboot + resolve_attribute + unreachable_cookbook? + ) + + def initialize(parent_run_context) + @parent_run_context = parent_run_context + + # We don't call super, because we don't bother initializing stuff we're + # going to delegate to the parent anyway. Just initialize things that + # every instance needs. + initialize_child_state + end + CHILD_STATE = %w( + audits + audits= + create_child + delayed_notification_collection + delayed_notification_collection= + delayed_notifications + immediate_notification_collection + immediate_notification_collection= + immediate_notifications + include_recipe + initialize_child_state + load_recipe + load_recipe_file + notifies_immediately + notifies_delayed + parent_run_context + resource_collection + resource_collection= + ).map { |x| x.to_sym } + + # Verify that we didn't miss any methods + missing_methods = superclass.instance_methods(false) - instance_methods(false) - CHILD_STATE + if !missing_methods.empty? + raise "ERROR: not all methods of RunContext accounted for in ChildRunContext! All methods must be marked as child methods with CHILD_STATE or delegated to the parent_run_context. Missing #{missing_methods.join(", ")}." + end + end end end diff --git a/lib/chef/run_list/versioned_recipe_list.rb b/lib/chef/run_list/versioned_recipe_list.rb index 7cce6fa48c..2824f08f31 100644 --- a/lib/chef/run_list/versioned_recipe_list.rb +++ b/lib/chef/run_list/versioned_recipe_list.rb @@ -70,15 +70,16 @@ class Chef # @return [Array] Array of strings with fully-qualified recipe names def with_fully_qualified_names_and_version_constraints self.map do |recipe_name| - ret = if recipe_name.include?('::') + qualified_recipe = if recipe_name.include?('::') recipe_name else "#{recipe_name}::default" end - if @versions[recipe_name] - ret << "@#{@versions[recipe_name]}" - end - ret + + version = @versions[recipe_name] + qualified_recipe = "#{qualified_recipe}@#{version}" if version + + qualified_recipe end end end diff --git a/lib/chef/user.rb b/lib/chef/user.rb index 717deb63c3..31ebeda86f 100644 --- a/lib/chef/user.rb +++ b/lib/chef/user.rb @@ -21,85 +21,45 @@ require 'chef/mixin/from_file' require 'chef/mash' require 'chef/json_compat' require 'chef/search/query' -require 'chef/mixin/api_version_request_handling' -require 'chef/exceptions' require 'chef/server_api' -# OSC 11 BACKWARDS COMPATIBILITY NOTE (remove after OSC 11 support ends) +# TODO +# DEPRECATION NOTE +# This class will be replaced by Chef::UserV1 in Chef 13. It is the code to support the User object +# corrosponding to the Open Source Chef Server 11 and only still exists to support +# users still on OSC 11. # -# In general, Chef::User is no longer expected to support Open Source Chef 11 Server requests. -# The object that handles those requests has been moved to the Chef::OscUser namespace. +# Chef::UserV1 now supports Chef Server 12 and will be moved to this namespace in Chef 13. # -# Exception: self.list is backwards compatible with OSC 11 +# New development should occur in Chef::UserV1. +# This file and corrosponding osc_user knife files +# should be removed once client support for Open Source Chef Server 11 expires. class Chef class User include Chef::Mixin::FromFile include Chef::Mixin::ParamsValidate - include Chef::Mixin::ApiVersionRequestHandling - - SUPPORTED_API_VERSIONS = [0,1] def initialize - @username = nil - @display_name = nil - @first_name = nil - @middle_name = nil - @last_name = nil - @email = nil - @password = nil + @name = '' @public_key = nil @private_key = nil - @create_key = nil @password = nil + @admin = false end - def chef_root_rest_v0 - @chef_root_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "0"}) - end - - def chef_root_rest_v1 - @chef_root_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "1"}) + def chef_rest_v0 + @chef_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}) end - def username(arg=nil) - set_or_return(:username, arg, + def name(arg=nil) + set_or_return(:name, arg, :regex => /^[a-z0-9\-_]+$/) end - def display_name(arg=nil) - set_or_return(:display_name, - arg, :kind_of => String) - end - - def first_name(arg=nil) - set_or_return(:first_name, - arg, :kind_of => String) - end - - def middle_name(arg=nil) - set_or_return(:middle_name, - arg, :kind_of => String) - end - - def last_name(arg=nil) - set_or_return(:last_name, - arg, :kind_of => String) - end - - def email(arg=nil) - set_or_return(:email, - arg, :kind_of => String) - end - - def password(arg=nil) - set_or_return(:password, - arg, :kind_of => String) - end - - def create_key(arg=nil) - set_or_return(:create_key, arg, - :kind_of => [TrueClass, FalseClass]) + def admin(arg=nil) + set_or_return(:admin, + arg, :kind_of => [TrueClass, FalseClass]) end def public_key(arg=nil) @@ -119,17 +79,12 @@ class Chef def to_hash result = { - "username" => @username + "name" => @name, + "public_key" => @public_key, + "admin" => @admin } - result["display_name"] = @display_name unless @display_name.nil? - result["first_name"] = @first_name unless @first_name.nil? - result["middle_name"] = @middle_name unless @middle_name.nil? - result["last_name"] = @last_name unless @last_name.nil? - result["email"] = @email unless @email.nil? - result["password"] = @password unless @password.nil? - result["public_key"] = @public_key unless @public_key.nil? - result["private_key"] = @private_key unless @private_key.nil? - result["create_key"] = @create_key unless @create_key.nil? + result["private_key"] = @private_key if @private_key + result["password"] = @password if @password result end @@ -138,86 +93,21 @@ class Chef end def destroy - # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) - Chef::REST.new(Chef::Config[:chef_server_url]).delete("users/#{@username}") + chef_rest_v0.delete("users/#{@name}") end def create - # try v1, fail back to v0 if v1 not supported - begin - payload = { - :username => @username, - :display_name => @display_name, - :first_name => @first_name, - :last_name => @last_name, - :email => @email, - :password => @password - } - payload[:public_key] = @public_key unless @public_key.nil? - payload[:create_key] = @create_key unless @create_key.nil? - payload[:middle_name] = @middle_name unless @middle_name.nil? - raise Chef::Exceptions::InvalidUserAttribute, "You cannot set both public_key and create_key for create." if !@create_key.nil? && !@public_key.nil? - new_user = chef_root_rest_v1.post("users", payload) - - # get the private_key out of the chef_key hash if it exists - if new_user['chef_key'] - if new_user['chef_key']['private_key'] - new_user['private_key'] = new_user['chef_key']['private_key'] - end - new_user['public_key'] = new_user['chef_key']['public_key'] - new_user.delete('chef_key') - end - rescue Net::HTTPServerException => e - # rescue API V0 if 406 and the server supports V0 - supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) - raise e unless supported_versions && supported_versions.include?(0) - payload = { - :username => @username, - :display_name => @display_name, - :first_name => @first_name, - :last_name => @last_name, - :email => @email, - :password => @password - } - payload[:middle_name] = @middle_name unless @middle_name.nil? - payload[:public_key] = @public_key unless @public_key.nil? - # under API V0, the server will create a key pair if public_key isn't passed - new_user = chef_root_rest_v0.post("users", payload) - end - + payload = {:name => self.name, :admin => self.admin, :password => self.password } + payload[:public_key] = public_key if public_key + new_user = chef_rest_v0.post("users", payload) Chef::User.from_hash(self.to_hash.merge(new_user)) end def update(new_key=false) - begin - payload = {:username => username} - payload[:display_name] = display_name unless display_name.nil? - payload[:first_name] = first_name unless first_name.nil? - payload[:middle_name] = middle_name unless middle_name.nil? - payload[:last_name] = last_name unless last_name.nil? - payload[:email] = email unless email.nil? - payload[:password] = password unless password.nil? - - # API V1 will fail if these key fields are defined, and try V0 below if relevant 400 is returned - payload[:public_key] = public_key unless public_key.nil? - payload[:private_key] = new_key if new_key - - updated_user = chef_root_rest_v1.put("users/#{username}", payload) - rescue Net::HTTPServerException => e - if e.response.code == "400" - # if a 400 is returned but the error message matches the error related to private / public key fields, try V0 - # else, raise the 400 - error = Chef::JSONCompat.from_json(e.response.body)["error"].first - error_match = /Since Server API v1, all keys must be updated via the keys endpoint/.match(error) - if error_match.nil? - raise e - end - else # for other types of errors, test for API versioning errors right away - supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) - raise e unless supported_versions && supported_versions.include?(0) - end - updated_user = chef_root_rest_v0.put("users/#{username}", payload) - end + payload = {:name => name, :admin => admin} + payload[:private_key] = new_key if new_key + payload[:password] = password if password + updated_user = chef_rest_v0.put("users/#{name}", payload) Chef::User.from_hash(self.to_hash.merge(updated_user)) end @@ -233,47 +123,30 @@ class Chef end end - # Note: remove after API v0 no longer supported by client (and knife command). def reregister - begin - payload = self.to_hash.merge({"private_key" => true}) - reregistered_self = chef_root_rest_v0.put("users/#{username}", payload) - private_key(reregistered_self["private_key"]) - # only V0 supported for reregister - rescue Net::HTTPServerException => e - # if there was a 406 related to versioning, give error explaining that - # only API version 0 is supported for reregister command - if e.response.code == "406" && e.response["x-ops-server-api-version"] - version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) - min_version = version_header["min_version"] - max_version = version_header["max_version"] - error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) - raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) - else - raise e - end - end + reregistered_self = chef_rest_v0.put("users/#{name}", { :name => name, :admin => admin, :private_key => true }) + private_key(reregistered_self["private_key"]) self end def to_s - "user[#{@username}]" + "user[#{@name}]" + end + + def inspect + "Chef::User name:'#{name}' admin:'#{admin.inspect}'" + + "public_key:'#{public_key}' private_key:#{private_key}" end # Class Methods def self.from_hash(user_hash) user = Chef::User.new - user.username user_hash['username'] - user.display_name user_hash['display_name'] if user_hash.key?('display_name') - user.first_name user_hash['first_name'] if user_hash.key?('first_name') - user.middle_name user_hash['middle_name'] if user_hash.key?('middle_name') - user.last_name user_hash['last_name'] if user_hash.key?('last_name') - user.email user_hash['email'] if user_hash.key?('email') - user.password user_hash['password'] if user_hash.key?('password') - user.public_key user_hash['public_key'] if user_hash.key?('public_key') + user.name user_hash['name'] user.private_key user_hash['private_key'] if user_hash.key?('private_key') - user.create_key user_hash['create_key'] if user_hash.key?('create_key') + user.password user_hash['password'] if user_hash.key?('password') + user.public_key user_hash['public_key'] + user.admin user_hash['admin'] user end @@ -286,19 +159,12 @@ class Chef end def self.list(inflate=false) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get('users') + response = Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}).get('users') users = if response.is_a?(Array) - # EC 11 / CS 12 V0, V1 - # GET /organizations/<org>/users - transform_list_response(response) - else - # OSC 11 - # GET /users - # EC 11 / CS 12 V0, V1 - # GET /users - response # OSC - end - + transform_ohc_list_response(response) # OHC/OPC + else + response # OSC + end if inflate users.inject({}) do |user_map, (name, _url)| user_map[name] = Chef::User.load(name) @@ -309,9 +175,8 @@ class Chef end end - def self.load(username) - # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) - response = Chef::REST.new(Chef::Config[:chef_server_url]).get("users/#{username}") + def self.load(name) + response = Chef::ServerAPI.new(Chef::Config[:chef_server_url], {:api_version => "0"}).get("users/#{name}") Chef::User.from_hash(response) end @@ -319,7 +184,7 @@ class Chef # [ { "user" => { "username" => USERNAME }}, ...] # into the form # { "USERNAME" => "URI" } - def self.transform_list_response(response) + def self.transform_ohc_list_response(response) new_response = Hash.new response.each do |u| name = u['user']['username'] @@ -328,7 +193,6 @@ class Chef new_response end - private_class_method :transform_list_response - + private_class_method :transform_ohc_list_response end end diff --git a/lib/chef/user_v1.rb b/lib/chef/user_v1.rb new file mode 100644 index 0000000000..31cb0576a2 --- /dev/null +++ b/lib/chef/user_v1.rb @@ -0,0 +1,335 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright 2012 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 'chef/config' +require 'chef/mixin/params_validate' +require 'chef/mixin/from_file' +require 'chef/mash' +require 'chef/json_compat' +require 'chef/search/query' +require 'chef/mixin/api_version_request_handling' +require 'chef/exceptions' +require 'chef/server_api' + +# OSC 11 BACKWARDS COMPATIBILITY NOTE (remove after OSC 11 support ends) +# +# In general, Chef::UserV1 is no longer expected to support Open Source Chef 11 Server requests. +# The object that handles those requests remain in the Chef::User namespace. +# This code will be moved to the Chef::User namespace as of Chef 13. +# +# Exception: self.list is backwards compatible with OSC 11 +class Chef + class UserV1 + + include Chef::Mixin::FromFile + include Chef::Mixin::ParamsValidate + include Chef::Mixin::ApiVersionRequestHandling + + SUPPORTED_API_VERSIONS = [0,1] + + def initialize + @username = nil + @display_name = nil + @first_name = nil + @middle_name = nil + @last_name = nil + @email = nil + @password = nil + @public_key = nil + @private_key = nil + @create_key = nil + @password = nil + end + + def chef_root_rest_v0 + @chef_root_rest_v0 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "0"}) + end + + def chef_root_rest_v1 + @chef_root_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_root], {:api_version => "1"}) + end + + def username(arg=nil) + set_or_return(:username, arg, + :regex => /^[a-z0-9\-_]+$/) + end + + def display_name(arg=nil) + set_or_return(:display_name, + arg, :kind_of => String) + end + + def first_name(arg=nil) + set_or_return(:first_name, + arg, :kind_of => String) + end + + def middle_name(arg=nil) + set_or_return(:middle_name, + arg, :kind_of => String) + end + + def last_name(arg=nil) + set_or_return(:last_name, + arg, :kind_of => String) + end + + def email(arg=nil) + set_or_return(:email, + arg, :kind_of => String) + end + + def password(arg=nil) + set_or_return(:password, + arg, :kind_of => String) + end + + def create_key(arg=nil) + set_or_return(:create_key, arg, + :kind_of => [TrueClass, FalseClass]) + end + + def public_key(arg=nil) + set_or_return(:public_key, + arg, :kind_of => String) + end + + def private_key(arg=nil) + set_or_return(:private_key, + arg, :kind_of => String) + end + + def password(arg=nil) + set_or_return(:password, + arg, :kind_of => String) + end + + def to_hash + result = { + "username" => @username + } + result["display_name"] = @display_name unless @display_name.nil? + result["first_name"] = @first_name unless @first_name.nil? + result["middle_name"] = @middle_name unless @middle_name.nil? + result["last_name"] = @last_name unless @last_name.nil? + result["email"] = @email unless @email.nil? + result["password"] = @password unless @password.nil? + result["public_key"] = @public_key unless @public_key.nil? + result["private_key"] = @private_key unless @private_key.nil? + result["create_key"] = @create_key unless @create_key.nil? + result + end + + def to_json(*a) + Chef::JSONCompat.to_json(to_hash, *a) + end + + def destroy + # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + Chef::REST.new(Chef::Config[:chef_server_url]).delete("users/#{@username}") + end + + def create + # try v1, fail back to v0 if v1 not supported + begin + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password + } + payload[:public_key] = @public_key unless @public_key.nil? + payload[:create_key] = @create_key unless @create_key.nil? + payload[:middle_name] = @middle_name unless @middle_name.nil? + raise Chef::Exceptions::InvalidUserAttribute, "You cannot set both public_key and create_key for create." if !@create_key.nil? && !@public_key.nil? + new_user = chef_root_rest_v1.post("users", payload) + + # get the private_key out of the chef_key hash if it exists + if new_user['chef_key'] + if new_user['chef_key']['private_key'] + new_user['private_key'] = new_user['chef_key']['private_key'] + end + new_user['public_key'] = new_user['chef_key']['public_key'] + new_user.delete('chef_key') + end + rescue Net::HTTPServerException => e + # rescue API V0 if 406 and the server supports V0 + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + payload = { + :username => @username, + :display_name => @display_name, + :first_name => @first_name, + :last_name => @last_name, + :email => @email, + :password => @password + } + payload[:middle_name] = @middle_name unless @middle_name.nil? + payload[:public_key] = @public_key unless @public_key.nil? + # under API V0, the server will create a key pair if public_key isn't passed + new_user = chef_root_rest_v0.post("users", payload) + end + + Chef::UserV1.from_hash(self.to_hash.merge(new_user)) + end + + def update(new_key=false) + begin + payload = {:username => username} + payload[:display_name] = display_name unless display_name.nil? + payload[:first_name] = first_name unless first_name.nil? + payload[:middle_name] = middle_name unless middle_name.nil? + payload[:last_name] = last_name unless last_name.nil? + payload[:email] = email unless email.nil? + payload[:password] = password unless password.nil? + + # API V1 will fail if these key fields are defined, and try V0 below if relevant 400 is returned + payload[:public_key] = public_key unless public_key.nil? + payload[:private_key] = new_key if new_key + + updated_user = chef_root_rest_v1.put("users/#{username}", payload) + rescue Net::HTTPServerException => e + if e.response.code == "400" + # if a 400 is returned but the error message matches the error related to private / public key fields, try V0 + # else, raise the 400 + error = Chef::JSONCompat.from_json(e.response.body)["error"].first + error_match = /Since Server API v1, all keys must be updated via the keys endpoint/.match(error) + if error_match.nil? + raise e + end + else # for other types of errors, test for API versioning errors right away + supported_versions = server_client_api_version_intersection(e, SUPPORTED_API_VERSIONS) + raise e unless supported_versions && supported_versions.include?(0) + end + updated_user = chef_root_rest_v0.put("users/#{username}", payload) + end + Chef::UserV1.from_hash(self.to_hash.merge(updated_user)) + end + + def save(new_key=false) + begin + create + rescue Net::HTTPServerException => e + if e.response.code == "409" + update(new_key) + else + raise e + end + end + end + + # Note: remove after API v0 no longer supported by client (and knife command). + def reregister + begin + payload = self.to_hash.merge({"private_key" => true}) + reregistered_self = chef_root_rest_v0.put("users/#{username}", payload) + private_key(reregistered_self["private_key"]) + # only V0 supported for reregister + rescue Net::HTTPServerException => e + # if there was a 406 related to versioning, give error explaining that + # only API version 0 is supported for reregister command + if e.response.code == "406" && e.response["x-ops-server-api-version"] + version_header = Chef::JSONCompat.from_json(e.response["x-ops-server-api-version"]) + min_version = version_header["min_version"] + max_version = version_header["max_version"] + error_msg = reregister_only_v0_supported_error_msg(max_version, min_version) + raise Chef::Exceptions::OnlyApiVersion0SupportedForAction.new(error_msg) + else + raise e + end + end + self + end + + def to_s + "user[#{@username}]" + end + + # Class Methods + + def self.from_hash(user_hash) + user = Chef::UserV1.new + user.username user_hash['username'] + user.display_name user_hash['display_name'] if user_hash.key?('display_name') + user.first_name user_hash['first_name'] if user_hash.key?('first_name') + user.middle_name user_hash['middle_name'] if user_hash.key?('middle_name') + user.last_name user_hash['last_name'] if user_hash.key?('last_name') + user.email user_hash['email'] if user_hash.key?('email') + user.password user_hash['password'] if user_hash.key?('password') + user.public_key user_hash['public_key'] if user_hash.key?('public_key') + user.private_key user_hash['private_key'] if user_hash.key?('private_key') + user.create_key user_hash['create_key'] if user_hash.key?('create_key') + user + end + + def self.from_json(json) + Chef::UserV1.from_hash(Chef::JSONCompat.from_json(json)) + end + + class << self + alias_method :json_create, :from_json + end + + def self.list(inflate=false) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get('users') + users = if response.is_a?(Array) + # EC 11 / CS 12 V0, V1 + # GET /organizations/<org>/users + transform_list_response(response) + else + # OSC 11 + # GET /users + # EC 11 / CS 12 V0, V1 + # GET /users + response # OSC + end + + if inflate + users.inject({}) do |user_map, (name, _url)| + user_map[name] = Chef::UserV1.load(name) + user_map + end + else + users + end + end + + def self.load(username) + # will default to the current API version (Chef::Authenticator::DEFAULT_SERVER_API_VERSION) + response = Chef::REST.new(Chef::Config[:chef_server_url]).get("users/#{username}") + Chef::UserV1.from_hash(response) + end + + # Gross. Transforms an API response in the form of: + # [ { "user" => { "username" => USERNAME }}, ...] + # into the form + # { "USERNAME" => "URI" } + def self.transform_list_response(response) + new_response = Hash.new + response.each do |u| + name = u['user']['username'] + new_response[name] = Chef::Config[:chef_server_url] + "/users/#{name}" + end + new_response + end + + private_class_method :transform_list_response + + end +end diff --git a/lib/chef/util/powershell/ps_credential.rb b/lib/chef/util/powershell/ps_credential.rb index 01f8c27b6c..3f4558a77c 100644 --- a/lib/chef/util/powershell/ps_credential.rb +++ b/lib/chef/util/powershell/ps_credential.rb @@ -29,6 +29,10 @@ class Chef::Util::Powershell "New-Object System.Management.Automation.PSCredential('#{@username}',('#{encrypt(@password)}' | ConvertTo-SecureString))" end + def to_s + to_psobject + end + private def encrypt(str) diff --git a/lib/chef/util/windows/net_group.rb b/lib/chef/util/windows/net_group.rb index 924bd392f9..2085747eb9 100644 --- a/lib/chef/util/windows/net_group.rb +++ b/lib/chef/util/windows/net_group.rb @@ -1,106 +1,85 @@ -# -# Author:: Doug MacEachern (<dougm@vmware.com>) -# Copyright:: Copyright (c) 2010 VMware, 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/util/windows' - -#wrapper around a subset of the NetGroup* APIs. -#nothing Chef specific, but not complete enough to be its own gem, so util for now. -class Chef::Util::Windows::NetGroup < Chef::Util::Windows - - private - - def pack_str(s) - [str_to_ptr(s)].pack('L') - end - - def modify_members(members, func) - buffer = 0.chr * (members.size * PTR_SIZE) - members.each_with_index do |member,offset| - buffer[offset*PTR_SIZE,PTR_SIZE] = pack_str(multi_to_wide(member)) - end - rc = func.call(nil, @name, 3, buffer, members.size) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - end - - public - - def initialize(groupname) - @name = multi_to_wide(groupname) - end - - def local_get_members - group_members = [] - handle = 0.chr * PTR_SIZE - rc = ERROR_MORE_DATA - - while rc == ERROR_MORE_DATA - ptr = 0.chr * PTR_SIZE - nread = 0.chr * PTR_SIZE - total = 0.chr * PTR_SIZE - - rc = NetLocalGroupGetMembers.call(nil, @name, 0, ptr, -1, - nread, total, handle) - if (rc == NERR_Success) || (rc == ERROR_MORE_DATA) - ptr = ptr.unpack('L')[0] - nread = nread.unpack('i')[0] - members = 0.chr * (nread * PTR_SIZE ) #nread * sizeof(LOCALGROUP_MEMBERS_INFO_0) - memcpy(members, ptr, members.size) - - # 1 pointer field in LOCALGROUP_MEMBERS_INFO_0, offset 0 is lgrmi0_sid - nread.times do |i| - sid_address = members[i * PTR_SIZE, PTR_SIZE].unpack('L')[0] - sid_ptr = FFI::Pointer.new(sid_address) - member_sid = Chef::ReservedNames::Win32::Security::SID.new(sid_ptr) - group_members << member_sid.to_s - end - NetApiBufferFree(ptr) - else - raise ArgumentError, get_last_error(rc) - end - end - group_members - end - - def local_add - rc = NetLocalGroupAdd.call(nil, 0, pack_str(@name), nil) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - end - - def local_set_members(members) - modify_members(members, NetLocalGroupSetMembers) - end - - def local_add_members(members) - modify_members(members, NetLocalGroupAddMembers) - end - - def local_delete_members(members) - modify_members(members, NetLocalGroupDelMembers) - end - - def local_delete - rc = NetLocalGroupDel.call(nil, @name) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) - end - end -end +#
+# Author:: Doug MacEachern (<dougm@vmware.com>)
+# Copyright:: Copyright (c) 2010 VMware, 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/util/windows'
+require 'chef/win32/net'
+
+#wrapper around a subset of the NetGroup* APIs.
+class Chef::Util::Windows::NetGroup
+
+ private
+
+ def groupname
+ @groupname
+ end
+
+ public
+
+ def initialize(groupname)
+ @groupname = groupname
+ end
+
+ def local_get_members
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_get_members(nil, groupname)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+
+ def local_add
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_add(nil, groupname)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+
+ def local_set_members(members)
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_set_members(nil, groupname, members)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+
+ def local_add_members(members)
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_add_members(nil, groupname, members)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+
+ def local_delete_members(members)
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_del_members(nil, groupname, members)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+
+ end
+
+ def local_delete
+ begin
+ Chef::ReservedNames::Win32::NetUser::net_local_group_del(nil, groupname)
+ rescue Chef::Exceptions::Win32NetAPIError => e
+ raise ArgumentError, e.msg
+ end
+ end
+end
diff --git a/lib/chef/util/windows/net_use.rb b/lib/chef/util/windows/net_use.rb index 62d7e169dc..b94576e702 100644 --- a/lib/chef/util/windows/net_use.rb +++ b/lib/chef/util/windows/net_use.rb @@ -21,61 +21,18 @@ #see also cmd.exe: net use /? require 'chef/util/windows' +require 'chef/win32/net' class Chef::Util::Windows::NetUse < Chef::Util::Windows - - private - - USE_NOFORCE = 0 - USE_FORCE = 1 - USE_LOTS_OF_FORCE = 2 #every windows API should support this flag - - USE_INFO_2 = [ - [:local, nil], - [:remote, nil], - [:password, nil], - [:status, 0], - [:asg_type, 0], - [:refcount, 0], - [:usecount, 0], - [:username, nil], - [:domainname, nil] - ] - - USE_INFO_2_TEMPLATE = - USE_INFO_2.collect { |field| field[1].class == Fixnum ? 'i' : 'L' }.join - - SIZEOF_USE_INFO_2 = #sizeof(USE_INFO_2) - USE_INFO_2.inject(0) do |sum, item| - sum + (item[1].class == Fixnum ? 4 : PTR_SIZE) - end - - def use_info_2(args) - USE_INFO_2.collect { |field| - args.include?(field[0]) ? args[field[0]] : field[1] - } - end - - def use_info_2_pack(use) - use.collect { |v| - v.class == Fixnum ? v : str_to_ptr(multi_to_wide(v)) - }.pack(USE_INFO_2_TEMPLATE) + def initialize(localname) + @use_name = localname end - def use_info_2_unpack(buffer) - use = Hash.new - USE_INFO_2.each_with_index do |field,offset| - use[field[0]] = field[1].class == Fixnum ? - dword_to_i(buffer, offset) : lpwstr_to_s(buffer, offset) + def to_ui2_struct(use_info) + use_info.inject({}) do |memo, (k,v)| + memo["ui2_#{k}".to_sym] = v + memo end - use - end - - public - - def initialize(localname) - @localname = localname - @name = multi_to_wide(localname) end def add(args) @@ -84,38 +41,45 @@ class Chef::Util::Windows::NetUse < Chef::Util::Windows args = Hash.new args[:remote] = remote end - args[:local] ||= @localname - use = use_info_2(args) - buffer = use_info_2_pack(use) - rc = NetUseAdd.call(nil, 2, buffer, nil) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + args[:local] ||= use_name + ui2_hash = to_ui2_struct(args) + + begin + Chef::ReservedNames::Win32::Net.net_use_add_l2(nil, ui2_hash) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end - def get_info - ptr = 0.chr * PTR_SIZE - rc = NetUseGetInfo.call(nil, @name, 2, ptr) - - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + def from_use_info_struct(ui2_hash) + ui2_hash.inject({}) do |memo, (k,v)| + memo[k.to_s.sub('ui2_', '').to_sym] = v + memo end + end - ptr = ptr.unpack('L')[0] - buffer = 0.chr * SIZEOF_USE_INFO_2 - memcpy(buffer, ptr, buffer.size) - NetApiBufferFree(ptr) - use_info_2_unpack(buffer) + def get_info + begin + ui2 = Chef::ReservedNames::Win32::Net.net_use_get_info_l2(nil, use_name) + from_use_info_struct(ui2) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e + end end def device get_info()[:remote] end - #XXX should we use some FORCE here? + def delete - rc = NetUseDel.call(nil, @name, USE_NOFORCE) - if rc != NERR_Success - raise ArgumentError, get_last_error(rc) + begin + Chef::ReservedNames::Win32::Net.net_use_del(nil, use_name, :use_noforce) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end + + def use_name + @use_name + end end diff --git a/lib/chef/util/windows/volume.rb b/lib/chef/util/windows/volume.rb index 08c3a53793..6e45594ba6 100644 --- a/lib/chef/util/windows/volume.rb +++ b/lib/chef/util/windows/volume.rb @@ -18,42 +18,42 @@ #simple wrapper around Volume APIs. might be possible with WMI, but possibly more complex. +require 'chef/win32/api/file' require 'chef/util/windows' -require 'windows/volume' class Chef::Util::Windows::Volume < Chef::Util::Windows - - private - include Windows::Volume - #XXX not defined in the current windows-pr release - DeleteVolumeMountPoint = - Windows::API.new('DeleteVolumeMountPoint', 'S', 'B') unless defined? DeleteVolumeMountPoint - - public + attr_reader :mount_point def initialize(name) name += "\\" unless name =~ /\\$/ #trailing slash required - @name = name + @mount_point = name end def device - buffer = 0.chr * 256 - if GetVolumeNameForVolumeMountPoint(@name, buffer, buffer.size) - return buffer[0,buffer.size].unpack("Z*")[0] - else - raise ArgumentError, get_last_error + begin + Chef::ReservedNames::Win32::File.get_volume_name_for_volume_mount_point(mount_point) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end def delete - unless DeleteVolumeMountPoint.call(@name) - raise ArgumentError, get_last_error + begin + Chef::ReservedNames::Win32::File.delete_volume_mount_point(mount_point) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end def add(args) - unless SetVolumeMountPoint(@name, args[:remote]) - raise ArgumentError, get_last_error + begin + Chef::ReservedNames::Win32::File.set_volume_mount_point(mount_point, args[:remote]) + rescue Chef::Exceptions::Win32APIError => e + raise ArgumentError, e end end + + def mount_point + @mount_point + end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 80fd422c55..faa61aee54 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -21,7 +21,7 @@ class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '12.4.0.rc.2' + VERSION = '12.5.0.current.0' end # @@ -29,6 +29,6 @@ end # # NOTE: DO NOT Use the Chef::Version class on Chef::VERSIONs. The # Chef::Version class is for _cookbooks_ only, and cannot handle -# pre-release chef-client versions like "10.14.0.rc.2". Please -# use Rubygem's Gem::Version class instead. +# pre-release versions like "10.14.0.rc.2". Please use Rubygem's +# Gem::Version class instead. # diff --git a/lib/chef/win32/api.rb b/lib/chef/win32/api.rb index e9d273808a..4786222bd4 100644 --- a/lib/chef/win32/api.rb +++ b/lib/chef/win32/api.rb @@ -188,6 +188,7 @@ class Chef host.typedef :pointer, :PCRYPTPROTECT_PROMPTSTRUCT # Pointer to a CRYPTOPROTECT_PROMPTSTRUCT. host.typedef :pointer, :PDATA_BLOB # Pointer to a DATA_BLOB. host.typedef :pointer, :PTSTR # A PWSTR if UNICODE is defined, a PSTR otherwise. + host.typedef :pointer, :PSID host.typedef :pointer, :PUCHAR # Pointer to a UCHAR. host.typedef :pointer, :PUHALF_PTR # Pointer to a UHALF_PTR. host.typedef :pointer, :PUINT # Pointer to a UINT. diff --git a/lib/chef/win32/api/file.rb b/lib/chef/win32/api/file.rb index 86b2b942c2..9ff1ad40d6 100644 --- a/lib/chef/win32/api/file.rb +++ b/lib/chef/win32/api/file.rb @@ -20,6 +20,7 @@ require 'chef/win32/api' require 'chef/win32/api/security' require 'chef/win32/api/system' +require 'chef/win32/unicode' class Chef module ReservedNames::Win32 @@ -450,6 +451,25 @@ BOOL WINAPI DeviceIoControl( =end safe_attach_function :DeviceIoControl, [:HANDLE, :DWORD, :LPVOID, :DWORD, :LPVOID, :DWORD, :LPDWORD, :pointer], :BOOL + +#BOOL WINAPI DeleteVolumeMountPoint( + #_In_ LPCTSTR lpszVolumeMountPoint +#); + safe_attach_function :DeleteVolumeMountPointW, [:LPCTSTR], :BOOL + +#BOOL WINAPI SetVolumeMountPoint( + #_In_ LPCTSTR lpszVolumeMountPoint, + #_In_ LPCTSTR lpszVolumeName +#); + safe_attach_function :SetVolumeMountPointW, [:LPCTSTR, :LPCTSTR], :BOOL + +#BOOL WINAPI GetVolumeNameForVolumeMountPoint( + #_In_ LPCTSTR lpszVolumeMountPoint, + #_Out_ LPTSTR lpszVolumeName, + #_In_ DWORD cchBufferLength +#); + safe_attach_function :GetVolumeNameForVolumeMountPointW, [:LPCTSTR, :LPTSTR, :DWORD], :BOOL + ############################################### # Helpers ############################################### diff --git a/lib/chef/win32/api/net.rb b/lib/chef/win32/api/net.rb index 72caf46628..b173987a05 100644 --- a/lib/chef/win32/api/net.rb +++ b/lib/chef/win32/api/net.rb @@ -17,6 +17,7 @@ # require 'chef/win32/api' +require 'chef/win32/unicode' class Chef module ReservedNames::Win32 @@ -40,6 +41,10 @@ class Chef UF_NORMAL_ACCOUNT = 0x000200 UF_DONT_EXPIRE_PASSWD = 0x010000 + USE_NOFORCE = 0 + USE_FORCE = 1 + USE_LOTS_OF_FORCE = 2 #every windows API should support this flag + NERR_Success = 0 NERR_InvalidComputer = 2351 NERR_NotPrimary = 2226 @@ -49,41 +54,13 @@ class Chef NERR_BadPassword = 2203 NERR_PasswordTooShort = 2245 NERR_UserNotFound = 2221 + NERR_GroupNotFound = 2220 ERROR_ACCESS_DENIED = 5 + ERROR_MORE_DATA = 234 ffi_lib "netapi32" - class USER_INFO_3 < FFI::Struct - layout :usri3_name, :LPWSTR, - :usri3_password, :LPWSTR, - :usri3_password_age, :DWORD, - :usri3_priv, :DWORD, - :usri3_home_dir, :LPWSTR, - :usri3_comment, :LPWSTR, - :usri3_flags, :DWORD, - :usri3_script_path, :LPWSTR, - :usri3_auth_flags, :DWORD, - :usri3_full_name, :LPWSTR, - :usri3_usr_comment, :LPWSTR, - :usri3_parms, :LPWSTR, - :usri3_workstations, :LPWSTR, - :usri3_last_logon, :DWORD, - :usri3_last_logoff, :DWORD, - :usri3_acct_expires, :DWORD, - :usri3_max_storage, :DWORD, - :usri3_units_per_week, :DWORD, - :usri3_logon_hours, :PBYTE, - :usri3_bad_pw_count, :DWORD, - :usri3_num_logons, :DWORD, - :usri3_logon_server, :LPWSTR, - :usri3_country_code, :DWORD, - :usri3_code_page, :DWORD, - :usri3_user_id, :DWORD, - :usri3_primary_group_id, :DWORD, - :usri3_profile, :LPWSTR, - :usri3_home_dir_drive, :LPWSTR, - :usri3_password_expired, :DWORD - + module StructHelpers def set(key, val) val = if val.is_a? String encoded = if val.encoding == Encoding::UTF_16LE @@ -115,6 +92,47 @@ class Chef end end + def as_ruby + members.inject({}) do |memo, key| + memo[key] = get(key) + memo + end + end + end + + + class USER_INFO_3 < FFI::Struct + include StructHelpers + layout :usri3_name, :LPWSTR, + :usri3_password, :LPWSTR, + :usri3_password_age, :DWORD, + :usri3_priv, :DWORD, + :usri3_home_dir, :LPWSTR, + :usri3_comment, :LPWSTR, + :usri3_flags, :DWORD, + :usri3_script_path, :LPWSTR, + :usri3_auth_flags, :DWORD, + :usri3_full_name, :LPWSTR, + :usri3_usr_comment, :LPWSTR, + :usri3_parms, :LPWSTR, + :usri3_workstations, :LPWSTR, + :usri3_last_logon, :DWORD, + :usri3_last_logoff, :DWORD, + :usri3_acct_expires, :DWORD, + :usri3_max_storage, :DWORD, + :usri3_units_per_week, :DWORD, + :usri3_logon_hours, :PBYTE, + :usri3_bad_pw_count, :DWORD, + :usri3_num_logons, :DWORD, + :usri3_logon_server, :LPWSTR, + :usri3_country_code, :DWORD, + :usri3_code_page, :DWORD, + :usri3_user_id, :DWORD, + :usri3_primary_group_id, :DWORD, + :usri3_profile, :LPWSTR, + :usri3_home_dir_drive, :LPWSTR, + :usri3_password_expired, :DWORD + def usri3_logon_hours val = self[:usri3_logon_hours] if !val.nil? && !val.null? @@ -123,19 +141,66 @@ class Chef nil end end + end - def as_ruby - members.inject({}) do |memo, key| - memo[key] = get(key) - memo - end - end + class LOCALGROUP_MEMBERS_INFO_0 < FFI::Struct + layout :lgrmi0_sid, :PSID end class LOCALGROUP_MEMBERS_INFO_3 < FFI::Struct layout :lgrmi3_domainandname, :LPWSTR end + class LOCALGROUP_INFO_0 < FFI::Struct + layout :lgrpi0_name, :LPWSTR + end + + class USE_INFO_2 < FFI::Struct + include StructHelpers + + layout :ui2_local, :LMSTR, + :ui2_remote, :LMSTR, + :ui2_password, :LMSTR, + :ui2_status, :DWORD, + :ui2_asg_type, :DWORD, + :ui2_refcount, :DWORD, + :ui2_usecount, :DWORD, + :ui2_username, :LPWSTR, + :ui2_domainname, :LMSTR + end + + +#NET_API_STATUS NetLocalGroupAdd( + #_In_ LPCWSTR servername, + #_In_ DWORD level, + #_In_ LPBYTE buf, + #_Out_ LPDWORD parm_err +#); + safe_attach_function :NetLocalGroupAdd, [ + :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD + ], :DWORD + +#NET_API_STATUS NetLocalGroupDel( + #_In_ LPCWSTR servername, + #_In_ LPCWSTR groupname +#); + safe_attach_function :NetLocalGroupDel, [:LPCWSTR, :LPCWSTR], :DWORD + +#NET_API_STATUS NetLocalGroupGetMembers( + #_In_ LPCWSTR servername, + #_In_ LPCWSTR localgroupname, + #_In_ DWORD level, + #_Out_ LPBYTE *bufptr, + #_In_ DWORD prefmaxlen, + #_Out_ LPDWORD entriesread, + #_Out_ LPDWORD totalentries, + #_Inout_ PDWORD_PTR resumehandle +#); + safe_attach_function :NetLocalGroupGetMembers, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD, + :LPDWORD, :LPDWORD, :PDWORD_PTR + ], :DWORD + # NET_API_STATUS NetUserEnum( # _In_ LPCWSTR servername, # _In_ DWORD level, @@ -146,12 +211,15 @@ class Chef # _Out_ LPDWORD totalentries, # _Inout_ LPDWORD resume_handle # ); - safe_attach_function :NetUserEnum, [ :LPCWSTR, :DWORD, :DWORD, :LPBYTE, :DWORD, :LPDWORD, :LPDWORD, :LPDWORD ], :DWORD + safe_attach_function :NetUserEnum, [ + :LPCWSTR, :DWORD, :DWORD, :LPBYTE, + :DWORD, :LPDWORD, :LPDWORD, :LPDWORD + ], :DWORD # NET_API_STATUS NetApiBufferFree( # _In_ LPVOID Buffer # ); - safe_attach_function :NetApiBufferFree, [ :LPVOID ], :DWORD + safe_attach_function :NetApiBufferFree, [:LPVOID], :DWORD #NET_API_STATUS NetUserAdd( #_In_ LMSTR servername, @@ -159,7 +227,9 @@ class Chef #_In_ LPBYTE buf, #_Out_ LPDWORD parm_err #); - safe_attach_function :NetUserAdd, [:LMSTR, :DWORD, :LPBYTE, :LPDWORD ], :DWORD + safe_attach_function :NetUserAdd, [ + :LMSTR, :DWORD, :LPBYTE, :LPDWORD + ], :DWORD #NET_API_STATUS NetLocalGroupAddMembers( # _In_ LPCWSTR servername, @@ -168,7 +238,31 @@ class Chef # _In_ LPBYTE buf, # _In_ DWORD totalentries #); - safe_attach_function :NetLocalGroupAddMembers, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD ], :DWORD + safe_attach_function :NetLocalGroupAddMembers, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD + ], :DWORD + +#NET_API_STATUS NetLocalGroupSetMembers( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR groupname, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _In_ DWORD totalentries +#); + safe_attach_function :NetLocalGroupSetMembers, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD + ], :DWORD + +#NET_API_STATUS NetLocalGroupDelMembers( +# _In_ LPCWSTR servername, +# _In_ LPCWSTR groupname, +# _In_ DWORD level, +# _In_ LPBYTE buf, +# _In_ DWORD totalentries +#); + safe_attach_function :NetLocalGroupDelMembers, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :DWORD + ], :DWORD #NET_API_STATUS NetUserGetInfo( # _In_ LPCWSTR servername, @@ -176,7 +270,9 @@ class Chef # _In_ DWORD level, # _Out_ LPBYTE *bufptr #); - safe_attach_function :NetUserGetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE], :DWORD + safe_attach_function :NetUserGetInfo, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE + ], :DWORD #NET_API_STATUS NetApiBufferFree( # _In_ LPVOID Buffer @@ -190,7 +286,9 @@ class Chef # _In_ LPBYTE buf, # _Out_ LPDWORD parm_err #); - safe_attach_function :NetUserSetInfo, [:LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD], :DWORD + safe_attach_function :NetUserSetInfo, [ + :LPCWSTR, :LPCWSTR, :DWORD, :LPBYTE, :LPDWORD + ], :DWORD #NET_API_STATUS NetUserDel( # _In_ LPCWSTR servername, @@ -198,6 +296,28 @@ class Chef #); safe_attach_function :NetUserDel, [:LPCWSTR, :LPCWSTR], :DWORD +#NET_API_STATUS NetUseDel( + #_In_ LMSTR UncServerName, + #_In_ LMSTR UseName, + #_In_ DWORD ForceCond +#); + safe_attach_function :NetUseDel, [:LMSTR, :LMSTR, :DWORD], :DWORD + +#NET_API_STATUS NetUseGetInfo( + #_In_ LMSTR UncServerName, + #_In_ LMSTR UseName, + #_In_ DWORD Level, + #_Out_ LPBYTE *BufPtr +#); + safe_attach_function :NetUseGetInfo, [:LMSTR, :LMSTR, :DWORD, :pointer], :DWORD + +#NET_API_STATUS NetUseAdd( + #_In_ LMSTR UncServerName, + #_In_ DWORD Level, + #_In_ LPBYTE Buf, + #_Out_ LPDWORD ParmError +#); + safe_attach_function :NetUseAdd, [:LMSTR, :DWORD, :LPBYTE, :LPDWORD], :DWORD end end end diff --git a/lib/chef/win32/api/registry.rb b/lib/chef/win32/api/registry.rb new file mode 100644 index 0000000000..45b91d7d32 --- /dev/null +++ b/lib/chef/win32/api/registry.rb @@ -0,0 +1,45 @@ +# +# Author:: Salim Alam (<salam@chef.io>) +# Copyright:: Copyright 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/win32/api' + +class Chef + module ReservedNames::Win32 + module API + module Registry + extend Chef::ReservedNames::Win32::API + + ############################################### + # Win32 API Bindings + ############################################### + + ffi_lib 'advapi32' + + # LONG WINAPI RegDeleteKeyEx( + # _In_ HKEY hKey, + # _In_ LPCTSTR lpSubKey, + # _In_ REGSAM samDesired, + # _Reserved_ DWORD Reserved + # ); + safe_attach_function :RegDeleteKeyExW, [ :HKEY, :LPCTSTR, :LONG, :DWORD ], :LONG + safe_attach_function :RegDeleteKeyExA, [ :HKEY, :LPCTSTR, :LONG, :DWORD ], :LONG + + end + end + end +end
\ No newline at end of file diff --git a/lib/chef/win32/api/system.rb b/lib/chef/win32/api/system.rb index d57699acb4..a485f89708 100644 --- a/lib/chef/win32/api/system.rb +++ b/lib/chef/win32/api/system.rb @@ -187,6 +187,29 @@ int WINAPI GetSystemMetrics( safe_attach_function :GetSystemMetrics, [:int], :int =begin +UINT WINAPI GetSystemWow64Directory( + _Out_ LPTSTR lpBuffer, + _In_ UINT uSize +); +=end + safe_attach_function :GetSystemWow64DirectoryW, [:LPTSTR, :UINT], :UINT + safe_attach_function :GetSystemWow64DirectoryA, [:LPTSTR, :UINT], :UINT + +=begin +BOOL WINAPI Wow64DisableWow64FsRedirection( + _Out_ PVOID *OldValue +); +=end + safe_attach_function :Wow64DisableWow64FsRedirection, [:PVOID], :BOOL + +=begin +BOOL WINAPI Wow64RevertWow64FsRedirection( + _In_ PVOID OldValue +); +=end + safe_attach_function :Wow64RevertWow64FsRedirection, [:PVOID], :BOOL + +=begin LRESULT WINAPI SendMessageTimeout( _In_ HWND hWnd, _In_ UINT Msg, diff --git a/lib/chef/win32/api/unicode.rb b/lib/chef/win32/api/unicode.rb index 2e3a599f0a..2a9166aa99 100644 --- a/lib/chef/win32/api/unicode.rb +++ b/lib/chef/win32/api/unicode.rb @@ -129,49 +129,6 @@ int WideCharToMultiByte( =end safe_attach_function :WideCharToMultiByte, [:UINT, :DWORD, :LPCWSTR, :int, :LPSTR, :int, :LPCSTR, :LPBOOL], :int - ############################################### - # Helpers - ############################################### - - def utf8_to_wide(ustring) - # ensure it is actually UTF-8 - # Ruby likes to mark binary data as ASCII-8BIT - ustring = (ustring + "").force_encoding('UTF-8') if ustring.respond_to?(:force_encoding) && ustring.encoding.name != "UTF-8" - - # ensure we have the double-null termination Windows Wide likes - ustring = ustring + "\000\000" if ustring.length == 0 or ustring[-1].chr != "\000" - - # encode it all as UTF-16LE AKA Windows Wide Character AKA Windows Unicode - ustring = begin - if ustring.respond_to?(:encode) - ustring.encode('UTF-16LE') - else - require 'iconv' - Iconv.conv("UTF-16LE", "UTF-8", ustring) - end - end - ustring - end - - def wide_to_utf8(wstring) - # ensure it is actually UTF-16LE - # Ruby likes to mark binary data as ASCII-8BIT - wstring = wstring.force_encoding('UTF-16LE') if wstring.respond_to?(:force_encoding) - - # encode it all as UTF-8 - wstring = begin - if wstring.respond_to?(:encode) - wstring.encode('UTF-8') - else - require 'iconv' - Iconv.conv("UTF-8", "UTF-16LE", wstring) - end - end - # remove trailing CRLF and NULL characters - wstring.strip! - wstring - end - end end end diff --git a/lib/chef/win32/crypto.rb b/lib/chef/win32/crypto.rb index 79cf51b002..aa20c2dfd4 100644 --- a/lib/chef/win32/crypto.rb +++ b/lib/chef/win32/crypto.rb @@ -19,6 +19,7 @@ require 'chef/win32/error'
require 'chef/win32/api/memory'
require 'chef/win32/api/crypto'
+require 'chef/win32/unicode'
require 'digest'
class Chef
diff --git a/lib/chef/win32/file.rb b/lib/chef/win32/file.rb index e6640caa3c..700ddb24d3 100644 --- a/lib/chef/win32/file.rb +++ b/lib/chef/win32/file.rb @@ -17,9 +17,11 @@ # limitations under the License. # +require 'chef/mixin/wide_string' require 'chef/win32/api/file' require 'chef/win32/api/security' require 'chef/win32/error' +require 'chef/win32/unicode' class Chef module ReservedNames::Win32 @@ -27,6 +29,9 @@ class Chef include Chef::ReservedNames::Win32::API::File extend Chef::ReservedNames::Win32::API::File + include Chef::Mixin::WideString + extend Chef::Mixin::WideString + # Creates a symbolic link called +new_name+ for the file or directory # +old_name+. # @@ -157,9 +162,9 @@ class Chef def self.file_access_check(path, desired_access) security_descriptor = Chef::ReservedNames::Win32::Security.get_file_security(path) - token_rights = Chef::ReservedNames::Win32::Security::TOKEN_IMPERSONATE | + token_rights = Chef::ReservedNames::Win32::Security::TOKEN_IMPERSONATE | Chef::ReservedNames::Win32::Security::TOKEN_QUERY | - Chef::ReservedNames::Win32::Security::TOKEN_DUPLICATE | + Chef::ReservedNames::Win32::Security::TOKEN_DUPLICATE | Chef::ReservedNames::Win32::Security::STANDARD_RIGHTS_READ token = Chef::ReservedNames::Win32::Security.open_process_token( Chef::ReservedNames::Win32::Process.get_current_process, @@ -172,10 +177,30 @@ class Chef mapping[:GenericExecute] = Chef::ReservedNames::Win32::Security::FILE_GENERIC_EXECUTE mapping[:GenericAll] = Chef::ReservedNames::Win32::Security::FILE_ALL_ACCESS - Chef::ReservedNames::Win32::Security.access_check(security_descriptor, duplicate_token, + Chef::ReservedNames::Win32::Security.access_check(security_descriptor, duplicate_token, desired_access, mapping) end + def self.delete_volume_mount_point(mount_point) + unless DeleteVolumeMountPointW(wstring(mount_point)) + Chef::ReservedNames::Win32::Error.raise! + end + end + + def self.set_volume_mount_point(mount_point, name) + unless SetVolumeMountPointW(wstring(mount_point), wstring(name)) + Chef::ReservedNames::Win32::Error.raise! + end + end + + def self.get_volume_name_for_volume_mount_point(mount_point) + buffer = FFI::MemoryPointer.new(2, 128) + unless GetVolumeNameForVolumeMountPointW(wstring(mount_point), buffer, buffer.size/buffer.type_size) + Chef::ReservedNames::Win32::Error.raise! + end + buffer.read_wstring + end + # ::File compat class << self alias :stat :info diff --git a/lib/chef/win32/mutex.rb b/lib/chef/win32/mutex.rb index 0b7d99f111..f4755e9019 100644 --- a/lib/chef/win32/mutex.rb +++ b/lib/chef/win32/mutex.rb @@ -17,6 +17,7 @@ # require 'chef/win32/api/synchronization' +require 'chef/win32/unicode' class Chef module ReservedNames::Win32 @@ -113,5 +114,3 @@ if the mutex is attempted to be acquired by other threads.") end end end - - diff --git a/lib/chef/win32/net.rb b/lib/chef/win32/net.rb index 1349091eb9..59f29c4d1b 100644 --- a/lib/chef/win32/net.rb +++ b/lib/chef/win32/net.rb @@ -18,11 +18,11 @@ require 'chef/win32/api/net' require 'chef/win32/error' -require 'chef/mixin/wstring' +require 'chef/mixin/wide_string' class Chef module ReservedNames::Win32 - class NetUser + class Net include Chef::ReservedNames::Win32::API::Error extend Chef::ReservedNames::Win32::API::Error @@ -91,19 +91,72 @@ The password is shorter than required. (The password could also be too long, be too recent in its change history, not have enough unique characters, or not meet another password policy requirement.) END + when NERR_GroupNotFound + "The group name could not be found." when ERROR_ACCESS_DENIED "The user does not have access to the requested information." else "Received unknown error code (#{code})" end - formatted_message = "" - formatted_message << "---- Begin Win32 API output ----\n" - formatted_message << "Net Api Error Code: #{code}\n" - formatted_message << "Net Api Error Message: #{msg}\n" - formatted_message << "---- End Win32 API output ----\n" + raise Chef::Exceptions::Win32NetAPIError.new(msg, code) + end + + def self.net_local_group_add(server_name, group_name) + server_name = wstring(server_name) + group_name = wstring(group_name) + + buf = LOCALGROUP_INFO_0.new + buf[:lgrpi0_name] = FFI::MemoryPointer.from_string(group_name) + + rc = NetLocalGroupAdd(server_name, 0, buf, nil) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_del(server_name, group_name) + server_name = wstring(server_name) + group_name = wstring(group_name) - raise Chef::Exceptions::Win32APIError, msg + "\n" + formatted_message + rc = NetLocalGroupDel(server_name, group_name) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_get_members(server_name, group_name) + server_name = wstring(server_name) + group_name = wstring(group_name) + + buf = FFI::MemoryPointer.new(:pointer) + entries_read_ptr = FFI::MemoryPointer.new(:long) + total_read_ptr = FFI::MemoryPointer.new(:long) + resume_handle_ptr = FFI::MemoryPointer.new(:pointer) + + rc = ERROR_MORE_DATA + group_members = [] + while rc == ERROR_MORE_DATA + rc = NetLocalGroupGetMembers( + server_name, group_name, 0, buf, -1, + entries_read_ptr, total_read_ptr, resume_handle_ptr + ) + + nread = entries_read_ptr.read_long + nread.times do |i| + member = LOCALGROUP_MEMBERS_INFO_0.new(buf.read_pointer + + (i * LOCALGROUP_MEMBERS_INFO_0.size)) + member_sid = Chef::ReservedNames::Win32::Security::SID.new(member[:lgrmi0_sid]) + group_members << member_sid.to_s + end + NetApiBufferFree(buf.read_pointer) + end + + if rc != NERR_Success + net_api_error!(rc) + end + + group_members end def self.net_user_add_l3(server_name, args) @@ -185,6 +238,107 @@ END end end + def self.members_to_lgrmi3(members) + buf = FFI::MemoryPointer.new(LOCALGROUP_MEMBERS_INFO_3, members.size) + members.size.times.collect do |i| + member_info = LOCALGROUP_MEMBERS_INFO_3.new( + buf + i * LOCALGROUP_MEMBERS_INFO_3.size) + member_info[:lgrmi3_domainandname] = FFI::MemoryPointer.from_string(wstring(members[i])) + member_info + end + end + + def self.net_local_group_add_members(server_name, group_name, members) + server_name = wstring(server_name) + group_name = wstring(group_name) + + lgrmi3s = members_to_lgrmi3(members) + rc = NetLocalGroupAddMembers( + server_name, group_name, 3, lgrmi3s[0], members.size) + + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_set_members(server_name, group_name, members) + server_name = wstring(server_name) + group_name = wstring(group_name) + + lgrmi3s = members_to_lgrmi3(members) + rc = NetLocalGroupSetMembers( + server_name, group_name, 3, lgrmi3s[0], members.size) + + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_local_group_del_members(server_name, group_name, members) + server_name = wstring(server_name) + group_name = wstring(group_name) + + lgrmi3s = members_to_lgrmi3(members) + rc = NetLocalGroupDelMembers( + server_name, group_name, 3, lgrmi3s[0], members.size) + + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_use_del(server_name, use_name, force=:use_noforce) + server_name = wstring(server_name) + use_name = wstring(use_name) + force_const = case force + when :use_noforce + USE_NOFORCE + when :use_force + USE_FORCE + when :use_lots_of_force + USE_LOTS_OF_FORCE + else + raise ArgumentError, "force must be one of [:use_noforce, :use_force, or :use_lots_of_force]" + end + + rc = NetUseDel(server_name, use_name, force_const) + if rc != NERR_Success + net_api_error!(rc) + end + end + + def self.net_use_get_info_l2(server_name, use_name) + server_name = wstring(server_name) + use_name = wstring(use_name) + ui2_p = FFI::MemoryPointer.new(:pointer) + + rc = NetUseGetInfo(server_name, use_name, 2, ui2_p) + if rc != NERR_Success + net_api_error!(rc) + end + + ui2 = USE_INFO_2.new(ui2_p.read_pointer).as_ruby + NetApiBufferFree(ui2_p.read_pointer) + + ui2 + end + + def self.net_use_add_l2(server_name, ui2_hash) + server_name = wstring(server_name) + group_name = wstring(group_name) + + buf = USE_INFO_2.new + + ui2_hash.each do |(k,v)| + buf.set(k,v) + end + + rc = NetUseAdd(server_name, 2, buf, nil) + if rc != NERR_Success + net_api_error!(rc) + end + end end + NetUser = Net # For backwards compatibility end end diff --git a/lib/chef/win32/process.rb b/lib/chef/win32/process.rb index 2df39bb918..767d4f390c 100644 --- a/lib/chef/win32/process.rb +++ b/lib/chef/win32/process.rb @@ -69,6 +69,19 @@ class Chef result end + def self.is_wow64_process + is_64_bit_process_result = FFI::MemoryPointer.new(:int) + + # The return value of IsWow64Process is nonzero value if the API call succeeds. + # The result data are returned in the last parameter, not the return value. + call_succeeded = IsWow64Process(GetCurrentProcess(), is_64_bit_process_result) + + # The result is nonzero if IsWow64Process's calling process, in the case here + # this process, is running under WOW64, i.e. the result is nonzero if this + # process is 32-bit (aka :i386). + (call_succeeded != 0) && (is_64_bit_process_result.get_int(0) != 0) + end + # Must have PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION rights, # AND the PROCESS_VM_READ right def self.get_process_memory_info(handle) diff --git a/lib/chef/win32/registry.rb b/lib/chef/win32/registry.rb index 18f12d26b8..b25ce7937e 100644 --- a/lib/chef/win32/registry.rb +++ b/lib/chef/win32/registry.rb @@ -17,8 +17,11 @@ # limitations under the License. # require 'chef/reserved_names' +require 'chef/win32/api' +require 'chef/mixin/wide_string' if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + require 'chef/win32/api/registry' require 'win32/registry' require 'win32/api' end @@ -27,6 +30,14 @@ class Chef class Win32 class Registry + if RUBY_PLATFORM =~ /mswin|mingw32|windows/ + include Chef::ReservedNames::Win32::API::Registry + extend Chef::ReservedNames::Win32::API::Registry + end + + include Chef::Mixin::WideString + extend Chef::Mixin::WideString + attr_accessor :run_context attr_accessor :architecture @@ -142,9 +153,8 @@ class Chef #Using the 'RegDeleteKeyEx' Windows API that correctly supports WOW64 systems (Win2003) #instead of the 'RegDeleteKey' def delete_key_ex(hive, key) - regDeleteKeyEx = ::Win32::API.new('RegDeleteKeyEx', 'LPLL', 'L', 'advapi32') hive_num = hive.hkey - (1 << 32) - regDeleteKeyEx.call(hive_num, key, ::Win32::Registry::KEY_WRITE | registry_system_architecture, 0) + RegDeleteKeyExW(hive_num, wstring(key), ::Win32::Registry::KEY_WRITE | registry_system_architecture, 0) == 0 end def key_exists?(key_path) @@ -203,7 +213,7 @@ class Chef key_exists!(key_path) hive, key = get_hive_and_key(key_path) hive.open(key, ::Win32::Registry::KEY_READ | registry_system_architecture) do |reg| - return true if reg.any? {|val| val == value[:name] } + return true if reg.any? {|val| safely_downcase(val) == safely_downcase(value[:name]) } end return false end @@ -213,7 +223,7 @@ class Chef hive, key = get_hive_and_key(key_path) hive.open(key, ::Win32::Registry::KEY_READ | registry_system_architecture) do |reg| reg.each do |val_name, val_type, val_data| - if val_name == value[:name] && + if safely_downcase(val_name) == safely_downcase(value[:name]) && val_type == get_type_from_name(value[:type]) && val_data == value[:data] return true @@ -289,6 +299,14 @@ class Chef private + + def safely_downcase(val) + if val.is_a? String + return val.downcase + end + return val + end + def node run_context && run_context.node end diff --git a/lib/chef/win32/security.rb b/lib/chef/win32/security.rb index 5c83180bc0..bc80517d80 100644 --- a/lib/chef/win32/security.rb +++ b/lib/chef/win32/security.rb @@ -22,7 +22,7 @@ require 'chef/win32/memory' require 'chef/win32/process' require 'chef/win32/unicode' require 'chef/win32/security/token' -require 'chef/mixin/wstring' +require 'chef/mixin/wide_string' class Chef module ReservedNames::Win32 diff --git a/lib/chef/win32/security/token.rb b/lib/chef/win32/security/token.rb index 9e494a73b9..8d4e54ad8c 100644 --- a/lib/chef/win32/security/token.rb +++ b/lib/chef/win32/security/token.rb @@ -18,7 +18,7 @@ require 'chef/win32/security' require 'chef/win32/api/security' - +require 'chef/win32/unicode' require 'ffi' class Chef diff --git a/lib/chef/win32/system.rb b/lib/chef/win32/system.rb new file mode 100755 index 0000000000..cdd063f174 --- /dev/null +++ b/lib/chef/win32/system.rb @@ -0,0 +1,62 @@ +# +# Author:: Salim Alam (<salam@chef.io>) +# Copyright:: Copyright 2015 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 'chef/win32/api/system' +require 'chef/win32/error' +require 'ffi' + +class Chef + module ReservedNames::Win32 + class System + include Chef::ReservedNames::Win32::API::System + extend Chef::ReservedNames::Win32::API::System + + def self.get_system_wow64_directory + ptr = FFI::MemoryPointer.new(:char, 255, true) + succeeded = GetSystemWow64DirectoryA(ptr, 255) + + if succeeded == 0 + raise Win32APIError, "Failed to get Wow64 system directory" + end + + ptr.read_string.strip + end + + def self.wow64_disable_wow64_fs_redirection + original_redirection_state = FFI::MemoryPointer.new(:pointer) + + succeeded = Wow64DisableWow64FsRedirection(original_redirection_state) + + if succeeded == 0 + raise Win32APIError, "Failed to disable Wow64 file redirection" + end + + original_redirection_state + end + + def self.wow64_revert_wow64_fs_redirection(original_redirection_state) + succeeded = Wow64RevertWow64FsRedirection(original_redirection_state) + + if succeeded == 0 + raise Win32APIError, "Failed to revert Wow64 file redirection" + end + end + + end + end +end diff --git a/lib/chef/win32/unicode.rb b/lib/chef/win32/unicode.rb index e7399d5255..562301a040 100644 --- a/lib/chef/win32/unicode.rb +++ b/lib/chef/win32/unicode.rb @@ -17,6 +17,7 @@ # limitations under the License. # +require 'chef/mixin/wide_string' require 'chef/win32/api/unicode' class Chef @@ -30,6 +31,8 @@ end module FFI class Pointer + include Chef::Mixin::WideString + def read_wstring(num_wchars = nil) if num_wchars.nil? # Find the length of the string @@ -43,13 +46,42 @@ module FFI num_wchars = length end - Chef::ReservedNames::Win32::Unicode.wide_to_utf8(self.get_bytes(0, num_wchars*2)) + wide_to_utf8(self.get_bytes(0, num_wchars*2)) end end end class String + include Chef::Mixin::WideString + def to_wstring - Chef::ReservedNames::Win32::Unicode.utf8_to_wide(self) + utf8_to_wide(self) end end + +# https://bugs.ruby-lang.org/issues/11439 +if RUBY_VERSION =~ /^2\.1/ + module Win32 + class Registry + def write(name, type, data) + case type + when REG_SZ, REG_EXPAND_SZ + data = data.to_s.encode(WCHAR) + WCHAR_NUL + when REG_MULTI_SZ + data = data.to_a.map {|s| s.encode(WCHAR)}.join(WCHAR_NUL) << WCHAR_NUL << WCHAR_NUL + when REG_BINARY + data = data.to_s + when REG_DWORD + data = API.packdw(data.to_i) + when REG_DWORD_BIG_ENDIAN + data = [data.to_i].pack('N') + when REG_QWORD + data = API.packqw(data.to_i) + else + raise TypeError, "Unsupported type #{type}" + end + API.SetValue(@hkey, name, type, data, data.bytesize) + end + end + end +end
\ No newline at end of file diff --git a/lib/chef/win32/version.rb b/lib/chef/win32/version.rb index 17c27e4780..6a7a65b01b 100644 --- a/lib/chef/win32/version.rb +++ b/lib/chef/win32/version.rb @@ -122,10 +122,6 @@ class Chef # WMI always returns the truth. See article at # http://msdn.microsoft.com/en-us/library/windows/desktop/ms724439(v=vs.85).aspx - # CHEF-4888: Work around ruby #2618, expected to be fixed in Ruby 2.1.0 - # https://github.com/ruby/ruby/commit/588504b20f5cc880ad51827b93e571e32446e5db - # https://github.com/ruby/ruby/commit/27ed294c7134c0de582007af3c915a635a6506cd - wmi = WmiLite::Wmi.new os_info = wmi.first_of('Win32_OperatingSystem') os_version = os_info['version'] diff --git a/lib/chef/workstation_config_loader.rb b/lib/chef/workstation_config_loader.rb index 2454c9cccf..8398c5d616 100644 --- a/lib/chef/workstation_config_loader.rb +++ b/lib/chef/workstation_config_loader.rb @@ -1,5 +1,5 @@ # -# Author:: Daniel DeLeo (<dan@getchef.com>) +# Author:: Claire McQuin (<claire@chef.io>) # Copyright:: Copyright (c) 2014 Chef Software, Inc. # License:: Apache License, Version 2.0 # @@ -16,163 +16,8 @@ # limitations under the License. # -require 'chef/config_fetcher' -require 'chef/config' -require 'chef/null_logger' -require 'chef/util/path_helper' +require 'chef-config/workstation_config_loader' class Chef - - class WorkstationConfigLoader - - # Path to a config file requested by user, (e.g., via command line option). Can be nil - attr_accessor :explicit_config_file - - # TODO: initialize this with a logger for Chef and Knife - def initialize(explicit_config_file, logger=nil) - @explicit_config_file = explicit_config_file - @config_location = nil - @logger = logger || NullLogger.new - end - - def no_config_found? - config_location.nil? - end - - def config_location - @config_location ||= (explicit_config_file || locate_local_config) - end - - def chef_config_dir - if @chef_config_dir.nil? - @chef_config_dir = false - full_path = working_directory.split(File::SEPARATOR) - (full_path.length - 1).downto(0) do |i| - candidate_directory = File.join(full_path[0..i] + [".chef" ]) - if File.exist?(candidate_directory) && File.directory?(candidate_directory) - @chef_config_dir = candidate_directory - break - end - end - end - @chef_config_dir - end - - def load - # Ignore it if there's no explicit_config_file and can't find one at a - # default path. - return false if config_location.nil? - - if explicit_config_file && !path_exists?(config_location) - raise Exceptions::ConfigurationError, "Specified config file #{config_location} does not exist" - end - - # Have to set Chef::Config.config_file b/c other config is derived from it. - Chef::Config.config_file = config_location - read_config(IO.read(config_location), config_location) - end - - # (Private API, public for test purposes) - def env - ENV - end - - # (Private API, public for test purposes) - def path_exists?(path) - Pathname.new(path).expand_path.exist? - end - - private - - def have_config?(path) - if path_exists?(path) - logger.info("Using config at #{path}") - true - else - logger.debug("Config not found at #{path}, trying next option") - false - end - end - - def locate_local_config - candidate_configs = [] - - # Look for $KNIFE_HOME/knife.rb (allow multiple knives config on same machine) - if env['KNIFE_HOME'] - candidate_configs << File.join(env['KNIFE_HOME'], 'config.rb') - candidate_configs << File.join(env['KNIFE_HOME'], 'knife.rb') - end - # Look for $PWD/knife.rb - if Dir.pwd - candidate_configs << File.join(Dir.pwd, 'config.rb') - candidate_configs << File.join(Dir.pwd, 'knife.rb') - end - # Look for $UPWARD/.chef/knife.rb - if chef_config_dir - candidate_configs << File.join(chef_config_dir, 'config.rb') - candidate_configs << File.join(chef_config_dir, 'knife.rb') - end - # Look for $HOME/.chef/knife.rb - Chef::Util::PathHelper.home('.chef') do |dot_chef_dir| - candidate_configs << File.join(dot_chef_dir, 'config.rb') - candidate_configs << File.join(dot_chef_dir, 'knife.rb') - end - - candidate_configs.find do | candidate_config | - have_config?(candidate_config) - end - end - - def working_directory - a = if Chef::Platform.windows? - env['CD'] - else - env['PWD'] - end || Dir.pwd - - a - end - - def read_config(config_content, config_file_path) - Chef::Config.from_string(config_content, config_file_path) - rescue SignalException - raise - rescue SyntaxError => e - message = "" - message << "You have invalid ruby syntax in your config file #{config_file_path}\n\n" - message << "#{e.class.name}: #{e.message}\n" - if file_line = e.message[/#{Regexp.escape(config_file_path)}:[\d]+/] - line = file_line[/:([\d]+)$/, 1].to_i - message << highlight_config_error(config_file_path, line) - end - raise Exceptions::ConfigurationError, message - rescue Exception => e - message = "You have an error in your config file #{config_file_path}\n\n" - message << "#{e.class.name}: #{e.message}\n" - filtered_trace = e.backtrace.grep(/#{Regexp.escape(config_file_path)}/) - filtered_trace.each {|bt_line| message << " " << bt_line << "\n" } - if !filtered_trace.empty? - line_nr = filtered_trace.first[/#{Regexp.escape(config_file_path)}:([\d]+)/, 1] - message << highlight_config_error(config_file_path, line_nr.to_i) - end - raise Exceptions::ConfigurationError, message - end - - - def highlight_config_error(file, line) - config_file_lines = [] - IO.readlines(file).each_with_index {|l, i| config_file_lines << "#{(i + 1).to_s.rjust(3)}: #{l.chomp}"} - if line == 1 - lines = config_file_lines[0..3] - else - lines = config_file_lines[Range.new(line - 2, line)] - end - "Relevant file content:\n" + lines.join("\n") + "\n" - end - - def logger - @logger - end - - end + WorkstationConfigLoader = ChefConfig::WorkstationConfigLoader end diff --git a/spec/data/cookbooks/openldap/templates/default/helpers.erb b/spec/data/cookbooks/openldap/templates/default/helpers.erb new file mode 100644 index 0000000000..b973a5287c --- /dev/null +++ b/spec/data/cookbooks/openldap/templates/default/helpers.erb @@ -0,0 +1,14 @@ +<%= @cookbook_name %> +<%= @recipe_name %> +<%= @recipe_line_string %> +<%= @recipe_path %> +<%= @recipe_line %> +<%= @template_name %> +<%= @template_path %> +<%= cookbook_name %> +<%= recipe_name %> +<%= recipe_line_string %> +<%= recipe_path %> +<%= recipe_line %> +<%= template_name %> +<%= template_path %> diff --git a/spec/data/dsc_lcm.pfx b/spec/data/dsc_lcm.pfx Binary files differnew file mode 100644 index 0000000000..3912ed3753 --- /dev/null +++ b/spec/data/dsc_lcm.pfx diff --git a/spec/data/run_context/cookbooks/include/recipes/default.rb b/spec/data/run_context/cookbooks/include/recipes/default.rb new file mode 100644 index 0000000000..8d22994252 --- /dev/null +++ b/spec/data/run_context/cookbooks/include/recipes/default.rb @@ -0,0 +1,24 @@ +module ::RanResources + def self.resources + @resources ||= [] + end +end +class RunContextCustomResource < Chef::Resource + action :create do + ruby_block '4' do + block { RanResources.resources << 4 } + end + recipe_eval do + ruby_block '1' do + block { RanResources.resources << 1 } + end + include_recipe 'include::includee' + ruby_block '3' do + block { RanResources.resources << 3 } + end + end + ruby_block '5' do + block { RanResources.resources << 5 } + end + end +end diff --git a/spec/data/run_context/cookbooks/include/recipes/includee.rb b/spec/data/run_context/cookbooks/include/recipes/includee.rb new file mode 100644 index 0000000000..87bb7f114e --- /dev/null +++ b/spec/data/run_context/cookbooks/include/recipes/includee.rb @@ -0,0 +1,3 @@ +ruby_block '2' do + block { RanResources.resources << 2 } +end diff --git a/spec/data/trusted_certs/opscode.pem b/spec/data/trusted_certs/opscode.pem index 37a3dd1ef2..e421a4e6e9 100644 --- a/spec/data/trusted_certs/opscode.pem +++ b/spec/data/trusted_certs/opscode.pem @@ -1,60 +1,57 @@ -----BEGIN CERTIFICATE----- -MIIFrDCCBJSgAwIBAgIQB1O/fCb6cEytJ4BP3HTbCTANBgkqhkiG9w0BAQUFADBI -MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMSIwIAYDVQQDExlE -aWdpQ2VydCBTZWN1cmUgU2VydmVyIENBMB4XDTE0MDYxMDAwMDAwMFoXDTE1MDcw -MTEyMDAwMFowaTELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAO -BgNVBAcTB1NlYXR0bGUxGzAZBgNVBAoTEkNoZWYgU29mdHdhcmUsIEluYzEWMBQG -A1UEAwwNKi5vcHNjb2RlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAMm+rf2RcPGBlZoM+hI4BxlaHbdRg1GZJ/T46UWFOBnZFVP++TX/pyjDsvns -xymcQywtoN/26+UIys6oWX1um9ikEokvf67LdsUeemQGFHFky8X1Ka2hVtKnxBhi -XZfvyHDR4IyFWU9AwmhnqySzxqCtynUu8Gktx7JVfqbRFMZ186pDcSw8LoaqjTVG -SzO7eNH2sM3doMueAHj7ITc2wUzmfa0Pdh+K8UoCn/HopU5LzycziJVPYvUkLT2m -YCV7VWRc+kObZseHhZAbyaDk3RgPQ/eRMhytAgbruBHWDqNesNw+ZA70w856Oj2Y -geO7JF+5V6WvkywrF8vydaoM2l8CAwEAAaOCAm8wggJrMB8GA1UdIwQYMBaAFJBx -2zfrc8jv3NUeErY0uitaoKaSMB0GA1UdDgQWBBQK5zjZwbcmcMNLnI2h1ioAldEV -ujCBygYDVR0RBIHCMIG/gg0qLm9wc2NvZGUuY29tghBjb3JwLm9wc2NvZGUuY29t -ghIqLmNvcnAub3BzY29kZS5jb22CDyoubGVhcm5jaGVmLmNvbYISKi5jb3JwLmdl -dGNoZWYuY29tgg0qLmdldGNoZWYuY29tggwqLm9wc2NvZGUudXOCC2dldGNoZWYu -Y29tggtvcHNjb2RlLmNvbYIRYXBpLmJlcmtzaGVsZi5jb22CDWxlYXJuY2hlZi5j -b22CCm9wc2NvZGUudXMwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUF -BwMBBggrBgEFBQcDAjBhBgNVHR8EWjBYMCqgKKAmhiRodHRwOi8vY3JsMy5kaWdp -Y2VydC5jb20vc3NjYS1nNi5jcmwwKqAooCaGJGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0 -LmNvbS9zc2NhLWc2LmNybDBCBgNVHSAEOzA5MDcGCWCGSAGG/WwBATAqMCgGCCsG -AQUFBwIBFhxodHRwczovL3d3dy5kaWdpY2VydC5jb20vQ1BTMHgGCCsGAQUFBwEB -BGwwajAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEIGCCsG -AQUFBzAChjZodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTZWN1 -cmVTZXJ2ZXJDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQUFAAOCAQEA -kgBpJ2t+St7SmWfeNU9EWAhy0NuUnRIi1jnqXdapfPmS6V/M0i2wP/p+crMty78e -+3ieuF5s0GJBLs85Hikcl3SlrrbIBJxozov1TY6zeOi6+TCsdXer6t6iQKz36zno -+k+T6lnMCyo9+pk1PhcAWyfo1Fz4xVOBVec/71VovFkkGD2//KB+sbDs+yh21N9M -ReO7duj16rQSctfO9R2h65djBNlgz6hXY2nlw8/x3uFfZobXOxDrTcH6Z8HIslkE -MiTXGix6zdqJaFRCWi+prnAztWs+jEy+v95VSEHPj3xpwZ9WjsxQN0kFA2EX61v/ -kGunmyhehGjblQRt7bpyiA== ------END CERTIFICATE----- ------BEGIN CERTIFICATE----- -MIIEjzCCA3egAwIBAgIQBp4dt3/PHfupevXlyaJANzANBgkqhkiG9w0BAQUFADBh +MIIElDCCA3ygAwIBAgIQAf2j627KdciIQ4tyS8+8kTANBgkqhkiG9w0BAQsFADBh MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD -QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaMEgxCzAJBgNVBAYTAlVT -MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxIjAgBgNVBAMTGURpZ2lDZXJ0IFNlY3Vy -ZSBTZXJ2ZXIgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7V+Qh -qdWbYDd+jqFhf4HiGsJ1ZNmRUAvkNkQkbjDSm3on+sJqrmpwCTi5IArIZRBKiKwx -8tyS8mOhXYBjWYCSIxzm73ZKUDXJ2HE4ue3w5kKu0zgmeTD5IpTG26Y/QXiQ2N5c -fml9+JAVOtChoL76srIZodgr0c6/a91Jq6OS/rWryME+7gEA2KlEuEJziMNh9atK -gygK0tRJ+mqxzd9XLJTl4sqDX7e6YlwvaKXwwLn9K9HpH9gaYhW9/z2m98vv5ttl -LyU47PvmIGZYljQZ0hXOIdMkzNkUb9j+Vcfnb7YPGoxJvinyulqagSY3JG/XSBJs -Lln1nBi72fZo4t9FAgMBAAGjggFaMIIBVjASBgNVHRMBAf8ECDAGAQH/AgEAMA4G -A1UdDwEB/wQEAwIBhjA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6 -Ly9vY3NwLmRpZ2ljZXJ0LmNvbTB7BgNVHR8EdDByMDegNaAzhjFodHRwOi8vY3Js -My5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290Q0EuY3JsMDegNaAzhjFo -dHRwOi8vY3JsNC5kaWdpY2VydC5jb20vRGlnaUNlcnRHbG9iYWxSb290Q0EuY3Js -MD0GA1UdIAQ2MDQwMgYEVR0gADAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5k -aWdpY2VydC5jb20vQ1BTMB0GA1UdDgQWBBSQcds363PI79zVHhK2NLorWqCmkjAf -BgNVHSMEGDAWgBQD3lA1VtFMu2bwo+IbG8OXsj3RVTANBgkqhkiG9w0BAQUFAAOC -AQEAMM7RlVEArgYLoQ4CwBestn+PIPZAdXQczHixpE/q9NDEnaLegQcmH0CIUfAf -z7dMQJnQ9DxxmHOIlywZ126Ej6QfnFog41FcsMWemWpPyGn3EP9OrRnZyVizM64M -2ZYpnnGycGOjtpkWQh1l8/egHn3F1GUUsmKE1GxcCAzYbJMrtHZZitF//wPYwl24 -LyLWOPD2nGt9RuuZdPfrSg6ppgTre87wXGuYMVqYQOtpxAX0IKjKCDplbDgV9Vws -slXkLGtB8L5cRspKKaBIXiDSRf8F3jSvcEuBOeLKB1d8tjHcISnivpcOd5AUUUDh -v+PMGxmcJcqnBrJT3yOyzxIZow== +QTAeFw0xMzAzMDgxMjAwMDBaFw0yMzAzMDgxMjAwMDBaME0xCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxJzAlBgNVBAMTHkRpZ2lDZXJ0IFNIQTIg +U2VjdXJlIFNlcnZlciBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANyuWJBNwcQwFZA1W248ghX1LFy949v/cUP6ZCWA1O4Yok3wZtAKc24RmDYXZK83 +nf36QYSvx6+M/hpzTc8zl5CilodTgyu5pnVILR1WN3vaMTIa16yrBvSqXUu3R0bd +KpPDkC55gIDvEwRqFDu1m5K+wgdlTvza/P96rtxcflUxDOg5B6TXvi/TC2rSsd9f +/ld0Uzs1gN2ujkSYs58O09rg1/RrKatEp0tYhG2SS4HD2nOLEpdIkARFdRrdNzGX +kujNVA075ME/OV4uuPNcfhCOhkEAjUVmR7ChZc6gqikJTvOX6+guqw9ypzAO+sf0 +/RR3w6RbKFfCs/mC/bdFWJsCAwEAAaOCAVowggFWMBIGA1UdEwEB/wQIMAYBAf8C +AQAwDgYDVR0PAQH/BAQDAgGGMDQGCCsGAQUFBwEBBCgwJjAkBggrBgEFBQcwAYYY +aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMHsGA1UdHwR0MHIwN6A1oDOGMWh0dHA6 +Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RDQS5jcmwwN6A1 +oDOGMWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEdsb2JhbFJvb3RD +QS5jcmwwPQYDVR0gBDYwNDAyBgRVHSAAMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8v +d3d3LmRpZ2ljZXJ0LmNvbS9DUFMwHQYDVR0OBBYEFA+AYRyCMWHVLyjnjUY4tCzh +xtniMB8GA1UdIwQYMBaAFAPeUDVW0Uy7ZvCj4hsbw5eyPdFVMA0GCSqGSIb3DQEB +CwUAA4IBAQAjPt9L0jFCpbZ+QlwaRMxp0Wi0XUvgBCFsS+JtzLHgl4+mUwnNqipl +5TlPHoOlblyYoiQm5vuh7ZPHLgLGTUq/sELfeNqzqPlt/yGFUzZgTHbO7Djc1lGA +8MXW5dRNJ2Srm8c+cftIl7gzbckTB+6WohsYFfZcTEDts8Ls/3HB40f/1LkAtDdC +2iDJ6m6K7hQGrn2iWZiIqBtvLfTyyRRfJs8sjX7tN8Cp1Tm5gr8ZDOo0rwAhaPit +c+LJMto4JQtV05od8GiG7S5BNO98pVAdvzr508EIDObtHopYJeS4d60tbvVS3bR0 +j6tJLp07kzQoH3jOlOrHvdPJbRzeXDLz +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFDTCCA/WgAwIBAgIQBZ8R1sZP2Lbc8x554UUQ2DANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMScwJQYDVQQDEx5E +aWdpQ2VydCBTSEEyIFNlY3VyZSBTZXJ2ZXIgQ0EwHhcNMTQxMTEwMDAwMDAwWhcN +MTcxMTE0MTIwMDAwWjBlMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv +bjEQMA4GA1UEBxMHU2VhdHRsZTEbMBkGA1UEChMSQ2hlZiBTb2Z0d2FyZSwgSW5j +MRIwEAYDVQQDDAkqLmNoZWYuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC3xCIczkV10O5jTDpbd4YlPLC6kfnVoOkno2N/OOlcLQu3ulj/Lj1j4r6e +2XthJLcFgTO+y+1/IKnnpLKDfkx1YngWEBXEBP+MrrpDUKKs053s45/bI9QBPISA +tXgnYxMH9Glo6FWWd13TUq++OKGw1p1wazH64XK4MAf5y/lkmWXIWumNuO35ZqtB +ME3wJISwVHzHB2CQjlDklt+Mb0APEiIFIZflgu9JNBYzLdvUtxiz15FUZQI7SsYL +TfXOD1KBNMWqN8snG2e5gRAzB2D161DFvAZt8OiYUe+3QurNlTYVzeHv1ok6UqgM +ZcLzg8m801rRip0D7FCGvMCU/ktdAgMBAAGjggHPMIIByzAfBgNVHSMEGDAWgBQP +gGEcgjFh1S8o541GOLQs4cbZ4jAdBgNVHQ4EFgQUwldjw4Pb4HV+wxGZ7MSSRh+d +pm4wHQYDVR0RBBYwFIIJKi5jaGVmLmlvggdjaGVmLmlvMA4GA1UdDwEB/wQEAwIF +oDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwawYDVR0fBGQwYjAvoC2g +K4YpaHR0cDovL2NybDMuZGlnaWNlcnQuY29tL3NzY2Etc2hhMi1nMy5jcmwwL6At +oCuGKWh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zc2NhLXNoYTItZzMuY3JsMEIG +A1UdIAQ7MDkwNwYJYIZIAYb9bAEBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3 +LmRpZ2ljZXJ0LmNvbS9DUFMwfAYIKwYBBQUHAQEEcDBuMCQGCCsGAQUFBzABhhho +dHRwOi8vb2NzcC5kaWdpY2VydC5jb20wRgYIKwYBBQUHMAKGOmh0dHA6Ly9jYWNl +cnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFNIQTJTZWN1cmVTZXJ2ZXJDQS5jcnQw +DAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAvcTWenNuvvrhX2omm8LQ +zWOuu8jqpoflACwD4lOSZ4TgOe4pQGCjXq8aRBD5k+goqQrPVf9lHnelUHFQac0Q +5WT4YUmisUbF0S4uY5OGQymM52MvUWG4ODL4gaWhFvN+HAXrDPP/9iitsjV0QOnl +CDq7Q4/XYRYW3opu5nLLbfW6v4QvF5yzZagEACGs7Vt32p6l391UcU8f6wiB3uMD +eioCvjpv/+2YOUNlDPCM3uBubjUhHOwO817wBxXkzdk1OSRe4jzcw/uX6wL7birt +fbaSkpilvVX529pSzB2Lvi9xWOoGMM578dpQ0h3PwhmmvKhhCWP+pI05k3oSkYCP +ng== -----END CERTIFICATE----- diff --git a/spec/functional/provider/whyrun_safe_ruby_block_spec.rb b/spec/functional/provider/whyrun_safe_ruby_block_spec.rb index b3c2333e9a..2b582feb05 100644 --- a/spec/functional/provider/whyrun_safe_ruby_block_spec.rb +++ b/spec/functional/provider/whyrun_safe_ruby_block_spec.rb @@ -43,7 +43,7 @@ describe Chef::Resource::WhyrunSafeRubyBlock do end it "updates the evil laugh, even in why-run mode" do - new_resource.run_action(new_resource.action) + Array(new_resource.action).each {|action| new_resource.run_action(action) } expect($evil_global_evil_laugh).to eq(:mwahahaha) expect(new_resource).to be_updated end diff --git a/spec/functional/rebooter_spec.rb b/spec/functional/rebooter_spec.rb index 485e98f247..a0e2665de5 100644 --- a/spec/functional/rebooter_spec.rb +++ b/spec/functional/rebooter_spec.rb @@ -43,7 +43,7 @@ describe Chef::Platform::Rebooter do let(:expected) do { - :windows => 'shutdown /r /t 5 /c "rebooter spec test"', + :windows => 'shutdown /r /t 300 /c "rebooter spec test"', :linux => 'shutdown -r +5 "rebooter spec test"' } end diff --git a/spec/functional/resource/deploy_revision_spec.rb b/spec/functional/resource/deploy_revision_spec.rb index e5f5341fcd..4bce309a51 100644 --- a/spec/functional/resource/deploy_revision_spec.rb +++ b/spec/functional/resource/deploy_revision_spec.rb @@ -819,7 +819,7 @@ describe Chef::Resource::DeployRevision, :unix_only => true do end before do - expect { deploy_that_fails.run_action(:deploy) }.to raise_error(Chef::Exceptions::Exec) + expect { deploy_that_fails.run_action(:deploy) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed) deploy_to_latest_with_callback_tracking.run_action(:deploy) end diff --git a/spec/functional/resource/dsc_script_spec.rb b/spec/functional/resource/dsc_script_spec.rb index a736949c6b..dc7704481f 100644 --- a/spec/functional/resource/dsc_script_spec.rb +++ b/spec/functional/resource/dsc_script_spec.rb @@ -19,6 +19,7 @@ require 'spec_helper' require 'chef/mixin/shell_out' require 'chef/mixin/windows_architecture_helper' +require 'support/shared/integration/integration_helper' describe Chef::Resource::DscScript, :windows_powershell_dsc_only do include Chef::Mixin::WindowsArchitectureHelper @@ -67,8 +68,7 @@ describe Chef::Resource::DscScript, :windows_powershell_dsc_only do node = Chef::Node.new node.automatic['platform'] = 'windows' node.automatic['platform_version'] = '6.1' - node.automatic['kernel'][:machine] = - is_i386_process_on_x86_64_windows? ? :x86_64 : :i386 + node.automatic['kernel'][:machine] = :x86_64 # Only 64-bit architecture is supported node.automatic[:languages][:powershell][:version] = '4.0' empty_events = Chef::EventDispatch::Dispatcher.new Chef::RunContext.new(node, {}, empty_events) @@ -379,4 +379,93 @@ EOH it_behaves_like 'a dsc_script with configuration data that takes parameters' it_behaves_like 'a dsc_script without configuration data that takes parameters' end + + context 'when using ps_credential' do + include IntegrationSupport + + before(:each) do + delete_user(dsc_user) + ohai_reader = Ohai::System.new + ohai_reader.all_plugins(["platform", "os", "languages/powershell"]) + dsc_test_run_context.node.consume_external_attrs(ohai_reader.data,{}) + end + + let(:configuration_data_path) { 'C:\\configurationdata.psd1' } + + let(:self_signed_cert_path) do + File.join(CHEF_SPEC_DATA, 'dsc_lcm.pfx') + end + + let(:dsc_configuration_script) do + <<-MYCODE +cd c:\\ +configuration LCM +{ + param ($thumbprint) + localconfigurationmanager + { + RebootNodeIfNeeded = $false + ConfigurationMode = 'ApplyOnly' + CertificateID = $thumbprint + } +} +$cert = ls Cert:\\LocalMachine\\My\\ | + Where-Object {$_.Subject -match "ChefTest"} | + Select -first 1 + +if($cert -eq $null) { + $pfxpath = '#{self_signed_cert_path}' + $password = '' + $cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($pfxpath, $password, ([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::MachineKeyset)) + $store = New-Object System.Security.Cryptography.X509Certificates.X509Store "My", ([System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine) + $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite) + $store.Add($cert) + $store.Close() +} + +lcm -thumbprint $cert.thumbprint +set-dsclocalconfigurationmanager -path ./LCM +$ConfigurationData = @" +@{ +AllNodes = @( + @{ + NodeName = "localhost"; + CertificateID = '$($cert.thumbprint)'; + }; +); +} +"@ +$ConfigurationData | out-file '#{configuration_data_path}' -force + MYCODE + end + + let(:powershell_script_resource) do + Chef::Resource::PowershellScript.new('configure-lcm', dsc_test_run_context).tap do |r| + r.code(dsc_configuration_script) + r.architecture(:x86_64) + end + end + + let(:dsc_script_resource) do + dsc_test_resource_base.tap do |r| + r.code <<-EOF +User dsctestusercreate +{ + UserName = '#{dsc_user}' + Password = #{r.ps_credential('jf9a8m49jrajf4#')} + Ensure = "Present" +} +EOF + r.configuration_data_script(configuration_data_path) + end + end + + it 'allows the use of ps_credential' do + expect(user_exists?(dsc_user)).to eq(false) + powershell_script_resource.run_action(:run) + expect(File).to exist(configuration_data_path) + dsc_script_resource.run_action(:run) + expect(user_exists?(dsc_user)).to eq(true) + end + end end diff --git a/spec/functional/resource/group_spec.rb b/spec/functional/resource/group_spec.rb index 529af52d4e..0862b8e15f 100644 --- a/spec/functional/resource/group_spec.rb +++ b/spec/functional/resource/group_spec.rb @@ -95,7 +95,7 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte def create_user(username) user(username).run_action(:create) if ! windows_domain_user?(username) - # TODO: User shouldn't exist + # TODO: User should exist end def remove_user(username) @@ -135,44 +135,75 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte group_should_not_exist(group_name) end - describe "when append is not set" do - let(:included_members) { [spec_members[1]] } - - before do - create_user(spec_members[1]) - create_user(spec_members[0]) - add_members_to_group([spec_members[0]]) - end + # dscl doesn't perform any error checking and will let you add users that don't exist. + describe "when no users exist", :not_supported_on_mac_osx do + describe "when append is not set" do + # excluded_members can only be used when append is set. It is ignored otherwise. + let(:excluded_members) { [] } - after do - remove_user(spec_members[1]) - remove_user(spec_members[0]) + it "should raise an error" do + expect { group_resource.run_action(tested_action) }.to raise_error() + end end - it "should remove the existing users and add the new users to the group" do - group_resource.run_action(tested_action) + describe "when append is set" do + before do + group_resource.append(true) + end - expect(user_exist_in_group?(spec_members[1])).to eq(true) - expect(user_exist_in_group?(spec_members[0])).to eq(false) + it "should raise an error" do + expect { group_resource.run_action(tested_action) }.to raise_error() + end end end - describe "when append is set" do - before(:each) do - group_resource.append(true) + describe "when the users exist" do + before do + (spec_members).each do |member| + create_user(member) + end end - describe "when the users exist" do - before do - (included_members + excluded_members).each do |member| - create_user(member) + after do + (spec_members).each do |member| + remove_user(member) + end + end + + describe "when append is not set" do + it "should set the group to to contain given members" do + group_resource.run_action(tested_action) + + included_members.each do |member| + expect(user_exist_in_group?(member)).to eq(true) + end + (spec_members - included_members).each do |member| + expect(user_exist_in_group?(member)).to eq(false) end end - after do - (included_members + excluded_members).each do |member| - remove_user(member) + describe "when group already contains some users" do + before do + add_members_to_group([included_members[0]]) + add_members_to_group(spec_members - included_members) end + + it "should remove all existing users and only add the new users to the group" do + group_resource.run_action(tested_action) + + included_members.each do |member| + expect(user_exist_in_group?(member)).to eq(true) + end + (spec_members - included_members).each do |member| + expect(user_exist_in_group?(member)).to eq(false) + end + end + end + end + + describe "when append is set" do + before(:each) do + group_resource.append(true) end it "should add included members to the group" do @@ -186,9 +217,9 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte end end - describe "when group contains some users" do + describe "when group already contains some users" do before(:each) do - add_members_to_group([ spec_members[0], spec_members[2] ]) + add_members_to_group([included_members[0], excluded_members[0]]) end it "should add the included users and remove excluded users" do @@ -203,20 +234,6 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte end end end - - describe "when the users doesn't exist" do - describe "when append is not set" do - it "should raise an error" do - expect { @grp_resource.run_action(tested_action) }.to raise_error - end - end - - describe "when append is set" do - it "should raise an error" do - expect { @grp_resource.run_action(tested_action) }.to raise_error - end - end - end end end @@ -231,6 +248,12 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte group_should_exist(group_name) end + after(:each) do + group_resource.run_action(:remove) + end + + # TODO: The ones below might actually return ArgumentError now - but I don't have + # a way to verify that. Change it and delete this comment if that's the case. describe "when updating membership" do it "raises an error for a non well-formed domain name" do group_resource.members [invalid_domain_user_name] @@ -256,7 +279,7 @@ describe Chef::Resource::Group, :requires_root_or_running_windows, :not_supporte end end - let(:group_name) { "t-#{SecureRandom.random_number(9999)}" } + let(:group_name) { "group#{SecureRandom.random_number(9999)}" } let(:included_members) { nil } let(:excluded_members) { nil } let(:group_resource) { @@ -300,7 +323,7 @@ theoldmanwalkingdownthestreetalwayshadagoodsmileonhisfacetheoldmanwalking\ downthestreetalwayshadagoodsmileonhisfacetheoldmanwalkingdownthestreeQQQQQQ" } it "should not create a group" do - expect { group_resource.run_action(:create) }.to raise_error + expect { group_resource.run_action(:create) }.to raise_error(ArgumentError) group_should_not_exist(group_name) end end diff --git a/spec/functional/resource/package_spec.rb b/spec/functional/resource/package_spec.rb index 5c17ca0107..8d37b072e8 100644 --- a/spec/functional/resource/package_spec.rb +++ b/spec/functional/resource/package_spec.rb @@ -386,5 +386,3 @@ describe Chef::Resource::Package, metadata do end end - - diff --git a/spec/functional/resource/powershell_spec.rb b/spec/functional/resource/powershell_script_spec.rb index 17ae8cbd2a..be744e748b 100644 --- a/spec/functional/resource/powershell_spec.rb +++ b/spec/functional/resource/powershell_script_spec.rb @@ -125,16 +125,16 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do expect { resource.run_action(:run) }.not_to raise_error end - it "raises an error if the script is not syntactically correct and returns is not set to 1" do + it "raises a Mixlib::ShellOut::ShellCommandFailed error if the script is not syntactically correct" do resource.code('if({)') resource.returns(0) expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed) end - it "returns 1 if the script provided to the code attribute is not syntactically correct" do + it "raises an error if the script is not syntactically correct even if returns is set to 1 which is what powershell.exe returns for syntactically invalid scripts" do resource.code('if({)') resource.returns(1) - expect { resource.run_action(:run) }.not_to raise_error + expect { resource.run_action(:run) }.to raise_error(Mixlib::ShellOut::ShellCommandFailed) end # This somewhat ambiguous case, two failures of different types, @@ -227,10 +227,25 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do expect { resource.should_skip?(:run) }.to raise_error(ArgumentError, /guard_interpreter does not support blocks/) end + context "when dsc is supported", :windows_powershell_dsc_only do + it "can execute LCM configuration code" do + resource.code <<-EOF +configuration LCM +{ + param ($thumbprint) + localconfigurationmanager + { + RebootNodeIfNeeded = $false + ConfigurationMode = 'ApplyOnly' + } +} + EOF + expect { resource.run_action(:run) }.not_to raise_error + end + end end - context "when running on a 32-bit version of Windows", :windows32_only do - + context "when running on a 32-bit version of Ruby", :ruby32_only do it "executes a script with a 32-bit process if process architecture :i386 is specified" do resource.code(processor_architecture_script_content + " | out-file -encoding ASCII #{script_output_path}") resource.architecture(:i386) @@ -240,15 +255,28 @@ describe Chef::Resource::WindowsScript::PowershellScript, :windows_only do expect(source_contains_case_insensitive_content?( get_script_output, 'x86' )).to eq(true) end - it "raises an exception if :x86_64 process architecture is specified" do - begin - expect(resource.architecture(:x86_64)).to raise_error Chef::Exceptions::Win32ArchitectureIncorrect - rescue Chef::Exceptions::Win32ArchitectureIncorrect + context "when running on a 64-bit version of Windows", :windows64_only do + it "executes a script with a 64-bit process if :x86_64 arch is specified" do + resource.code(processor_architecture_script_content + " | out-file -encoding ASCII #{script_output_path}") + resource.architecture(:x86_64) + resource.returns(0) + resource.run_action(:run) + + expect(source_contains_case_insensitive_content?( get_script_output, 'AMD64' )).to eq(true) + end + end + + context "when running on a 32-bit version of Windows", :windows32_only do + it "raises an exception if :x86_64 process architecture is specified" do + begin + expect(resource.architecture(:x86_64)).to raise_error Chef::Exceptions::Win32ArchitectureIncorrect + rescue Chef::Exceptions::Win32ArchitectureIncorrect + end end end end - context "when running on a 64-bit version of Windows", :windows64_only do + context "when running on a 64-bit version of Ruby", :ruby64_only do it "executes a script with a 64-bit process if :x86_64 arch is specified" do resource.code(processor_architecture_script_content + " | out-file -encoding ASCII #{script_output_path}") resource.architecture(:x86_64) diff --git a/spec/functional/win32/registry_helper_spec.rb b/spec/functional/win32/registry_helper_spec.rb index 7b070e6fe1..9ef6fd006f 100644 --- a/spec/functional/win32/registry_helper_spec.rb +++ b/spec/functional/win32/registry_helper_spec.rb @@ -130,6 +130,9 @@ describe 'Chef::Win32::Registry', :windows_only do it "returns true if the value exists" do expect(@registry.value_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"Petals"})).to eq(true) end + it "returns true if the value exists with a case mismatch on the value name" do + expect(@registry.value_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"petals"})).to eq(true) + end it "returns false if the value does not exist" do expect(@registry.value_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"FOOBAR"})).to eq(false) end @@ -145,6 +148,9 @@ describe 'Chef::Win32::Registry', :windows_only do it "returns true if the value exists" do expect(@registry.value_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"Petals"})).to eq(true) end + it "returns true if the value exists with a case mismatch on the value name" do + expect(@registry.value_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"petals"})).to eq(true) + end it "throws an exception if the value does not exist" do expect {@registry.value_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"FOOBAR"})}.to raise_error(Chef::Exceptions::Win32RegValueMissing) end @@ -160,6 +166,9 @@ describe 'Chef::Win32::Registry', :windows_only do it "returns true if all the data matches" do expect(@registry.data_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"Petals", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(true) end + it "returns true if all the data matches with a case mismatch on the data name" do + expect(@registry.data_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"petals", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(true) + end it "returns false if the name does not exist" do expect(@registry.data_exists?("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"slateP", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(false) end @@ -181,6 +190,9 @@ describe 'Chef::Win32::Registry', :windows_only do it "returns true if all the data matches" do expect(@registry.data_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"Petals", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(true) end + it "returns true if all the data matches with a case mismatch on the data name" do + expect(@registry.data_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"petals", :type=>:multi_string, :data=>["Pink", "Delicate"]})).to eq(true) + end it "throws an exception if the name does not exist" do expect {@registry.data_exists!("HKCU\\Software\\Root\\Branch\\Flower", {:name=>"slateP", :type=>:multi_string, :data=>["Pink", "Delicate"]})}.to raise_error(Chef::Exceptions::Win32RegDataMissing) end diff --git a/spec/functional/win32/service_manager_spec.rb b/spec/functional/win32/service_manager_spec.rb index d2474deace..a1ce36146f 100644 --- a/spec/functional/win32/service_manager_spec.rb +++ b/spec/functional/win32/service_manager_spec.rb @@ -33,7 +33,7 @@ end # directories. # -describe "Chef::Application::WindowsServiceManager", :windows_only, :system_windows_service_gem_only do +describe "Chef::Application::WindowsServiceManager", :windows_only, :system_windows_service_gem_only, :appveyor_only do include_context "using Win32::Service" @@ -43,7 +43,7 @@ describe "Chef::Application::WindowsServiceManager", :windows_only, :system_wind end it "throws an error with required missing options" do - test_service.each do |key,value| + [:service_name, :service_display_name, :service_description, :service_file_path].each do |key| service_def = test_service.dup service_def.delete(key) diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb index 8c72048965..1a030c130b 100644 --- a/spec/integration/client/client_spec.rb +++ b/spec/integration/client/client_spec.rb @@ -303,6 +303,59 @@ EOM end + when_the_repository "has a cookbook that generates deprecation warnings" do + before do + file 'cookbooks/x/recipes/default.rb', <<-EOM + class ::MyResource < Chef::Resource + use_automatic_resource_name + property :x, default: [] + property :y, default: {} + end + + my_resource 'blah' do + 1.upto(10) do + x nil + end + x nil + end + EOM + end + + def match_indices(regex, str) + result = [] + pos = 0 + while match = regex.match(str, pos) + result << match.begin(0) + pos = match.end(0) + 1 + end + result + end + + it "should output each deprecation warning only once, at the end of the run" do + file 'config/client.rb', <<EOM +local_mode true +cookbook_path "#{path_to('cookbooks')}" +# Mimick what happens when you are on the console +formatters << :doc +log_level :warn +EOM + + ENV.delete('CHEF_TREAT_DEPRECATION_WARNINGS_AS_ERRORS') + + result = shell_out!("#{chef_client} -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + expect(result.error?).to be_falsey + + # Search to the end of the client run in the output + run_complete = result.stdout.index("Running handlers complete") + expect(run_complete).to be >= 0 + + # Make sure there is exactly one result for each, and that it occurs *after* the complete message. + expect(match_indices(/MyResource.x has an array or hash default/, result.stdout)).to match([ be > run_complete ]) + expect(match_indices(/MyResource.y has an array or hash default/, result.stdout)).to match([ be > run_complete ]) + expect(match_indices(/nil currently does not overwrite the value of/, result.stdout)).to match([ be > run_complete ]) + end + end + when_the_repository "has a cookbook with only an audit recipe" do before do diff --git a/spec/integration/knife/chef_repo_path_spec.rb b/spec/integration/knife/chef_repo_path_spec.rb index 874b33901f..908657e5f7 100644 --- a/spec/integration/knife/chef_repo_path_spec.rb +++ b/spec/integration/knife/chef_repo_path_spec.rb @@ -24,6 +24,8 @@ describe 'chef_repo_path tests', :workstation do include IntegrationSupport include KnifeSupport + let(:error_rel_path_outside_repo) { /^ERROR: Attempt to use relative path '' when current directory is outside the repository path/ } + # TODO alternate repo_path / *_path context 'alternate *_path' do when_the_repository 'has clients and clients2, cookbooks and cookbooks2, etc.' do @@ -109,14 +111,14 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end context 'when cwd is inside the data_bags directory' do before { cwd 'data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -192,14 +194,14 @@ EOM context 'when cwd is inside the data_bags directory' do before { cwd 'data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end context 'when cwd is inside chef_repo2' do before { cwd 'chef_repo2' } it 'knife list -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -225,14 +227,14 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end context 'when cwd is inside the data_bags directory' do before { cwd 'data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -445,7 +447,7 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -621,14 +623,14 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end context 'when cwd is inside the data_bags directory' do before { cwd 'data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -782,7 +784,7 @@ EOM context 'when cwd is at the top level' do before { cwd '.' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end @@ -823,7 +825,7 @@ EOM context 'when cwd is inside chef_repo2/data_bags' do before { cwd 'chef_repo2/data_bags' } it 'knife list --local -Rfp fails' do - knife('list --local -Rfp').should_fail("ERROR: Attempt to use relative path '' when current directory is outside the repository path\n") + knife('list --local -Rfp').should_fail(error_rel_path_outside_repo) end end end diff --git a/spec/integration/recipes/lwrp_spec.rb b/spec/integration/recipes/lwrp_spec.rb index e93763fddc..7ecdfc7c3a 100644 --- a/spec/integration/recipes/lwrp_spec.rb +++ b/spec/integration/recipes/lwrp_spec.rb @@ -45,12 +45,8 @@ log_level :warn EOM result = shell_out("#{chef_client} -c \"#{path_to('config/client.rb')}\" --no-color -F doc -o 'l-w-r-p::default'", :cwd => chef_dir) - actual = result.stdout.lines.map { |l| l.chomp }.join("\n") - expected = <<EOM - * l_w_r_p_foo[me] action create (up to date) -EOM - expected = expected.lines.map { |l| l.chomp }.join("\n") - expect(actual).to include(expected) + expect(result.stdout).to match(/\* l_w_r_p_foo\[me\] action create \(up to date\)/) + expect(result.stdout).not_to match(/WARN: You are overriding l_w_r_p_foo/) result.error! end end diff --git a/spec/integration/recipes/recipe_dsl_spec.rb b/spec/integration/recipes/recipe_dsl_spec.rb index 3f4bf9fd5f..52bca87c99 100644 --- a/spec/integration/recipes/recipe_dsl_spec.rb +++ b/spec/integration/recipes/recipe_dsl_spec.rb @@ -11,19 +11,15 @@ describe "Recipe DSL methods" do before(:all) { Namer.current_index = 1 } before { Namer.current_index += 1 } - context "With resource 'base_thingy' declared as BaseThingy" do + context "with resource 'base_thingy' declared as BaseThingy" do before(:context) { class BaseThingy < Chef::Resource - def initialize(*args, &block) - super - @allowed_actions = [ :create ] - @action = :create - end - resource_name 'base_thingy' + default_action :create class<<self + attr_accessor :created_name attr_accessor :created_resource attr_accessor :created_provider end @@ -35,6 +31,7 @@ describe "Recipe DSL methods" do def load_current_resource end def action_create + BaseThingy.created_name = new_resource.name BaseThingy.created_resource = new_resource.class BaseThingy.created_provider = self.class end @@ -52,20 +49,42 @@ describe "Recipe DSL methods" do BaseThingy.created_provider = nil end + it "creates base_thingy when you call base_thingy in a recipe" do + recipe = converge { + base_thingy 'blah' do; end + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_name).to eq 'blah' + expect(BaseThingy.created_resource).to eq BaseThingy + end + + it "errors out when you call base_thingy do ... end in a recipe" do + expect_converge { + base_thingy do; end + }.to raise_error(ArgumentError, 'You must supply a name when declaring a base_thingy resource') + end + + it "emits a warning when you call base_thingy 'foo', 'bar' do ... end in a recipe" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + recipe = converge { + base_thingy 'foo', 'bar' do + end + } + expect(recipe.logged_warnings).to match(/Cannot create resource base_thingy with more than one argument. All arguments except the name \("foo"\) will be ignored. This will cause an error in Chef 13. Arguments: \["foo", "bar"\]/) + expect(BaseThingy.created_name).to eq 'foo' + expect(BaseThingy.created_resource).to eq BaseThingy + end + context "Deprecated automatic resource DSL" do before do Chef::Config[:treat_deprecation_warnings_as_errors] = false end - context "With a resource 'backcompat_thingy' declared in Chef::Resource and Chef::Provider" do + context "with a resource 'backcompat_thingy' declared in Chef::Resource and Chef::Provider" do before(:context) { class Chef::Resource::BackcompatThingy < Chef::Resource - def initialize(*args, &block) - super - @allowed_actions = [ :create ] - @action = :create - end + default_action :create end class Chef::Provider::BackcompatThingy < Chef::Provider def load_current_resource @@ -100,13 +119,13 @@ describe "Recipe DSL methods" do recipe = converge { backcompat_thingy 'blah' do; end } - expect(recipe.logged_warnings).to eq '' + expect(recipe.logged_warnings).to match(/Class Chef::Provider::BackcompatThingy does not declare 'provides :backcompat_thingy'./) expect(BaseThingy.created_resource).not_to be_nil end end end - context "With a resource named RecipeDSLSpecNamespace::Bar::BarThingy" do + context "with a resource named RecipeDSLSpecNamespace::Bar::BarThingy" do before(:context) { class RecipeDSLSpecNamespace::Bar::BarThingy < BaseThingy @@ -114,19 +133,17 @@ describe "Recipe DSL methods" do } - it "bar_thingy works" do - recipe = converge { + it "bar_thingy does not work" do + expect_converge { bar_thingy 'blah' do; end - } - expect(recipe.logged_warnings).to eq '' - expect(BaseThingy.created_resource).to eq(RecipeDSLSpecNamespace::Bar::BarThingy) + }.to raise_error(NoMethodError) end end - context "With a resource named NoNameThingy with resource_name nil" do + context "with a resource named Chef::Resource::NoNameThingy with resource_name nil" do before(:context) { - class NoNameThingy < BaseThingy + class Chef::Resource::NoNameThingy < BaseThingy resource_name nil end @@ -134,12 +151,12 @@ describe "Recipe DSL methods" do it "no_name_thingy does not work" do expect_converge { - thingy 'blah' do; end + no_name_thingy 'blah' do; end }.to raise_error(NoMethodError) end end - context "With a resource named AnotherNoNameThingy with resource_name :another_thingy_name" do + context "with a resource named AnotherNoNameThingy with resource_name :another_thingy_name" do before(:context) { class AnotherNoNameThingy < BaseThingy @@ -163,7 +180,7 @@ describe "Recipe DSL methods" do end end - context "With a resource named AnotherNoNameThingy2 with resource_name :another_thingy_name2; resource_name :another_thingy_name3" do + context "with a resource named AnotherNoNameThingy2 with resource_name :another_thingy_name2; resource_name :another_thingy_name3" do before(:context) { class AnotherNoNameThingy2 < BaseThingy @@ -195,10 +212,11 @@ describe "Recipe DSL methods" do end context "provides overriding resource_name" do - context "With a resource named AnotherNoNameThingy3 with provides :another_no_name_thingy3, os: 'blarghle'" do + context "with a resource named AnotherNoNameThingy3 with provides :another_no_name_thingy3, os: 'blarghle'" do before(:context) { class AnotherNoNameThingy3 < BaseThingy + resource_name :another_no_name_thingy_3 provides :another_no_name_thingy3, os: 'blarghle' end @@ -223,10 +241,11 @@ describe "Recipe DSL methods" do end end - context "With a resource named AnotherNoNameThingy4 with two provides" do + context "with a resource named AnotherNoNameThingy4 with two provides" do before(:context) { class AnotherNoNameThingy4 < BaseThingy + resource_name :another_no_name_thingy_4 provides :another_no_name_thingy4, os: 'blarghle' provides :another_no_name_thingy4, platform_family: 'foo' end @@ -262,7 +281,7 @@ describe "Recipe DSL methods" do end end - context "With a resource named AnotherNoNameThingy5, a different resource_name, and a provides with the original resource_name" do + context "with a resource named AnotherNoNameThingy5, a different resource_name, and a provides with the original resource_name" do before(:context) { class AnotherNoNameThingy5 < BaseThingy @@ -299,7 +318,7 @@ describe "Recipe DSL methods" do end end - context "With a resource named AnotherNoNameThingy6, a provides with the original resource name, and a different resource_name" do + context "with a resource named AnotherNoNameThingy6, a provides with the original resource name, and a different resource_name" do before(:context) { class AnotherNoNameThingy6 < BaseThingy @@ -336,7 +355,7 @@ describe "Recipe DSL methods" do end end - context "With a resource named AnotherNoNameThingy7, a new resource_name, and provides with that new resource name" do + context "with a resource named AnotherNoNameThingy7, a new resource_name, and provides with that new resource name" do before(:context) { class AnotherNoNameThingy7 < BaseThingy @@ -374,7 +393,7 @@ describe "Recipe DSL methods" do end # opposite order from the previous test (provides, then resource_name) - context "With a resource named AnotherNoNameThingy8, a provides with a new resource name, and resource_name with that new resource name" do + context "with a resource named AnotherNoNameThingy8, a provides with a new resource name, and resource_name with that new resource name" do before(:context) { class AnotherNoNameThingy8 < BaseThingy @@ -410,116 +429,6 @@ describe "Recipe DSL methods" do }.to raise_error(NoMethodError) end end - - context "With a resource TwoClassesOneDsl" do - let(:class_name) { "TwoClassesOneDsl#{Namer.current_index}" } - let(:dsl_method) { :"two_classes_one_dsl#{Namer.current_index}" } - - before { - eval <<-EOM, nil, __FILE__, __LINE__+1 - class #{class_name} < BaseThingy - end - EOM - } - context "and resource BlahModule::TwoClassesOneDsl" do - before { - eval <<-EOM, nil, __FILE__, __LINE__+1 - module BlahModule - class #{class_name} < BaseThingy - end - end - EOM - } - it "two_classes_one_dsl resolves to BlahModule::TwoClassesOneDsl (last declared)" do - dsl_method = self.dsl_method - recipe = converge { - instance_eval("#{dsl_method} 'blah' do; end") - } - expect(recipe.logged_warnings).to eq '' - expect(BaseThingy.created_resource).to eq eval("BlahModule::#{class_name}") - end - it "resource_matching_short_name returns BlahModule::TwoClassesOneDsl" do - expect(Chef::Resource.resource_matching_short_name(dsl_method)).to eq eval("BlahModule::#{class_name}") - end - end - context "and resource BlahModule::TwoClassesOneDsl with resource_name nil" do - before { - eval <<-EOM, nil, __FILE__, __LINE__+1 - module BlahModule - class BlahModule::#{class_name} < BaseThingy - resource_name nil - end - end - EOM - } - it "two_classes_one_dsl resolves to ::TwoClassesOneDsl" do - dsl_method = self.dsl_method - recipe = converge { - instance_eval("#{dsl_method} 'blah' do; end") - } - expect(recipe.logged_warnings).to eq '' - expect(BaseThingy.created_resource).to eq eval("::#{class_name}") - end - it "resource_matching_short_name returns ::TwoClassesOneDsl" do - expect(Chef::Resource.resource_matching_short_name(dsl_method)).to eq eval("::#{class_name}") - end - end - context "and resource BlahModule::TwoClassesOneDsl with resource_name :argh" do - before { - eval <<-EOM, nil, __FILE__, __LINE__+1 - module BlahModule - class BlahModule::#{class_name} < BaseThingy - resource_name :argh - end - end - EOM - } - it "two_classes_one_dsl resolves to ::TwoClassesOneDsl" do - dsl_method = self.dsl_method - recipe = converge { - instance_eval("#{dsl_method} 'blah' do; end") - } - expect(recipe.logged_warnings).to eq '' - expect(BaseThingy.created_resource).to eq eval("::#{class_name}") - end - it "resource_matching_short_name returns ::TwoClassesOneDsl" do - expect(Chef::Resource.resource_matching_short_name(dsl_method)).to eq eval("::#{class_name}") - end - end - context "and resource BlahModule::TwoClassesOneDsl with provides :two_classes_one_dsl, os: 'blarghle'" do - before { - eval <<-EOM, nil, __FILE__, __LINE__+1 - module BlahModule - class BlahModule::#{class_name} < BaseThingy - provides #{dsl_method.inspect}, os: 'blarghle' - end - end - EOM - } - - it "and os = blarghle, two_classes_one_dsl resolves to BlahModule::TwoClassesOneDsl" do - dsl_method = self.dsl_method - recipe = converge { - # this is an ugly way to test, make Cheffish expose node attrs - run_context.node.automatic[:os] = 'blarghle' - instance_eval("#{dsl_method} 'blah' do; end") - } - expect(recipe.logged_warnings).to eq '' - expect(BaseThingy.created_resource).to eq eval("BlahModule::#{class_name}") - end - - it "and os = linux, two_classes_one_dsl resolves to ::TwoClassesOneDsl" do - dsl_method = self.dsl_method - recipe = converge { - # this is an ugly way to test, make Cheffish expose node attrs - run_context.node.automatic[:os] = 'linux' - instance_eval("#{dsl_method} 'blah' do; end") - } - expect(recipe.logged_warnings).to eq '' - expect(BaseThingy.created_resource).to eq eval("::#{class_name}") - end - end - end end end @@ -572,11 +481,11 @@ describe "Recipe DSL methods" do } - it "thingy3 works in a recipe and yields Foo::Thingy4 (the explicit one)" do + it "thingy3 works in a recipe and yields Thingy3 (the alphabetical one)" do recipe = converge { thingy3 'blah' do; end } - expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy4 + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 end it "thingy4 does not work in a recipe" do @@ -586,16 +495,19 @@ describe "Recipe DSL methods" do end it "resource_matching_short_name returns Thingy4" do - expect(Chef::Resource.resource_matching_short_name(:thingy3)).to eq RecipeDSLSpecNamespace::Thingy4 + expect(Chef::Resource.resource_matching_short_name(:thingy3)).to eq RecipeDSLSpecNamespace::Thingy3 end end end - context "when Thingy5 has resource_name :thingy5" do + context "when Thingy5 has resource_name :thingy5 and provides :thingy5reverse, :thingy5_2 and :thingy5_2reverse" do before(:context) { class RecipeDSLSpecNamespace::Thingy5 < BaseThingy resource_name :thingy5 + provides :thingy5reverse + provides :thingy5_2 + provides :thingy5_2reverse end } @@ -611,6 +523,7 @@ describe "Recipe DSL methods" do before(:context) { class RecipeDSLSpecNamespace::Thingy6 < BaseThingy + resource_name :thingy6 provides :thingy5 end @@ -623,23 +536,151 @@ describe "Recipe DSL methods" do expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy6 end - it "thingy5 works in a recipe and yields Foo::Thingy6 (the later one)" do + it "thingy5 works in a recipe and yields Foo::Thingy5 (the alphabetical one)" do recipe = converge { thingy5 'blah' do; end } - expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy6 + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 end it "resource_matching_short_name returns Thingy5" do expect(Chef::Resource.resource_matching_short_name(:thingy5)).to eq RecipeDSLSpecNamespace::Thingy5 end + + context "and AThingy5 provides :thingy5reverse" do + before(:context) { + + class RecipeDSLSpecNamespace::AThingy5 < BaseThingy + resource_name :thingy5reverse + end + + } + + it "thingy5reverse works in a recipe and yields AThingy5 (the alphabetical one)" do + recipe = converge { + thingy5reverse 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::AThingy5 + end + end + + context "and ZRecipeDSLSpecNamespace::Thingy5 provides :thingy5_2" do + before(:context) { + + module ZRecipeDSLSpecNamespace + class Thingy5 < BaseThingy + resource_name :thingy5_2 + end + end + + } + + it "thingy5_2 works in a recipe and yields the RecipeDSLSpaceNamespace one (the alphabetical one)" do + recipe = converge { + thingy5_2 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + end + end + + context "and ARecipeDSLSpecNamespace::Thingy5 provides :thingy5_2" do + before(:context) { + + module ARecipeDSLSpecNamespace + class Thingy5 < BaseThingy + resource_name :thingy5_2reverse + end + end + + } + + it "thingy5_2reverse works in a recipe and yields the ARecipeDSLSpaceNamespace one (the alphabetical one)" do + recipe = converge { + thingy5_2reverse 'blah' do; end + } + expect(BaseThingy.created_resource).to eq ARecipeDSLSpecNamespace::Thingy5 + end + end + end + + context "when Thingy3 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy3 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe" do + expect_recipe { + thingy3 'blah' do; end + }.to emit_no_warnings_or_errors + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + context "and Thingy4 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy4 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe and yields Thingy3 (the alphabetical one)" do + recipe = converge { + thingy3 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + it "thingy4 does not work in a recipe" do + expect_converge { + thingy4 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "resource_matching_short_name returns Thingy4" do + expect(Chef::Resource.resource_matching_short_name(:thingy3)).to eq RecipeDSLSpecNamespace::Thingy3 + end + end + + context "and Thingy4 has resource_name :thingy3" do + before(:context) { + + class RecipeDSLSpecNamespace::Thingy4 < BaseThingy + resource_name :thingy3 + end + + } + + it "thingy3 works in a recipe and yields Thingy3 (the alphabetical one)" do + recipe = converge { + thingy3 'blah' do; end + } + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy3 + end + + it "thingy4 does not work in a recipe" do + expect_converge { + thingy4 'blah' do; end + }.to raise_error(NoMethodError) + end + + it "resource_matching_short_name returns Thingy4" do + expect(Chef::Resource.resource_matching_short_name(:thingy3)).to eq RecipeDSLSpecNamespace::Thingy3 + end + end end + end context "when Thingy7 provides :thingy8" do before(:context) { class RecipeDSLSpecNamespace::Thingy7 < BaseThingy + resource_name :thingy7 provides :thingy8 end @@ -661,11 +702,11 @@ describe "Recipe DSL methods" do expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy7 end - it "thingy8 works in a recipe and yields Thingy8 (the later one)" do + it "thingy8 works in a recipe and yields Thingy7 (alphabetical)" do recipe = converge { thingy8 'blah' do; end } - expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy8 + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy7 end it "resource_matching_short_name returns Thingy8" do @@ -674,40 +715,40 @@ describe "Recipe DSL methods" do end end - context "when Thingy5 provides :thingy5, :twizzle and :twizzle2" do + context "when Thingy12 provides :thingy12, :twizzle and :twizzle2" do before(:context) { - class RecipeDSLSpecNamespace::Thingy5 < BaseThingy - resource_name :thingy5 + class RecipeDSLSpecNamespace::Thingy12 < BaseThingy + resource_name :thingy12 provides :twizzle provides :twizzle2 end } - it "thingy5 works in a recipe and yields Thingy5" do + it "thingy12 works in a recipe and yields Thingy12" do expect_recipe { - thingy5 'blah' do; end + thingy12 'blah' do; end }.to emit_no_warnings_or_errors - expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy12 end - it "twizzle works in a recipe and yields Thingy5" do + it "twizzle works in a recipe and yields Thingy12" do expect_recipe { twizzle 'blah' do; end }.to emit_no_warnings_or_errors - expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy12 end - it "twizzle2 works in a recipe and yields Thingy5" do + it "twizzle2 works in a recipe and yields Thingy12" do expect_recipe { twizzle2 'blah' do; end }.to emit_no_warnings_or_errors - expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy5 + expect(BaseThingy.created_resource).to eq RecipeDSLSpecNamespace::Thingy12 end end - context "With platform-specific resources 'my_super_thingy_foo' and 'my_super_thingy_bar'" do + context "with platform-specific resources 'my_super_thingy_foo' and 'my_super_thingy_bar'" do before(:context) { class MySuperThingyFoo < BaseThingy resource_name :my_super_thingy_foo @@ -752,6 +793,700 @@ describe "Recipe DSL methods" do }.to raise_error(Chef::Exceptions::NoSuchResourceType) end end + + context "when Thingy10 provides :thingy10" do + before(:context) { + class RecipeDSLSpecNamespace::Thingy10 < BaseThingy + resource_name :thingy10 + end + } + + it "declaring a resource providing the same :thingy10 with override: true does not produce a warning" do + expect(Chef::Log).not_to receive(:warn) + class RecipeDSLSpecNamespace::Thingy10AlternateProvider < BaseThingy + provides :thingy10, override: true + end + end + end + + context "when Thingy11 provides :thingy11" do + before(:context) { + class RecipeDSLSpecNamespace::Thingy11 < BaseThingy + resource_name :thingy10 + end + } + + it "declaring a resource providing the same :thingy11 with os: 'linux' does not produce a warning" do + expect(Chef::Log).not_to receive(:warn) + class RecipeDSLSpecNamespace::Thingy11AlternateProvider < BaseThingy + provides :thingy11, os: 'linux' + end + end + end + end + + context "with a resource named 'B' with resource name :two_classes_one_dsl" do + let(:two_classes_one_dsl) { :"two_classes_one_dsl#{Namer.current_index}" } + let(:resource_class) { + result = Class.new(BaseThingy) do + def self.name + "B" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.resource_name two_classes_one_dsl + result + } + before { resource_class } # pull on it so it gets defined before the recipe runs + + context "and another resource named 'A' with resource_name :two_classes_one_dsl" do + let(:resource_class_a) { + result = Class.new(BaseThingy) do + def self.name + "A" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.resource_name two_classes_one_dsl + result + } + before { resource_class_a } # pull on it so it gets defined before the recipe runs + + it "two_classes_one_dsl resolves to A (alphabetically earliest)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class_a + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class_a + end + end + + context "and another resource named 'Z' with resource_name :two_classes_one_dsl" do + let(:resource_class_z) { + result = Class.new(BaseThingy) do + def self.name + "Z" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.resource_name two_classes_one_dsl + result + } + before { resource_class_z } # pull on it so it gets defined before the recipe runs + + it "two_classes_one_dsl resolves to B (alphabetically earliest)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + + context "and a priority array [ Z, B ]" do + before do + Chef.set_resource_priority_array(two_classes_one_dsl, [ resource_class_z, resource_class ]) + end + + it "two_classes_one_dsl resolves to Z (respects the priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class_z + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + + context "when Z provides(:two_classes_one_dsl) { false }" do + before do + resource_class_z.provides(two_classes_one_dsl) { false } + end + + it "two_classes_one_dsl resolves to B (picks the next thing in the priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + end + end + + context "and priority arrays [ B ] and [ Z ]" do + before do + Chef.set_resource_priority_array(two_classes_one_dsl, [ resource_class ]) + Chef.set_resource_priority_array(two_classes_one_dsl, [ resource_class_z ]) + end + + it "two_classes_one_dsl resolves to Z (respects the most recent priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class_z + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + + context "when Z provides(:two_classes_one_dsl) { false }" do + before do + resource_class_z.provides(two_classes_one_dsl) { false } + end + + it "two_classes_one_dsl resolves to B (picks the first match from the other priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + end + end + + context "and a priority array [ Z ]" do + before do + Chef.set_resource_priority_array(two_classes_one_dsl, [ resource_class_z ]) + end + + context "when Z provides(:two_classes_one_dsl) { false }" do + before do + resource_class_z.provides(two_classes_one_dsl) { false } + end + + it "two_classes_one_dsl resolves to B (picks the first match outside the priority array)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + + it "resource_matching_short_name returns B" do + expect(Chef::Resource.resource_matching_short_name(two_classes_one_dsl)).to eq resource_class + end + end + end + + end + + context "and a provider named 'B' which provides :two_classes_one_dsl" do + before do + resource_class.send(:define_method, :provider) { nil } + end + + let(:provider_class) { + result = Class.new(BaseThingy::Provider) do + def self.name + "B" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.provides two_classes_one_dsl + result + } + before { provider_class } # pull on it so it gets defined before the recipe runs + + context "and another provider named 'A'" do + let(:provider_class_a) { + result = Class.new(BaseThingy::Provider) do + def self.name + "A" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result + } + context "which provides :two_classes_one_dsl" do + before { provider_class_a.provides two_classes_one_dsl } + + it "two_classes_one_dsl resolves to A (alphabetically earliest)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class_a + end + end + context "which provides(:two_classes_one_dsl) { false }" do + before { provider_class_a.provides(two_classes_one_dsl) { false } } + + it "two_classes_one_dsl resolves to B (since A declined)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + end + end + + context "and another provider named 'Z'" do + let(:provider_class_z) { + result = Class.new(BaseThingy::Provider) do + def self.name + "Z" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result + } + before { provider_class_z } # pull on it so it gets defined before the recipe runs + + context "which provides :two_classes_one_dsl" do + before { provider_class_z.provides two_classes_one_dsl } + + it "two_classes_one_dsl resolves to B (alphabetically earliest)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + + context "with a priority array [ Z, B ]" do + before { Chef.set_provider_priority_array two_classes_one_dsl, [ provider_class_z, provider_class ] } + + it "two_classes_one_dsl resolves to Z (respects the priority map)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class_z + end + end + end + + context "which provides(:two_classes_one_dsl) { false }" do + before { provider_class_z.provides(two_classes_one_dsl) { false } } + + context "with a priority array [ Z, B ]" do + before { Chef.set_provider_priority_array two_classes_one_dsl, [ provider_class_z, provider_class ] } + + it "two_classes_one_dsl resolves to B (the next one in the priority map)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + end + + context "with priority arrays [ B ] and [ Z ]" do + before { Chef.set_provider_priority_array two_classes_one_dsl, [ provider_class_z ] } + before { Chef.set_provider_priority_array two_classes_one_dsl, [ provider_class ] } + + it "two_classes_one_dsl resolves to B (the one in the next priority map)" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + instance_eval("#{two_classes_one_dsl} 'blah'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + end + end + end + end + + context "and another resource Blarghle with provides :two_classes_one_dsl, os: 'blarghle'" do + let(:resource_class_blarghle) { + result = Class.new(BaseThingy) do + def self.name + "Blarghle" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + end + result.resource_name two_classes_one_dsl + result.provides two_classes_one_dsl, os: 'blarghle' + result + } + before { resource_class_blarghle } # pull on it so it gets defined before the recipe runs + + it "on os = blarghle, two_classes_one_dsl resolves to Blarghle" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'blarghle' + instance_eval("#{two_classes_one_dsl} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class_blarghle + end + + it "on os = linux, two_classes_one_dsl resolves to B" do + two_classes_one_dsl = self.two_classes_one_dsl + recipe = converge { + # this is an ugly way to test, make Cheffish expose node attrs + run_context.node.automatic[:os] = 'linux' + instance_eval("#{two_classes_one_dsl} 'blah' do; end") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + end + end + end + + context "with a resource MyResource" do + let(:resource_class) { Class.new(BaseThingy) do + def self.called_provides + @called_provides + end + def to_s + "MyResource" + end + end } + let(:my_resource) { :"my_resource#{Namer.current_index}" } + let(:blarghle_blarghle_little_star) { :"blarghle_blarghle_little_star#{Namer.current_index}" } + + context "with resource_name :my_resource" do + before { + resource_class.resource_name my_resource + } + + context "with provides? returning true to my_resource" do + before { + my_resource = self.my_resource + resource_class.define_singleton_method(:provides?) do |node, resource_name| + @called_provides = true + resource_name == my_resource + end + } + + it "my_resource returns the resource and calls provides?, but does not emit a warning" do + dsl_name = self.my_resource + recipe = converge { + instance_eval("#{dsl_name} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_resource).to eq resource_class + expect(resource_class.called_provides).to be_truthy + end + end + + context "with provides? returning true to blarghle_blarghle_little_star and not resource_name" do + before do + blarghle_blarghle_little_star = self.blarghle_blarghle_little_star + resource_class.define_singleton_method(:provides?) do |node, resource_name| + @called_provides = true + resource_name == blarghle_blarghle_little_star + end + end + + it "my_resource does not return the resource" do + dsl_name = self.my_resource + expect_converge { + instance_eval("#{dsl_name} 'foo'") + }.to raise_error(Chef::Exceptions::NoSuchResourceType) + expect(resource_class.called_provides).to be_truthy + end + + it "blarghle_blarghle_little_star 'foo' returns the resource and emits a warning" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + dsl_name = self.blarghle_blarghle_little_star + recipe = converge { + instance_eval("#{dsl_name} 'foo'") + } + expect(recipe.logged_warnings).to include "WARN: #{resource_class}.provides? returned true when asked if it provides DSL #{dsl_name}, but provides :#{dsl_name} was never called!" + expect(BaseThingy.created_resource).to eq resource_class + expect(resource_class.called_provides).to be_truthy + end + end + + context "and a provider" do + let(:provider_class) do + Class.new(BaseThingy::Provider) do + def self.name + "MyProvider" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + def self.called_provides + @called_provides + end + end + end + + before do + resource_class.send(:define_method, :provider) { nil } + end + + context "that provides :my_resource" do + before do + provider_class.provides my_resource + end + + context "with supports? returning true" do + before do + provider_class.define_singleton_method(:supports?) { |resource,action| true } + end + + it "my_resource runs the provider and does not emit a warning" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + + context "and another provider supporting :my_resource with supports? false" do + let(:provider_class2) do + Class.new(BaseThingy::Provider) do + def self.name + "MyProvider2" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + def self.called_provides + @called_provides + end + provides my_resource + def self.supports?(resource, action) + false + end + end + end + + it "my_resource runs the first provider" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + end + end + + context "with supports? returning false" do + before do + provider_class.define_singleton_method(:supports?) { |resource,action| false } + end + + # TODO no warning? ick + it "my_resource runs the provider anyway" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + end + + context "and another provider supporting :my_resource with supports? true" do + let(:provider_class2) do + my_resource = self.my_resource + Class.new(BaseThingy::Provider) do + def self.name + "MyProvider2" + end + def self.to_s; name; end + def self.inspect; name.inspect; end + def self.called_provides + @called_provides + end + provides my_resource + def self.supports?(resource, action) + true + end + end + end + before { provider_class2 } # make sure the provider class shows up + + it "my_resource runs the other provider" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class2 + end + end + end + end + + context "with provides? returning true" do + before { + my_resource = self.my_resource + provider_class.define_singleton_method(:provides?) do |node, resource| + @called_provides = true + resource.declared_type == my_resource + end + } + + context "that provides :my_resource" do + before { + provider_class.provides my_resource + } + + it "my_resource calls the provider (and calls provides?), but does not emit a warning" do + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to eq '' + expect(BaseThingy.created_provider).to eq provider_class + expect(provider_class.called_provides).to be_truthy + end + end + + context "that does not call provides :my_resource" do + it "my_resource calls the provider (and calls provides?), and emits a warning" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + my_resource = self.my_resource + recipe = converge { + instance_eval("#{my_resource} 'foo'") + } + expect(recipe.logged_warnings).to include("WARN: #{provider_class}.provides? returned true when asked if it provides DSL #{my_resource}, but provides :#{my_resource} was never called!") + expect(BaseThingy.created_provider).to eq provider_class + expect(provider_class.called_provides).to be_truthy + end + end + end + + context "with provides? returning false to my_resource" do + before { + my_resource = self.my_resource + provider_class.define_singleton_method(:provides?) do |node, resource| + @called_provides = true + false + end + } + + context "that provides :my_resource" do + before { + provider_class.provides my_resource + } + + it "my_resource fails to find a provider (and calls provides)" do + my_resource = self.my_resource + expect_converge { + instance_eval("#{my_resource} 'foo'") + }.to raise_error(Chef::Exceptions::ProviderNotFound) + expect(provider_class.called_provides).to be_truthy + end + end + + context "that does not provide :my_resource" do + it "my_resource fails to find a provider (and calls provides)" do + my_resource = self.my_resource + expect_converge { + instance_eval("#{my_resource} 'foo'") + }.to raise_error(Chef::Exceptions::ProviderNotFound) + expect(provider_class.called_provides).to be_truthy + end + end + end + end + end + end + end + + before(:all) { Namer.current_index = 0 } + before { Namer.current_index += 1 } + + context "with an LWRP that declares actions" do + let(:resource_class) { + Class.new(Chef::Resource::LWRPBase) do + provides :"recipe_dsl_spec#{Namer.current_index}" + actions :create + end + } + let(:resource) { + resource_class.new("blah", run_context) + } + it "The actions are part of actions along with :nothing" do + expect(resource_class.actions).to eq [ :nothing, :create ] + end + it "The actions are part of allowed_actions along with :nothing" do + expect(resource.allowed_actions).to eq [ :nothing, :create ] + end + + context "and a subclass that declares more actions" do + let(:subresource_class) { + Class.new(Chef::Resource::LWRPBase) do + provides :"recipe_dsl_spec_sub#{Namer.current_index}" + actions :delete + end + } + let(:subresource) { + subresource_class.new("subblah", run_context) + } + + it "The parent class actions are not part of actions" do + expect(subresource_class.actions).to eq [ :nothing, :delete ] + end + it "The parent class actions are not part of allowed_actions" do + expect(subresource.allowed_actions).to eq [ :nothing, :delete ] + end + it "The parent class actions do not change" do + expect(resource_class.actions).to eq [ :nothing, :create ] + expect(resource.allowed_actions).to eq [ :nothing, :create ] + end + end + end + + context "with a dynamically defined resource and regular provider" do + before(:context) do + Class.new(Chef::Resource) do + resource_name :lw_resource_with_hw_provider_test_case + default_action :create + attr_accessor :created_provider + end + class Chef::Provider::LwResourceWithHwProviderTestCase < Chef::Provider + def load_current_resource + end + def action_create + new_resource.created_provider = self.class + end + end + end + + it "looks up the provider in Chef::Provider converting the resource name from snake case to camel case" do + resource = nil + recipe = converge { + resource = lw_resource_with_hw_provider_test_case 'blah' do; end + } + expect(resource.created_provider).to eq(Chef::Provider::LwResourceWithHwProviderTestCase) end end end diff --git a/spec/integration/recipes/resource_action_spec.rb b/spec/integration/recipes/resource_action_spec.rb new file mode 100644 index 0000000000..53611c144f --- /dev/null +++ b/spec/integration/recipes/resource_action_spec.rb @@ -0,0 +1,356 @@ +require 'support/shared/integration/integration_helper' + +describe "Resource.action" do + include IntegrationSupport + + shared_context "ActionJackson" do + it "The default action is the first declared action" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + end + EOM + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "The action can access recipe DSL" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_recipe_dsl + end + EOM + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "The action can access attributes" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_attribute + end + EOM + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq 'foo!' + end + + it "The action can access public methods" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_method + expect(ActionJackson.succeeded).to eq 'foo_public!' + end + + it "The action can access protected methods" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_protected_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_protected_method + expect(ActionJackson.succeeded).to eq 'foo_protected!' + end + + it "The action cannot access private methods" do + expect { + converge(<<-EOM, __FILE__, __LINE__+1) + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_private_method + end + EOM + }.to raise_error(NameError) + expect(ActionJackson.ran_action).to eq :access_private_method + end + + it "The action cannot access resource instance variables" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_instance_variable + end + EOM + expect(ActionJackson.ran_action).to eq :access_instance_variable + expect(ActionJackson.succeeded).to be_nil + end + + it "The action does not compile until the prior resource has converged" do + converge <<-EOM, __FILE__, __LINE__+1 + ruby_block 'wow' do + block do + ActionJackson.ruby_block_converged = 'ruby_block_converged!' + end + end + + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_class_method + end + EOM + expect(ActionJackson.ran_action).to eq :access_class_method + expect(ActionJackson.succeeded).to eq 'ruby_block_converged!' + end + + it "The action's resources converge before the next resource converges" do + converge <<-EOM, __FILE__, __LINE__+1 + #{resource_dsl} 'hi' do + foo 'foo!' + action :access_attribute + end + + ruby_block 'wow' do + block do + ActionJackson.ruby_block_converged = ActionJackson.succeeded + end + end + EOM + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq 'foo!' + expect(ActionJackson.ruby_block_converged).to eq 'foo!' + end + end + + context "With resource 'action_jackson'" do + before(:context) { + class ActionJackson < Chef::Resource + use_automatic_resource_name + def foo(value=nil) + @foo = value if value + @foo + end + def blarghle(value=nil) + @blarghle = value if value + @blarghle + end + + class <<self + attr_accessor :ran_action + attr_accessor :succeeded + attr_accessor :ruby_block_converged + end + + public + def foo_public + 'foo_public!' + end + protected + def foo_protected + 'foo_protected!' + end + private + def foo_private + 'foo_private!' + end + + public + action :access_recipe_dsl do + ActionJackson.ran_action = :access_recipe_dsl + ruby_block 'hi there' do + block do + ActionJackson.succeeded = true + end + end + end + action :access_attribute do + ActionJackson.ran_action = :access_attribute + ActionJackson.succeeded = foo + ActionJackson.succeeded += " #{blarghle}" if blarghle + ActionJackson.succeeded += " #{bar}" if respond_to?(:bar) + end + action :access_attribute2 do + ActionJackson.ran_action = :access_attribute2 + ActionJackson.succeeded = foo + ActionJackson.succeeded += " #{blarghle}" if blarghle + ActionJackson.succeeded += " #{bar}" if respond_to?(:bar) + end + action :access_method do + ActionJackson.ran_action = :access_method + ActionJackson.succeeded = foo_public + end + action :access_protected_method do + ActionJackson.ran_action = :access_protected_method + ActionJackson.succeeded = foo_protected + end + action :access_private_method do + ActionJackson.ran_action = :access_private_method + ActionJackson.succeeded = foo_private + end + action :access_instance_variable do + ActionJackson.ran_action = :access_instance_variable + ActionJackson.succeeded = @foo + end + action :access_class_method do + ActionJackson.ran_action = :access_class_method + ActionJackson.succeeded = ActionJackson.ruby_block_converged + end + end + } + before(:each) { + ActionJackson.ran_action = :error + ActionJackson.succeeded = :error + ActionJackson.ruby_block_converged = :error + } + + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackson } + end + + context "And 'action_jackgrandson' inheriting from ActionJackson and changing nothing" do + before(:context) { + class ActionJackgrandson < ActionJackson + use_automatic_resource_name + end + } + + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackgrandson } + end + end + + context "And 'action_jackalope' inheriting from ActionJackson with an extra attribute and action" do + before(:context) { + class ActionJackalope < ActionJackson + use_automatic_resource_name + + def foo(value=nil) + @foo = "#{value}alope" if value + @foo + end + def bar(value=nil) + @bar = "#{value}alope" if value + @bar + end + class <<self + attr_accessor :jackalope_ran + end + action :access_jackalope do + ActionJackalope.jackalope_ran = :access_jackalope + ActionJackalope.succeeded = "#{foo} #{blarghle} #{bar}" + end + action :access_attribute do + super() + ActionJackalope.jackalope_ran = :access_attribute + ActionJackalope.succeeded = ActionJackson.succeeded + end + end + } + before do + ActionJackalope.jackalope_ran = nil + end + + context "action_jackson still behaves the same" do + it_behaves_like "ActionJackson" do + let(:resource_dsl) { :action_jackson } + end + end + + it "The default action remains the same even though new actions were specified first" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + end + } + expect(ActionJackson.ran_action).to eq :access_recipe_dsl + expect(ActionJackson.succeeded).to eq true + end + + it "new actions run, and can access overridden, new, and overridden attributes" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_jackalope + end + } + expect(ActionJackalope.jackalope_ran).to eq :access_jackalope + expect(ActionJackalope.succeeded).to eq "foo!alope blarghle! bar!alope" + end + + it "overridden actions run, call super, and can access overridden, new, and overridden attributes" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_attribute + end + } + expect(ActionJackson.ran_action).to eq :access_attribute + expect(ActionJackson.succeeded).to eq "foo!alope blarghle! bar!alope" + expect(ActionJackalope.jackalope_ran).to eq :access_attribute + expect(ActionJackalope.succeeded).to eq "foo!alope blarghle! bar!alope" + end + + it "non-overridden actions run and can access overridden and non-overridden variables (but not necessarily new ones)" do + converge { + action_jackalope 'hi' do + foo 'foo!' + bar 'bar!' + blarghle 'blarghle!' + action :access_attribute2 + end + } + expect(ActionJackson.ran_action).to eq :access_attribute2 + expect(ActionJackson.succeeded).to eq("foo!alope blarghle! bar!alope").or(eq("foo!alope blarghle!")) + end + end + end + + context "With a resource with no actions" do + before(:context) { + class NoActionJackson < Chef::Resource + use_automatic_resource_name + + def foo(value=nil) + @foo = value if value + @foo + end + + class <<self + attr_accessor :action_was + end + end + } + it "The default action is :nothing" do + converge { + no_action_jackson 'hi' do + foo 'foo!' + NoActionJackson.action_was = action + end + } + expect(NoActionJackson.action_was).to eq [:nothing] + end + end + + context "With a resource with action a-b-c d" do + before(:context) { + class WeirdActionJackson < Chef::Resource + use_automatic_resource_name + + class <<self + attr_accessor :action_was + end + + action "a-b-c d" do + WeirdActionJackson.action_was = action + end + end + } + + it "Running the action works" do + expect_recipe { + weird_action_jackson 'hi' + }.to be_up_to_date + expect(WeirdActionJackson.action_was).to eq :"a-b-c d" + end + end +end diff --git a/spec/integration/recipes/resource_converge_if_changed_spec.rb b/spec/integration/recipes/resource_converge_if_changed_spec.rb new file mode 100644 index 0000000000..d00252a717 --- /dev/null +++ b/spec/integration/recipes/resource_converge_if_changed_spec.rb @@ -0,0 +1,423 @@ +require 'support/shared/integration/integration_helper' + +describe "Resource::ActionProvider#converge_if_changed" do + include IntegrationSupport + + module Namer + extend self + attr_accessor :current_index + def incrementing_value + @incrementing_value += 1 + @incrementing_value + end + attr_writer :incrementing_value + end + + before(:all) { Namer.current_index = 1 } + before { Namer.current_index += 1 } + before { Namer.incrementing_value = 0 } + + context "when the resource has identity, state and control properties" do + let(:resource_name) { :"converge_if_changed_dsl#{Namer.current_index}" } + let(:resource_class) { + result = Class.new(Chef::Resource) do + def self.to_s; resource_name; end + def self.inspect; resource_name.inspect; end + property :identity1, identity: true, default: 'default_identity1' + property :control1, desired_state: false, default: 'default_control1' + property :state1, default: 'default_state1' + property :state2, default: 'default_state2' + attr_accessor :converged + def initialize(*args) + super + @converged = 0 + end + end + result.resource_name resource_name + result + } + let(:converged_recipe) { converge(converge_recipe) } + let(:resource) { converged_recipe.resources.first } + + context "and converge_if_changed with no parameters" do + before :each do + resource_class.action :create do + converge_if_changed do + new_resource.converged += 1 + end + end + end + + context "and current_resource with state1=current, state2=current" do + before :each do + resource_class.load_current_value do + state1 'current_state1' + state2 'current_state2' + end + end + + context "and nothing is set" do + let(:converge_recipe) { "#{resource_name} 'blah'" } + + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) + EOM + end + end + + context "and state1 is set to a new value" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + end + EOM + } + + it "the resource updates state1" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state1 to "new_state1" (was "current_state1") + EOM + end + end + + context "and state1 and state2 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + state2 'new_state2' + end + EOM + } + + it "the resource updates state1 and state2" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state1 to "new_state1" (was "current_state1") + - set state2 to "new_state2" (was "current_state2") +EOM + end + end + + context "and state1 is set to its current value but state2 is set to a new value" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'current_state1' + state2 'new_state2' + end + EOM + } + + it "the resource updates state2" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state2 to "new_state2" (was "current_state2") +EOM + end + end + + context "and state1 and state2 are set to their current values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'current_state1' + state2 'current_state2' + end + EOM + } + + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) +EOM + end + end + + context "and identity1 and control1 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + identity1 'new_identity1' + control1 'new_control1' + end + EOM + } + + # Because the identity value is copied over to the new resource, by + # default they do not register as "changed" + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) +EOM + end + end + end + + context "and current_resource with identity1=current, control1=current" do + before :each do + resource_class.load_current_value do + identity1 'current_identity1' + control1 'current_control1' + end + end + + context "and identity1 and control1 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + identity1 'new_identity1' + control1 'new_control1' + end + EOM + } + + # Control values are not desired state and are therefore not considered + # a reason for converging. + it "the resource updates identity1" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update current_identity1 + - set identity1 to "new_identity1" (was "current_identity1") + EOM + end + end + end + + context "and has no current_resource" do + before :each do + resource_class.load_current_value do + current_value_does_not_exist! + end + end + + context "and nothing is set" do + let(:converge_recipe) { "#{resource_name} 'blah'" } + + it "the resource is created" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - create default_identity1 + - set identity1 to "default_identity1" (default value) + - set state1 to "default_state1" (default value) + - set state2 to "default_state2" (default value) +EOM + end + end + + context "and state1 and state2 are set" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + state2 'new_state2' + end + EOM + } + + it "the resource is created" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - create default_identity1 + - set identity1 to "default_identity1" (default value) + - set state1 to "new_state1" + - set state2 to "new_state2" +EOM + end + end + end + end + + context "and separate converge_if_changed :state1 and converge_if_changed :state2" do + before :each do + resource_class.action :create do + converge_if_changed :state1 do + new_resource.converged += 1 + end + converge_if_changed :state2 do + new_resource.converged += 1 + end + end + end + + context "and current_resource with state1=current, state2=current" do + before :each do + resource_class.load_current_value do + state1 'current_state1' + state2 'current_state2' + end + end + + context "and nothing is set" do + let(:converge_recipe) { "#{resource_name} 'blah'" } + + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) +EOM + end + end + + context "and state1 is set to a new value" do + + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + end + EOM + } + + it "the resource updates state1" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state1 to "new_state1" (was "current_state1") +EOM + end + end + + context "and state1 and state2 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + state2 'new_state2' + end + EOM + } + + it "the resource updates state1 and state2" do + expect(resource.converged).to eq 2 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state1 to "new_state1" (was "current_state1") + - update default_identity1 + - set state2 to "new_state2" (was "current_state2") +EOM + end + end + + context "and state1 is set to its current value but state2 is set to a new value" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'current_state1' + state2 'new_state2' + end + EOM + } + + it "the resource updates state2" do + expect(resource.converged).to eq 1 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - update default_identity1 + - set state2 to "new_state2" (was "current_state2") +EOM + end + end + + context "and state1 and state2 are set to their current values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'current_state1' + state2 'current_state2' + end + EOM + } + + it "the resource updates nothing" do + expect(resource.converged).to eq 0 + expect(resource.updated?).to be_falsey + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create (up to date) +EOM + end + end + end + + context "and no current_resource" do + before :each do + resource_class.load_current_value do + current_value_does_not_exist! + end + end + + context "and nothing is set" do + let(:converge_recipe) { + "#{resource_name} 'blah'" + } + + it "the resource is created" do + expect(resource.converged).to eq 2 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - create default_identity1 + - set state1 to "default_state1" (default value) + - create default_identity1 + - set state2 to "default_state2" (default value) +EOM + end + end + + context "and state1 and state2 are set to new values" do + let(:converge_recipe) { + <<-EOM + #{resource_name} 'blah' do + state1 'new_state1' + state2 'new_state2' + end + EOM + } + + it "the resource is created" do + expect(resource.converged).to eq 2 + expect(resource.updated?).to be_truthy + expect(converged_recipe.stdout).to eq <<-EOM +* #{resource_name}[blah] action create + - create default_identity1 + - set state1 to "new_state1" + - create default_identity1 + - set state2 to "new_state2" +EOM + end + end + end + end + + end +end diff --git a/spec/integration/recipes/resource_load_spec.rb b/spec/integration/recipes/resource_load_spec.rb new file mode 100644 index 0000000000..c29b877b59 --- /dev/null +++ b/spec/integration/recipes/resource_load_spec.rb @@ -0,0 +1,206 @@ +require 'support/shared/integration/integration_helper' + +describe "Resource.load_current_value" do + include IntegrationSupport + + module Namer + extend self + attr_accessor :current_index + def incrementing_value + @incrementing_value += 1 + @incrementing_value + end + attr_writer :incrementing_value + end + + before(:all) { Namer.current_index = 1 } + before { Namer.current_index += 1 } + before { Namer.incrementing_value = 0 } + + let(:resource_name) { :"load_current_value_dsl#{Namer.current_index}" } + let(:resource_class) { + result = Class.new(Chef::Resource) do + def self.to_s; resource_name; end + def self.inspect; resource_name.inspect; end + property :x, default: lazy { "default #{Namer.incrementing_value}" } + def self.created_x=(value) + @created = value + end + def self.created_x + @created + end + action :create do + new_resource.class.created_x = x + end + end + result.resource_name resource_name + result + } + + # Pull on resource_class to initialize it + before { resource_class } + + context "with a resource with load_current_value" do + before :each do + resource_class.load_current_value do + x "loaded #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + end + + context "and a resource with x set to a desired value" do + let(:resource) do + e = self + r = nil + converge { + r = public_send(e.resource_name, 'blah') do + x 'desired' + end + } + r + end + + it "current_resource is passed name but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + end + + it "resource.current_resource returns a different resource" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + expect(resource.x).to eq 'desired' + end + + it "resource.current_resource constructs the resource anew each time" do + expect(resource.current_resource.x).to eq 'loaded 2 (name=blah)' + expect(resource.current_resource.x).to eq 'loaded 3 (name=blah)' + end + + it "the provider accesses the current value of x" do + expect(resource.class.created_x).to eq 'desired' + end + + context "and identity: :i and :d with desired_state: false" do + before { + resource_class.class_eval do + property :i, identity: true + property :d, desired_state: false + end + } + + before { + resource.i 'desired_i' + resource.d 'desired_d' + } + + it "i, name and d are passed to load_current_value, but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (d=desired_d, i=desired_i, name=blah)' + end + end + + context "and name_property: :i and :d with desired_state: false" do + before { + resource_class.class_eval do + property :i, name_property: true + property :d, desired_state: false + end + } + + before { + resource.i 'desired_i' + resource.d 'desired_d' + } + + it "i, name and d are passed to load_current_value, but not x" do + expect(resource.current_resource.x).to eq 'loaded 2 (d=desired_d, i=desired_i, name=blah)' + end + end + end + + context "and a resource with no values set" do + let(:resource) do + e = self + r = nil + converge { + r = public_send(e.resource_name, 'blah') do + end + } + r + end + + it "the provider accesses values from load_current_value" do + expect(resource.class.created_x).to eq 'loaded 1 (name=blah)' + end + end + + let (:subresource_name) { + :"load_current_value_subresource_dsl#{Namer.current_index}" + } + let (:subresource_class) { + r = Class.new(resource_class) do + property :y, default: lazy { "default_y #{Namer.incrementing_value}" } + end + r.resource_name subresource_name + r + } + + # Pull on subresource_class to initialize it + before { subresource_class } + + let(:subresource) do + e = self + r = nil + converge { + r = public_send(e.subresource_name, 'blah') do + x 'desired' + end + } + r + end + + context "and a child resource class with no load_current_value" do + it "the parent load_current_value is used" do + expect(subresource.current_resource.x).to eq 'loaded 2 (name=blah)' + end + it "load_current_value yields a copy of the child class" do + expect(subresource.current_resource).to be_kind_of(subresource_class) + end + end + + context "And a child resource class with load_current_value" do + before { + subresource_class.load_current_value do + y "loaded_y #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + } + + it "the overridden load_current_value is used" do + current_resource = subresource.current_resource + expect(current_resource.x).to eq 'default 3' + expect(current_resource.y).to eq 'loaded_y 2 (name=blah)' + end + end + + context "and a child resource class with load_current_value calling super()" do + before { + subresource_class.load_current_value do + super() + y "loaded_y #{Namer.incrementing_value} (#{self.class.properties.sort_by { |name,p| name }. + select { |name,p| p.is_set?(self) }. + map { |name,p| "#{name}=#{p.get(self)}" }. + join(", ") })" + end + } + + it "the original load_current_value is called as well as the child one" do + current_resource = subresource.current_resource + expect(current_resource.x).to eq 'loaded 3 (name=blah)' + expect(current_resource.y).to eq 'loaded_y 4 (name=blah, x=loaded 3 (name=blah))' + end + end + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index dcf244c3cc..aadf55f64b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -87,12 +87,14 @@ Dir["spec/support/**/*.rb"]. OHAI_SYSTEM = Ohai::System.new OHAI_SYSTEM.all_plugins("platform") -TEST_PLATFORM = - (OHAI_SYSTEM['platform'] || - 'unknown_test_platform').dup.freeze -TEST_PLATFORM_VERSION = - (OHAI_SYSTEM['platform_version'] || - 'unknown_platform_version').dup.freeze +test_node = Chef::Node.new +test_node.automatic['os'] = (OHAI_SYSTEM['os'] || 'unknown_os').dup.freeze +test_node.automatic['platform_family'] = (OHAI_SYSTEM['platform_family'] || 'unknown_platform_family').dup.freeze +test_node.automatic['platform'] = (OHAI_SYSTEM['platform'] || 'unknown_platform').dup.freeze +test_node.automatic['platform_version'] = (OHAI_SYSTEM['platform_version'] || 'unknown_platform_version').dup.freeze +TEST_NODE = test_node.freeze +TEST_PLATFORM = TEST_NODE['platform'] +TEST_PLATFORM_VERSION = TEST_NODE['platform_version'] RSpec.configure do |config| config.include(Matchers) @@ -116,6 +118,7 @@ RSpec.configure do |config| config.filter_run_excluding :volatile_from_verify => false config.filter_run_excluding :skip_appveyor => true if ENV["APPVEYOR"] + config.filter_run_excluding :appveyor_only => true unless ENV["APPVEYOR"] config.filter_run_excluding :windows_only => true unless windows? config.filter_run_excluding :not_supported_on_mac_osx_106 => true if mac_osx_106? @@ -127,6 +130,8 @@ RSpec.configure do |config| config.filter_run_excluding :windows_2008r2_or_later => true unless windows_2008r2_or_later? config.filter_run_excluding :windows64_only => true unless windows64? config.filter_run_excluding :windows32_only => true unless windows32? + config.filter_run_excluding :ruby64_only => true unless ruby_64bit? + config.filter_run_excluding :ruby32_only => true unless ruby_32bit? config.filter_run_excluding :windows_powershell_dsc_only => true unless windows_powershell_dsc? config.filter_run_excluding :windows_powershell_no_dsc_only => true unless ! windows_powershell_dsc? config.filter_run_excluding :windows_domain_joined_only => true unless windows_domain_joined? @@ -162,13 +167,17 @@ RSpec.configure do |config| config.filter_run_excluding :provider => lambda {|criteria| type, target_provider = criteria.first - platform = TEST_PLATFORM.dup - platform_version = TEST_PLATFORM_VERSION.dup - - begin - provider_for_running_platform = Chef::Platform.find_provider(platform, platform_version, type) - provider_for_running_platform != target_provider - rescue ArgumentError # no provider for platform + node = TEST_NODE.dup + resource_class = Chef::ResourceResolver.resolve(type, node: node) + if resource_class + resource = resource_class.new('test', Chef::RunContext.new(node, nil, nil)) + begin + provider = resource.provider_for_action(Array(resource_class.default_action).first) + provider.class != target_provider + rescue Chef::Exceptions::ProviderNotFound # no provider for platform + true + end + else true end } @@ -176,6 +185,8 @@ RSpec.configure do |config| config.run_all_when_everything_filtered = true config.before(:each) do + Chef.reset! + Chef::Config.reset # By default, treat deprecation warnings as errors in tests. diff --git a/spec/support/platform_helpers.rb b/spec/support/platform_helpers.rb index da0313758b..1cfad05172 100644 --- a/spec/support/platform_helpers.rb +++ b/spec/support/platform_helpers.rb @@ -26,6 +26,14 @@ def ruby_20? !!(RUBY_VERSION =~ /^2.0/) end +def ruby_64bit? + !!(RbConfig::CONFIG['host_cpu'] =~ /x86_64/) +end + +def ruby_32bit? + !!(RbConfig::CONFIG['host_cpu'] =~ /i686/) +end + def windows? !!(RUBY_PLATFORM =~ /mswin|mingw|windows/) end diff --git a/spec/support/shared/functional/win32_service.rb b/spec/support/shared/functional/win32_service.rb index 7dd1920418..2ee1a8ad88 100644 --- a/spec/support/shared/functional/win32_service.rb +++ b/spec/support/shared/functional/win32_service.rb @@ -46,7 +46,8 @@ shared_context "using Win32::Service" do :service_name => "spec-service", :service_display_name => "Spec Test Service", :service_description => "Service for testing Chef::Application::WindowsServiceManager.", - :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../platforms/win32/spec_service.rb')) + :service_file_path => File.expand_path(File.join(File.dirname(__FILE__), '../../platforms/win32/spec_service.rb')), + :delayed_start => true } } diff --git a/spec/support/shared/shared_examples.rb b/spec/support/shared/shared_examples.rb index b20c65f8b6..550fa2eb68 100644 --- a/spec/support/shared/shared_examples.rb +++ b/spec/support/shared/shared_examples.rb @@ -1,7 +1,7 @@ # For storing any examples shared between multiple tests # Any object which defines a .to_json should import this test -shared_examples "to_json equalivent to Chef::JSONCompat.to_json" do +shared_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { raise "You must define the subject when including this test" diff --git a/spec/support/shared/unit/api_versioning.rb b/spec/support/shared/unit/api_versioning.rb index a4f353de60..05a4117f8e 100644 --- a/spec/support/shared/unit/api_versioning.rb +++ b/spec/support/shared/unit/api_versioning.rb @@ -26,7 +26,7 @@ shared_examples_for "version handling" do allow(rest_v1).to receive(http_verb).and_raise(exception_406) end - context "when the server does not support the min or max server API version that Chef::User supports" do + context "when the server does not support the min or max server API version that Chef::UserV1 supports" do before do allow(object).to receive(:server_client_api_version_intersection).and_return([]) end @@ -34,7 +34,7 @@ shared_examples_for "version handling" do it "raises the original exception" do expect{ object.send(method) }.to raise_error(exception_406) end - end # when the server does not support the min or max server API version that Chef::User supports + end # when the server does not support the min or max server API version that Chef::UserV1 supports end # version handling shared_examples_for "user and client reregister" do diff --git a/spec/unit/api_client_spec.rb b/spec/unit/api_client_spec.rb index ba0eca3284..a0e399b470 100644 --- a/spec/unit/api_client_spec.rb +++ b/spec/unit/api_client_spec.rb @@ -21,6 +21,11 @@ require 'spec_helper' require 'chef/api_client' require 'tempfile' +# DEPRECATION NOTE +# +# This code will be removed in Chef 13 in favor of the code in Chef::ApiClientV1, +# which will be moved to this namespace. New development should occur in +# Chef::ApiClientV1 until the time before Chef 13. describe Chef::ApiClient do before(:each) do @client = Chef::ApiClient.new @@ -53,20 +58,6 @@ describe Chef::ApiClient do expect { @client.admin(Hash.new) }.to raise_error(ArgumentError) end - it "has an create_key flag attribute" do - @client.create_key(true) - expect(@client.create_key).to be_truthy - end - - it "create_key defaults to false" do - expect(@client.create_key).to be_falsey - end - - it "allows only boolean values for the create_key flag" do - expect { @client.create_key(false) }.not_to raise_error - expect { @client.create_key(Hash.new) }.to raise_error(ArgumentError) - end - it "has a 'validator' flag attribute" do @client.validator(true) expect(@client.validator).to be_truthy @@ -129,12 +120,6 @@ describe Chef::ApiClient do expect(@json).to include(%q{"validator":false}) end - it "includes the 'create_key' flag when present" do - @client.create_key(true) - @json = @client.to_json - expect(@json).to include(%q{"create_key":true}) - end - it "includes the private key when present" do @client.private_key("monkeypants") expect(@client.to_json).to include(%q{"private_key":"monkeypants"}) @@ -143,15 +128,11 @@ describe Chef::ApiClient do it "does not include the private key if not present" do expect(@json).not_to include("private_key") end - - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do - let(:jsonable) { @client } - end end describe "when deserializing from JSON (string) using ApiClient#from_json" do let(:client_string) do - "{\"name\":\"black\",\"public_key\":\"crowes\",\"private_key\":\"monkeypants\",\"admin\":true,\"validator\":true,\"create_key\":true}" + "{\"name\":\"black\",\"public_key\":\"crowes\",\"private_key\":\"monkeypants\",\"admin\":true,\"validator\":true}" end let(:client) do @@ -178,10 +159,6 @@ describe Chef::ApiClient do expect(client.admin).to be_truthy end - it "preserves the create_key status" do - expect(client.create_key).to be_truthy - end - it "preserves the 'validator' status" do expect(client.validator).to be_truthy end @@ -199,7 +176,6 @@ describe Chef::ApiClient do "private_key" => "monkeypants", "admin" => true, "validator" => true, - "create_key" => true, "json_class" => "Chef::ApiClient" } end @@ -224,10 +200,6 @@ describe Chef::ApiClient do expect(client.admin).to be_truthy end - it "preserves the create_key status" do - expect(client.create_key).to be_truthy - end - it "preserves the 'validator' status" do expect(client.validator).to be_truthy end @@ -243,18 +215,16 @@ describe Chef::ApiClient do before(:each) do client = { - "name" => "black", - "clientname" => "black", - "public_key" => "crowes", - "private_key" => "monkeypants", - "admin" => true, - "create_key" => true, - "validator" => true, - "json_class" => "Chef::ApiClient" + "name" => "black", + "clientname" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "validator" => true, + "json_class" => "Chef::ApiClient" } - - @http_client = double("Chef::REST mock") - allow(Chef::REST).to receive(:new).and_return(@http_client) + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) expect(@http_client).to receive(:get).with("clients/black").and_return(client) @client = Chef::ApiClient.load(client['name']) end @@ -275,10 +245,6 @@ describe Chef::ApiClient do expect(@client.admin).to be_a_kind_of(TrueClass) end - it "preserves the create_key status" do - expect(@client.create_key).to be_a_kind_of(TrueClass) - end - it "preserves the 'validator' status" do expect(@client.validator).to be_a_kind_of(TrueClass) end @@ -304,18 +270,13 @@ describe Chef::ApiClient do File.open(Chef::Config[:client_key], "r") {|f| f.read.chomp } end - it "has an HTTP client configured with default credentials" do - expect(@client.http_api).to be_a_kind_of(Chef::REST) - expect(@client.http_api.client_name).to eq("silent-bob") - expect(@client.http_api.signing_key.to_s).to eq(private_key_data) - end end describe "when requesting a new key" do before do @http_client = double("Chef::REST mock") - allow(Chef::REST).to receive(:new).and_return(@http_client) + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) end context "and the client does not exist on the server" do @@ -332,34 +293,24 @@ describe Chef::ApiClient do end context "and the client exists" do - let(:chef_rest_v0_mock) { double('chef rest root v0 object') } - let(:payload) { - {:name => "lost-my-key", :admin => false, :validator => false, :private_key => true} - } - before do @api_client_without_key = Chef::ApiClient.new @api_client_without_key.name("lost-my-key") - allow(@api_client_without_key).to receive(:chef_rest_v0).and_return(chef_rest_v0_mock) - #allow(@api_client_with_key).to receive(:http_api).and_return(_api_mock) - - allow(chef_rest_v0_mock).to receive(:put).with("clients/lost-my-key", payload).and_return(@api_client_with_key) - allow(chef_rest_v0_mock).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) - allow(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) + expect(@http_client).to receive(:get).with("clients/lost-my-key").and_return(@api_client_without_key) end + context "and the client exists on a Chef 11-like server" do before do @api_client_with_key = Chef::ApiClient.new @api_client_with_key.name("lost-my-key") @api_client_with_key.private_key("the new private key") - allow(@api_client_with_key).to receive(:chef_rest_v0).and_return(chef_rest_v0_mock) + expect(@http_client).to receive(:put). + with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true). + and_return(@api_client_with_key) end it "returns an ApiClient with a private key" do - expect(chef_rest_v0_mock).to receive(:put).with("clients/lost-my-key", payload). - and_return(@api_client_with_key) - response = Chef::ApiClient.reregister("lost-my-key") # no sane == method for ApiClient :'( expect(response).to eq(@api_client_without_key) @@ -372,7 +323,7 @@ describe Chef::ApiClient do context "and the client exists on a Chef 10-like server" do before do @api_client_with_key = {"name" => "lost-my-key", "private_key" => "the new private key"} - expect(chef_rest_v0_mock).to receive(:put). + expect(@http_client).to receive(:put). with("clients/lost-my-key", :name => "lost-my-key", :admin => false, :validator => false, :private_key => true). and_return(@api_client_with_key) end @@ -390,134 +341,4 @@ describe Chef::ApiClient do end end - - describe "Versioned API Interactions" do - let(:response_406) { OpenStruct.new(:code => '406') } - let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } - let(:payload) { - { - :name => "some_name", - :validator => true, - :admin => true - } - } - - before do - @client = Chef::ApiClient.new - allow(@client).to receive(:chef_rest_v0).and_return(double('chef rest root v0 object')) - allow(@client).to receive(:chef_rest_v1).and_return(double('chef rest root v1 object')) - @client.name "some_name" - @client.validator true - @client.admin true - end - - describe "create" do - - # from spec/support/shared/unit/user_and_client_shared.rb - it_should_behave_like "user or client create" do - let(:object) { @client } - let(:error) { Chef::Exceptions::InvalidClientAttribute } - let(:rest_v0) { @client.chef_rest_v0 } - let(:rest_v1) { @client.chef_rest_v1 } - let(:url) { "clients" } - end - - context "when API V1 is not supported by the server" do - # from spec/support/shared/unit/api_versioning.rb - it_should_behave_like "version handling" do - let(:object) { @client } - let(:method) { :create } - let(:http_verb) { :post } - let(:rest_v1) { @client.chef_rest_v1 } - end - end - - end # create - - describe "update" do - context "when a valid client is defined" do - - shared_examples_for "client updating" do - it "updates the client" do - expect(rest). to receive(:put).with("clients/some_name", payload) - @client.update - end - - context "when only the name field exists" do - - before do - # needed since there is no way to set to nil via code - @client.instance_variable_set(:@validator, nil) - @client.instance_variable_set(:@admin, nil) - end - - after do - @client.validator true - @client.admin true - end - - it "updates the client with only the name" do - expect(rest). to receive(:put).with("clients/some_name", {:name => "some_name"}) - @client.update - end - end - - end - - context "when API V1 is supported by the server" do - - it_should_behave_like "client updating" do - let(:rest) { @client.chef_rest_v1 } - end - - end # when API V1 is supported by the server - - context "when API V1 is not supported by the server" do - context "when no version is supported" do - # from spec/support/shared/unit/api_versioning.rb - it_should_behave_like "version handling" do - let(:object) { @client } - let(:method) { :create } - let(:http_verb) { :post } - let(:rest_v1) { @client.chef_rest_v1 } - end - end # when no version is supported - - context "when API V0 is supported" do - - before do - allow(@client.chef_rest_v1).to receive(:put).and_raise(exception_406) - allow(@client).to receive(:server_client_api_version_intersection).and_return([0]) - end - - it_should_behave_like "client updating" do - let(:rest) { @client.chef_rest_v0 } - end - - end - - end # when API V1 is not supported by the server - end # when a valid client is defined - end # update - - # DEPRECATION - # This can be removed after API V0 support is gone - describe "reregister" do - context "when server API V0 is valid on the Chef Server receiving the request" do - it "creates a new object via the API" do - expect(@client.chef_rest_v0).to receive(:put).with("clients/#{@client.name}", payload.merge({:private_key => true})).and_return({}) - @client.reregister - end - end # when server API V0 is valid on the Chef Server receiving the request - - context "when server API V0 is not supported by the Chef Server" do - # from spec/support/shared/unit/api_versioning.rb - it_should_behave_like "user and client reregister" do - let(:object) { @client } - let(:rest_v0) { @client.chef_rest_v0 } - end - end # when server API V0 is not supported by the Chef Server - end # reregister - - end end diff --git a/spec/unit/api_client_v1_spec.rb b/spec/unit/api_client_v1_spec.rb new file mode 100644 index 0000000000..17aba8c3af --- /dev/null +++ b/spec/unit/api_client_v1_spec.rb @@ -0,0 +1,457 @@ +# +# Author:: Adam Jacob (<adam@opscode.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/api_client_v1' +require 'tempfile' + +describe Chef::ApiClientV1 do + before(:each) do + @client = Chef::ApiClientV1.new + end + + it "has a name attribute" do + @client.name("ops_master") + expect(@client.name).to eq("ops_master") + end + + it "does not allow spaces in the name" do + expect { @client.name "ops master" }.to raise_error(ArgumentError) + end + + it "only allows string values for the name" do + expect { @client.name Hash.new }.to raise_error(ArgumentError) + end + + it "has an admin flag attribute" do + @client.admin(true) + expect(@client.admin).to be_truthy + end + + it "defaults to non-admin" do + expect(@client.admin).to be_falsey + end + + it "allows only boolean values for the admin flag" do + expect { @client.admin(false) }.not_to raise_error + expect { @client.admin(Hash.new) }.to raise_error(ArgumentError) + end + + it "has an create_key flag attribute" do + @client.create_key(true) + expect(@client.create_key).to be_truthy + end + + it "create_key defaults to false" do + expect(@client.create_key).to be_falsey + end + + it "allows only boolean values for the create_key flag" do + expect { @client.create_key(false) }.not_to raise_error + expect { @client.create_key(Hash.new) }.to raise_error(ArgumentError) + end + + it "has a 'validator' flag attribute" do + @client.validator(true) + expect(@client.validator).to be_truthy + end + + it "defaults to non-validator" do + expect(@client.validator).to be_falsey + end + + it "allows only boolean values for the 'validator' flag" do + expect { @client.validator(false) }.not_to raise_error + expect { @client.validator(Hash.new) }.to raise_error(ArgumentError) + end + + it "has a public key attribute" do + @client.public_key("super public") + expect(@client.public_key).to eq("super public") + end + + it "accepts only String values for the public key" do + expect { @client.public_key "" }.not_to raise_error + expect { @client.public_key Hash.new }.to raise_error(ArgumentError) + end + + + it "has a private key attribute" do + @client.private_key("super private") + expect(@client.private_key).to eq("super private") + end + + it "accepts only String values for the private key" do + expect { @client.private_key "" }.not_to raise_error + expect { @client.private_key Hash.new }.to raise_error(ArgumentError) + end + + describe "when serializing to JSON" do + before(:each) do + @client.name("black") + @client.public_key("crowes") + @json = @client.to_json + end + + it "serializes as a JSON object" do + expect(@json).to match(/^\{.+\}$/) + end + + it "includes the name value" do + expect(@json).to include(%q{"name":"black"}) + end + + it "includes the public key value" do + expect(@json).to include(%{"public_key":"crowes"}) + end + + it "includes the 'admin' flag" do + expect(@json).to include(%q{"admin":false}) + end + + it "includes the 'validator' flag" do + expect(@json).to include(%q{"validator":false}) + end + + it "includes the 'create_key' flag when present" do + @client.create_key(true) + @json = @client.to_json + expect(@json).to include(%q{"create_key":true}) + end + + it "includes the private key when present" do + @client.private_key("monkeypants") + expect(@client.to_json).to include(%q{"private_key":"monkeypants"}) + end + + it "does not include the private key if not present" do + expect(@json).not_to include("private_key") + end + + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do + let(:jsonable) { @client } + end + end + + describe "when deserializing from JSON (string) using ApiClient#from_json" do + let(:client_string) do + "{\"name\":\"black\",\"public_key\":\"crowes\",\"private_key\":\"monkeypants\",\"admin\":true,\"validator\":true,\"create_key\":true}" + end + + let(:client) do + Chef::ApiClientV1.from_json(client_string) + end + + it "does not require a 'json_class' string" do + expect(Chef::JSONCompat.parse(client_string)["json_class"]).to eq(nil) + end + + it "should deserialize to a Chef::ApiClientV1 object" do + expect(client).to be_a_kind_of(Chef::ApiClientV1) + end + + it "preserves the name" do + expect(client.name).to eq("black") + end + + it "preserves the public key" do + expect(client.public_key).to eq("crowes") + end + + it "preserves the admin status" do + expect(client.admin).to be_truthy + end + + it "preserves the create_key status" do + expect(client.create_key).to be_truthy + end + + it "preserves the 'validator' status" do + expect(client.validator).to be_truthy + end + + it "includes the private key if present" do + expect(client.private_key).to eq("monkeypants") + end + end + + describe "when deserializing from JSON (hash) using ApiClientV1#from_json" do + let(:client_hash) do + { + "name" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "validator" => true, + "create_key" => true + } + end + + let(:client) do + Chef::ApiClientV1.from_json(Chef::JSONCompat.to_json(client_hash)) + end + + it "should deserialize to a Chef::ApiClientV1 object" do + expect(client).to be_a_kind_of(Chef::ApiClientV1) + end + + it "preserves the name" do + expect(client.name).to eq("black") + end + + it "preserves the public key" do + expect(client.public_key).to eq("crowes") + end + + it "preserves the admin status" do + expect(client.admin).to be_truthy + end + + it "preserves the create_key status" do + expect(client.create_key).to be_truthy + end + + it "preserves the 'validator' status" do + expect(client.validator).to be_truthy + end + + it "includes the private key if present" do + expect(client.private_key).to eq("monkeypants") + end + end + + describe "when loading from JSON" do + before do + end + + before(:each) do + client = { + "name" => "black", + "clientname" => "black", + "public_key" => "crowes", + "private_key" => "monkeypants", + "admin" => true, + "create_key" => true, + "validator" => true + } + + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) + expect(@http_client).to receive(:get).with("clients/black").and_return(client) + @client = Chef::ApiClientV1.load(client['name']) + end + + it "should deserialize to a Chef::ApiClientV1 object" do + expect(@client).to be_a_kind_of(Chef::ApiClientV1) + end + + it "preserves the name" do + expect(@client.name).to eq("black") + end + + it "preserves the public key" do + expect(@client.public_key).to eq("crowes") + end + + it "preserves the admin status" do + expect(@client.admin).to be_a_kind_of(TrueClass) + end + + it "preserves the create_key status" do + expect(@client.create_key).to be_a_kind_of(TrueClass) + end + + it "preserves the 'validator' status" do + expect(@client.validator).to be_a_kind_of(TrueClass) + end + + it "includes the private key if present" do + expect(@client.private_key).to eq("monkeypants") + end + + end + + describe "with correctly configured API credentials" do + before do + Chef::Config[:node_name] = "silent-bob" + Chef::Config[:client_key] = File.expand_path('ssl/private_key.pem', CHEF_SPEC_DATA) + end + + after do + Chef::Config[:node_name] = nil + Chef::Config[:client_key] = nil + end + + let :private_key_data do + File.open(Chef::Config[:client_key], "r") {|f| f.read.chomp } + end + + end + + + describe "when requesting a new key" do + before do + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) + end + + context "and the client does not exist on the server" do + before do + @a_404_response = Net::HTTPNotFound.new("404 not found and such", nil, nil) + @a_404_exception = Net::HTTPServerException.new("404 not found exception", @a_404_response) + + expect(@http_client).to receive(:get).with("clients/lost-my-key").and_raise(@a_404_exception) + end + + it "raises a 404 error" do + expect { Chef::ApiClientV1.reregister("lost-my-key") }.to raise_error(Net::HTTPServerException) + end + end + end + + describe "Versioned API Interactions" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + let(:payload) { + { + :name => "some_name", + :validator => true, + :admin => true + } + } + + before do + @client = Chef::ApiClientV1.new + allow(@client).to receive(:chef_rest_v0).and_return(double('chef rest root v0 object')) + allow(@client).to receive(:chef_rest_v1).and_return(double('chef rest root v1 object')) + @client.name "some_name" + @client.validator true + @client.admin true + end + + describe "create" do + + # from spec/support/shared/unit/user_and_client_shared.rb + it_should_behave_like "user or client create" do + let(:object) { @client } + let(:error) { Chef::Exceptions::InvalidClientAttribute } + let(:rest_v0) { @client.chef_rest_v0 } + let(:rest_v1) { @client.chef_rest_v1 } + let(:url) { "clients" } + end + + context "when API V1 is not supported by the server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @client } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @client.chef_rest_v1 } + end + end + + end # create + + describe "update" do + context "when a valid client is defined" do + + shared_examples_for "client updating" do + it "updates the client" do + expect(rest). to receive(:put).with("clients/some_name", payload).and_return(payload) + @client.update + end + + context "when only the name field exists" do + + before do + # needed since there is no way to set to nil via code + @client.instance_variable_set(:@validator, nil) + @client.instance_variable_set(:@admin, nil) + end + + after do + @client.validator true + @client.admin true + end + + it "updates the client with only the name" do + expect(rest). to receive(:put).with("clients/some_name", {:name => "some_name"}).and_return({:name => "some_name"}) + @client.update + end + end + + end + + context "when API V1 is supported by the server" do + + it_should_behave_like "client updating" do + let(:rest) { @client.chef_rest_v1 } + end + + end # when API V1 is supported by the server + + context "when API V1 is not supported by the server" do + context "when no version is supported" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @client } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @client.chef_rest_v1 } + end + end # when no version is supported + + context "when API V0 is supported" do + + before do + allow(@client.chef_rest_v1).to receive(:put).and_raise(exception_406) + allow(@client).to receive(:server_client_api_version_intersection).and_return([0]) + end + + it_should_behave_like "client updating" do + let(:rest) { @client.chef_rest_v0 } + end + + end + + end # when API V1 is not supported by the server + end # when a valid client is defined + end # update + + # DEPRECATION + # This can be removed after API V0 support is gone + describe "reregister" do + context "when server API V0 is valid on the Chef Server receiving the request" do + it "creates a new object via the API" do + expect(@client.chef_rest_v0).to receive(:put).with("clients/#{@client.name}", payload.merge({:private_key => true})).and_return({}) + @client.reregister + end + end # when server API V0 is valid on the Chef Server receiving the request + + context "when server API V0 is not supported by the Chef Server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "user and client reregister" do + let(:object) { @client } + let(:rest_v0) { @client.chef_rest_v0 } + end + end # when server API V0 is not supported by the Chef Server + end # reregister + + end +end diff --git a/spec/unit/application/solo_spec.rb b/spec/unit/application/solo_spec.rb index 1785ecfc86..7013bfa0bc 100644 --- a/spec/unit/application/solo_spec.rb +++ b/spec/unit/application/solo_spec.rb @@ -106,7 +106,8 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config describe "when the recipe_url configuration option is specified" do let(:tarfile) { StringIO.new("remote_tarball_content") } let(:target_file) { StringIO.new } - + let(:shellout) { double(run_command: nil, error!: nil, stdout: '') } + before do Chef::Config[:cookbook_path] = "#{Dir.tmpdir}/chef-solo/cookbooks" Chef::Config[:recipe_url] = "http://junglist.gen.nz/recipes.tgz" @@ -117,7 +118,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config allow(app).to receive(:open).with("http://junglist.gen.nz/recipes.tgz").and_yield(tarfile) allow(File).to receive(:open).with("#{Dir.tmpdir}/chef-solo/recipes.tgz", "wb").and_yield(target_file) - allow(Chef::Mixin::Command).to receive(:run_command).and_return(true) + allow(Mixlib::ShellOut).to receive(:new).and_return(shellout) end it "should create the recipes path based on the parent of the cookbook path" do @@ -136,7 +137,7 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config end it "should untar the target file to the parent of the cookbook path" do - expect(Chef::Mixin::Command).to receive(:run_command).with({:command => "tar zxvf #{Dir.tmpdir}/chef-solo/recipes.tgz -C #{Dir.tmpdir}/chef-solo"}).and_return(true) + expect(Mixlib::ShellOut).to receive(:new).with("tar zxvf #{Dir.tmpdir}/chef-solo/recipes.tgz -C #{Dir.tmpdir}/chef-solo") app.reconfigure end end diff --git a/spec/unit/chef_class_spec.rb b/spec/unit/chef_class_spec.rb index 2528246be6..f1b877520c 100644 --- a/spec/unit/chef_class_spec.rb +++ b/spec/unit/chef_class_spec.rb @@ -46,10 +46,6 @@ describe "Chef class" do Chef.set_provider_priority_map(provider_priority_map) end - after do - Chef.reset! - end - context "priority maps" do context "#get_provider_priority_array" do it "should use the current node to get the right priority_map" do @@ -88,4 +84,27 @@ describe "Chef class" do expect(Chef.node).to eql(node) end end + + context '#event_handler' do + it 'adds a new handler' do + x = 1 + Chef.event_handler do + on :converge_start do + x = 2 + end + end + expect(Chef::Config[:event_handlers]).to_not be_empty + Chef::Config[:event_handlers].first.send(:converge_start) + expect(x).to eq(2) + end + + it 'raise error if unknown event type is passed' do + expect do + Chef.event_handler do + on :yolo do + end + end + end.to raise_error(Chef::Exceptions::InvalidEventType) + end + end end diff --git a/spec/unit/chef_fs/path_util_spec.rb b/spec/unit/chef_fs/path_util_spec.rb new file mode 100644 index 0000000000..42eb126dfb --- /dev/null +++ b/spec/unit/chef_fs/path_util_spec.rb @@ -0,0 +1,108 @@ +# +# Author:: Kartik Null Cating-Subramanian (<ksubramanian@chef.io>) +# Copyright:: Copyright (c) 2015 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/chef_fs/path_utils' + +describe Chef::ChefFS::PathUtils do + context 'invoking join' do + it 'joins well-behaved distinct path elements' do + expect(Chef::ChefFS::PathUtils.join('a', 'b', 'c')).to eq('a/b/c') + end + + it 'strips extraneous slashes in the middle of paths' do + expect(Chef::ChefFS::PathUtils.join('a/', '/b', '/c/')).to eq('a/b/c') + expect(Chef::ChefFS::PathUtils.join('a/', '/b', '///c/')).to eq('a/b/c') + end + + it 'preserves the whether the first element was absolute or not' do + expect(Chef::ChefFS::PathUtils.join('/a/', '/b', 'c/')).to eq('/a/b/c') + expect(Chef::ChefFS::PathUtils.join('///a/', '/b', 'c/')).to eq('/a/b/c') + end + end + + context 'invoking is_absolute?' do + it 'confirms that paths starting with / are absolute' do + expect(Chef::ChefFS::PathUtils.is_absolute?('/foo/bar/baz')).to be true + expect(Chef::ChefFS::PathUtils.is_absolute?('/foo')).to be true + end + + it 'confirms that paths starting with // are absolute even though that looks like some windows network path' do + expect(Chef::ChefFS::PathUtils.is_absolute?('//foo/bar/baz')).to be true + end + + it 'confirms that root is indeed absolute' do + expect(Chef::ChefFS::PathUtils.is_absolute?('/')).to be true + end + + it 'confirms that paths starting without / are relative' do + expect(Chef::ChefFS::PathUtils.is_absolute?('foo/bar/baz')).to be false + expect(Chef::ChefFS::PathUtils.is_absolute?('a')).to be false + end + + it 'returns false for an empty path.' do + expect(Chef::ChefFS::PathUtils.is_absolute?('')).to be false + end + end + + context 'invoking realest_path' do + let(:good_path) { File.dirname(__FILE__) } + let(:parent_path) { File.dirname(good_path) } + + it 'handles paths with no wildcards or globs' do + expect(Chef::ChefFS::PathUtils.realest_path(good_path)).to eq(File.expand_path(good_path)) + end + + it 'handles paths with .. and .' do + expect(Chef::ChefFS::PathUtils.realest_path(good_path+'/../.')).to eq(File.expand_path(parent_path)) + end + + it 'handles paths with *' do + expect(Chef::ChefFS::PathUtils.realest_path(good_path + '/*/foo')).to eq(File.expand_path(good_path + '/*/foo')) + end + + it 'handles directories that do not exist' do + expect(Chef::ChefFS::PathUtils.realest_path(good_path + '/something/or/other')).to eq(File.expand_path(good_path + '/something/or/other')) + end + + it 'handles root correctly' do + if Chef::Platform.windows? + expect(Chef::ChefFS::PathUtils.realest_path('C:/')).to eq('C:/') + else + expect(Chef::ChefFS::PathUtils.realest_path('/')).to eq('/') + end + end + end + + context 'invoking descendant_path' do + it 'handles paths with various casing on windows' do + allow(Chef::ChefFS).to receive(:windows?) { true } + expect(Chef::ChefFS::PathUtils.descendant_path('C:/ab/b/c', 'C:/AB/B')).to eq('c') + expect(Chef::ChefFS::PathUtils.descendant_path('C:/ab/b/c', 'c:/ab/B')).to eq('c') + end + + it 'returns nil if the path does not have the given ancestor' do + expect(Chef::ChefFS::PathUtils.descendant_path('/D/E/F', '/A/B/C')).to be_nil + expect(Chef::ChefFS::PathUtils.descendant_path('/A/B/D', '/A/B/C')).to be_nil + end + + it 'returns blank if the ancestor equals the path' do + expect(Chef::ChefFS::PathUtils.descendant_path('/A/B/D', '/A/B/D')).to eq('') + end + end +end diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb index 1e4bbb5c56..8146774764 100644 --- a/spec/unit/client_spec.rb +++ b/spec/unit/client_spec.rb @@ -238,23 +238,24 @@ describe Chef::Client do describe "when converge completes successfully" do include_context "a client run" include_context "converge completed" - - describe "when audit phase errors" do - include_context "audit phase failed with error" - include_examples "a completed run with audit failure" do - let(:run_errors) { [audit_error] } + context 'when audit mode is enabled' do + describe "when audit phase errors" do + include_context "audit phase failed with error" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } + end end - end - describe "when audit phase completed" do - include_context "audit phase completed" - include_examples "a completed run" - end + describe "when audit phase completed" do + include_context "audit phase completed" + include_examples "a completed run" + end - describe "when audit phase completed with failed controls" do - include_context "audit phase completed with failed controls" - include_examples "a completed run with audit failure" do - let(:run_errors) { [audit_error] } + describe "when audit phase completed with failed controls" do + include_context "audit phase completed with failed controls" + include_examples "a completed run with audit failure" do + let(:run_errors) { [audit_error] } + end end end end @@ -512,11 +513,26 @@ describe Chef::Client do allow_any_instance_of(Chef::RunLock).to receive(:save_pid).and_raise(NoMethodError) end - it "should run exception handlers on early fail" do - expect(subject).to receive(:run_failed) - expect { subject.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| - expect(error.wrapped_errors.size).to eq 1 - expect(error.wrapped_errors).to include(NoMethodError) + context 'when audit mode is enabled' do + before do + Chef::Config[:audit_mode] = :enabled + end + it "should run exception handlers on early fail" do + expect(subject).to receive(:run_failed) + expect { subject.run }.to raise_error(Chef::Exceptions::RunFailedWrappingError) do |error| + expect(error.wrapped_errors.size).to eq 1 + expect(error.wrapped_errors).to include(NoMethodError) + end + end + end + + context 'when audit mode is disabled' do + before do + Chef::Config[:audit_mode] = :disabled + end + it "should run exception handlers on early fail" do + expect(subject).to receive(:run_failed) + expect { subject.run }.to raise_error(NoMethodError) end end end diff --git a/spec/unit/config_spec.rb b/spec/unit/config_spec.rb new file mode 100644 index 0000000000..8d155c61ab --- /dev/null +++ b/spec/unit/config_spec.rb @@ -0,0 +1,31 @@ + +require 'spec_helper' + +require 'chef/config' + +RSpec.describe Chef::Config do + + shared_examples_for "deprecated by ohai but not deprecated" do + it "does not emit a deprecation warning when set" do + expect(Chef::Log).to_not receive(:warn). + with(/Ohai::Config\[:#{option}\] is deprecated/) + Chef::Config[option] = value + expect(Chef::Config[option]).to eq(value) + end + end + + describe ":log_level" do + include_examples "deprecated by ohai but not deprecated" do + let(:option) { :log_level } + let(:value) { :debug } + end + end + + describe ":log_location" do + include_examples "deprecated by ohai but not deprecated" do + let(:option) { :log_location } + let(:value) { "path/to/log" } + end + end + +end diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb index d2954726e8..1b30286f51 100644 --- a/spec/unit/cookbook/metadata_spec.rb +++ b/spec/unit/cookbook/metadata_spec.rb @@ -30,7 +30,7 @@ describe Chef::Cookbook::Metadata do :maintainer_email, :license, :platforms, :dependencies, :recommendations, :suggestions, :conflicting, :providing, :replacing, :attributes, :groupings, :recipes, :version, - :source_url, :issues_url ] + :source_url, :issues_url, :privacy ] end it "does not depend on object identity for equality" do @@ -148,6 +148,10 @@ describe Chef::Cookbook::Metadata do it "has an empty issues_url string" do expect(metadata.issues_url).to eq('') end + + it "is not private" do + expect(metadata.privacy).to eq(false) + end end describe "validation" do @@ -198,7 +202,8 @@ describe Chef::Cookbook::Metadata do :long_description => "Much Longer\nSeriously", :version => "0.6.0", :source_url => "http://example.com", - :issues_url => "http://example.com/issues" + :issues_url => "http://example.com/issues", + :privacy => true } params.sort { |a,b| a.to_s <=> b.to_s }.each do |field, field_value| describe field do @@ -360,7 +365,8 @@ describe Chef::Cookbook::Metadata do "recipes" => [ "mysql::server", "mysql::master" ], "default" => [ ], "source_url" => "http://example.com", - "issues_url" => "http://example.com/issues" + "issues_url" => "http://example.com/issues", + "privacy" => true } expect(metadata.attribute("/db/mysql/databases", attrs)).to eq(attrs) end @@ -401,6 +407,18 @@ describe Chef::Cookbook::Metadata do }.to raise_error(ArgumentError) end + it "should not accept anything but true or false for the privacy flag" do + expect { + metadata.attribute("db/mysql/databases", :privacy => true) + }.not_to raise_error + expect { + metadata.attribute("db/mysql/databases", :privacy => false) + }.not_to raise_error + expect { + metadata.attribute("db/mysql/databases", :privacy => 'true') + }.to raise_error(ArgumentError) + end + it "should not accept anything but an array of strings for choice" do expect { metadata.attribute("db/mysql/databases", :choice => ['dedicated', 'shared']) @@ -699,6 +717,7 @@ describe Chef::Cookbook::Metadata do version source_url issues_url + privacy }.each do |t| it "should include '#{t}'" do expect(deserialized_metadata[t]).to eq(metadata.send(t.to_sym)) @@ -734,6 +753,7 @@ describe Chef::Cookbook::Metadata do version source_url issues_url + privacy }.each do |t| it "should match '#{t}'" do expect(deserialized_metadata.send(t.to_sym)).to eq(metadata.send(t.to_sym)) diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb index 318c2a7e85..efdb5b7926 100644 --- a/spec/unit/cookbook/syntax_check_spec.rb +++ b/spec/unit/cookbook/syntax_check_spec.rb @@ -53,6 +53,7 @@ describe Chef::Cookbook::SyntaxCheck do @ruby_files = @attr_files + @libr_files + @defn_files + @recipes + [File.join(cookbook_path, "metadata.rb")] basenames = %w{ helpers_via_partial_test.erb helper_test.erb + helpers.erb openldap_stuff.conf.erb nested_openldap_partials.erb nested_partial.erb diff --git a/spec/unit/cookbook_version_spec.rb b/spec/unit/cookbook_version_spec.rb index 4990aef004..2bccddcaec 100644 --- a/spec/unit/cookbook_version_spec.rb +++ b/spec/unit/cookbook_version_spec.rb @@ -336,7 +336,7 @@ describe Chef::CookbookVersion do end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { Chef::CookbookVersion.new("tatft", '/tmp/blah') } end diff --git a/spec/unit/data_bag_item_spec.rb b/spec/unit/data_bag_item_spec.rb index 4348252388..497817ecf1 100644 --- a/spec/unit/data_bag_item_spec.rb +++ b/spec/unit/data_bag_item_spec.rb @@ -193,7 +193,7 @@ describe Chef::DataBagItem do expect(deserial["snooze"]).to eq({ "finally" => "world_will" }) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { data_bag_item } end end diff --git a/spec/unit/data_bag_spec.rb b/spec/unit/data_bag_spec.rb index bd9a99a1de..13b835d120 100644 --- a/spec/unit/data_bag_spec.rb +++ b/spec/unit/data_bag_spec.rb @@ -73,7 +73,7 @@ describe Chef::DataBag do expect(@deserial.send(t.to_sym)).to eq(@data_bag.send(t.to_sym)) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @data_bag } end end diff --git a/spec/unit/deprecation_spec.rb b/spec/unit/deprecation_spec.rb index 2e1f3c39f3..674de5ec1d 100644 --- a/spec/unit/deprecation_spec.rb +++ b/spec/unit/deprecation_spec.rb @@ -65,19 +65,16 @@ describe Chef::Deprecation do end context 'deprecation warning messages' do - before(:each) do - @warning_output = [ ] - allow(Chef::Log).to receive(:warn) { |msg| @warning_output << msg } - end + RSpec::Matchers.define_negated_matcher :a_non_empty_array, :be_empty it 'should be enabled for deprecated methods' do + expect(Chef::Log).to receive(:warn).with(a_non_empty_array) TestClass.new.deprecated_method(10) - expect(@warning_output).not_to be_empty end it 'should contain stack trace' do + expect(Chef::Log).to receive(:warn).with(a_string_including(".rb")) TestClass.new.deprecated_method(10) - expect(@warning_output.join("").include?(".rb")).to be_truthy end end diff --git a/spec/unit/dsl/reboot_pending_spec.rb b/spec/unit/dsl/reboot_pending_spec.rb index 0f2288740f..a55f91d5e6 100644 --- a/spec/unit/dsl/reboot_pending_spec.rb +++ b/spec/unit/dsl/reboot_pending_spec.rb @@ -46,7 +46,7 @@ describe Chef::DSL::RebootPending do end it 'should return true if key "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired" exists' do - allow(recipe).to receive(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootRequired').and_return(true) + allow(recipe).to receive(:registry_key_exists?).with('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending').and_return(true) expect(recipe.reboot_pending?).to be_truthy end diff --git a/spec/unit/environment_spec.rb b/spec/unit/environment_spec.rb index ee3b8b21e1..64617e0888 100644 --- a/spec/unit/environment_spec.rb +++ b/spec/unit/environment_spec.rb @@ -208,7 +208,7 @@ describe Chef::Environment do expect(@json).to match(/"chef_type":"environment"/) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @environment } end end diff --git a/spec/unit/event_dispatch/dispatcher_spec.rb b/spec/unit/event_dispatch/dispatcher_spec.rb index 7e43b1933f..1014feea89 100644 --- a/spec/unit/event_dispatch/dispatcher_spec.rb +++ b/spec/unit/event_dispatch/dispatcher_spec.rb @@ -47,14 +47,33 @@ describe Chef::EventDispatch::Dispatcher do expect(event_sink).to receive(:run_start).with("12.4.0") dispatcher.run_start("12.4.0") - expect(event_sink).to receive(:synchronized_cookbook).with("apache2") - dispatcher.synchronized_cookbook("apache2") + cookbook_version = double("cookbook_version") + expect(event_sink).to receive(:synchronized_cookbook).with("apache2", cookbook_version) + dispatcher.synchronized_cookbook("apache2", cookbook_version) exception = StandardError.new("foo") expect(event_sink).to receive(:recipe_file_load_failed).with("/path/to/file.rb", exception) dispatcher.recipe_file_load_failed("/path/to/file.rb", exception) end + context "when an event sink has fewer arguments for an event" do + # Can't use a double because they don't report arity correctly. + let(:event_sink) do + Class.new(Chef::EventDispatch::Base) do + attr_reader :synchronized_cookbook_args + def synchronized_cookbook(cookbook_name) + @synchronized_cookbook_args = [cookbook_name] + end + end.new + end + + it "trims the arugment list" do + cookbook_version = double("cookbook_version") + dispatcher.synchronized_cookbook("apache2", cookbook_version) + expect(event_sink.synchronized_cookbook_args).to eq ["apache2"] + end + end + end end diff --git a/spec/unit/event_dispatch/dsl_spec.rb b/spec/unit/event_dispatch/dsl_spec.rb new file mode 100644 index 0000000000..0f7adce7a8 --- /dev/null +++ b/spec/unit/event_dispatch/dsl_spec.rb @@ -0,0 +1,83 @@ +# +# Author:: Ranjib Dey (<ranjib@linux.com>) +# +# Copyright:: Copyright (c) 2015 Ranjib Dey +# 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/event_dispatch/dsl' + +describe Chef::EventDispatch::DSL do + let(:events) do + Chef::EventDispatch::Dispatcher.new + end + + let(:run_context) do + Chef::RunContext.new(Chef::Node.new, nil, events) + end + + before do + Chef.set_run_context(run_context) + end + + subject{ described_class.new('test') } + + it 'set handler name' do + subject.on(:run_started) {} + expect(events.subscribers.first.name).to eq('test') + end + + it 'raise error when invalid event type is supplied' do + expect do + subject.on(:foo_bar) {} + end.to raise_error(Chef::Exceptions::InvalidEventType) + end + + it 'register user hooks against valid event type' do + subject.on(:run_failed) {'testhook'} + expect(events.subscribers.first.run_failed).to eq('testhook') + end + + it 'preserve state across event hooks' do + calls = [] + Chef.event_handler do + on :resource_updated do + calls << :updated + end + on :resource_action_start do + calls << :started + end + end + resource = Chef::Resource::RubyBlock.new('foo', run_context) + resource.block { } + resource.run_action(:run) + expect(calls).to eq([:started, :updated]) + end + + it 'preserve instance variables across handler callbacks' do + Chef.event_handler do + on :resource_action_start do + @ivar = [1] + end + on :resource_updated do + @ivar << 2 + end + end + resource = Chef::Resource::RubyBlock.new('foo', run_context) + resource.block { } + resource.run_action(:run) + expect(events.subscribers.first.instance_variable_get(:@ivar)).to eq([1, 2]) + end +end diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb index fd90aeab71..85c54aa693 100644 --- a/spec/unit/exceptions_spec.rb +++ b/spec/unit/exceptions_spec.rb @@ -76,7 +76,7 @@ describe Chef::Exceptions do end if exception.methods.include?(:to_json) - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { exception } end end diff --git a/spec/unit/formatters/doc_spec.rb b/spec/unit/formatters/doc_spec.rb index d018207f49..eb98f5abd3 100644 --- a/spec/unit/formatters/doc_spec.rb +++ b/spec/unit/formatters/doc_spec.rb @@ -43,4 +43,10 @@ describe Chef::Formatters::Base do expect(out.string).to include("Using policy 'jenkins' at revision '613f803bdd035d574df7fa6da525b38df45a74ca82b38b79655efed8a189e073'") end + it "prints cookbook name and version" do + cookbook_version = double(name: "apache2", version: "1.2.3") + formatter.synchronized_cookbook("apache2", cookbook_version) + expect(out.string).to include("- apache2 (1.2.3") + end + end diff --git a/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb b/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb index 5f95beb259..3c8d5dfa29 100644 --- a/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb +++ b/spec/unit/formatters/error_inspectors/compile_error_inspector_spec.rb @@ -110,6 +110,32 @@ describe Chef::Formatters::ErrorInspectors::CompileErrorInspector do end end + context "when the error is a RuntimeError about frozen object" do + let(:exception) do + e = RuntimeError.new("can't modify frozen Array") + e.set_backtrace(trace) + e + end + + let(:path_to_failed_file) { "/tmp/kitchen/cache/cookbooks/foo/recipes/default.rb" } + + let(:trace) do + [ + "/tmp/kitchen/cache/cookbooks/foo/recipes/default.rb:2:in `block in from_file'", + "/tmp/kitchen/cache/cookbooks/foo/recipes/default.rb:1:in `from_file'" + ] + end + + describe "when explaining a runtime error in the compile phase" do + it "correctly detects RuntimeError for frozen objects" do + expect(inspector.exception_message_modifying_frozen?).to be(true) + end + + # could also test for description.section to be called, but would have + # to adjust every other test to begin using a test double for description + end + end + context "when the error does not contain any lines from cookbooks" do let(:trace) do diff --git a/spec/unit/json_compat_spec.rb b/spec/unit/json_compat_spec.rb index 7482ba8a28..fd6469c146 100644 --- a/spec/unit/json_compat_spec.rb +++ b/spec/unit/json_compat_spec.rb @@ -67,13 +67,11 @@ describe Chef::JSONCompat do expect(Chef::JSONCompat.to_json_pretty(f)).to eql("{\n \"foo\": 1234,\n \"bar\": {\n \"baz\": 5678\n }\n}\n") end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { Foo.new } end end - # On FreeBSD 10.1 i386 rspec fails with a SystemStackError loading the expect line with more that 252 entries - # https://github.com/chef/chef/issues/3101 describe "with the file with 252 or less nested entries" do let(:json) { IO.read(File.join(CHEF_SPEC_DATA, 'nested.json')) } let(:hash) { Chef::JSONCompat.from_json(json) } @@ -84,7 +82,10 @@ describe Chef::JSONCompat do end it "should has 'test' as a 252 nested value" do - expect(hash['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']['key']).to eq('test') + v = 252.times.inject(hash) do |memo, _| + memo['key'] + end + expect(v).to eq('test') end end end diff --git a/spec/unit/knife/bootstrap/client_builder_spec.rb b/spec/unit/knife/bootstrap/client_builder_spec.rb index e6aa307c7e..930ae8c9d3 100644 --- a/spec/unit/knife/bootstrap/client_builder_spec.rb +++ b/spec/unit/knife/bootstrap/client_builder_spec.rb @@ -149,6 +149,22 @@ describe Chef::Knife::Bootstrap::ClientBuilder do client_builder.run end + it "does not add tags by default" do + allow(node).to receive(:run_list).with([]) + expect(node).to_not receive(:tags) + client_builder.run + end + + it "adds tags to the node when given" do + tag_receiver = [] + + knife_config[:tags] = %w[foo bar] + allow(node).to receive(:run_list).with([]) + allow(node).to receive(:tags).and_return(tag_receiver) + client_builder.run + expect(tag_receiver).to eq %w[foo bar] + end + it "builds a node when the run_list is a string" do knife_config[:run_list] = "role[base],role[app]" expect(node).to receive(:run_list).with(["role[base]", "role[app]"]) diff --git a/spec/unit/knife/client_bulk_delete_spec.rb b/spec/unit/knife/client_bulk_delete_spec.rb index 45bb4dd16c..1a6317ac00 100644 --- a/spec/unit/knife/client_bulk_delete_spec.rb +++ b/spec/unit/knife/client_bulk_delete_spec.rb @@ -45,7 +45,7 @@ describe Chef::Knife::ClientBulkDelete do clients = Hash.new nonvalidator_client_names.each do |client_name| - client = Chef::ApiClient.new() + client = Chef::ApiClientV1.new() client.name(client_name) allow(client).to receive(:destroy).and_return(true) clients[client_name] = client @@ -59,7 +59,7 @@ describe Chef::Knife::ClientBulkDelete do clients = Hash.new validator_client_names.each do |validator_client_name| - validator_client = Chef::ApiClient.new() + validator_client = Chef::ApiClientV1.new() validator_client.name(validator_client_name) allow(validator_client).to receive(:validator).and_return(true) allow(validator_client).to receive(:destroy).and_return(true) @@ -75,7 +75,7 @@ describe Chef::Knife::ClientBulkDelete do } before(:each) do - allow(Chef::ApiClient).to receive(:list).and_return(clients) + allow(Chef::ApiClientV1).to receive(:list).and_return(clients) end describe "run" do @@ -89,7 +89,7 @@ describe Chef::Knife::ClientBulkDelete do describe "with any clients" do it "should get the list of the clients" do - expect(Chef::ApiClient).to receive(:list) + expect(Chef::ApiClientV1).to receive(:list) knife.run end diff --git a/spec/unit/knife/client_create_spec.rb b/spec/unit/knife/client_create_spec.rb index 8fecfc885f..a1dcc564e2 100644 --- a/spec/unit/knife/client_create_spec.rb +++ b/spec/unit/knife/client_create_spec.rb @@ -34,7 +34,7 @@ describe Chef::Knife::ClientCreate do end let(:client) do - Chef::ApiClient.new + Chef::ApiClientV1.new end let(:knife) do diff --git a/spec/unit/knife/client_delete_spec.rb b/spec/unit/knife/client_delete_spec.rb index 0fb5e0bab7..619009979b 100644 --- a/spec/unit/knife/client_delete_spec.rb +++ b/spec/unit/knife/client_delete_spec.rb @@ -30,7 +30,7 @@ describe Chef::Knife::ClientDelete do describe 'run' do it 'should delete the client' do - expect(@knife).to receive(:delete_object).with(Chef::ApiClient, 'adam', 'client') + expect(@knife).to receive(:delete_object).with(Chef::ApiClientV1, 'adam', 'client') @knife.run end @@ -46,8 +46,8 @@ describe Chef::Knife::ClientDelete do before(:each) do allow(Chef::Knife::UI).to receive(:confirm).and_return(true) allow(@knife).to receive(:confirm).and_return(true) - @client = Chef::ApiClient.new - expect(Chef::ApiClient).to receive(:load).and_return(@client) + @client = Chef::ApiClientV1.new + expect(Chef::ApiClientV1).to receive(:load).and_return(@client) end it 'should delete non-validator client if --delete-validators is not set' do diff --git a/spec/unit/knife/client_edit_spec.rb b/spec/unit/knife/client_edit_spec.rb index c040c5e2f2..ad56d9212d 100644 --- a/spec/unit/knife/client_edit_spec.rb +++ b/spec/unit/knife/client_edit_spec.rb @@ -17,16 +17,29 @@ # require 'spec_helper' +require 'chef/api_client_v1' describe Chef::Knife::ClientEdit do before(:each) do @knife = Chef::Knife::ClientEdit.new @knife.name_args = [ 'adam' ] + @knife.config[:disable_editing] = true end describe 'run' do + let(:data) { + { + "name" => "adam", + "validator" => false, + "admin" => false, + "chef_type" => "client", + "create_key" => true + } + } + it 'should edit the client' do - expect(@knife).to receive(:edit_object).with(Chef::ApiClient, 'adam') + allow(Chef::ApiClientV1).to receive(:load).with('adam').and_return(data) + expect(@knife).to receive(:edit_data).with(data).and_return(data) @knife.run end diff --git a/spec/unit/knife/client_list_spec.rb b/spec/unit/knife/client_list_spec.rb index eff01da4e9..ce0fa4f5e8 100644 --- a/spec/unit/knife/client_list_spec.rb +++ b/spec/unit/knife/client_list_spec.rb @@ -26,7 +26,7 @@ describe Chef::Knife::ClientList do describe 'run' do it 'should list the clients' do - expect(Chef::ApiClient).to receive(:list) + expect(Chef::ApiClientV1).to receive(:list) expect(@knife).to receive(:format_list_for_display) @knife.run end diff --git a/spec/unit/knife/client_reregister_spec.rb b/spec/unit/knife/client_reregister_spec.rb index f1be4ed570..7e763242e4 100644 --- a/spec/unit/knife/client_reregister_spec.rb +++ b/spec/unit/knife/client_reregister_spec.rb @@ -41,7 +41,7 @@ describe Chef::Knife::ClientReregister do context 'when not configured for file output' do it 'reregisters the client and prints the key' do - expect(Chef::ApiClient).to receive(:reregister).with('adam').and_return(@client_mock) + expect(Chef::ApiClientV1).to receive(:reregister).with('adam').and_return(@client_mock) @knife.run expect(@stdout.string).to match( /foo_key/ ) end @@ -49,7 +49,7 @@ describe Chef::Knife::ClientReregister do context 'when configured for file output' do it 'should write the private key to a file' do - expect(Chef::ApiClient).to receive(:reregister).with('adam').and_return(@client_mock) + expect(Chef::ApiClientV1).to receive(:reregister).with('adam').and_return(@client_mock) @knife.config[:file] = '/tmp/monkeypants' filehandle = StringIO.new diff --git a/spec/unit/knife/client_show_spec.rb b/spec/unit/knife/client_show_spec.rb index 8404e8d019..73a876cee0 100644 --- a/spec/unit/knife/client_show_spec.rb +++ b/spec/unit/knife/client_show_spec.rb @@ -27,7 +27,7 @@ describe Chef::Knife::ClientShow do describe 'run' do it 'should list the client' do - expect(Chef::ApiClient).to receive(:load).with('adam').and_return(@client_mock) + expect(Chef::ApiClientV1).to receive(:load).with('adam').and_return(@client_mock) expect(@knife).to receive(:format_for_display).with(@client_mock) @knife.run end @@ -37,7 +37,7 @@ describe Chef::Knife::ClientShow do @stdout = StringIO.new allow(@knife.ui).to receive(:stdout).and_return(@stdout) fake_client_contents = {"foo"=>"bar", "baz"=>"qux"} - expect(Chef::ApiClient).to receive(:load).with('adam').and_return(fake_client_contents) + expect(Chef::ApiClientV1).to receive(:load).with('adam').and_return(fake_client_contents) @knife.run expect(@stdout.string).to eql("{\n \"foo\": \"bar\",\n \"baz\": \"qux\"\n}\n") end diff --git a/spec/unit/knife/core/bootstrap_context_spec.rb b/spec/unit/knife/core/bootstrap_context_spec.rb index 3718cb228c..268b61c756 100644 --- a/spec/unit/knife/core/bootstrap_context_spec.rb +++ b/spec/unit/knife/core/bootstrap_context_spec.rb @@ -97,6 +97,13 @@ EXPECTED end end + describe "when tags are given" do + let(:config) { {:tags => [ "unicorn" ] } } + it "adds the attributes to first_boot" do + expect(Chef::JSONCompat.to_json(bootstrap_context.first_boot)).to eq(Chef::JSONCompat.to_json({:run_list => run_list, :tags => ["unicorn"]})) + end + end + describe "when JSON attributes are given" do let(:config) { {:first_boot_attributes => {:baz => :quux}} } it "adds the attributes to first_boot" do diff --git a/spec/unit/knife/core/custom_manifest_loader_spec.rb b/spec/unit/knife/core/custom_manifest_loader_spec.rb new file mode 100644 index 0000000000..1edbedd3c8 --- /dev/null +++ b/spec/unit/knife/core/custom_manifest_loader_spec.rb @@ -0,0 +1,41 @@ +# +# 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 'spec_helper' + +describe Chef::Knife::SubcommandLoader::CustomManifestLoader do + let(:ec2_server_create_plugin) { "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_server_create.rb" } + let(:manifest_content) do + { "plugins" => { + "knife-ec2" => { + "paths" => [ + ec2_server_create_plugin + ] + } + } + } + end + let(:loader) do + Chef::Knife::SubcommandLoader::CustomManifestLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands'), + manifest_content) + end + + it "uses paths from the manifest instead of searching gems" do + expect(Gem::Specification).not_to receive(:latest_specs).and_call_original + expect(loader.subcommand_files).to include(ec2_server_create_plugin) + end +end diff --git a/spec/unit/knife/core/gem_glob_loader_spec.rb b/spec/unit/knife/core/gem_glob_loader_spec.rb new file mode 100644 index 0000000000..465eea2656 --- /dev/null +++ b/spec/unit/knife/core/gem_glob_loader_spec.rb @@ -0,0 +1,210 @@ +# +# 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 'spec_helper' + +describe Chef::Knife::SubcommandLoader::GemGlobLoader do + let(:loader) { Chef::Knife::SubcommandLoader::GemGlobLoader.new(File.join(CHEF_SPEC_DATA, 'knife-site-subcommands')) } + let(:home) { File.join(CHEF_SPEC_DATA, 'knife-home') } + let(:plugin_dir) { File.join(home, '.chef', 'plugins', 'knife') } + + before do + allow(ChefConfig).to receive(:windows?) { false } + Chef::Util::PathHelper.class_variable_set(:@@home_dir, home) + end + + after do + Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) + end + + it "builds a list of the core subcommand file require paths" do + expect(loader.subcommand_files).not_to be_empty + loader.subcommand_files.each do |require_path| + expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) + end + end + + it "finds files installed via rubygems" do + expect(loader.find_subcommands_via_rubygems).to include('chef/knife/node_create') + loader.find_subcommands_via_rubygems.each {|rel_path, abs_path| expect(abs_path).to match(%r[chef/knife/.+])} + end + + it "finds files from latest version of installed gems" do + gems = [ double('knife-ec2-0.5.12') ] + gem_files = [ + '/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_base.rb', + '/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_otherstuff.rb' + ] + expect($LOAD_PATH).to receive(:map).and_return([]) + if Gem::Specification.respond_to? :latest_specs + expect(Gem::Specification).to receive(:latest_specs).with(true).and_return(gems) + expect(gems[0]).to receive(:matches_for_glob).with(/chef\/knife\/\*\.rb\{(.*),\.rb,(.*)\}/).and_return(gem_files) + else + expect(Gem.source_index).to receive(:latest_specs).with(true).and_return(gems) + expect(gems[0]).to receive(:require_paths).twice.and_return(['lib']) + expect(gems[0]).to receive(:full_gem_path).and_return('/usr/lib/ruby/gems/knife-ec2-0.5.12') + expect(Dir).to receive(:[]).with('/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/*.rb').and_return(gem_files) + end + expect(loader).to receive(:find_subcommands_via_dirglob).and_return({}) + expect(loader.subcommand_files.select { |file| file =~ /knife-ec2/ }.sort).to eq(gem_files) + end + + it "finds files using a dirglob when rubygems is not available" do + expect(loader.find_subcommands_via_dirglob).to include('chef/knife/node_create') + loader.find_subcommands_via_dirglob.each {|rel_path, abs_path| expect(abs_path).to match(%r[chef/knife/.+])} + end + + it "finds user-specific subcommands in the user's ~/.chef directory" do + expected_command = File.join(home, '.chef', 'plugins', 'knife', 'example_home_subcommand.rb') + expect(loader.site_subcommands).to include(expected_command) + end + + it "finds repo specific subcommands by searching for a .chef directory" do + expected_command = File.join(CHEF_SPEC_DATA, 'knife-site-subcommands', 'plugins', 'knife', 'example_subcommand.rb') + expect(loader.site_subcommands).to include(expected_command) + end + + # https://github.com/opscode/chef-dk/issues/227 + # + # `knife` in ChefDK isn't from a gem install, it's directly run from a clone + # of the source, but there can be one or more versions of chef also installed + # as a gem. If the gem install contains a command that doesn't exist in the + # source tree of the "primary" chef install, it can be loaded and cause an + # error. We also want to ensure that we only load builtin commands from the + # "primary" chef install. + context "when a different version of chef is also installed as a gem" do + + let(:all_found_commands) do + [ + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", + + # We use the fake version 1.0.0 because that version doesn't exist, + # which ensures it won't ever equal "chef-#{Chef::VERSION}" + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/bootstrap.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/client_bulk_delete.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/client_create.rb", + + # Test that we don't accept a version number that is different only in + # trailing characters, e.g. we are running Chef 12.0.0 but there is a + # Chef 12.0.0.rc.0 gem also: + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0/lib/chef/knife/thing.rb", + + # Test that we ignore the platform suffix when checking for different + # gem versions. + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", + # ...but don't ignore the .rc / .dev parts in the case when we have + # platform suffixes + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0-x86-mingw32/lib/chef/knife/invalid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev-mswin32/lib/chef/knife/invalid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev.0-x86-mingw64/lib/chef/knife/still-invalid.rb", + + # This command is "extra" compared to what's in the embedded/apps/chef install: + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/data_bag_secret_options.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", + + # These are fake commands that have names designed to test that the + # regex is strict enough + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/foo-chef-#{Chef::VERSION}/lib/chef/knife/foo-chef.rb", + + # In a real scenario, we'd use rubygems APIs to only select the most + # recent gem, but for this test we want to check that we're doing the + # right thing both when the plugin version matches and does not match + # the current chef version. Looking at + # `SubcommandLoader::MATCHES_THIS_CHEF_GEM` and + # `SubcommandLoader::MATCHES_CHEF_GEM` should make it clear why we want + # to test these two cases. + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-bar-1.0.0/lib/chef/knife/chef-bar.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/bar-chef-1.0.0/lib/chef/knife/bar-chef.rb" + ] + end + + let(:expected_valid_commands) do + [ + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", + "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/foo-chef-#{Chef::VERSION}/lib/chef/knife/foo-chef.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-bar-1.0.0/lib/chef/knife/chef-bar.rb", + "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/bar-chef-1.0.0/lib/chef/knife/bar-chef.rb" + ] + end + + before do + expect(loader).to receive(:find_files_latest_gems).with("chef/knife/*.rb").and_return(all_found_commands) + expect(loader).to receive(:find_subcommands_via_dirglob).and_return({}) + end + + it "ignores commands from the non-matching gem install" do + expect(loader.find_subcommands_via_rubygems.values).to eq(expected_valid_commands) + end + + end + + describe "finding 3rd party plugins" do + let(:env_home) { "/home/alice" } + let(:manifest_path) { env_home + "/.chef/plugin_manifest.json" } + + before do + env_dup = ENV.to_hash + allow(ENV).to receive(:[]) { |key| env_dup[key] } + allow(ENV).to receive(:[]).with("HOME").and_return(env_home) + end + + + it "searches rubygems for plugins" do + if Gem::Specification.respond_to?(:latest_specs) + expect(Gem::Specification).to receive(:latest_specs).and_call_original + else + expect(Gem.source_index).to receive(:latest_specs).and_call_original + end + loader.subcommand_files.each do |require_path| + expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) + end + end + + context "and HOME environment variable is not set" do + before do + allow(ENV).to receive(:[]).with("HOME").and_return(nil) + end + + it "searches rubygems for plugins" do + if Gem::Specification.respond_to?(:latest_specs) + expect(Gem::Specification).to receive(:latest_specs).and_call_original + else + expect(Gem.source_index).to receive(:latest_specs).and_call_original + end + loader.subcommand_files.each do |require_path| + expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) + end + end + end + end +end diff --git a/spec/unit/knife/core/hashed_command_loader_spec.rb b/spec/unit/knife/core/hashed_command_loader_spec.rb new file mode 100644 index 0000000000..00e7ba377b --- /dev/null +++ b/spec/unit/knife/core/hashed_command_loader_spec.rb @@ -0,0 +1,93 @@ +# +# 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 'spec_helper' + +describe Chef::Knife::SubcommandLoader::HashedCommandLoader do + before do + allow(ChefConfig).to receive(:windows?) { false } + end + + let(:plugin_manifest) { + { + "_autogenerated_command_paths" => { + "plugins_paths" => { + "cool_a" => ["/file/for/plugin/a"], + "cooler_b" => ["/file/for/plugin/b"] + }, + "plugins_by_category" => { + "cool" => [ + "cool_a" + ], + "cooler" => [ + "cooler_b" + ] + } + } + } + } + + let(:loader) { Chef::Knife::SubcommandLoader::HashedCommandLoader.new( + File.join(CHEF_SPEC_DATA, 'knife-site-subcommands'), + plugin_manifest)} + + describe "#list_commands" do + it "lists all commands by category when no argument is given" do + expect(loader.list_commands).to eq({"cool" => ["cool_a"], "cooler" => ["cooler_b"]}) + end + + it "lists only commands in the given category when a category is given" do + expect(loader.list_commands("cool")).to eq({"cool" => ["cool_a"]}) + end + end + + describe "#subcommand_files" do + it "lists all the files" do + expect(loader.subcommand_files).to eq(["/file/for/plugin/a", "/file/for/plugin/b"]) + end + end + + describe "#load_commands" do + before do + allow(Kernel).to receive(:load).and_return(true) + end + + it "returns false for non-existant commands" do + expect(loader.load_command(["nothere"])).to eq(false) + end + + it "loads the correct file and returns true if the command exists" do + allow(File).to receive(:exists?).and_return(true) + expect(Kernel).to receive(:load).with("/file/for/plugin/a").and_return(true) + expect(loader.load_command(["cool_a"])).to eq(true) + end + end + + describe "#subcommand_for_args" do + it "returns the subcommands for an exact match" do + expect(loader.subcommand_for_args(["cooler_b"])).to eq("cooler_b") + end + + it "finds the right subcommand even when _'s are elided" do + expect(loader.subcommand_for_args(["cooler", "b"])).to eq("cooler_b") + end + + it "returns nil if the the subcommand isn't in our manifest" do + expect(loader.subcommand_for_args(["cooler c"])).to eq(nil) + end + end +end diff --git a/spec/unit/knife/core/subcommand_loader_spec.rb b/spec/unit/knife/core/subcommand_loader_spec.rb index 219a1f2906..2386465c75 100644 --- a/spec/unit/knife/core/subcommand_loader_spec.rb +++ b/spec/unit/knife/core/subcommand_loader_spec.rb @@ -1,6 +1,5 @@ # -# Author:: Daniel DeLeo (<dan@opscode.com>) -# Copyright:: Copyright (c) 2011 Opscode, Inc. +# Copyright:: Copyright (c) 2015 Chef Software, Inc # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -32,209 +31,34 @@ describe Chef::Knife::SubcommandLoader do Chef::Util::PathHelper.class_variable_set(:@@home_dir, nil) end - it "builds a list of the core subcommand file require paths" do - expect(loader.subcommand_files).not_to be_empty - loader.subcommand_files.each do |require_path| - expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) - end - end - - it "finds files installed via rubygems" do - expect(loader.find_subcommands_via_rubygems).to include('chef/knife/node_create') - loader.find_subcommands_via_rubygems.each {|rel_path, abs_path| expect(abs_path).to match(%r[chef/knife/.+])} - end - - it "finds files from latest version of installed gems" do - gems = [ double('knife-ec2-0.5.12') ] - gem_files = [ - '/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_base.rb', - '/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_otherstuff.rb' - ] - expect($LOAD_PATH).to receive(:map).and_return([]) - if Gem::Specification.respond_to? :latest_specs - expect(Gem::Specification).to receive(:latest_specs).with(true).and_return(gems) - expect(gems[0]).to receive(:matches_for_glob).with(/chef\/knife\/\*\.rb\{(.*),\.rb,(.*)\}/).and_return(gem_files) - else - expect(Gem.source_index).to receive(:latest_specs).with(true).and_return(gems) - expect(gems[0]).to receive(:require_paths).twice.and_return(['lib']) - expect(gems[0]).to receive(:full_gem_path).and_return('/usr/lib/ruby/gems/knife-ec2-0.5.12') - expect(Dir).to receive(:[]).with('/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/*.rb').and_return(gem_files) - end - expect(loader).to receive(:find_subcommands_via_dirglob).and_return({}) - expect(loader.find_subcommands_via_rubygems.values.select { |file| file =~ /knife-ec2/ }.sort).to eq(gem_files) - end - - it "finds files using a dirglob when rubygems is not available" do - expect(loader.find_subcommands_via_dirglob).to include('chef/knife/node_create') - loader.find_subcommands_via_dirglob.each {|rel_path, abs_path| expect(abs_path).to match(%r[chef/knife/.+])} - end - - it "finds user-specific subcommands in the user's ~/.chef directory" do - expected_command = File.join(home, '.chef', 'plugins', 'knife', 'example_home_subcommand.rb') - expect(loader.site_subcommands).to include(expected_command) - end - - it "finds repo specific subcommands by searching for a .chef directory" do - expected_command = File.join(CHEF_SPEC_DATA, 'knife-site-subcommands', 'plugins', 'knife', 'example_subcommand.rb') - expect(loader.site_subcommands).to include(expected_command) - end - - # https://github.com/opscode/chef-dk/issues/227 - # - # `knife` in ChefDK isn't from a gem install, it's directly run from a clone - # of the source, but there can be one or more versions of chef also installed - # as a gem. If the gem install contains a command that doesn't exist in the - # source tree of the "primary" chef install, it can be loaded and cause an - # error. We also want to ensure that we only load builtin commands from the - # "primary" chef install. - context "when a different version of chef is also installed as a gem" do - - let(:all_found_commands) do - [ - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", - - # We use the fake version 1.0.0 because that version doesn't exist, - # which ensures it won't ever equal "chef-#{Chef::VERSION}" - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/bootstrap.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/client_bulk_delete.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/client_create.rb", - - # Test that we don't accept a version number that is different only in - # trailing characters, e.g. we are running Chef 12.0.0 but there is a - # Chef 12.0.0.rc.0 gem also: - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0/lib/chef/knife/thing.rb", - - # Test that we ignore the platform suffix when checking for different - # gem versions. - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", - # ...but don't ignore the .rc / .dev parts in the case when we have - # platform suffixes - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.rc.0-x86-mingw32/lib/chef/knife/invalid.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev-mswin32/lib/chef/knife/invalid-too.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}.dev.0-x86-mingw64/lib/chef/knife/still-invalid.rb", - - # This command is "extra" compared to what's in the embedded/apps/chef install: - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-1.0.0/lib/chef/knife/data_bag_secret_options.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", - - # These are fake commands that have names designed to test that the - # regex is strict enough - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/foo-chef-#{Chef::VERSION}/lib/chef/knife/foo-chef.rb", + let(:config_dir) { File.join(CHEF_SPEC_DATA, 'knife-site-subcommands') } - # In a real scenario, we'd use rubygems APIs to only select the most - # recent gem, but for this test we want to check that we're doing the - # right thing both when the plugin version matches and does not match - # the current chef version. Looking at - # `SubcommandLoader::MATCHES_THIS_CHEF_GEM` and - # `SubcommandLoader::MATCHES_CHEF_GEM` should make it clear why we want - # to test these two cases. - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-bar-1.0.0/lib/chef/knife/chef-bar.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/bar-chef-1.0.0/lib/chef/knife/bar-chef.rb" - ] - end - - let(:expected_valid_commands) do - [ - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/bootstrap.rb", - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_bulk_delete.rb", - "/opt/chefdk/embedded/apps/chef/lib/chef/knife/client_create.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-x86-mingw32/lib/chef/knife/valid.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-i386-mingw64/lib/chef/knife/valid-too.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-mswin32/lib/chef/knife/also-valid.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-#{Chef::VERSION}-universal-mingw32/lib/chef/knife/universal-is-valid.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-vault-2.2.4/lib/chef/knife/decrypt.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/knife-spork-1.4.1/lib/chef/knife/spork-bump.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-foo-#{Chef::VERSION}/lib/chef/knife/chef-foo.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/foo-chef-#{Chef::VERSION}/lib/chef/knife/foo-chef.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/chef-bar-1.0.0/lib/chef/knife/chef-bar.rb", - "/opt/chefdk/embedded/lib/ruby/gems/2.1.0/gems/bar-chef-1.0.0/lib/chef/knife/bar-chef.rb" - ] - end - - before do - expect(loader).to receive(:find_files_latest_gems).with("chef/knife/*.rb").and_return(all_found_commands) - expect(loader).to receive(:find_subcommands_via_dirglob).and_return({}) - end - - it "ignores commands from the non-matching gem install" do - expect(loader.find_subcommands_via_rubygems.values).to eq(expected_valid_commands) - end - - end - - describe "finding 3rd party plugins" do - let(:home) { "/home/alice" } - let(:manifest_path) { home + "/.chef/plugin_manifest.json" } - - context "when there is not a ~/.chef/plugin_manifest.json file" do + describe "#for_config" do + context "when ~/.chef/plugin_manifest.json exists" do before do - allow(File).to receive(:exist?).with(manifest_path).and_return(false) + allow(File).to receive(:exist?).with(File.join(home, '.chef', 'plugin_manifest.json')).and_return(true) end - it "searches rubygems for plugins" do - if Gem::Specification.respond_to?(:latest_specs) - expect(Gem::Specification).to receive(:latest_specs).and_call_original - else - expect(Gem.source_index).to receive(:latest_specs).and_call_original - end - loader.subcommand_files.each do |require_path| - expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) - end + it "creates a HashedCommandLoader with the manifest has _autogenerated_command_paths" do + allow(File).to receive(:read).with(File.join(home, '.chef', 'plugin_manifest.json')).and_return("{ \"_autogenerated_command_paths\": {}}") + expect(Chef::Knife::SubcommandLoader.for_config(config_dir)).to be_a Chef::Knife::SubcommandLoader::HashedCommandLoader end - context "and HOME environment variable is not set" do - before do - allow(Chef::Util::PathHelper).to receive(:home).and_return(nil) - end - - it "searches rubygems for plugins" do - if Gem::Specification.respond_to?(:latest_specs) - expect(Gem::Specification).to receive(:latest_specs).and_call_original - else - expect(Gem.source_index).to receive(:latest_specs).and_call_original - end - loader.subcommand_files.each do |require_path| - expect(require_path).to match(/chef\/knife\/.*|plugins\/knife\/.*/) - end - end + it "creates a CustomManifestLoader with then manifest has a key other than _autogenerated_command_paths" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + allow(File).to receive(:read).with(File.join(home, '.chef', 'plugin_manifest.json')).and_return("{ \"plugins\": {}}") + expect(Chef::Knife::SubcommandLoader.for_config(config_dir)).to be_a Chef::Knife::SubcommandLoader::CustomManifestLoader end - end - context "when there is a ~/.chef/plugin_manifest.json file" do - let(:ec2_server_create_plugin) { "/usr/lib/ruby/gems/knife-ec2-0.5.12/lib/chef/knife/ec2_server_create.rb" } - - let(:manifest_content) do - { "plugins" => { - "knife-ec2" => { - "paths" => [ - ec2_server_create_plugin - ] - } - } - } - end - - let(:manifest_json) { Chef::JSONCompat.to_json(manifest_content) } - + context "when ~/.chef/plugin_manifest.json does not exist" do before do - allow(File).to receive(:exist?).with(manifest_path).and_return(true) - allow(File).to receive(:read).with(manifest_path).and_return(manifest_json) + allow(File).to receive(:exist?).with(File.join(home, '.chef', 'plugin_manifest.json')).and_return(false) end - it "uses paths from the manifest instead of searching gems" do - expect(Gem::Specification).not_to receive(:latest_specs).and_call_original - expect(loader.subcommand_files).to include(ec2_server_create_plugin) + it "creates a GemGlobLoader" do + expect(Chef::Knife::SubcommandLoader.for_config(config_dir)).to be_a Chef::Knife::SubcommandLoader::GemGlobLoader end - end end - end diff --git a/spec/unit/knife/node_run_list_remove_spec.rb b/spec/unit/knife/node_run_list_remove_spec.rb index ceceef7178..a279a59635 100644 --- a/spec/unit/knife/node_run_list_remove_spec.rb +++ b/spec/unit/knife/node_run_list_remove_spec.rb @@ -84,6 +84,23 @@ describe Chef::Knife::NodeRunListRemove do expect(@node.run_list).not_to include('role[monkey]') expect(@node.run_list).not_to include('recipe[duck::type]') end + + it "should warn when the thing to remove is not in the runlist" do + @node.run_list << 'role[blah]' + @node.run_list << 'recipe[duck::type]' + @knife.name_args = [ 'adam', 'role[blork]' ] + expect(@knife.ui).to receive(:warn).with("role[blork] is not in the run list") + @knife.run + end + + it "should warn even more when the thing to remove is not in the runlist and unqualified" do + @node.run_list << 'role[blah]' + @node.run_list << 'recipe[duck::type]' + @knife.name_args = [ 'adam', 'blork' ] + expect(@knife.ui).to receive(:warn).with("blork is not in the run list") + expect(@knife.ui).to receive(:warn).with(/did you forget recipe\[\] or role\[\]/) + @knife.run + end end end end diff --git a/spec/unit/knife/osc_user_create_spec.rb b/spec/unit/knife/osc_user_create_spec.rb index 1b17d0d22f..e4ed78fe2b 100644 --- a/spec/unit/knife/osc_user_create_spec.rb +++ b/spec/unit/knife/osc_user_create_spec.rb @@ -36,19 +36,19 @@ describe Chef::Knife::OscUserCreate do @knife.name_args = [ 'a_user' ] @knife.config[:user_password] = "foobar" - @user = Chef::OscUser.new + @user = Chef::User.new @user.name "a_user" - @user_with_private_key = Chef::OscUser.new + @user_with_private_key = Chef::User.new @user_with_private_key.name "a_user" @user_with_private_key.private_key 'private_key' allow(@user).to receive(:create).and_return(@user_with_private_key) - allow(Chef::OscUser).to receive(:new).and_return(@user) - allow(Chef::OscUser).to receive(:from_hash).and_return(@user) + allow(Chef::User).to receive(:new).and_return(@user) + allow(Chef::User).to receive(:from_hash).and_return(@user) allow(@knife).to receive(:edit_data).and_return(@user.to_hash) end it "creates a new user" do - expect(Chef::OscUser).to receive(:new).and_return(@user) + expect(Chef::User).to receive(:new).and_return(@user) expect(@user).to receive(:create) @knife.run expect(@stderr.string).to match /created user.+a_user/i diff --git a/spec/unit/knife/osc_user_delete_spec.rb b/spec/unit/knife/osc_user_delete_spec.rb index 0e16393ffe..4a3ec4228f 100644 --- a/spec/unit/knife/osc_user_delete_spec.rb +++ b/spec/unit/knife/osc_user_delete_spec.rb @@ -31,7 +31,7 @@ describe Chef::Knife::OscUserDelete do end it 'deletes the user' do - expect(@knife).to receive(:delete_object).with(Chef::OscUser, 'my_user') + expect(@knife).to receive(:delete_object).with(Chef::User, 'my_user') @knife.run end diff --git a/spec/unit/knife/osc_user_edit_spec.rb b/spec/unit/knife/osc_user_edit_spec.rb index 71a9192389..279f2e30ef 100644 --- a/spec/unit/knife/osc_user_edit_spec.rb +++ b/spec/unit/knife/osc_user_edit_spec.rb @@ -38,7 +38,7 @@ describe Chef::Knife::OscUserEdit do it 'loads and edits the user' do data = { :name => "my_user" } - allow(Chef::OscUser).to receive(:load).with("my_user").and_return(data) + allow(Chef::User).to receive(:load).with("my_user").and_return(data) expect(@knife).to receive(:edit_data).with(data).and_return(data) @knife.run end diff --git a/spec/unit/knife/osc_user_list_spec.rb b/spec/unit/knife/osc_user_list_spec.rb index 59a15be058..f496a414b8 100644 --- a/spec/unit/knife/osc_user_list_spec.rb +++ b/spec/unit/knife/osc_user_list_spec.rb @@ -30,7 +30,7 @@ describe Chef::Knife::OscUserList do end it 'lists the users' do - expect(Chef::OscUser).to receive(:list) + expect(Chef::User).to receive(:list) expect(@knife).to receive(:format_list_for_display) @knife.run end diff --git a/spec/unit/knife/osc_user_reregister_spec.rb b/spec/unit/knife/osc_user_reregister_spec.rb index 406bbf1f3e..989eb180f1 100644 --- a/spec/unit/knife/osc_user_reregister_spec.rb +++ b/spec/unit/knife/osc_user_reregister_spec.rb @@ -29,7 +29,7 @@ describe Chef::Knife::OscUserReregister do @knife = Chef::Knife::OscUserReregister.new @knife.name_args = [ 'a_user' ] @user_mock = double('user_mock', :private_key => "private_key") - allow(Chef::OscUser).to receive(:load).and_return(@user_mock) + allow(Chef::User).to receive(:load).and_return(@user_mock) @stdout = StringIO.new allow(@knife.ui).to receive(:stdout).and_return(@stdout) end diff --git a/spec/unit/knife/osc_user_show_spec.rb b/spec/unit/knife/osc_user_show_spec.rb index 67b9b45809..18d2086099 100644 --- a/spec/unit/knife/osc_user_show_spec.rb +++ b/spec/unit/knife/osc_user_show_spec.rb @@ -32,7 +32,7 @@ describe Chef::Knife::OscUserShow do end it 'loads and displays the user' do - expect(Chef::OscUser).to receive(:load).with('my_user').and_return(@user_mock) + expect(Chef::User).to receive(:load).with('my_user').and_return(@user_mock) expect(@knife).to receive(:format_for_display).with(@user_mock) @knife.run end diff --git a/spec/unit/knife/ssl_check_spec.rb b/spec/unit/knife/ssl_check_spec.rb index 8eda555108..fd46c47d99 100644 --- a/spec/unit/knife/ssl_check_spec.rb +++ b/spec/unit/knife/ssl_check_spec.rb @@ -163,6 +163,7 @@ E expect(ssl_check).to receive(:verify_X509).and_return(true) # X509 valid certs (no warn) expect(ssl_socket).to receive(:connect) # no error expect(ssl_socket).to receive(:post_connection_check).with("foo.example.com") # no error + expect(ssl_socket).to receive(:hostname=).with("foo.example.com") # no error end it "prints a success message" do @@ -197,6 +198,7 @@ E expect(ssl_socket).to receive(:post_connection_check). with("foo.example.com"). and_raise(OpenSSL::SSL::SSLError) + expect(ssl_socket).to receive(:hostname=).with("foo.example.com") # no error expect(ssl_socket_for_debug).to receive(:connect) expect(ssl_socket_for_debug).to receive(:peer_cert).and_return(self_signed_crt) end @@ -215,6 +217,8 @@ E expect(ssl_check).to receive(:verify_X509).and_return(true) # X509 valid certs expect(ssl_socket).to receive(:connect). and_raise(OpenSSL::SSL::SSLError) + expect(ssl_socket).to receive(:hostname=). + with("foo.example.com") # no error expect(ssl_socket_for_debug).to receive(:connect) expect(ssl_socket_for_debug).to receive(:peer_cert).and_return(self_signed_crt) end diff --git a/spec/unit/knife/user_create_spec.rb b/spec/unit/knife/user_create_spec.rb index 49d62cc2d7..fa5c8324b4 100644 --- a/spec/unit/knife/user_create_spec.rb +++ b/spec/unit/knife/user_create_spec.rb @@ -186,7 +186,7 @@ describe Chef::Knife::UserCreate do context "when a private_key is returned" do before do - allow(knife).to receive(:create_user_from_hash).and_return(Chef::User.from_hash(knife.user.to_hash.merge({"private_key" => "some_private_key"}))) + allow(knife).to receive(:create_user_from_hash).and_return(Chef::UserV1.from_hash(knife.user.to_hash.merge({"private_key" => "some_private_key"}))) end context "when --file is passed" do diff --git a/spec/unit/knife/user_delete_spec.rb b/spec/unit/knife/user_delete_spec.rb index e49c781358..a24160624a 100644 --- a/spec/unit/knife/user_delete_spec.rb +++ b/spec/unit/knife/user_delete_spec.rb @@ -26,7 +26,7 @@ describe Chef::Knife::UserDelete do before(:each) do Chef::Knife::UserDelete.load_deps knife.name_args = [ 'my_user' ] - allow(Chef::User).to receive(:load).and_return(user) + allow(Chef::UserV1).to receive(:load).and_return(user) allow(user).to receive(:username).and_return('my_user') allow(knife.ui).to receive(:stderr).and_return(stdout) allow(knife.ui).to receive(:stdout).and_return(stdout) @@ -51,7 +51,7 @@ describe Chef::Knife::UserDelete do end it 'deletes the user' do - #expect(knife).to receive(:delete_object).with(Chef::User, 'my_user') + #expect(knife).to receive(:delete_object).with(Chef::UserV1, 'my_user') expect(knife).to receive(:delete_object).with('my_user') knife.run end diff --git a/spec/unit/knife/user_edit_spec.rb b/spec/unit/knife/user_edit_spec.rb index 15a7726b20..a21d982d29 100644 --- a/spec/unit/knife/user_edit_spec.rb +++ b/spec/unit/knife/user_edit_spec.rb @@ -36,7 +36,7 @@ describe Chef::Knife::UserEdit do context "when the username field is not supported by the server" do before do allow(knife).to receive(:run_osc_11_user_edit).and_raise(SystemExit) - allow(Chef::User).to receive(:load).and_return({"username" => nil}) + allow(Chef::UserV1).to receive(:load).and_return({"username" => nil}) end it "displays the osc warning" do @@ -52,7 +52,7 @@ describe Chef::Knife::UserEdit do it 'loads and edits the user' do data = { "username" => "my_user" } - allow(Chef::User).to receive(:load).with("my_user").and_return(data) + allow(Chef::UserV1).to receive(:load).with("my_user").and_return(data) expect(knife).to receive(:edit_data).with(data).and_return(data) knife.run end diff --git a/spec/unit/knife/user_list_spec.rb b/spec/unit/knife/user_list_spec.rb index 9990cc802d..fa2bac426e 100644 --- a/spec/unit/knife/user_list_spec.rb +++ b/spec/unit/knife/user_list_spec.rb @@ -29,7 +29,7 @@ describe Chef::Knife::UserList do end it 'lists the users' do - expect(Chef::User).to receive(:list) + expect(Chef::UserV1).to receive(:list) expect(knife).to receive(:format_list_for_display) knife.run end diff --git a/spec/unit/knife/user_reregister_spec.rb b/spec/unit/knife/user_reregister_spec.rb index 412a6ec374..89aa6726cd 100644 --- a/spec/unit/knife/user_reregister_spec.rb +++ b/spec/unit/knife/user_reregister_spec.rb @@ -26,7 +26,7 @@ describe Chef::Knife::UserReregister do before do Chef::Knife::UserReregister.load_deps knife.name_args = [ 'a_user' ] - allow(Chef::User).to receive(:load).and_return(user_mock) + allow(Chef::UserV1).to receive(:load).and_return(user_mock) allow(knife.ui).to receive(:stdout).and_return(stdout) allow(knife.ui).to receive(:stderr).and_return(stdout) allow(user_mock).to receive(:username).and_return('a_user') diff --git a/spec/unit/knife/user_show_spec.rb b/spec/unit/knife/user_show_spec.rb index 43392a3a5c..7c39e428c0 100644 --- a/spec/unit/knife/user_show_spec.rb +++ b/spec/unit/knife/user_show_spec.rb @@ -35,7 +35,7 @@ describe Chef::Knife::UserShow do context "when the username field is not supported by the server" do before do allow(knife).to receive(:run_osc_11_user_show).and_raise(SystemExit) - allow(Chef::User).to receive(:load).with('my_user').and_return(user_mock) + allow(Chef::UserV1).to receive(:load).with('my_user').and_return(user_mock) allow(user_mock).to receive(:username).and_return(nil) end @@ -51,7 +51,7 @@ describe Chef::Knife::UserShow do end it 'loads and displays the user' do - expect(Chef::User).to receive(:load).with('my_user').and_return(user_mock) + expect(Chef::UserV1).to receive(:load).with('my_user').and_return(user_mock) expect(knife).to receive(:format_for_display).with(user_mock) knife.run end diff --git a/spec/unit/lwrp_spec.rb b/spec/unit/lwrp_spec.rb index 34c6f6f1c5..bcb64cb21e 100644 --- a/spec/unit/lwrp_spec.rb +++ b/spec/unit/lwrp_spec.rb @@ -163,7 +163,7 @@ describe "LWRP" do it "Should load the old content, and not the new" do resource = Chef::ResourceResolver.resolve(:lwrp_foo) expect(resource).to eq @original_resource - expect(resource.default_action).to eq(:pass_buck) + expect(resource.default_action).to eq([:pass_buck]) expect(Chef.method_defined?(:method_created_by_override_lwrp_foo)).to be_falsey end end @@ -177,10 +177,6 @@ describe "LWRP" do end end - it "should load the resource into a properly-named class and emit a warning when it is initialized" do - expect { Chef::Resource::LwrpFoo.new('hi') }.to raise_error(Chef::Exceptions::DeprecatedFeatureError) - end - it "should be resolvable with Chef::ResourceResolver.resolve(:lwrp_foo)" do expect(Chef::ResourceResolver.resolve(:lwrp_foo, node: Chef::Node.new)).to eq(get_lwrp(:lwrp_foo)) end @@ -202,7 +198,7 @@ describe "LWRP" do end it "should set the specified action as the default action" do - expect(get_lwrp(:lwrp_foo).new("blah").action).to eq(:pass_buck) + expect(get_lwrp(:lwrp_foo).new("blah").action).to eq([:pass_buck]) end it "should create a method for each attribute" do @@ -228,127 +224,6 @@ describe "LWRP" do expect(cls.node[:penguin_name]).to eql("jackass") end - context "resource class created" do - before do - @old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] - Chef::Config[:treat_deprecation_warnings_as_errors] = false - end - after do - Chef::Config[:treat_deprecation_warnings_as_errors] = @old_treat_deprecation_warnings_as_errors - end - - it "should load the resource into a properly-named class" do - expect(Chef::Resource::LwrpFoo).to be_kind_of(Class) - expect(Chef::Resource::LwrpFoo <= Chef::Resource::LWRPBase).to be_truthy - end - - it "get_lwrp(:lwrp_foo).new is a Chef::Resource::LwrpFoo" do - lwrp = get_lwrp(:lwrp_foo).new('hi') - expect(lwrp.kind_of?(Chef::Resource::LwrpFoo)).to be_truthy - expect(lwrp.is_a?(Chef::Resource::LwrpFoo)).to be_truthy - expect(get_lwrp(:lwrp_foo) === lwrp).to be_truthy - expect(Chef::Resource::LwrpFoo === lwrp).to be_truthy - end - - it "Chef::Resource::LwrpFoo.new is a get_lwrp(:lwrp_foo)" do - lwrp = Chef::Resource::LwrpFoo.new('hi') - expect(lwrp.kind_of?(get_lwrp(:lwrp_foo))).to be_truthy - expect(lwrp.is_a?(get_lwrp(:lwrp_foo))).to be_truthy - expect(get_lwrp(:lwrp_foo) === lwrp).to be_truthy - expect(Chef::Resource::LwrpFoo === lwrp).to be_truthy - end - - it "works even if LwrpFoo exists in the top level" do - module ::LwrpFoo - end - expect(Chef::Resource::LwrpFoo).not_to eq(::LwrpFoo) - end - - context "with a subclass of get_lwrp(:lwrp_foo)" do - let(:subclass) do - Class.new(get_lwrp(:lwrp_foo)) - end - - it "subclass.new is a subclass" do - lwrp = subclass.new('hi') - expect(lwrp.kind_of?(subclass)).to be_truthy - expect(lwrp.is_a?(subclass)).to be_truthy - expect(subclass === lwrp).to be_truthy - expect(lwrp.class === subclass) - end - it "subclass.new is a Chef::Resource::LwrpFoo" do - lwrp = subclass.new('hi') - expect(lwrp.kind_of?(Chef::Resource::LwrpFoo)).to be_truthy - expect(lwrp.is_a?(Chef::Resource::LwrpFoo)).to be_truthy - expect(Chef::Resource::LwrpFoo === lwrp).to be_truthy - expect(lwrp.class === Chef::Resource::LwrpFoo) - end - it "subclass.new is a get_lwrp(:lwrp_foo)" do - lwrp = subclass.new('hi') - expect(lwrp.kind_of?(get_lwrp(:lwrp_foo))).to be_truthy - expect(lwrp.is_a?(get_lwrp(:lwrp_foo))).to be_truthy - expect(get_lwrp(:lwrp_foo) === lwrp).to be_truthy - expect(lwrp.class === get_lwrp(:lwrp_foo)) - end - it "Chef::Resource::LwrpFoo.new is *not* a subclass" do - lwrp = Chef::Resource::LwrpFoo.new('hi') - expect(lwrp.kind_of?(subclass)).to be_falsey - expect(lwrp.is_a?(subclass)).to be_falsey - expect(subclass === lwrp.class).to be_falsey - expect(subclass === Chef::Resource::LwrpFoo).to be_falsey - end - it "get_lwrp(:lwrp_foo).new is *not* a subclass" do - lwrp = get_lwrp(:lwrp_foo).new('hi') - expect(lwrp.kind_of?(subclass)).to be_falsey - expect(lwrp.is_a?(subclass)).to be_falsey - expect(subclass === lwrp.class).to be_falsey - expect(subclass === get_lwrp(:lwrp_foo)).to be_falsey - end - end - - context "with a subclass of Chef::Resource::LwrpFoo" do - let(:subclass) do - Class.new(Chef::Resource::LwrpFoo) - end - - it "subclass.new is a subclass" do - lwrp = subclass.new('hi') - expect(lwrp.kind_of?(subclass)).to be_truthy - expect(lwrp.is_a?(subclass)).to be_truthy - expect(subclass === lwrp).to be_truthy - expect(lwrp.class === subclass) - end - it "subclass.new is a Chef::Resource::LwrpFoo" do - lwrp = subclass.new('hi') - expect(lwrp.kind_of?(Chef::Resource::LwrpFoo)).to be_truthy - expect(lwrp.is_a?(Chef::Resource::LwrpFoo)).to be_truthy - expect(Chef::Resource::LwrpFoo === lwrp).to be_truthy - expect(lwrp.class === Chef::Resource::LwrpFoo) - end - it "subclass.new is a get_lwrp(:lwrp_foo)" do - lwrp = subclass.new('hi') - expect(lwrp.kind_of?(get_lwrp(:lwrp_foo))).to be_truthy - expect(lwrp.is_a?(get_lwrp(:lwrp_foo))).to be_truthy - expect(get_lwrp(:lwrp_foo) === lwrp).to be_truthy - expect(lwrp.class === get_lwrp(:lwrp_foo)) - end - it "Chef::Resource::LwrpFoo.new is *not* a subclass" do - lwrp = Chef::Resource::LwrpFoo.new('hi') - expect(lwrp.kind_of?(subclass)).to be_falsey - expect(lwrp.is_a?(subclass)).to be_falsey - expect(subclass === lwrp.class).to be_falsey - expect(subclass === Chef::Resource::LwrpFoo).to be_falsey - end - it "get_lwrp(:lwrp_foo).new is *not* a subclass" do - lwrp = get_lwrp(:lwrp_foo).new('hi') - expect(lwrp.kind_of?(subclass)).to be_falsey - expect(lwrp.is_a?(subclass)).to be_falsey - expect(subclass === lwrp.class).to be_falsey - expect(subclass === get_lwrp(:lwrp_foo)).to be_falsey - end - end - end - context "resource_name" do let(:klass) { Class.new(Chef::Resource::LWRPBase) } @@ -409,17 +284,17 @@ describe "LWRP" do end end - context "when the child does not defined the methods" do + context "when the child does not define the methods" do let(:child) do Class.new(parent) end it "delegates #actions to the parent" do - expect(child.actions).to eq([:eat, :sleep]) + expect(child.actions).to eq([:nothing, :eat, :sleep]) end it "delegates #default_action to the parent" do - expect(child.default_action).to eq(:eat) + expect(child.default_action).to eq([:eat]) end end @@ -432,11 +307,11 @@ describe "LWRP" do end it "does not delegate #actions to the parent" do - expect(child.actions).to eq([:dont_eat, :dont_sleep]) + expect(child.actions).to eq([:nothing, :dont_eat, :dont_sleep]) end it "does not delegate #default_action to the parent" do - expect(child.default_action).to eq(:dont_eat) + expect(child.default_action).to eq([:dont_eat]) end end @@ -457,11 +332,50 @@ describe "LWRP" do it "amends actions when they are already defined" do raise_if_deprecated! - expect(child.actions).to eq([:eat, :sleep, :drink]) + expect(child.actions).to eq([:nothing, :eat, :sleep, :drink]) + end + end + end + + describe "when actions is set to an array" do + let(:resource_class) do + Class.new(Chef::Resource::LWRPBase) do + actions [ :eat, :sleep ] end end + let(:resource) do + resource_class.new('blah') + end + it "actions includes those actions" do + expect(resource_class.actions).to eq [ :nothing, :eat, :sleep ] + end + it "allowed_actions includes those actions" do + expect(resource_class.allowed_actions).to eq [ :nothing, :eat, :sleep ] + end + it "resource.allowed_actions includes those actions" do + expect(resource.allowed_actions).to eq [ :nothing, :eat, :sleep ] + end end + describe "when allowed_actions is set to an array" do + let(:resource_class) do + Class.new(Chef::Resource::LWRPBase) do + allowed_actions [ :eat, :sleep ] + end + end + let(:resource) do + resource_class.new('blah') + end + it "actions includes those actions" do + expect(resource_class.actions).to eq [ :nothing, :eat, :sleep ] + end + it "allowed_actions includes those actions" do + expect(resource_class.allowed_actions).to eq [ :nothing, :eat, :sleep ] + end + it "resource.allowed_actions includes those actions" do + expect(resource.allowed_actions).to eq [ :nothing, :eat, :sleep ] + end + end end describe "Lightweight Chef::Provider" do @@ -525,7 +439,7 @@ describe "LWRP" do end it "sets itself as a provider for a resource of the same name" do - found_providers = Chef::Platform::ProviderPriorityMap.instance.list_handlers(node, :lwrp_buck_passer) + found_providers = Chef::Platform::ProviderHandlerMap.instance.list(node, :lwrp_buck_passer) # we bypass the per-file loading to get the file to load each time, # which creates the LWRP class repeatedly. New things get prepended to # the list of providers. @@ -537,7 +451,7 @@ describe "LWRP" do let(:lwrp_cookbok_name) { "l_w_r_p" } it "sets itself as a provider for a resource of the same name" do - found_providers = Chef::Platform::ProviderPriorityMap.instance.list_handlers(node, :l_w_r_p_buck_passer) + found_providers = Chef::Platform::ProviderHandlerMap.instance.list(node, :l_w_r_p_buck_passer) expect(found_providers.size).to eq(1) expect(found_providers.last).to eq(get_lwrp_provider(:l_w_r_p_buck_passer)) end @@ -548,10 +462,10 @@ describe "LWRP" do let(:lwrp_cookbok_name) { "l-w-r-p" } it "sets itself as a provider for a resource of the same name" do - incorrect_providers = Chef::Platform::ProviderPriorityMap.instance.list_handlers(node, :'l-w-r-p_buck_passer') + incorrect_providers = Chef::Platform::ProviderHandlerMap.instance.list(node, :'l-w-r-p_buck_passer') expect(incorrect_providers).to eq([]) - found_providers = Chef::Platform::ProviderPriorityMap.instance.list_handlers(node, :l_w_r_p_buck_passer) + found_providers = Chef::Platform::ProviderHandlerMap.instance.list(node, :l_w_r_p_buck_passer) expect(found_providers.first).to eq(get_lwrp_provider(:l_w_r_p_buck_passer)) end end @@ -665,7 +579,144 @@ describe "LWRP" do end end - end + context "resource class created" do + before(:context) do + @tmpdir = Dir.mktmpdir("lwrp_test") + resource_path = File.join(@tmpdir, "once.rb") + IO.write(resource_path, "default_action :create") + + @old_treat_deprecation_warnings_as_errors = Chef::Config[:treat_deprecation_warnings_as_errors] + Chef::Config[:treat_deprecation_warnings_as_errors] = false + Chef::Resource::LWRPBase.build_from_file("lwrp", resource_path, nil) + end + + after(:context) do + FileUtils.remove_entry @tmpdir + Chef::Config[:treat_deprecation_warnings_as_errors] = @old_treat_deprecation_warnings_as_errors + end + + it "should load the resource into a properly-named class" do + expect(Chef::Resource::LwrpOnce).to be_kind_of(Class) + expect(Chef::Resource::LwrpOnce <= Chef::Resource::LWRPBase).to be_truthy + end + + it "get_lwrp(:lwrp_once).new is a Chef::Resource::LwrpOnce" do + lwrp = get_lwrp(:lwrp_once).new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpOnce)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpOnce)).to be_truthy + expect(get_lwrp(:lwrp_once) === lwrp).to be_truthy + expect(Chef::Resource::LwrpOnce === lwrp).to be_truthy + end + + it "Chef::Resource::LwrpOnce.new is a get_lwrp(:lwrp_once)" do + lwrp = Chef::Resource::LwrpOnce.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_once))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_once))).to be_truthy + expect(get_lwrp(:lwrp_once) === lwrp).to be_truthy + expect(Chef::Resource::LwrpOnce === lwrp).to be_truthy + end + + it "works even if LwrpOnce exists in the top level" do + module ::LwrpOnce + end + expect(Chef::Resource::LwrpOnce).not_to eq(::LwrpOnce) + end + + it "allows monkey patching of the lwrp through Chef::Resource" do + monkey = Module.new do + def issue_3607 + end + end + Chef::Resource::LwrpOnce.send(:include, monkey) + expect { get_lwrp(:lwrp_once).new("blah").issue_3607 }.not_to raise_error + end + + context "with a subclass of get_lwrp(:lwrp_once)" do + let(:subclass) do + Class.new(get_lwrp(:lwrp_once)) + end + + it "subclass.new is a subclass" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(subclass)).to be_truthy + expect(lwrp.is_a?(subclass)).to be_truthy + expect(subclass === lwrp).to be_truthy + expect(lwrp.class === subclass) + end + it "subclass.new is a Chef::Resource::LwrpOnce" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpOnce)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpOnce)).to be_truthy + expect(Chef::Resource::LwrpOnce === lwrp).to be_truthy + expect(lwrp.class === Chef::Resource::LwrpOnce) + end + it "subclass.new is a get_lwrp(:lwrp_once)" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_once))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_once))).to be_truthy + expect(get_lwrp(:lwrp_once) === lwrp).to be_truthy + expect(lwrp.class === get_lwrp(:lwrp_once)) + end + it "Chef::Resource::LwrpOnce.new is *not* a subclass" do + lwrp = Chef::Resource::LwrpOnce.new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === Chef::Resource::LwrpOnce).to be_falsey + end + it "get_lwrp(:lwrp_once).new is *not* a subclass" do + lwrp = get_lwrp(:lwrp_once).new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === get_lwrp(:lwrp_once)).to be_falsey + end + end + + context "with a subclass of Chef::Resource::LwrpOnce" do + let(:subclass) do + Class.new(Chef::Resource::LwrpOnce) + end + + it "subclass.new is a subclass" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(subclass)).to be_truthy + expect(lwrp.is_a?(subclass)).to be_truthy + expect(subclass === lwrp).to be_truthy + expect(lwrp.class === subclass) + end + it "subclass.new is a Chef::Resource::LwrpOnce" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(Chef::Resource::LwrpOnce)).to be_truthy + expect(lwrp.is_a?(Chef::Resource::LwrpOnce)).to be_truthy + expect(Chef::Resource::LwrpOnce === lwrp).to be_truthy + expect(lwrp.class === Chef::Resource::LwrpOnce) + end + it "subclass.new is a get_lwrp(:lwrp_once)" do + lwrp = subclass.new('hi') + expect(lwrp.kind_of?(get_lwrp(:lwrp_once))).to be_truthy + expect(lwrp.is_a?(get_lwrp(:lwrp_once))).to be_truthy + expect(get_lwrp(:lwrp_once) === lwrp).to be_truthy + expect(lwrp.class === get_lwrp(:lwrp_once)) + end + it "Chef::Resource::LwrpOnce.new is *not* a subclass" do + lwrp = Chef::Resource::LwrpOnce.new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === Chef::Resource::LwrpOnce).to be_falsey + end + it "get_lwrp(:lwrp_once).new is *not* a subclass" do + lwrp = get_lwrp(:lwrp_once).new('hi') + expect(lwrp.kind_of?(subclass)).to be_falsey + expect(lwrp.is_a?(subclass)).to be_falsey + expect(subclass === lwrp.class).to be_falsey + expect(subclass === get_lwrp(:lwrp_once)).to be_falsey + end + end + end end + + diff --git a/spec/unit/mixin/params_validate_spec.rb b/spec/unit/mixin/params_validate_spec.rb index 85e1c1abab..3724bbf583 100644 --- a/spec/unit/mixin/params_validate_spec.rb +++ b/spec/unit/mixin/params_validate_spec.rb @@ -21,6 +21,8 @@ require 'spec_helper' class TinyClass include Chef::Mixin::ParamsValidate + attr_reader :name + def music(is_good=true) is_good end @@ -331,11 +333,11 @@ describe Chef::Mixin::ParamsValidate do it "asserts that a value returns false from a predicate method" do expect do @vo.validate({:not_blank => "should pass"}, - {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}}) + {:not_blank => {:cannot_be => [ :nil, :empty ]}}) end.not_to raise_error expect do @vo.validate({:not_blank => ""}, - {:not_blank => {:cannot_be => :nil, :cannot_be => :empty}}) + {:not_blank => {:cannot_be => [ :nil, :empty ]}}) end.to raise_error(Chef::Exceptions::ValidationFailed) end diff --git a/spec/unit/mixin/windows_architecture_helper_spec.rb b/spec/unit/mixin/windows_architecture_helper_spec.rb index 3803d69371..55eca28dc2 100644 --- a/spec/unit/mixin/windows_architecture_helper_spec.rb +++ b/spec/unit/mixin/windows_architecture_helper_spec.rb @@ -60,23 +60,28 @@ describe Chef::Mixin::WindowsArchitectureHelper do end end - it "returns true for each supported desired architecture for all nodes with each valid architecture passed to node_supports_windows_architecture" do - enumerate_architecture_node_combinations(true) + it "returns true only for supported desired architecture passed to node_supports_windows_architecture" do + with_node_architecture_combinations do | node, desired_arch | + expect(node_supports_windows_architecture?(node, desired_arch)).to be true if (node_windows_architecture(node) == :x86_64 || desired_arch == :i386 ) + expect(node_supports_windows_architecture?(node, desired_arch)).to be false if (node_windows_architecture(node) == :i386 && desired_arch == :x86_64 ) + end end - it "returns false for each unsupported desired architecture for all nodes with each valid architecture passed to node_supports_windows_architecture?" do - enumerate_architecture_node_combinations(true) + it "returns true only when forced_32bit_override_required? has 64-bit node architecture and 32-bit desired architecture" do + with_node_architecture_combinations do | node, desired_arch | + expect(forced_32bit_override_required?(node, desired_arch)).to be true if ((node_windows_architecture(node) == :x86_64) && (desired_arch == :i386) && !is_i386_process_on_x86_64_windows?) + expect(forced_32bit_override_required?(node, desired_arch)).to be false if ! ((node_windows_architecture(node) == :x86_64) && (desired_arch == :i386)) + end end - def enumerate_architecture_node_combinations(only_valid_combinations) + def with_node_architecture_combinations @valid_architectures.each do | node_architecture | new_node = Chef::Node.new new_node.default["kernel"] = Hash.new new_node.default["kernel"][:machine] = node_architecture.to_s - @valid_architectures.each do | supported_architecture | - expect(node_supports_windows_architecture?(new_node, supported_architecture)).to eq(true) if only_valid_combinations && (supported_architecture != :x86_64 && node_architecture != :i386 ) - expect(node_supports_windows_architecture?(new_node, supported_architecture)).to eq(false) if ! only_valid_combinations && (supported_architecture == :x86_64 && node_architecture == :i386 ) + @valid_architectures.each do | architecture | + yield new_node, architecture if block_given? end end end diff --git a/spec/unit/node_map_spec.rb b/spec/unit/node_map_spec.rb index 9b5ff5e8c6..7b37ea59f4 100644 --- a/spec/unit/node_map_spec.rb +++ b/spec/unit/node_map_spec.rb @@ -131,6 +131,18 @@ describe Chef::NodeMap do allow(node).to receive(:[]).with(:platform_version).and_return("6.0") expect(node_map.get(node, :thing)).to eql(nil) end + + context "when there is a less specific definition" do + before do + node_map.set(:thing, :bar, platform_family: "rhel") + end + + it "returns the value when the node matches" do + allow(node).to receive(:[]).with(:platform_family).and_return("rhel") + allow(node).to receive(:[]).with(:platform_version).and_return("7.0") + expect(node_map.get(node, :thing)).to eql(:foo) + end + end end describe "resource back-compat testing" do diff --git a/spec/unit/node_spec.rb b/spec/unit/node_spec.rb index 5939403ce6..b7752eb734 100644 --- a/spec/unit/node_spec.rb +++ b/spec/unit/node_spec.rb @@ -672,6 +672,13 @@ describe Chef::Node do expect(node.run_list).to eq([ "role[base]", "recipe[chef::server]" ]) end + it "sets the node chef_environment" do + attrs = { "chef_environment" => "foo_environment", "bar" => "baz" } + expect(node.consume_chef_environment(attrs)).to eq({ "bar" => "baz" }) + expect(node.chef_environment).to eq("foo_environment") + expect(node['chef_environment']).to be nil + end + it "should overwrites the run list with the run list it consumes" do node.consume_run_list "recipes" => [ "one", "two" ] node.consume_run_list "recipes" => [ "three" ] @@ -1106,7 +1113,7 @@ describe Chef::Node do expect(serialized_node.run_list).to eq(node.run_list) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { node.from_file(File.expand_path("nodes/test.example.com.rb", CHEF_SPEC_DATA)) node diff --git a/spec/unit/osc_user_spec.rb b/spec/unit/osc_user_spec.rb deleted file mode 100644 index 678486a16d..0000000000 --- a/spec/unit/osc_user_spec.rb +++ /dev/null @@ -1,276 +0,0 @@ -# -# Author:: Steven Danna (steve@opscode.com) -# Copyright:: Copyright (c) 2012 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. -# - -# DEPRECATION NOTE -# This code only remains to support users still operating with -# Open Source Chef Server 11 and should be removed once support -# for OSC 11 ends. New development should occur in user_spec.rb. - -require 'spec_helper' - -require 'chef/osc_user' -require 'tempfile' - -describe Chef::OscUser do - before(:each) do - @user = Chef::OscUser.new - end - - describe "initialize" do - it "should be a Chef::OscUser" do - expect(@user).to be_a_kind_of(Chef::OscUser) - end - end - - describe "name" do - it "should let you set the name to a string" do - expect(@user.name("ops_master")).to eq("ops_master") - end - - it "should return the current name" do - @user.name "ops_master" - expect(@user.name).to eq("ops_master") - end - - # It is not feasible to check all invalid characters. Here are a few - # that we probably care about. - it "should not accept invalid characters" do - # capital letters - expect { @user.name "Bar" }.to raise_error(ArgumentError) - # slashes - expect { @user.name "foo/bar" }.to raise_error(ArgumentError) - # ? - expect { @user.name "foo?" }.to raise_error(ArgumentError) - # & - expect { @user.name "foo&" }.to raise_error(ArgumentError) - end - - - it "should not accept spaces" do - expect { @user.name "ops master" }.to raise_error(ArgumentError) - end - - it "should throw an ArgumentError if you feed it anything but a string" do - expect { @user.name Hash.new }.to raise_error(ArgumentError) - end - end - - describe "admin" do - it "should let you set the admin bit" do - expect(@user.admin(true)).to eq(true) - end - - it "should return the current admin value" do - @user.admin true - expect(@user.admin).to eq(true) - end - - it "should default to false" do - expect(@user.admin).to eq(false) - end - - it "should throw an ArgumentError if you feed it anything but true or false" do - expect { @user.name Hash.new }.to raise_error(ArgumentError) - end - end - - describe "public_key" do - it "should let you set the public key" do - expect(@user.public_key("super public")).to eq("super public") - end - - it "should return the current public key" do - @user.public_key("super public") - expect(@user.public_key).to eq("super public") - end - - it "should throw an ArgumentError if you feed it something lame" do - expect { @user.public_key Hash.new }.to raise_error(ArgumentError) - end - end - - describe "private_key" do - it "should let you set the private key" do - expect(@user.private_key("super private")).to eq("super private") - end - - it "should return the private key" do - @user.private_key("super private") - expect(@user.private_key).to eq("super private") - end - - it "should throw an ArgumentError if you feed it something lame" do - expect { @user.private_key Hash.new }.to raise_error(ArgumentError) - end - end - - describe "when serializing to JSON" do - before(:each) do - @user.name("black") - @user.public_key("crowes") - @json = @user.to_json - end - - it "serializes as a JSON object" do - expect(@json).to match(/^\{.+\}$/) - end - - it "includes the name value" do - expect(@json).to include(%q{"name":"black"}) - end - - it "includes the public key value" do - expect(@json).to include(%{"public_key":"crowes"}) - end - - it "includes the 'admin' flag" do - expect(@json).to include(%q{"admin":false}) - end - - it "includes the private key when present" do - @user.private_key("monkeypants") - expect(@user.to_json).to include(%q{"private_key":"monkeypants"}) - end - - it "does not include the private key if not present" do - expect(@json).not_to include("private_key") - end - - it "includes the password if present" do - @user.password "password" - expect(@user.to_json).to include(%q{"password":"password"}) - end - - it "does not include the password if not present" do - expect(@json).not_to include("password") - end - - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do - let(:jsonable) { @user } - end - end - - describe "when deserializing from JSON" do - before(:each) do - user = { "name" => "mr_spinks", - "public_key" => "turtles", - "private_key" => "pandas", - "password" => "password", - "admin" => true } - @user = Chef::OscUser.from_json(Chef::JSONCompat.to_json(user)) - end - - it "should deserialize to a Chef::OscUser object" do - expect(@user).to be_a_kind_of(Chef::OscUser) - end - - it "preserves the name" do - expect(@user.name).to eq("mr_spinks") - end - - it "preserves the public key" do - expect(@user.public_key).to eq("turtles") - end - - it "preserves the admin status" do - expect(@user.admin).to be_truthy - end - - it "includes the private key if present" do - expect(@user.private_key).to eq("pandas") - end - - it "includes the password if present" do - expect(@user.password).to eq("password") - end - - end - - describe "API Interactions" do - before (:each) do - @user = Chef::OscUser.new - @user.name "foobar" - @http_client = double("Chef::REST mock") - allow(Chef::REST).to receive(:new).and_return(@http_client) - end - - describe "list" do - before(:each) do - Chef::Config[:chef_server_url] = "http://www.example.com" - @osc_response = { "admin" => "http://www.example.com/users/admin"} - @ohc_response = [ { "user" => { "username" => "admin" }} ] - allow(Chef::OscUser).to receive(:load).with("admin").and_return(@user) - @osc_inflated_response = { "admin" => @user } - end - - it "lists all clients on an OSC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) - expect(Chef::OscUser.list).to eq(@osc_response) - end - - it "inflate all clients on an OSC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@osc_response) - expect(Chef::OscUser.list(true)).to eq(@osc_inflated_response) - end - - it "lists all clients on an OHC/OPC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) - # We expect that Chef::OscUser.list will give a consistent response - # so OHC API responses should be transformed to OSC-style output. - expect(Chef::OscUser.list).to eq(@osc_response) - end - - it "inflate all clients on an OHC/OPC server" do - allow(@http_client).to receive(:get_rest).with("users").and_return(@ohc_response) - expect(Chef::OscUser.list(true)).to eq(@osc_inflated_response) - end - end - - describe "create" do - it "creates a new user via the API" do - @user.password "password" - expect(@http_client).to receive(:post_rest).with("users", {:name => "foobar", :admin => false, :password => "password"}).and_return({}) - @user.create - end - end - - describe "read" do - it "loads a named user from the API" do - expect(@http_client).to receive(:get_rest).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) - user = Chef::OscUser.load("foobar") - expect(user.name).to eq("foobar") - expect(user.admin).to eq(true) - expect(user.public_key).to eq("pubkey") - end - end - - describe "update" do - it "updates an existing user on via the API" do - expect(@http_client).to receive(:put_rest).with("users/foobar", {:name => "foobar", :admin => false}).and_return({}) - @user.update - end - end - - describe "destroy" do - it "deletes the specified user via the API" do - expect(@http_client).to receive(:delete_rest).with("users/foobar") - @user.destroy - end - end - end -end diff --git a/spec/unit/platform_spec.rb b/spec/unit/platform_spec.rb index 36325d5411..34b46f657f 100644 --- a/spec/unit/platform_spec.rb +++ b/spec/unit/platform_spec.rb @@ -103,7 +103,7 @@ describe Chef::Platform do end it "should raise an exception if a provider cannot be found for a resource type" do - expect { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.to raise_error(ArgumentError) + expect { Chef::Platform.find_provider("Darwin", "9.2.2", :coffee) }.to raise_error(Chef::Exceptions::ProviderNotFound) end it "should look up a provider for a resource with a Chef::Resource object" do diff --git a/spec/unit/property/state_spec.rb b/spec/unit/property/state_spec.rb new file mode 100644 index 0000000000..e7fee0387f --- /dev/null +++ b/spec/unit/property/state_spec.rb @@ -0,0 +1,506 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource#identity and #state" do + include IntegrationSupport + + class NewResourceNamer + @i = 0 + def self.next + "chef_resource_property_spec_#{@i += 1}" + end + end + + def self.new_resource_name + NewResourceNamer.next + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + # identity + context "Chef::Resource#identity_properties" do + with_property ":x" do + it "name is the default identity" do + expect(resource_class.identity_properties).to eq [ Chef::Resource.properties[:name] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(resource.name).to eq 'blah' + expect(resource.identity).to eq 'blah' + end + + it "identity_properties :x changes the identity" do + expect(resource_class.identity_properties :x).to eq [ resource_class.properties[:x] ] + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(resource_class.properties[:x].identity?).to be_truthy + + expect(resource.x 'woo').to eq 'woo' + expect(resource.x).to eq 'woo' + + expect(resource.name).to eq 'blah' + expect(resource.identity).to eq 'woo' + end + + with_property ":y, identity: true" do + context "and identity_properties :x" do + before do + resource_class.class_eval do + identity_properties :x + end + end + + it "only returns :x as identity" do + resource.x 'foo' + resource.y 'bar' + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(resource.identity).to eq 'foo' + end + it "does not flip y.desired_state off" do + resource.x 'foo' + resource.y 'bar' + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:y] + ] + expect(resource.state_for_resource_reporter).to eq(x: 'foo', y: 'bar') + end + end + end + + context "With a subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('sub') + end + + it "name is the default identity on the subclass" do + expect(subresource_class.identity_properties).to eq [ Chef::Resource.properties[:name] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(subresource.name).to eq 'sub' + expect(subresource.identity).to eq 'sub' + end + + context "With identity_properties :x on the superclass" do + before do + resource_class.class_eval do + identity_properties :x + end + end + + it "The subclass inherits :x as identity" do + expect(subresource_class.identity_properties).to eq [ subresource_class.properties[:x] ] + expect(Chef::Resource.properties[:name].identity?).to be_falsey + expect(subresource_class.properties[:x].identity?).to be_truthy + + subresource.x 'foo' + expect(subresource.identity).to eq 'foo' + end + + context "With property :y, identity: true on the subclass" do + before do + subresource_class.class_eval do + property :y, identity: true + end + end + it "The subclass's identity includes both x and y" do + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x], + subresource_class.properties[:y] + ] + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.identity).to eq(x: 'foo', y: 'bar') + end + end + + with_property ":y, String" do + context "With identity_properties :y on the subclass" do + before do + subresource_class.class_eval do + identity_properties :y + end + end + it "y is part of state" do + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.state_for_resource_reporter).to eq(x: 'foo', y: 'bar') + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:x], + subresource_class.properties[:y] + ] + end + it "y is the identity" do + expect(subresource_class.identity_properties).to eq [ subresource_class.properties[:y] ] + subresource.x 'foo' + subresource.y 'bar' + expect(subresource.identity).to eq 'bar' + end + it "y still has validation" do + expect { subresource.y 12 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + end + end + end + + with_property ":string_only, String, identity: true", ":string_only2, String" do + it "identity_properties does not change validation" do + resource_class.identity_properties :string_only + expect { resource.string_only 12 }.to raise_error Chef::Exceptions::ValidationFailed + expect { resource.string_only2 12 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + with_property ":x, desired_state: false" do + it "identity_properties does not change desired_state" do + resource_class.identity_properties :x + resource.x 'hi' + expect(resource.identity).to eq 'hi' + expect(resource_class.properties[:x].desired_state?).to be_falsey + expect(resource_class.state_properties).to eq [] + expect(resource.state_for_resource_reporter).to eq({}) + end + end + + context "With custom property custom_property defined only as methods, using different variables for storage" do + before do + resource_class.class_eval do + def custom_property + @blarghle ? @blarghle*3 : nil + end + def custom_property=(x) + @blarghle = x*2 + end + end + end + + context "And identity_properties :custom_property" do + before do + resource_class.class_eval do + identity_properties :custom_property + end + end + + it "identity_properties comes back as :custom_property" do + expect(resource_class.properties[:custom_property].identity?).to be_truthy + expect(resource_class.identity_properties).to eq [ resource_class.properties[:custom_property] ] + end + it "custom_property becomes part of desired_state" do + resource.custom_property = 1 + expect(resource.state_for_resource_reporter).to eq(custom_property: 6) + expect(resource_class.properties[:custom_property].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:custom_property] + ] + end + it "identity_properties does not change custom_property's getter or setter" do + resource.custom_property = 1 + expect(resource.custom_property).to eq 6 + end + it "custom_property is returned as the identity" do + expect(resource.identity).to be_nil + resource.custom_property = 1 + expect(resource.identity).to eq 6 + end + end + end + end + + context "Property#identity" do + with_property ":x, identity: true" do + it "name is only part of the identity if an identity attribute is defined" do + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + resource.x 'woo' + expect(resource.identity).to eq 'woo' + end + end + + with_property ":x, identity: true, default: 'xxx'", + ":y, identity: true, default: 'yyy'", + ":z, identity: true, default: 'zzz'" do + it "identity_property raises an error if multiple identity values are defined" do + expect { resource_class.identity_property }.to raise_error Chef::Exceptions::MultipleIdentityError + end + it "identity_attr raises an error if multiple identity values are defined" do + expect { resource_class.identity_attr }.to raise_error Chef::Exceptions::MultipleIdentityError + end + it "identity returns all identity values in a hash if multiple are defined" do + resource.x 'foo' + resource.y 'bar' + resource.z 'baz' + expect(resource.identity).to eq(x: 'foo', y: 'bar', z: 'baz') + end + it "identity returns all values whether any value is set or not" do + expect(resource.identity).to eq(x: 'xxx', y: 'yyy', z: 'zzz') + end + it "identity_properties wipes out any other identity attributes if multiple are defined" do + resource_class.identity_properties :y + resource.x 'foo' + resource.y 'bar' + resource.z 'baz' + expect(resource.identity).to eq 'bar' + end + end + + with_property ":x, identity: true, name_property: true" do + it "identity when x is not defined returns the value of x" do + expect(resource.identity).to eq 'blah' + end + it "state when x is not defined returns the value of x" do + expect(resource.state_for_resource_reporter).to eq(x: 'blah') + end + end + end + + # state_properties + context "Chef::Resource#state_properties" do + it "state_properties is empty by default" do + expect(Chef::Resource.state_properties).to eq [] + expect(resource.state_for_resource_reporter).to eq({}) + end + + with_property ":x", ":y", ":z" do + it "x, y and z are state attributes" do + resource.x 1 + resource.y 2 + resource.z 3 + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:y], + resource_class.properties[:z] + ] + expect(resource.state_for_resource_reporter).to eq(x: 1, y: 2, z: 3) + end + it "values that are not set are not included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + it "when no values are set, nothing is included in state" do + end + end + + with_property ":x", ":y, desired_state: false", ":z, desired_state: true" do + it "x and z are state attributes, and y is not" do + resource.x 1 + resource.y 2 + resource.z 3 + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x], + resource_class.properties[:z] + ] + expect(resource.state_for_resource_reporter).to eq(x: 1, z: 3) + end + end + + with_property ":x, name_property: true" do + # it "Unset values with name_property are included in state" do + # expect(resource.state_for_resource_reporter).to eq({ x: 'blah' }) + # end + it "Set values with name_property are included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + end + + with_property ":x, default: 1" do + it "Unset values with defaults are not included in state" do + expect(resource.state_for_resource_reporter).to eq({}) + end + it "Set values with defaults are included in state" do + resource.x 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + end + + context "With a class with a normal getter and setter" do + before do + resource_class.class_eval do + def x + @blah*3 + end + def x=(value) + @blah = value*2 + end + end + end + it "state_properties(:x) causes the value to be included in properties" do + resource_class.state_properties(:x) + resource.x = 1 + + expect(resource.x).to eq 6 + expect(resource.state_for_resource_reporter).to eq(x: 6) + end + end + + context "When state_properties happens before properties are declared" do + before do + resource_class.class_eval do + state_properties :x + property :x + end + end + it "the property works and is in state_properties" do + expect(resource_class.state_properties).to include(resource_class.properties[:x]) + resource.x = 1 + expect(resource.x).to eq 1 + expect(resource.state_for_resource_reporter).to eq(x: 1) + end + end + + with_property ":x, Integer, identity: true" do + it "state_properties(:x) leaves the property in desired_state" do + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.properties[:x].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.state_for_resource_reporter).to eq(x: 10) + end + it "state_properties(:x) does not turn off validation" do + resource_class.state_properties(:x) + expect { resource.x 'ouch' }.to raise_error Chef::Exceptions::ValidationFailed + end + it "state_properties(:x) does not turn off identity" do + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.identity_properties).to eq [ resource_class.properties[:x] ] + expect(resource_class.properties[:x].identity?).to be_truthy + expect(resource.identity).to eq 10 + end + end + + with_property ":x, Integer, identity: true, desired_state: false" do + before do + resource_class.class_eval do + def y + 20 + end + end + end + + it "state_properties(:x) leaves x identical" do + old_value = resource_class.properties[:y] + resource_class.state_properties(:x) + resource.x 10 + + expect(resource_class.properties[:y].object_id).to eq old_value.object_id + + expect(resource_class.properties[:x].desired_state?).to be_truthy + expect(resource_class.properties[:x].identity?).to be_truthy + expect(resource_class.identity_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.identity).to eq(10) + expect(resource_class.state_properties).to eq [ + resource_class.properties[:x] + ] + expect(resource.state_for_resource_reporter).to eq(x: 10) + end + + it "state_properties(:y) adds y to desired state" do + old_value = resource_class.properties[:x] + resource_class.state_properties(:y) + resource.x 10 + + expect(resource_class.properties[:x].object_id).to eq old_value.object_id + expect(resource_class.properties[:x].desired_state?).to be_falsey + expect(resource_class.properties[:y].desired_state?).to be_truthy + expect(resource_class.state_properties).to eq [ + resource_class.properties[:y] + ] + expect(resource.state_for_resource_reporter).to eq(y: 20) + end + + context "With a subclassed resource" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('blah') + end + + it "state_properties(:x) adds x to desired state" do + old_value = resource_class.properties[:y] + subresource_class.state_properties(:x) + subresource.x 10 + + expect(subresource_class.properties[:y].object_id).to eq old_value.object_id + + expect(subresource_class.properties[:x].desired_state?).to be_truthy + expect(subresource_class.properties[:x].identity?).to be_truthy + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.identity).to eq(10) + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.state_for_resource_reporter).to eq(x: 10) + end + + it "state_properties(:y) adds y to desired state" do + old_value = resource_class.properties[:x] + subresource_class.state_properties(:y) + subresource.x 10 + + expect(subresource_class.properties[:x].object_id).to eq old_value.object_id + expect(subresource_class.properties[:y].desired_state?).to be_truthy + expect(subresource_class.state_properties).to eq [ + subresource_class.properties[:y] + ] + expect(subresource.state_for_resource_reporter).to eq(y: 20) + + expect(subresource_class.properties[:x].identity?).to be_truthy + expect(subresource_class.identity_properties).to eq [ + subresource_class.properties[:x] + ] + expect(subresource.identity).to eq(10) + end + end + end + end + +end diff --git a/spec/unit/property/validation_spec.rb b/spec/unit/property/validation_spec.rb new file mode 100644 index 0000000000..31bb3f0739 --- /dev/null +++ b/spec/unit/property/validation_spec.rb @@ -0,0 +1,663 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property validation" do + include IntegrationSupport + + module Namer + @i = 0 + def self.next_resource_name + "chef_resource_property_spec_#{@i += 1}" + end + def self.reset_index + @current_index = 0 + end + def self.current_index + @current_index + end + def self.next_index + @current_index += 1 + end + end + + def lazy(&block) + Chef::DelayedEvaluator.new(&block) + end + + before do + Namer.reset_index + end + + def self.new_resource_name + Namer.next_resource_name + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + def blah + Namer.next_index + end + def self.blah + "class#{Namer.next_index}" + end + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + properties = properties.map { |property| "property #{property}" } + context "With properties #{english_join(properties)}", *tags do + before do + properties.each do |property_str| + resource_class.class_eval(property_str, __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + def self.validation_test(validation, success_values, failure_values, getter_values=[], *tags) + with_property ":x, #{validation}", *tags do + it "gets nil when retrieving the initial (non-set) value" do + expect(resource.x).to be_nil + end + success_values.each do |v| + it "value #{v.inspect} is valid" do + resource.instance_eval { @x = 'default' } + expect(resource.x v).to eq v + expect(resource.x).to eq v + end + end + failure_values.each do |v| + it "value #{v.inspect} is invalid" do + expect { resource.x v }.to raise_error Chef::Exceptions::ValidationFailed + resource.instance_eval { @x = 'default' } + expect { resource.x v }.to raise_error Chef::Exceptions::ValidationFailed + end + end + getter_values.each do |v| + it "setting value to #{v.inspect} does not change the value" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.instance_eval { @x = 'default' } + expect(resource.x v).to eq 'default' + expect(resource.x).to eq 'default' + end + end + end + end + + context "basic get, set, and nil set" do + with_property ":x, kind_of: String" do + context "when the variable already has a value" do + before do + resource.instance_eval { @x = 'default' } + end + it "get succeeds" do + expect(resource.x).to eq 'default' + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + it "set to nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 'str' + expect(resource.x nil).to eq 'str' + expect(resource.x).to eq 'str' + end + end + context "when the variable does not have an initial value" do + it "get succeeds" do + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + it "set to nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 'str' + expect(resource.x nil).to eq 'str' + expect(resource.x).to eq 'str' + end + end + end + with_property ":x, [ String, nil ]" do + context "when the variable already has a value" do + before do + resource.instance_eval { @x = 'default' } + end + it "get succeeds" do + expect(resource.x).to eq 'default' + end + it "set(nil) sets the value" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + context "when the variable does not have an initial value" do + it "get succeeds" do + expect(resource.x).to be_nil + end + it "set(nil) sets the value" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "set to valid value succeeds" do + expect(resource.x 'str').to eq 'str' + expect(resource.x).to eq 'str' + end + it "set to invalid value raises ValidationFailed" do + expect { resource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + end + end + + # Bare types + context "bare types" do + validation_test 'String', + [ 'hi' ], + [ 10 ], + [ nil ] + + validation_test ':a', + [ :a ], + [ :b ], + [ nil ] + + validation_test ':a, is: :b', + [ :a, :b ], + [ :c ], + [ nil ] + + validation_test ':a, is: [ :b, :c ]', + [ :a, :b, :c ], + [ :d ], + [ nil ] + + validation_test '[ :a, :b ], is: :c', + [ :a, :b, :c ], + [ :d ], + [ nil ] + + validation_test '[ :a, :b ], is: [ :c, :d ]', + [ :a, :b, :c, :d ], + [ :e ], + [ nil ] + + validation_test 'nil', + [ nil ], + [ :a ] + + validation_test '[ nil ]', + [ nil ], + [ :a ] + + validation_test '[]', + [], + [ :a ], + [ nil ] + end + + # is + context "is" do + # Class + validation_test 'is: String', + [ 'a', '' ], + [ :a, 1 ], + [ nil ] + + # Value + validation_test 'is: :a', + [ :a ], + [ :b ], + [ nil ] + + validation_test 'is: [ :a, :b ]', + [ :a, :b ], + [ [ :a, :b ] ], + [ nil ] + + validation_test 'is: [ [ :a, :b ] ]', + [ [ :a, :b ] ], + [ :a, :b ], + [ nil ] + + # Regex + validation_test 'is: /abc/', + [ 'abc', 'wowabcwow' ], + [ '', 'abac' ], + [ nil ] + + # Property + validation_test 'is: Chef::Property.new(is: :a)', + [ :a ], + [ :b, nil ] + + # RSpec Matcher + class Globalses + extend RSpec::Matchers + end + + validation_test "is: Globalses.eq(10)", + [ 10 ], + [ 1 ], + [ nil ] + + # Proc + validation_test 'is: proc { |x| x }', + [ true, 1 ], + [ false ], + [ nil ] + + validation_test 'is: proc { |x| x > blah }', + [ 10 ], + [ -1 ] + + validation_test 'is: nil', + [ nil ], + [ 'a' ] + + validation_test 'is: [ String, nil ]', + [ 'a', nil ], + [ :b ] + + validation_test 'is: []', + [], + [ :a ], + [ nil ] + end + + # Combination + context "combination" do + validation_test 'kind_of: String, equal_to: "a"', + [ 'a' ], + [ 'b' ], + [ nil ] + end + + # equal_to + context "equal_to" do + # Value + validation_test 'equal_to: :a', + [ :a ], + [ :b ], + [ nil ] + + validation_test 'equal_to: [ :a, :b ]', + [ :a, :b ], + [ [ :a, :b ] ], + [ nil ] + + validation_test 'equal_to: [ [ :a, :b ] ]', + [ [ :a, :b ] ], + [ :a, :b ], + [ nil ] + + validation_test 'equal_to: nil', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'equal_to: [ "a", nil ]', + [ 'a' ], + [ 'b' ], + [ nil ] + + validation_test 'equal_to: [ nil, "a" ]', + [ 'a' ], + [ 'b' ], + [ nil ] + + validation_test 'equal_to: []', + [], + [ :a ], + [ nil ] + end + + # kind_of + context "kind_of" do + validation_test 'kind_of: String', + [ 'a' ], + [ :b ], + [ nil ] + + validation_test 'kind_of: [ String, Symbol ]', + [ 'a', :b ], + [ 1 ], + [ nil ] + + validation_test 'kind_of: [ Symbol, String ]', + [ 'a', :b ], + [ 1 ], + [ nil ] + + validation_test 'kind_of: NilClass', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'kind_of: [ NilClass, String ]', + [ 'a' ], + [ :a ], + [ nil ] + + validation_test 'kind_of: []', + [], + [ :a ], + [ nil ] + + validation_test 'kind_of: nil', + [], + [ :a ], + [ nil ] + end + + # regex + context "regex" do + validation_test 'regex: /abc/', + [ 'xabcy' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: [ /abc/, /z/ ]', + [ 'xabcy', 'aza' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: [ /z/, /abc/ ]', + [ 'xabcy', 'aza' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: [ [ /z/, /abc/ ], [ /n/ ] ]', + [ 'xabcy', 'aza', 'ana' ], + [ 'gbh', 123 ], + [ nil ] + + validation_test 'regex: []', + [], + [ :a ], + [ nil ] + + validation_test 'regex: nil', + [], + [ :a ], + [ nil ] + end + + # callbacks + context "callbacks" do + validation_test 'callbacks: { "a" => proc { |x| x > 10 }, "b" => proc { |x| x%2 == 0 } }', + [ 12 ], + [ 11, 4 ] + + validation_test 'callbacks: { "a" => proc { |x| x%2 == 0 }, "b" => proc { |x| x > 10 } }', + [ 12 ], + [ 11, 4 ] + + validation_test 'callbacks: { "a" => proc { |x| x.nil? } }', + [ ], + [ 'a' ], + [ nil ] + + validation_test 'callbacks: {}', + [ :a ], + [], + [ nil ] + end + + # respond_to + context "respond_to" do + validation_test 'respond_to: :split', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: "split"', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: :to_s', + [ :a ], + [], + [ nil ] + + validation_test 'respond_to: [ :split, :to_s ]', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: %w(split to_s)', + [ 'hi' ], + [ 1 ], + [ nil ] + + validation_test 'respond_to: [ :to_s, :split ]', + [ 'hi' ], + [ 1, ], + [ nil ] + + validation_test 'respond_to: []', + [ :a ], + [], + [ nil ] + + validation_test 'respond_to: nil', + [ :a ], + [], + [ nil ] + end + + context "cannot_be" do + validation_test 'cannot_be: :empty', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: "empty"', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :empty, :nil ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ "empty", "nil" ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :nil, :empty ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: [ :empty, :nil, :blahblah ]', + [ 1, [1,2], { a: 10 } ], + [ [] ], + [ nil ] + + validation_test 'cannot_be: []', + [ :a ], + [], + [ nil ] + + validation_test 'cannot_be: nil', + [ :a ], + [], + [ nil ] + + end + + context "required" do + with_property ':x, required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + expect(resource.x nil).to eq 1 + expect(resource.x).to eq 1 + end + end + + with_property ':x, [String, nil], required: true' do + it "if x is not specified, retrieval fails" do + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + end + it "value nil is valid" do + expect(resource.x nil).to be_nil + expect(resource.x).to be_nil + end + it "value '1' is valid" do + expect(resource.x '1').to eq '1' + expect(resource.x).to eq '1' + end + it "value 1 is invalid" do + expect { resource.x 1 }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + with_property ':x, name_property: true, required: true' do + it "if x is not specified, the name property is returned" do + expect(resource.x).to eq 'blah' + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil emits a deprecation warning and does a get" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + expect(resource.x nil).to eq 1 + expect(resource.x).to eq 1 + end + end + + with_property ':x, default: 10, required: true' do + it "if x is not specified, the default is returned" do + expect(resource.x).to eq 10 + end + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + it "value nil is invalid" do + expect { resource.x nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + expect(resource.x nil).to eq 1 + expect(resource.x).to eq 1 + end + end + end + + context "custom validators (def _pv_blarghle)" do + before do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + end + + with_property ':x, blarghle: 1' do + context "and a class that implements _pv_blarghle" do + before do + resource_class.class_eval do + def _pv_blarghle(opts, key, value) + if _pv_opts_lookup(opts, key) != value + raise Chef::Exceptions::ValidationFailed, "ouch" + end + end + end + end + + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + + it "value '1' is invalid" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect { resource.x '1' }.to raise_error Chef::Exceptions::ValidationFailed + end + + it "value nil does a get" do + Chef::Config[:treat_deprecation_warnings_as_errors] = false + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + end + + with_property ':x, blarghle: 1' do + context "and a class that implements _pv_blarghle" do + before do + resource_class.class_eval do + def _pv_blarghle(opts, key, value) + if _pv_opts_lookup(opts, key) != value + raise Chef::Exceptions::ValidationFailed, "ouch" + end + end + end + end + + it "value 1 is valid" do + expect(resource.x 1).to eq 1 + expect(resource.x).to eq 1 + end + + it "value '1' is invalid" do + expect { resource.x '1' }.to raise_error Chef::Exceptions::ValidationFailed + end + + it "value nil does a get" do + resource.x 1 + resource.x nil + expect(resource.x).to eq 1 + end + end + end + end +end diff --git a/spec/unit/property_spec.rb b/spec/unit/property_spec.rb new file mode 100644 index 0000000000..50764aa7a2 --- /dev/null +++ b/spec/unit/property_spec.rb @@ -0,0 +1,972 @@ +require 'support/shared/integration/integration_helper' + +describe "Chef::Resource.property" do + include IntegrationSupport + + module Namer + @i = 0 + def self.next_resource_name + "chef_resource_property_spec_#{@i += 1}" + end + def self.reset_index + @current_index = 0 + end + def self.current_index + @current_index + end + def self.next_index + @current_index += 1 + end + end + + def lazy(&block) + Chef::DelayedEvaluator.new(&block) + end + + before do + Namer.reset_index + end + + def self.new_resource_name + Namer.next_resource_name + end + + let(:resource_class) do + new_resource_name = self.class.new_resource_name + Class.new(Chef::Resource) do + resource_name new_resource_name + def next_index + Namer.next_index + end + end + end + + let(:resource) do + resource_class.new("blah") + end + + def self.english_join(values) + return '<nothing>' if values.size == 0 + return values[0].inspect if values.size == 1 + "#{values[0..-2].map { |v| v.inspect }.join(", ")} and #{values[-1].inspect}" + end + + def self.with_property(*properties, &block) + tags_index = properties.find_index { |p| !p.is_a?(String)} + if tags_index + properties, tags = properties[0..tags_index-1], properties[tags_index..-1] + else + tags = [] + end + if properties.size == 1 + description = "With property #{properties.first}" + else + description = "With properties #{english_join(properties.map { |property| "#{property.inspect}" })}" + end + context description, *tags do + before do + properties.each do |property_str| + resource_class.class_eval("property #{property_str}", __FILE__, __LINE__) + end + end + instance_eval(&block) + end + end + + # Basic properties + with_property ':bare_property' do + it "can be set" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property).to eq 10 + end + it "emits a deprecation warning and does a get, if set to nil" do + expect(resource.bare_property 10).to eq 10 + expect { resource.bare_property nil }.to raise_error Chef::Exceptions::DeprecatedFeatureError + Chef::Config[:treat_deprecation_warnings_as_errors] = false + expect(resource.bare_property nil).to eq 10 + expect(resource.bare_property).to eq 10 + end + it "can be updated" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property 20).to eq 20 + expect(resource.bare_property).to eq 20 + end + it "can be set with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property).to eq 10 + end + it "can be set to nil with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property = nil).to be_nil + expect(resource.bare_property).to be_nil + end + it "can be updated with =" do + expect(resource.bare_property 10).to eq 10 + expect(resource.bare_property = 20).to eq 20 + expect(resource.bare_property).to eq 20 + end + end + + with_property ":x, Integer" do + context "and subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) do + subresource_class.new('blah') + end + + it "x is inherited" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + + context "with property :y on the subclass" do + before do + subresource_class.class_eval do + property :y + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + end + it "y is there" do + expect(subresource.y 10).to eq 10 + expect(subresource.y).to eq 10 + expect(subresource.y = 20).to eq 20 + expect(subresource.y).to eq 20 + expect(subresource_class.properties[:y]).not_to be_nil + end + it "y is not on the superclass" do + expect { resource_class.y 10 }.to raise_error + expect(resource_class.properties[:y]).to be_nil + end + end + + context "with property :x on the subclass" do + before do + subresource_class.class_eval do + property :x + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + context "with property :x, default: 80 on the subclass" do + before do + subresource_class.class_eval do + property :x, default: 80 + end + end + + it "x is still there" do + expect(subresource.x 10).to eq 10 + expect(subresource.x).to eq 10 + expect(subresource.x = 20).to eq 20 + expect(subresource.x).to eq 20 + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x defaults to 80" do + expect(subresource.x).to eq 80 + end + + it "x's validation is inherited" do + expect { subresource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + end + end + + context "with property :x, String on the subclass" do + before do + subresource_class.class_eval do + property :x, String + end + end + + it "x is still there" do + expect(subresource.x "10").to eq "10" + expect(subresource.x).to eq "10" + expect(subresource.x = "20").to eq "20" + expect(subresource.x).to eq "20" + expect(subresource_class.properties[:x]).not_to be_nil + expect(subresource_class.properties[:x]).not_to eq resource_class.properties[:x] + end + + it "x's validation is overwritten" do + expect { subresource.x 10 }.to raise_error Chef::Exceptions::ValidationFailed + expect(subresource.x 'ohno').to eq 'ohno' + expect(subresource.x).to eq 'ohno' + end + + it "the superclass's validation for x is still there" do + expect { resource.x 'ohno' }.to raise_error Chef::Exceptions::ValidationFailed + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + end + end + end + end + + context "Chef::Resource::Property#reset_property" do + it "when a resource is newly created, reset_property(:name) sets property to nil" do + expect(resource.property_is_set?(:name)).to be_truthy + resource.reset_property(:name) + expect(resource.property_is_set?(:name)).to be_falsey + expect(resource.name).to be_nil + end + + it "when referencing an undefined property, reset_property(:x) raises an error" do + expect { resource.reset_property(:x) }.to raise_error(ArgumentError) + end + + with_property ':x' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + it "when x is set, reset_property resets it" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + end + + with_property ':x, Integer' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + it "when x is set, reset_property resets it even though `nil` is technically invalid" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to be_nil + end + end + + with_property ':x, default: 10' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + it "when x is set, reset_property resets it and it returns the default" do + resource.x 20 + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + end + + with_property ':x, default: lazy { 10 }' do + it "when the resource is newly created, reset_property(:x) does nothing" do + expect(resource.property_is_set?(:x)).to be_falsey + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + it "when x is set, reset_property resets it and it returns the default" do + resource.x 20 + resource.reset_property(:x) + expect(resource.property_is_set?(:x)).to be_falsey + expect(resource.x).to eq 10 + end + end + end + + context "Chef::Resource::Property#property_is_set?" do + it "when a resource is newly created, property_is_set?(:name) is true" do + expect(resource.property_is_set?(:name)).to be_truthy + end + + it "when referencing an undefined property, property_is_set?(:x) raises an error" do + expect { resource.property_is_set?(:x) }.to raise_error(ArgumentError) + end + + with_property ':x' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + + with_property ':x, default: 10' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + + with_property ':x, default: nil' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set to a lazy value, property_is_set?(:x) is true" do + resource.x lazy { 10 } + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + + with_property ':x, default: lazy { 10 }' do + it "when the resource is newly created, property_is_set?(:x) is false" do + expect(resource.property_is_set?(:x)).to be_falsey + end + it "when x is set, property_is_set?(:x) is true" do + resource.x 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is set with =, property_is_set?(:x) is true" do + resource.x = 10 + expect(resource.property_is_set?(:x)).to be_truthy + end + it "when x is retrieved, property_is_set?(:x) is false" do + resource.x + expect(resource.property_is_set?(:x)).to be_falsey + end + end + end + + context "Chef::Resource::Property#default" do + with_property ':x, default: 10' do + it "when x is set, it returns its value" do + expect(resource.x 20).to eq 20 + expect(resource.property_is_set?(:x)).to be_truthy + expect(resource.x).to eq 20 + end + it "when x is not set, it returns 10" do + expect(resource.x).to eq 10 + end + it "when x is not set, it is not included in state" do + expect(resource.state_for_resource_reporter).to eq({}) + end + it "when x is set to nil, it returns nil" do + resource.instance_eval { @x = nil } + expect(resource.x).to be_nil + end + + context "With a subclass" do + let(:subresource_class) do + new_resource_name = self.class.new_resource_name + Class.new(resource_class) do + resource_name new_resource_name + end + end + let(:subresource) { subresource_class.new('blah') } + it "The default is inherited" do + expect(subresource.x).to eq 10 + end + end + end + + with_property ':x, default: 10, identity: true' do + it "when x is not set, it is included in identity" do + expect(resource.identity).to eq(10) + end + end + + with_property ':x, default: 1, identity: true', ':y, default: 2, identity: true' do + it "when x is not set, it is still included in identity" do + resource.y 20 + expect(resource.identity).to eq(x: 1, y: 20) + end + end + + with_property ':x, default: nil' do + it "when x is not set, it returns nil" do + expect(resource.x).to be_nil + end + end + + with_property ':x' do + it "when x is not set, it returns nil" do + expect(resource.x).to be_nil + end + end + + context "hash default" do + context "(deprecations allowed)" do + before { Chef::Config[:treat_deprecation_warnings_as_errors] = false } + + with_property ':x, default: {}' do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + it "The same exact value is returned multiple times in a row" do + value = resource.x + expect(value).to eq({}) + expect(resource.x.object_id).to eq(value.object_id) + end + it "Multiple instances of x receive the exact same value" do + expect(resource.x.object_id).to eq(resource_class.new('blah2').x.object_id) + end + end + end + + it "when a property is declared with default: {}, a warning is issued" do + expect(Chef::Log).to receive(:deprecation).with( /^Property .+\.x has an array or hash default \(\{\}\)\. This means that if one resource modifies or appends to it, all other resources of the same type will also see the changes\. Either freeze the constant with \`\.freeze\` to prevent appending, or use lazy \{ \{\} \}\.$/, /property_spec\.rb/ ) + resource_class.class_eval("property :x, default: {}", __FILE__, __LINE__) + expect(resource.x).to eq({}) + end + + with_property ':x, default: lazy { {} }' do + it "when x is not set, it returns {}" do + expect(resource.x).to eq({}) + end + # it "The value is different each time it is called" do + # value = resource.x + # expect(value).to eq({}) + # expect(resource.x.object_id).not_to eq(value.object_id) + # end + it "Multiple instances of x receive different values" do + expect(resource.x.object_id).not_to eq(resource_class.new('blah2').x.object_id) + end + end + end + + context "with a class with 'blah' as both class and instance methods" do + before do + resource_class.class_eval do + def self.blah + 'class' + end + def blah + "#{name}#{next_index}" + end + end + end + + with_property ':x, default: lazy { blah }' do + it "x is run in context of the instance" do + expect(resource.x).to eq "blah1" + end + it "x is run in the context of each instance it is run in" do + expect(resource.x).to eq "blah1" + expect(resource_class.new('another').x).to eq "another2" + # expect(resource.x).to eq "blah3" + end + end + + with_property ':x, default: lazy { |x| "#{blah}#{x.blah}" }' do + it "x is run in context of the class (where it was defined) and passed the instance" do + expect(resource.x).to eq "classblah1" + end + it "x is passed the value of each instance it is run in" do + expect(resource.x).to eq "classblah1" + expect(resource_class.new('another').x).to eq "classanother2" + # expect(resource.x).to eq "classblah3" + end + end + end + + context "validation of defaults" do + with_property ':x, String, default: 10' do + it "when the resource is created, no error is raised" do + resource + end + it "when x is set, no error is raised" do + expect(resource.x 'hi').to eq 'hi' + expect(resource.x).to eq 'hi' + end + it "when x is retrieved, no validation error is raised" do + expect(resource.x).to eq 10 + end + # it "when x is retrieved, a validation error is raised" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + + with_property ":x, String, default: lazy { Namer.next_index }" do + it "when the resource is created, no error is raised" do + resource + end + it "when x is set, no error is raised" do + expect(resource.x 'hi').to eq 'hi' + expect(resource.x).to eq 'hi' + end + it "when x is retrieved, no validation error is raised" do + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + # it "when x is retrieved, a validation error is raised" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # expect(Namer.current_index).to eq 1 + # end + end + + with_property ":x, default: lazy { Namer.next_index.to_s }, is: proc { |v| Namer.next_index; true }" do + it "validation is not run at all on the default value" do + expect(resource.x).to eq '1' + expect(Namer.current_index).to eq 1 + end + # it "validation is run each time" do + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # end + end + + with_property ":x, default: lazy { Namer.next_index.to_s.freeze }, is: proc { |v| Namer.next_index; true }" do + it "validation is not run at all on the default value" do + expect(resource.x).to eq '1' + expect(Namer.current_index).to eq 1 + end + # it "validation is only run the first time" do + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # expect(resource.x).to eq '1' + # expect(Namer.current_index).to eq 2 + # end + end + end + + context "coercion of defaults" do + # Frozen default, non-frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run exactly once" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + # Frozen default, frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}".freeze }, default: 10' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run each time" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '102' + expect(Namer.current_index).to eq 2 + end + end + + # Frozen lazy default, non-frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run exactly once" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + # Non-frozen lazy default, frozen coerce + with_property ':x, coerce: proc { |v| "#{v}#{next_index}".freeze }, default: lazy { "10" }' do + it "when the resource is created, the proc is not yet run" do + resource + expect(Namer.current_index).to eq 0 + end + it "when x is set, coercion is run" do + expect(resource.x 'hi').to eq 'hi1' + expect(resource.x).to eq 'hi1' + expect(Namer.current_index).to eq 1 + end + it "when x is retrieved, coercion is run each time" do + expect(resource.x).to eq '101' + expect(resource.x).to eq '102' + expect(Namer.current_index).to eq 2 + end + end + + with_property ':x, proc { |v| Namer.next_index; true }, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "coercion is only run the first time x is retrieved, and validation is not run" do + expect(Namer.current_index).to eq 0 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + + context "validation and coercion of defaults" do + with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when x is retrieved, it is coerced before validating and passes" do + expect(resource.x).to eq '101' + end + end + with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: 10' do + it "when x is retrieved, it is coerced and not validated" do + expect(resource.x).to eq '101' + end + # it "when x is retrieved, it is coerced before validating and fails" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + with_property ':x, String, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when x is retrieved, it is coerced before validating and passes" do + expect(resource.x).to eq '101' + end + end + with_property ':x, Integer, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "when x is retrieved, it is coerced and not validated" do + expect(resource.x).to eq '101' + end + # it "when x is retrieved, it is coerced before validating and fails" do + # expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + # end + end + with_property ':x, proc { |v| Namer.next_index; true }, coerce: proc { |v| "#{v}#{next_index}" }, default: lazy { 10 }' do + it "coercion is only run the first time x is retrieved, and validation is not run" do + expect(Namer.current_index).to eq 0 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + expect(resource.x).to eq '101' + expect(Namer.current_index).to eq 1 + end + end + end + end + end + + context "Chef::Resource#lazy" do + with_property ':x' do + it "setting x to a lazy value does not run it immediately" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "you can set x to a lazy value in the instance" do + resource.instance_eval do + x lazy { Namer.next_index } + end + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "retrieving a lazy value pops it open" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 1 + end + it "retrieving a lazy value twice evaluates it twice" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(resource.x).to eq 2 + expect(Namer.current_index).to eq 2 + end + it "setting the same lazy value on two different instances runs it on each instancee" do + resource2 = resource_class.new("blah2") + l = lazy { Namer.next_index } + resource.x l + resource2.x l + expect(resource2.x).to eq 1 + expect(resource.x).to eq 2 + expect(resource2.x).to eq 3 + end + + context "when the class has a class and instance method named blah" do + before do + resource_class.class_eval do + def self.blah + "class" + end + def blah + "#{name}#{Namer.next_index}" + end + end + end + def blah + "example" + end + # it "retrieving lazy { blah } gets the instance variable" do + # resource.x lazy { blah } + # expect(resource.x).to eq "blah1" + # end + # it "retrieving lazy { blah } from two different instances gets two different instance variables" do + # resource2 = resource_class.new("another") + # l = lazy { blah } + # resource2.x l + # resource.x l + # expect(resource2.x).to eq "another1" + # expect(resource.x).to eq "blah2" + # expect(resource2.x).to eq "another3" + # end + it 'retrieving lazy { |x| "#{blah}#{x.blah}" } gets the example and instance variables' do + resource.x lazy { |x| "#{blah}#{x.blah}" } + expect(resource.x).to eq "exampleblah1" + end + it 'retrieving lazy { |x| "#{blah}#{x.blah}" } from two different instances gets two different instance variables' do + resource2 = resource_class.new("another") + l = lazy { |x| "#{blah}#{x.blah}" } + resource2.x l + resource.x l + expect(resource2.x).to eq "exampleanother1" + expect(resource.x).to eq "exampleblah2" + expect(resource2.x).to eq "exampleanother3" + end + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are not coerced on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are coerced on get" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + it "lazy values are coerced on each access" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq "34" + expect(Namer.current_index).to eq 4 + end + end + + with_property ':x, String' do + it "lazy values are not validated on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are validated on get" do + resource.x lazy { Namer.next_index } + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 1 + end + end + + with_property ':x, is: proc { |v| Namer.next_index; true }' do + it "lazy values are validated on each access" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq 1 + expect(Namer.current_index).to eq 2 + expect(resource.x).to eq 3 + expect(Namer.current_index).to eq 4 + end + end + + with_property ':x, Integer, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are not validated or coerced on set" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + end + it "lazy values are coerced before being validated, which fails" do + resource.x lazy { Namer.next_index } + expect(Namer.current_index).to eq 0 + expect { resource.x }.to raise_error Chef::Exceptions::ValidationFailed + expect(Namer.current_index).to eq 2 + end + end + + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }, is: proc { |v| Namer.next_index; true }' do + it "lazy values are coerced and validated exactly once" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 3 + expect(resource.x).to eq "45" + expect(Namer.current_index).to eq 6 + end + end + + with_property ':x, String, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "lazy values are coerced before being validated, which succeeds" do + resource.x lazy { Namer.next_index } + expect(resource.x).to eq "12" + expect(Namer.current_index).to eq 2 + end + end + end + + context "Chef::Resource::Property#coerce" do + with_property ':x, coerce: proc { |v| "#{v}#{Namer.next_index}" }' do + it "coercion runs on set" do + expect(resource.x 10).to eq "101" + expect(Namer.current_index).to eq 1 + end + it "coercion sets the value (and coercion does not run on get)" do + expect(resource.x 10).to eq "101" + expect(resource.x).to eq "101" + expect(Namer.current_index).to eq 1 + end + it "coercion runs each time set happens" do + expect(resource.x 10).to eq "101" + expect(Namer.current_index).to eq 1 + expect(resource.x 10).to eq "102" + expect(Namer.current_index).to eq 2 + end + end + with_property ':x, coerce: proc { |x| Namer.next_index; raise "hi" if x == 10; x }, is: proc { |x| Namer.next_index; x != 10 }' do + it "failed coercion fails to set the value" do + resource.x 20 + expect(resource.x).to eq 20 + expect(Namer.current_index).to eq 2 + expect { resource.x 10 }.to raise_error 'hi' + expect(resource.x).to eq 20 + expect(Namer.current_index).to eq 3 + end + it "validation does not run if coercion fails" do + expect { resource.x 10 }.to raise_error 'hi' + expect(Namer.current_index).to eq 1 + end + end + end + + context "Chef::Resource::Property validation" do + with_property ':x, is: proc { |v| Namer.next_index; v.is_a?(Integer) }' do + it "validation runs on set" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + end + it "validation sets the value (and validation does not run on get)" do + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + expect(Namer.current_index).to eq 1 + end + it "validation runs each time set happens" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 2 + end + it "failed validation fails to set the value" do + expect(resource.x 10).to eq 10 + expect(Namer.current_index).to eq 1 + expect { resource.x 'blah' }.to raise_error Chef::Exceptions::ValidationFailed + expect(resource.x).to eq 10 + expect(Namer.current_index).to eq 2 + end + end + end + + [ 'name_attribute', 'name_property' ].each do |name| + context "Chef::Resource::Property##{name}" do + with_property ":x, #{name}: true" do + it "defaults x to resource.name" do + expect(resource.x).to eq 'blah' + end + it "does not pick up resource.name if set" do + expect(resource.x 10).to eq 10 + expect(resource.x).to eq 10 + end + it "binds to the latest resource.name when run" do + resource.name 'foo' + expect(resource.x).to eq 'foo' + end + it "caches resource.name" do + expect(resource.x).to eq 'blah' + resource.name 'foo' + expect(resource.x).to eq 'blah' + end + end + with_property ":x, default: 10, #{name}: true" do + it "chooses default over #{name}" do + expect(resource.x).to eq 10 + end + end + with_property ":x, #{name}: true, default: 10" do + it "chooses default over #{name}" do + expect(resource.x).to eq 10 + end + end + with_property ":x, #{name}: true, required: true" do + it "defaults x to resource.name" do + expect(resource.x).to eq 'blah' + end + end + end + end +end diff --git a/spec/unit/provider/deploy_spec.rb b/spec/unit/provider/deploy_spec.rb index 63658ac601..e6a7125e32 100644 --- a/spec/unit/provider/deploy_spec.rb +++ b/spec/unit/provider/deploy_spec.rb @@ -362,7 +362,7 @@ describe Chef::Provider::Deploy do it "skips the migration when resource.migrate => false but runs symlinks before migration" do @resource.migrate false - expect(@provider).not_to receive :run_command + expect(@provider).not_to receive :shell_out! expect(@provider).to receive :run_symlinks_before_migrate @provider.migrate end @@ -378,7 +378,7 @@ describe Chef::Provider::Deploy do allow(STDOUT).to receive(:tty?).and_return(true) allow(Chef::Log).to receive(:info?).and_return(true) - expect(@provider).to receive(:run_command).with(:command => "migration_foo", :cwd => @expected_release_dir, + expect(@provider).to receive(:shell_out!).with("migration_foo",:cwd => @expected_release_dir, :user => "deployNinja", :group => "deployNinjas", :log_level => :info, :live_stream => STDOUT, :log_tag => "deploy[/my/deploy/dir]", @@ -445,13 +445,13 @@ describe Chef::Provider::Deploy do end it "does nothing for restart if restart_command is empty" do - expect(@provider).not_to receive(:run_command) + expect(@provider).not_to receive(:shell_out!) @provider.restart end it "runs the restart command in the current application dir when the resource has a restart_command" do @resource.restart_command "restartcmd" - expect(@provider).to receive(:run_command).with(:command => "restartcmd", :cwd => "/my/deploy/dir/current", :log_tag => "deploy[/my/deploy/dir]", :log_level => :debug) + expect(@provider).to receive(:shell_out!).with("restartcmd", :cwd => "/my/deploy/dir/current", :log_tag => "deploy[/my/deploy/dir]", :log_level => :debug) @provider.restart end @@ -509,7 +509,7 @@ describe Chef::Provider::Deploy do it "shouldn't give a no method error on migrate if the environment is nil" do allow(@provider).to receive(:enforce_ownership) allow(@provider).to receive(:run_symlinks_before_migrate) - allow(@provider).to receive(:run_command) + allow(@provider).to receive(:shell_out!) @provider.migrate end @@ -622,7 +622,7 @@ describe Chef::Provider::Deploy do gems = @provider.send(:gem_packages) - expect(gems.map { |g| g.action }).to eq([:install]) + expect(gems.map { |g| g.action }).to eq([%i{install}]) expect(gems.map { |g| g.name }).to eq(%w{eventmachine}) expect(gems.map { |g| g.version }).to eq(%w{0.12.9}) end diff --git a/spec/unit/provider/directory_spec.rb b/spec/unit/provider/directory_spec.rb index 38d6db8320..4fad8c8906 100644 --- a/spec/unit/provider/directory_spec.rb +++ b/spec/unit/provider/directory_spec.rb @@ -197,6 +197,41 @@ describe Chef::Provider::Directory do expect { directory.run_action(:create) }.to raise_error(Chef::Exceptions::EnclosingDirectoryDoesNotExist) end end + + describe "on OS X" do + before do + allow(node).to receive(:[]).with("platform").and_return('mac_os_x') + new_resource.path "/usr/bin/chef_test" + new_resource.recursive false + allow_any_instance_of(Chef::Provider::File).to receive(:do_selinux) + end + + it "os x 10.10 can write to sip locations" do + allow(node).to receive(:[]).with("platform_version").and_return('10.10') + allow(Dir).to receive(:mkdir).and_return([true], []) + allow(::File).to receive(:directory?).and_return(true) + allow(Chef::FileAccessControl).to receive(:writable?).and_return(true) + directory.run_action(:create) + expect(new_resource).to be_updated + end + + it "os x 10.11 cannot write to sip locations" do + allow(node).to receive(:[]).with("platform_version").and_return('10.11') + allow(::File).to receive(:directory?).and_return(true) + allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) + expect {directory.run_action(:create) }.to raise_error(Chef::Exceptions::InsufficientPermissions) + end + + it "os x 10.11 can write to sip exlcusions" do + new_resource.path "/usr/local/chef_test" + allow(node).to receive(:[]).with("platform_version").and_return('10.11') + allow(::File).to receive(:directory?).and_return(true) + allow(Dir).to receive(:mkdir).and_return([true], []) + allow(Chef::FileAccessControl).to receive(:writable?).and_return(false) + directory.run_action(:create) + expect(new_resource).to be_updated + end + end end describe "#run_action(:create)" do diff --git a/spec/unit/provider/dsc_resource_spec.rb b/spec/unit/provider/dsc_resource_spec.rb index 0a6c22bdcf..65c1c019f0 100644 --- a/spec/unit/provider/dsc_resource_spec.rb +++ b/spec/unit/provider/dsc_resource_spec.rb @@ -35,10 +35,10 @@ describe Chef::Provider::DscResource do node } - it 'raises a NoProviderAvailable exception' do + it 'raises a ProviderNotFound exception' do expect(provider).not_to receive(:meta_configuration) expect{provider.run_action(:run)}.to raise_error( - Chef::Exceptions::NoProviderAvailable, /5\.0\.10018\.0/) + Chef::Exceptions::ProviderNotFound, /5\.0\.10018\.0/) end end @@ -56,7 +56,7 @@ describe Chef::Provider::DscResource do expect(provider).to receive(:meta_configuration).and_return( meta_configuration) expect { provider.run_action(:run) }.to raise_error( - Chef::Exceptions::NoProviderAvailable, /Disabled/) + Chef::Exceptions::ProviderNotFound, /Disabled/) end end diff --git a/spec/unit/provider/dsc_script_spec.rb b/spec/unit/provider/dsc_script_spec.rb index d4b2eb3b22..76589e71c1 100644 --- a/spec/unit/provider/dsc_script_spec.rb +++ b/spec/unit/provider/dsc_script_spec.rb @@ -158,14 +158,14 @@ describe Chef::Provider::DscScript do expect { provider.run_action(:run) - }.to raise_error(Chef::Exceptions::NoProviderAvailable) + }.to raise_error(Chef::Exceptions::ProviderNotFound) end end it 'raises an exception if Powershell is not present' do expect { provider.run_action(:run) - }.to raise_error(Chef::Exceptions::NoProviderAvailable) + }.to raise_error(Chef::Exceptions::ProviderNotFound) end end diff --git a/spec/unit/provider/ifconfig_spec.rb b/spec/unit/provider/ifconfig_spec.rb index d290ab7066..4940f19a45 100644 --- a/spec/unit/provider/ifconfig_spec.rb +++ b/spec/unit/provider/ifconfig_spec.rb @@ -46,7 +46,7 @@ describe Chef::Provider::Ifconfig do allow(@provider).to receive(:shell_out).and_return(@status) @provider.load_current_resource end - it "should track state of ifconfig failure." do + it "should track state of ifconfig failure" do expect(@provider.instance_variable_get("@status").exitstatus).not_to eq(0) end it "should thrown an exception when ifconfig fails" do @@ -68,6 +68,16 @@ describe Chef::Provider::Ifconfig do expect(@new_resource).to be_updated end + it "should set the address to target if specified" do + allow(@provider).to receive(:load_current_resource) + @new_resource.target "172.16.32.2" + command = "ifconfig eth0 172.16.32.2 netmask 255.255.254.0 metric 1 mtu 1500" + expect(@provider).to receive(:run_command).with(:command => command) + + @provider.run_action(:add) + expect(@new_resource).to be_updated + end + it "should not add an interface if it already exists" do allow(@provider).to receive(:load_current_resource) expect(@provider).not_to receive(:run_command) @@ -85,7 +95,7 @@ describe Chef::Provider::Ifconfig do describe Chef::Provider::Ifconfig, "action_enable" do - it "should enable interface if does not exist" do + it "should enable interface if it does not exist" do allow(@provider).to receive(:load_current_resource) @current_resource.inet_addr nil command = "ifconfig eth0 10.0.0.1 netmask 255.255.254.0 metric 1 mtu 1500" @@ -96,6 +106,16 @@ describe Chef::Provider::Ifconfig do expect(@new_resource).to be_updated end + it "should set the address to target if specified" do + allow(@provider).to receive(:load_current_resource) + @new_resource.target "172.16.32.2" + command = "ifconfig eth0 172.16.32.2 netmask 255.255.254.0 metric 1 mtu 1500" + expect(@provider).to receive(:run_command).with(:command => command) + + @provider.run_action(:enable) + expect(@new_resource).to be_updated + end + it "should not enable interface if it already exists" do allow(@provider).to receive(:load_current_resource) expect(@provider).not_to receive(:run_command) diff --git a/spec/unit/provider/mount/aix_spec.rb b/spec/unit/provider/mount/aix_spec.rb index ca0ddd006c..e232592275 100644 --- a/spec/unit/provider/mount/aix_spec.rb +++ b/spec/unit/provider/mount/aix_spec.rb @@ -126,9 +126,10 @@ ENABLED @provider.run_action(:mount) end - it "should not mount resource if it is already mounted" do + it "should not mount resource if it is already mounted and the options have not changed" do stub_mounted_enabled(@provider, @mounted_output, "") + allow(@provider).to receive(:mount_options_unchanged?).and_return(true) expect(@provider).not_to receive(:mount_fs) @provider.run_action(:mount) diff --git a/spec/unit/provider/mount/mount_spec.rb b/spec/unit/provider/mount/mount_spec.rb index 7a37ffe74e..dd13a62796 100644 --- a/spec/unit/provider/mount/mount_spec.rb +++ b/spec/unit/provider/mount/mount_spec.rb @@ -323,6 +323,12 @@ describe Chef::Provider::Mount::Mount do @provider.mount_fs() end + it "should not mount the filesystem if it is mounted and the options have not changed" do + allow(@current_resource).to receive(:mounted).and_return(true) + expect(@provider).to_not receive(:shell_out!) + @provider.mount_fs() + end + end describe "umount_fs" do diff --git a/spec/unit/provider/mount/windows_spec.rb b/spec/unit/provider/mount/windows_spec.rb index 467d923c6a..2de6f11d43 100644 --- a/spec/unit/provider/mount/windows_spec.rb +++ b/spec/unit/provider/mount/windows_spec.rb @@ -111,6 +111,20 @@ describe Chef::Provider::Mount::Windows do allow(@current_resource).to receive(:mounted).and_return(true) @provider.mount_fs end + + it "should remount the filesystem if it is mounted and the options have changed" do + expect(@vol).to receive(:add).with(:remote => @new_resource.device, + :username => @new_resource.username, + :domainname => @new_resource.domain, + :password => @new_resource.password) + @provider.mount_fs + end + + it "should not mount the filesystem if it is mounted and the options have not changed" do + expect(@vol).to_not receive(:add) + allow(@current_resource).to receive(:mounted).and_return(true) + @provider.mount_fs + end end describe "when unmounting a file system" do diff --git a/spec/unit/provider/mount_spec.rb b/spec/unit/provider/mount_spec.rb index e9fe3fa050..cc2a456440 100644 --- a/spec/unit/provider/mount_spec.rb +++ b/spec/unit/provider/mount_spec.rb @@ -61,8 +61,19 @@ describe Chef::Provider::Mount do expect(new_resource).to be_updated_by_last_action end - it "should not mount the filesystem if it is mounted" do + it "should remount the filesystem if it is mounted and the options have changed" do allow(current_resource).to receive(:mounted).and_return(true) + allow(provider).to receive(:mount_options_unchanged?).and_return(false) + expect(provider).to receive(:umount_fs).and_return(true) + expect(provider).to receive(:wait_until_unmounted) + expect(provider).to receive(:mount_fs).and_return(true) + provider.run_action(:mount) + expect(new_resource).to be_updated_by_last_action + end + + it "should not mount the filesystem if it is mounted and the options have not changed" do + allow(current_resource).to receive(:mounted).and_return(true) + expect(provider).to receive(:mount_options_unchanged?).and_return(true) expect(provider).not_to receive(:mount_fs) provider.run_action(:mount) expect(new_resource).not_to be_updated_by_last_action diff --git a/spec/unit/provider/package/dpkg_spec.rb b/spec/unit/provider/package/dpkg_spec.rb index 4974cff934..b868128147 100644 --- a/spec/unit/provider/package/dpkg_spec.rb +++ b/spec/unit/provider/package/dpkg_spec.rb @@ -54,7 +54,6 @@ describe Chef::Provider::Package::Dpkg do allow(@provider).to receive(:shell_out).with("dpkg-deb -W #{@new_resource.source}", timeout: 900).and_return(@status) @provider.load_current_resource expect(@provider.current_resource.package_name).to eq("wget") - expect(@new_resource.version).to eq(version) expect(@provider.candidate_version).to eq(version) end @@ -83,6 +82,14 @@ describe Chef::Provider::Package::Dpkg do expect(@provider.current_resource.package_name).to eq("f.o.o-pkg++2") end + it "gets the source package version from dpkg-deb correctly when the package version has `~', `-', `+' or `.' characters" do + stdout = "b.a.r-pkg++1\t1.2.3+3141592-1ubuntu1~lucid" + status = double(:stdout => stdout, :exitstatus => 1) + allow(@provider).to receive(:shell_out).and_return(status) + @provider.load_current_resource + expect(@provider.candidate_version).to eq('1.2.3+3141592-1ubuntu1~lucid') + end + it "should raise an exception if the source is not set but we are installing" do @new_resource = Chef::Resource::Package.new("wget") @provider.new_resource = @new_resource diff --git a/spec/unit/provider/package/rpm_spec.rb b/spec/unit/provider/package/rpm_spec.rb index 48164b145c..e0e45d0b4f 100644 --- a/spec/unit/provider/package/rpm_spec.rb +++ b/spec/unit/provider/package/rpm_spec.rb @@ -23,183 +23,394 @@ describe Chef::Provider::Package::Rpm do let(:node) { Chef::Node.new } let(:events) { Chef::EventDispatch::Dispatcher.new } let(:run_context) { Chef::RunContext.new(node, {}, events) } + + let(:package_source) { "/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + let(:package_name) { "ImageMagick-c++" } + let(:new_resource) do - Chef::Resource::Package.new("ImageMagick-c++").tap do |resource| - resource.source "/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" + Chef::Resource::Package.new(package_name).tap do |resource| + resource.source(package_source) end end - let(:exitstatus) { 0 } - let(:stdout) { String.new('') } - let(:status) { double('Process::Status', exitstatus: exitstatus, stdout: stdout) } + + # `rpm -qp [stuff] $source` + let(:rpm_qp_status) { instance_double('Mixlib::ShellOut', exitstatus: rpm_qp_exitstatus, stdout: rpm_qp_stdout) } + + # `rpm -q [stuff] $package_name` + let(:rpm_q_status) { instance_double('Mixlib::ShellOut', exitstatus: rpm_q_exitstatus, stdout: rpm_q_stdout) } before(:each) do - allow(::File).to receive(:exists?).and_return(true) - allow(provider).to receive(:shell_out!).and_return(status) + allow(::File).to receive(:exists?).with("PLEASE STUB File.exists? EXACTLY").and_return(true) + + # Ensure all shell out usage is stubbed with exact arguments + allow(provider).to receive(:shell_out!).with("PLEASE STUB YOUR SHELLOUT CALLS").and_return(nil) + allow(provider).to receive(:shell_out).with("PLEASE STUB YOUR SHELLOUT CALLS").and_return(nil) end - describe "when determining the current state of the package" do - it "should create a current resource with the name of new_resource" do - provider.load_current_resource - expect(provider.current_resource.name).to eq("ImageMagick-c++") - end + describe "when the package source is not valid" do - it "should set the current reource package name to the new resource package name" do - provider.load_current_resource - expect(provider.current_resource.package_name).to eq('ImageMagick-c++') - end + context "when source is not defiend" do + let(:new_resource) { Chef::Resource::Package.new("ImageMagick-c++") } - it "should raise an exception if a source is supplied but not found" do - allow(::File).to receive(:exists?).and_return(false) - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + it "should raise an exception when attempting any action" do + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + end end - context "installation exists" do - let(:stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + context "when the source is a file that doesn't exist" do - it "should get the source package version from rpm if provided" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900).and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++", timeout: 900).and_return(status) - provider.load_current_resource - expect(provider.current_resource.package_name).to eq("ImageMagick-c++") - expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + it "should raise an exception when attempting any action" do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end + end - it "should return the current version installed if found by rpm" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900).and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' ImageMagick-c++", timeout: 900).and_return(status) - provider.load_current_resource - expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + context "when the source is an unsupported URI scheme" do + + let(:package_source) { "foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + it "should raise an exception if an uri formed source is non-supported scheme" do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + + # verify let bindings are as we expect + expect(new_resource.source).to eq("foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + expect(provider.load_current_resource).to be_nil + expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end end - context "source is uri formed" do - before(:each) do - allow(::File).to receive(:exists?).and_return(false) + end + + describe "when the package source is valid" do + + before do + expect(provider).to receive(:shell_out!). + with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{package_source}", timeout: 900). + and_return(rpm_qp_status) + + expect(provider).to receive(:shell_out). + with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' #{package_name}", timeout: 900). + and_return(rpm_q_status) + end + + context "when rpm fails when querying package installed state" do + + before do + allow(::File).to receive(:exists?).with(package_source).and_return(true) end - %w(http HTTP https HTTPS ftp FTP).each do |scheme| - it "should accept uri formed source (#{scheme})" do - new_resource.source "#{scheme}://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).not_to be_nil + let(:rpm_qp_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + let(:rpm_q_stdout) { "" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { -1 } + + it "raises an exception when attempting any action" do + expected_message = "Unable to determine current version due to RPM failure." + + expect { provider.run_action(:install) }.to raise_error do |error| + expect(error).to be_a_kind_of(Chef::Exceptions::Package) + expect(error.to_s).to include(expected_message) end end + end + + + context "when the package is installed" do + + let(:rpm_qp_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + let(:rpm_q_stdout) { "ImageMagick-c++ 6.5.4.7-7.el6_5" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } - %w(file FILE).each do |scheme| - it "should accept uri formed source (#{scheme})" do - new_resource.source "#{scheme}:///ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).not_to be_nil + let(:action) { :install } + + context "when the source is a file system path" do + + before do + allow(::File).to receive(:exists?).with(package_source).and_return(true) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements end - end - it "should raise an exception if an uri formed source is non-supported scheme" do - new_resource.source "foobar://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" - expect(provider.load_current_resource).to be_nil - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) - end - end + it "should get the source package version from rpm if provided" do + expect(provider.current_resource.package_name).to eq("ImageMagick-c++") + expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + end - context "source is not defiend" do - let(:new_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + it "should return the current version installed if found by rpm" do + expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + end + + describe "action install" do + + context "when at the desired version already" do + it "does nothing when the correct version is installed" do + expect(provider).to_not receive(:shell_out!).with("rpm -i /tmp/imagemagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + + provider.action_install + end + end + + context "when a newer version is desired" do + + let(:rpm_q_stdout) { "imagemagick-c++ 0.5.4.7-7.el6_5" } + + it "runs rpm -u with the package source to upgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_install + end + end + + context "when an older version is desired" do + let(:new_resource) do + Chef::Resource::RpmPackage.new(package_name).tap do |r| + r.source(package_source) + r.allow_downgrade(true) + end + end + + let(:rpm_q_stdout) { "imagemagick-c++ 21.4-19.el6_5" } + + it "should run rpm -u --oldpackage with the package source to downgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_install + end + + end + + end + + describe "action upgrade" do + + let(:action) { :upgrade } + + context "when at the desired version already" do + it "does nothing when the correct version is installed" do + expect(provider).to_not receive(:shell_out!).with("rpm -i /tmp/imagemagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + + provider.action_upgrade + end + end + + context "when a newer version is desired" do + + let(:rpm_q_stdout) { "imagemagick-c++ 0.5.4.7-7.el6_5" } + + it "runs rpm -u with the package source to upgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_upgrade + end + end + + context "when an older version is desired" do + let(:new_resource) do + Chef::Resource::RpmPackage.new(package_name).tap do |r| + r.source(package_source) + r.allow_downgrade(true) + end + end + + let(:rpm_q_stdout) { "imagemagick-c++ 21.4-19.el6_5" } + + it "should run rpm -u --oldpackage with the package source to downgrade" do + expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.action_upgrade + end + + end + end + + describe "action :remove" do + + let(:action) { :remove } + + it "should remove the package" do + expect(provider).to receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5", timeout: 900) + provider.action_remove + end + end + + + context "when the package name contains a tilde (chef#3503)" do + + let(:package_name) { "supermarket" } + + let(:package_source) { "/tmp/supermarket-1.10.1~alpha.0-1.el5.x86_64.rpm" } + + let(:rpm_qp_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + let(:rpm_q_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } + + it "should correctly determine the candidate version and installed version" do + expect(provider.current_resource.package_name).to eq("supermarket") + expect(provider.new_resource.version).to eq("1.10.1~alpha.0-1.el5") + end + end - it "should raise an exception if the source is not set but we are installing" do - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) end - end - context "installation does not exist" do - let(:stdout) { String.new("package openssh-askpass is not installed") } - let(:exitstatus) { -1 } - let(:new_resource) do - Chef::Resource::Package.new("openssh-askpass").tap do |resource| - resource.source "openssh-askpass" + context "when the source is given as an URI" do + before(:each) do + allow(::File).to receive(:exists?).with(package_source).and_return(false) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements + end + + %w(http HTTP https HTTPS ftp FTP file FILE).each do |scheme| + + context "when the source URI uses protocol scheme '#{scheme}'" do + + let(:package_source) { "#{scheme}://example.com/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm" } + + it "should get the source package version from rpm if provided" do + expect(provider.current_resource.package_name).to eq("ImageMagick-c++") + expect(provider.new_resource.version).to eq("6.5.4.7-7.el6_5") + end + + it "should return the current version installed if found by rpm" do + expect(provider.current_resource.version).to eq("6.5.4.7-7.el6_5") + end + + end end + end - it "should raise an exception if rpm fails to run" do - allow(provider).to receive(:shell_out).and_return(status) - expect { provider.run_action(:any) }.to raise_error(Chef::Exceptions::Package) + end + + context "when the package is not installed" do + + let(:package_name) { "openssh-askpass" } + + let(:package_source) { "/tmp/openssh-askpass-1.2.3-4.el6_5.x86_64.rpm" } + + let(:rpm_qp_stdout) { "openssh-askpass 1.2.3-4.el6_5" } + let(:rpm_q_stdout) { "package openssh-askpass is not installed" } + + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } + + let(:action) { :install } + + before do + allow(File).to receive(:exists?).with(package_source).and_return(true) + + provider.action = action + + provider.load_current_resource + provider.define_resource_requirements + provider.process_resource_requirements end it "should not detect the package name as version when not installed" do - expect(provider).to receive(:shell_out!).with("rpm -qp --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass", timeout: 900).and_return(status) - expect(provider).to receive(:shell_out).with("rpm -q --queryformat '%{NAME} %{VERSION}-%{RELEASE}\n' openssh-askpass", timeout: 900).and_return(status) - provider.load_current_resource expect(provider.current_resource.version).to be_nil end - end - end - describe "after the current resource is loaded" do - let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } - let(:provider) do - Chef::Provider::Package::Rpm.new(new_resource, run_context).tap do |provider| - provider.current_resource = current_resource - end - end + context "when the package name contains a tilde (chef#3503)" do - describe "when installing or upgrading" do - it "should run rpm -i with the package source to install" do - expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) - provider.install_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:package_name) { "supermarket" } - it "should run rpm -U with the package source to upgrade" do - current_resource.version("21.4-19.el5") - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:package_source) { "/tmp/supermarket-1.10.1~alpha.0-1.el5.x86_64.rpm" } - it "should install package if missing and set to upgrade" do - current_resource.version("ImageMagick-c++") - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + let(:rpm_qp_stdout) { "supermarket 1.10.1~alpha.0-1.el5" } + let(:rpm_q_stdout) { "package supermarket is not installed" } - context "allowing downgrade" do - let(:new_resource) { Chef::Resource::RpmPackage.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") } - let(:current_resource) { Chef::Resource::RpmPackage.new("ImageMagick-c++") } + let(:rpm_qp_exitstatus) { 0 } + let(:rpm_q_exitstatus) { 0 } - it "should run rpm -U --oldpackage with the package source to downgrade" do - new_resource.allow_downgrade(true) - current_resource.version("21.4-19.el5") - expect(provider).to receive(:shell_out!).with("rpm -U --oldpackage /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) - provider.upgrade_package("ImageMagick-c++", "6.5.4.7-7.el6_5") + it "should correctly determine the candidate version" do + expect(provider.new_resource.version).to eq("1.10.1~alpha.0-1.el5") end end - context "installing when the name is a path" do - let(:new_resource) { Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") } - let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + describe "managing the package" do + + describe "action install" do + + it "installs the package" do + expect(provider).to receive(:shell_out!).with("rpm -i #{package_source}", timeout: 900) - it "should install from a path when the package is a path and the source is nil" do - expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - provider.current_resource = current_resource - expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) - provider.install_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + provider.action_install + end + + context "when custom resource options are given" do + it "installs with custom options specified in the resource" do + new_resource.options("--dbpath /var/lib/rpm") + expect(provider).to receive(:shell_out!).with("rpm --dbpath /var/lib/rpm -i #{package_source}", timeout: 900) + provider.action_install + end + end end - it "should uprgrade from a path when the package is a path and the source is nil" do - expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") - current_resource.version("21.4-19.el5") - provider.current_resource = current_resource - expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) - provider.upgrade_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + describe "action upgrade" do + + let(:action) { :upgrade } + + it "installs the package" do + expect(provider).to receive(:shell_out!).with("rpm -i #{package_source}", timeout: 900) + + provider.action_upgrade + end + end + + describe "when removing the package" do + + let(:action) { :remove } + + it "should do nothing" do + expect(provider).to_not receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5", timeout: 900) + provider.action_remove + end end - end - it "installs with custom options specified in the resource" do - provider.candidate_version = '11' - new_resource.options("--dbpath /var/lib/rpm") - expect(provider).to receive(:shell_out!).with("rpm --dbpath /var/lib/rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) - provider.install_package(new_resource.name, provider.candidate_version) end + + end + end - describe "when removing the package" do - it "should run rpm -e to remove the package" do - expect(provider).to receive(:shell_out!).with("rpm -e ImageMagick-c++-6.5.4.7-7.el6_5", timeout: 900) - provider.remove_package("ImageMagick-c++", "6.5.4.7-7.el6_5") - end + context "when the resource name is the path to the package" do + + let(:new_resource) do + # When we pass a source in as the name, then #initialize in the + # provider will call File.exists?. Because of the ordering in our + # let() bindings and such, we have to set the stub here and not in a + # before block. + allow(::File).to receive(:exists?).with(package_source).and_return(true) + Chef::Resource::Package.new("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + end + + let(:current_resource) { Chef::Resource::Package.new("ImageMagick-c++") } + + it "should install from a path when the package is a path and the source is nil" do + expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + provider.current_resource = current_resource + expect(provider).to receive(:shell_out!).with("rpm -i /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.install_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") + end + + it "should uprgrade from a path when the package is a path and the source is nil" do + expect(new_resource.source).to eq("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm") + current_resource.version("21.4-19.el5") + provider.current_resource = current_resource + expect(provider).to receive(:shell_out!).with("rpm -U /tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", timeout: 900) + provider.upgrade_package("/tmp/ImageMagick-c++-6.5.4.7-7.el6_5.x86_64.rpm", "6.5.4.7-7.el6_5") end end + + end + diff --git a/spec/unit/provider/package/rubygems_spec.rb b/spec/unit/provider/package/rubygems_spec.rb index 67ffb7bb9e..f790bdb1ce 100644 --- a/spec/unit/provider/package/rubygems_spec.rb +++ b/spec/unit/provider/package/rubygems_spec.rb @@ -369,6 +369,24 @@ describe Chef::Provider::Package::Rubygems do expect(provider.gem_env.gem_binary_location).to eq('/usr/weird/bin/gem') end + it "recognizes chef as omnibus" do + allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/opt/chef/embedded/bin") + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + expect(provider.is_omnibus?).to be true + end + + it "recognizes opscode as omnibus" do + allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/opt/opscode/embedded/bin") + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + expect(provider.is_omnibus?).to be true + end + + it "recognizes chefdk as omnibus" do + allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/opt/chefdk/embedded/bin") + provider = Chef::Provider::Package::Rubygems.new(@new_resource, @run_context) + expect(provider.is_omnibus?).to be true + end + it "searches for a gem binary when running on Omnibus on Unix" do platform_mock :unix do allow(RbConfig::CONFIG).to receive(:[]).with('bindir').and_return("/opt/chef/embedded/bin") diff --git a/spec/unit/provider/package/yum_spec.rb b/spec/unit/provider/package/yum_spec.rb index e878b92621..3fc0b807c9 100644 --- a/spec/unit/provider/package/yum_spec.rb +++ b/spec/unit/provider/package/yum_spec.rb @@ -1,6 +1,6 @@ # # Author:: Adam Jacob (<adam@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ describe Chef::Provider::Package::Yum do @node = Chef::Node.new @events = Chef::EventDispatch::Dispatcher.new @run_context = Chef::RunContext.new(@node, {}, @events) - @new_resource = Chef::Resource::Package.new('cups') + @new_resource = Chef::Resource::YumPackage.new('cups') @status = double("Status", :exitstatus => 0) @yum_cache = double( 'Chef::Provider::Yum::YumCache', @@ -39,6 +39,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @pid = double("PID") end @@ -88,6 +89,46 @@ describe Chef::Provider::Package::Yum do end end + describe "yum_binary accessor" do + it "when yum-deprecated exists" do + expect(File).to receive(:exist?).with("/usr/bin/yum-deprecated").and_return(true) + expect(@yum_cache).to receive(:yum_binary=).with("yum-deprecated") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("yum-deprecated") + end + + it "when yum-deprecated does not exist" do + expect(File).to receive(:exist?).with("/usr/bin/yum-deprecated").and_return(false) + expect(@yum_cache).to receive(:yum_binary=).with("yum") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("yum") + end + + it "when the yum_binary is set on the resource" do + @new_resource.yum_binary "/usr/bin/yum-something" + expect(File).not_to receive(:exist?) + expect(@yum_cache).to receive(:yum_binary=).with("/usr/bin/yum-something") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("/usr/bin/yum-something") + end + + it "when the new_resource is a vanilla package class and yum-deprecated exists" do + @new_resource = Chef::Resource::Package.new('cups') + expect(File).to receive(:exist?).with("/usr/bin/yum-deprecated").and_return(true) + expect(@yum_cache).to receive(:yum_binary=).with("yum-deprecated") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("yum-deprecated") + end + + it "when the new_resource is a vanilla package class and yum-deprecated does not exist" do + @new_resource = Chef::Resource::Package.new('cups') + expect(File).to receive(:exist?).with("/usr/bin/yum-deprecated").and_return(false) + expect(@yum_cache).to receive(:yum_binary=).with("yum") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + expect(@provider.yum_binary).to eql("yum") + end + end + describe "when arch in package_name" do it "should set the arch if no existing package_name is found and new_package_name+new_arch is available" do @new_resource = Chef::Resource::YumPackage.new('testing.noarch') @@ -109,6 +150,7 @@ describe Chef::Provider::Package::Yum do allow(@yum_cache).to receive(:package_available?).and_return(true) allow(@yum_cache).to receive(:disable_extra_repo_control).and_return(true) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider.new_resource.package_name).to eq("testing") @@ -163,6 +205,7 @@ describe Chef::Provider::Package::Yum do allow(@yum_cache).to receive(:package_available?).and_return(true) allow(@yum_cache).to receive(:disable_extra_repo_control).and_return(true) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) # annoying side effect of the fun stub'ing above @provider.load_current_resource @@ -194,6 +237,7 @@ describe Chef::Provider::Package::Yum do allow(@yum_cache).to receive(:package_available?).and_return(true) allow(@yum_cache).to receive(:disable_extra_repo_control).and_return(true) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider.new_resource.package_name).to eq("testing.beta3") @@ -229,6 +273,7 @@ describe Chef::Provider::Package::Yum do allow(@yum_cache).to receive(:package_available?).and_return(true) allow(@yum_cache).to receive(:disable_extra_repo_control).and_return(true) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider.new_resource.package_name).to eq("testing.i386") @@ -281,6 +326,7 @@ describe Chef::Provider::Package::Yum do before do allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(yum_cache) + allow(yum_cache).to receive(:yum_binary=).with("yum") @pkg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "2.0.1.el5", "x86_64", []) expect(yum_cache).to receive(:packages_from_require).and_return([@pkg]) end @@ -352,6 +398,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") pkg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "2.0.1.el5", "x86_64", []) expect(@yum_cache).to receive(:packages_from_require).and_return([pkg]) @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @@ -373,6 +420,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") pkg = Chef::Provider::Package::Yum::RPMPackage.new("test-package", "2.0.1.el5", "x86_64", []) expect(@yum_cache).to receive(:packages_from_require).and_return([pkg]) @new_resource = Chef::Resource::YumPackage.new('test-package = 2.0.1.el5') @@ -395,6 +443,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") expect(@yum_cache).to receive(:packages_from_require).exactly(4).times.and_return([]) expect(@yum_cache).to receive(:reload_provides).twice @@ -419,6 +468,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") expect(@yum_cache).to receive(:packages_from_require).twice.and_return([]) expect(@yum_cache).to receive(:reload_provides) @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @@ -438,6 +488,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) expect(@yum_cache).to receive(:packages_from_require).once.and_return([]) expect(@yum_cache).not_to receive(:reload_provides) @@ -462,6 +513,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") expect(@yum_cache).to receive(:packages_from_require).twice.and_return([]) @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource @@ -474,7 +526,7 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5" + "-d0 -e0 -y install cups-1.2.4-11.19.el5" ) @provider.install_package("cups", "1.2.4-11.19.el5") end @@ -482,7 +534,7 @@ describe Chef::Provider::Package::Yum do it "should run yum localinstall if given a path to an rpm" do allow(@new_resource).to receive(:source).and_return("/tmp/emacs-21.4-20.el5.i386.rpm") expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" + "-d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" ) @provider.install_package("emacs", "21.4-20.el5") end @@ -493,7 +545,7 @@ describe Chef::Provider::Package::Yum do @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) expect(@new_resource.source).to eq("/tmp/emacs-21.4-20.el5.i386.rpm") expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" + "-d0 -e0 -y localinstall /tmp/emacs-21.4-20.el5.i386.rpm" ) @provider.install_package("/tmp/emacs-21.4-20.el5.i386.rpm", "21.4-20.el5") end @@ -503,7 +555,7 @@ describe Chef::Provider::Package::Yum do allow(@new_resource).to receive(:arch).and_return("i386") allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5.i386" + "-d0 -e0 -y install cups-1.2.4-11.19.el5.i386" ) @provider.install_package("cups", "1.2.4-11.19.el5") end @@ -514,7 +566,7 @@ describe Chef::Provider::Package::Yum do allow(@new_resource).to receive(:options).and_return("--disablerepo epmd") allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y --disablerepo epmd install cups-11" + "-d0 -e0 -y --disablerepo epmd install cups-11" ) @provider.install_package(@new_resource.name, @provider.candidate_version) end @@ -531,6 +583,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) expect { @provider.install_package("lolcats", "0.99") }.to raise_error(Chef::Exceptions::Package, %r{Version .* not found}) end @@ -549,6 +602,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect { @provider.install_package("cups", "1.2.4-11.15.el5") }.to raise_error(Chef::Exceptions::Package, %r{is newer than candidate package}) @@ -568,10 +622,11 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" + "-d0 -e0 -y install cups-1.2.4-11.15.el5" ) @provider.install_package("cups", "1.2.4-11.15.el5") end @@ -591,10 +646,11 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y downgrade cups-1.2.4-11.15.el5" + "-d0 -e0 -y downgrade cups-1.2.4-11.15.el5" ) @provider.install_package("cups", "1.2.4-11.15.el5") end @@ -604,7 +660,7 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" + "-d0 -e0 -y install cups-1.2.4-11.15.el5" ) expect(@yum_cache).to receive(:reload).once @provider.install_package("cups", "1.2.4-11.15.el5") @@ -615,7 +671,7 @@ describe Chef::Provider::Package::Yum do @provider.load_current_resource allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.15.el5" + "-d0 -e0 -y install cups-1.2.4-11.15.el5" ) expect(@yum_cache).not_to receive(:reload) @provider.install_package("cups", "1.2.4-11.15.el5") @@ -628,7 +684,7 @@ describe Chef::Provider::Package::Yum do allow(@provider).to receive(:candidate_version).and_return('11') allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-11" + "-d0 -e0 -y install cups-11" ) @provider.upgrade_package(@new_resource.name, @provider.candidate_version) end @@ -639,7 +695,7 @@ describe Chef::Provider::Package::Yum do allow(@provider).to receive(:candidate_version).and_return('11') allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-11" + "-d0 -e0 -y install cups-11" ) @provider.upgrade_package(@new_resource.name, @provider.candidate_version) end @@ -657,6 +713,7 @@ describe Chef::Provider::Package::Yum do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @provider.load_current_resource expect { @provider.upgrade_package("cups", "1.2.4-11.15.el5") }.to raise_error(Chef::Exceptions::Package, %r{is newer than candidate package}) @@ -706,7 +763,7 @@ describe Chef::Provider::Package::Yum do describe "when removing a package" do it "should run yum remove with the package name" do expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y remove emacs-1.0" + "-d0 -e0 -y remove emacs-1.0" ) @provider.remove_package("emacs", "1.0") end @@ -714,7 +771,7 @@ describe Chef::Provider::Package::Yum do it "should run yum remove with the package name and arch" do allow(@new_resource).to receive(:arch).and_return("x86_64") expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y remove emacs-1.0.x86_64" + "-d0 -e0 -y remove emacs-1.0.x86_64" ) @provider.remove_package("emacs", "1.0") end @@ -723,7 +780,7 @@ describe Chef::Provider::Package::Yum do describe "when purging a package" do it "should run yum remove with the package name" do expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y remove emacs-1.0" + "-d0 -e0 -y remove emacs-1.0" ) @provider.purge_package("emacs", "1.0") end @@ -737,7 +794,7 @@ describe Chef::Provider::Package::Yum do "yum -d0 -e0 -y install emacs-1.0", {:timeout => Chef::Config[:yum_timeout]} ) - @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") + @provider.yum_command("-d0 -e0 -y install emacs-1.0") end it "should run yum once if it exits with a return code > 0 and no scriptlet failures" do @@ -747,7 +804,7 @@ describe Chef::Provider::Package::Yum do "yum -d0 -e0 -y install emacs-1.0", {:timeout => Chef::Config[:yum_timeout]} ) - expect { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) + expect { @provider.yum_command("-d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) end it "should run yum once if it exits with a return code of 1 and %pre scriptlet failures" do @@ -759,7 +816,7 @@ describe Chef::Provider::Package::Yum do {:timeout => Chef::Config[:yum_timeout]} ) # will still raise an exception, can't stub out the subsequent call - expect { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) + expect { @provider.yum_command("-d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) end it "should run yum twice if it exits with a return code of 1 and %post scriptlet failures" do @@ -771,7 +828,20 @@ describe Chef::Provider::Package::Yum do {:timeout => Chef::Config[:yum_timeout]} ) # will still raise an exception, can't stub out the subsequent call - expect { @provider.yum_command("yum -d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) + expect { @provider.yum_command("-d0 -e0 -y install emacs-1.0") }.to raise_error(Chef::Exceptions::Exec) + end + + it "should pass the yum_binary to the command if its specified" do + @new_resource.yum_binary "yum-deprecated" + expect(@yum_cache).to receive(:yum_binary=).with("yum-deprecated") + @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) + @status = double("Status", :exitstatus => 0, :stdout => "", :stderr => "") + allow(@provider).to receive(:shell_out).and_return(@status) + expect(@provider).to receive(:shell_out).once.with( + "yum-deprecated -d0 -e0 -y install emacs-1.0", + {:timeout => Chef::Config[:yum_timeout]} + ) + @provider.yum_command("-d0 -e0 -y install emacs-1.0") end end end @@ -1737,6 +1807,7 @@ EOF Chef::Provider::Package::Yum::YumCache.reset_instance @yc = Chef::Provider::Package::Yum::YumCache.instance # load valid data + @yc.yum_binary = "yum" allow(@yc).to receive(:shell_out!).and_return(@status) allow_any_instance_of(described_class).to receive(:which).with("yum").and_return("/usr/bin/yum") allow(::File).to receive(:open).with("/usr/bin/yum", "r") do |&block| @@ -2053,6 +2124,8 @@ describe "Chef::Provider::Package::Yum - Multi" do :disable_extra_repo_control => true ) allow(Chef::Provider::Package::Yum::YumCache).to receive(:instance).and_return(@yum_cache) + allow(@yum_cache).to receive(:yum_binary=).with("yum") + allow(@yum_cache).to receive(:yum_binary=).with("yum") @provider = Chef::Provider::Package::Yum.new(@new_resource, @run_context) @pid = double("PID") end @@ -2134,7 +2207,7 @@ describe "Chef::Provider::Package::Yum - Multi" do allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('0.9') expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5 vim-1.0" + "-d0 -e0 -y install cups-1.2.4-11.19.el5 vim-1.0" ) @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) end @@ -2144,7 +2217,7 @@ describe "Chef::Provider::Package::Yum - Multi" do allow(@new_resource).to receive(:arch).and_return("i386") allow(Chef::Provider::Package::Yum::RPMUtils).to receive(:rpmvercmp).and_return(-1) expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5.i386 vim-1.0.i386" + "-d0 -e0 -y install cups-1.2.4-11.19.el5.i386 vim-1.0.i386" ) @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", "1.0"]) end @@ -2155,7 +2228,7 @@ describe "Chef::Provider::Package::Yum - Multi" do allow(@yum_cache).to receive(:installed_version).with('cups', nil).and_return('1.2.4-11.18.el5') allow(@yum_cache).to receive(:installed_version).with('vim', nil).and_return('0.9') expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y --disablerepo epmd install cups-1.2.4-11.19.el5 vim-1.0" + "-d0 -e0 -y --disablerepo epmd install cups-1.2.4-11.19.el5 vim-1.0" ) allow(@new_resource).to receive(:options).and_return("--disablerepo epmd") @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) @@ -2181,7 +2254,7 @@ describe "Chef::Provider::Package::Yum - Multi" do @provider.load_current_resource expect(@provider).to receive(:yum_command).with( - "yum -d0 -e0 -y install cups-1.2.4-11.19.el5.x86_64 vim-1.0" + "-d0 -e0 -y install cups-1.2.4-11.19.el5.x86_64 vim-1.0" ) @provider.install_package(["cups", "vim"], ["1.2.4-11.19.el5", '1.0']) end diff --git a/spec/unit/provider/powershell_spec.rb b/spec/unit/provider/powershell_script_spec.rb index 855c18af9b..855c18af9b 100644 --- a/spec/unit/provider/powershell_spec.rb +++ b/spec/unit/provider/powershell_script_spec.rb diff --git a/spec/unit/provider/registry_key_spec.rb b/spec/unit/provider/registry_key_spec.rb index 79811fdab8..47543ffe39 100644 --- a/spec/unit/provider/registry_key_spec.rb +++ b/spec/unit/provider/registry_key_spec.rb @@ -77,6 +77,18 @@ shared_examples_for "a registry key" do end describe "action_create" do + context "when a case insensitive match for the key exists" do + before(:each) do + expect(@double_registry).to receive(:key_exists?).twice.with(keyname.downcase).and_return(true) + end + it "should do nothing if the if a case insensitive key and the value both exist" do + @provider.new_resource.key(keyname.downcase) + expect(@double_registry).to receive(:get_values).with(keyname.downcase).and_return( testval1 ) + expect(@double_registry).not_to receive(:set_value) + @provider.load_current_resource + @provider.action_create + end + end context "when the key exists" do before(:each) do expect(@double_registry).to receive(:key_exists?).twice.with(keyname).and_return(true) diff --git a/spec/unit/provider/service/aix_service_spec.rb b/spec/unit/provider/service/aix_service_spec.rb index a0c8bb3407..5cca7d6f0a 100644 --- a/spec/unit/provider/service/aix_service_spec.rb +++ b/spec/unit/provider/service/aix_service_spec.rb @@ -94,7 +94,7 @@ describe Chef::Provider::Service::Aix do end it "service is a group" do - expect(@provider).to receive(:shell_out!).with("lssrc -g chef").and_return(@status) + expect(@provider).to receive(:shell_out).with("lssrc -g chef").and_return(@status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_truthy end @@ -106,7 +106,7 @@ describe Chef::Provider::Service::Aix do end it "service is a group" do - expect(@provider).to receive(:shell_out!).with("lssrc -g chef").and_return(@status) + expect(@provider).to receive(:shell_out).with("lssrc -g chef").and_return(@status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_truthy end @@ -119,7 +119,7 @@ describe Chef::Provider::Service::Aix do end it "service is a subsystem" do - expect(@provider).to receive(:shell_out!).with("lssrc -g chef").and_return(@group_status) + expect(@provider).to receive(:shell_out).with("lssrc -g chef").and_return(@group_status) expect(@provider).to receive(:shell_out!).with("lssrc -s chef").and_return(@service_status) @provider.load_current_resource expect(@provider.instance_eval("@is_resource_group")).to be_falsey diff --git a/spec/unit/provider/service/gentoo_service_spec.rb b/spec/unit/provider/service/gentoo_service_spec.rb index c08982acc3..0aa7bf4529 100644 --- a/spec/unit/provider/service/gentoo_service_spec.rb +++ b/spec/unit/provider/service/gentoo_service_spec.rb @@ -1,7 +1,7 @@ # # Author:: Lee Jensen (<ljensen@engineyard.com>) # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -108,17 +108,17 @@ describe Chef::Provider::Service::Gentoo do it "should support the status command automatically" do @provider.load_current_resource - expect(@new_resource.supports[:status]).to be_truthy + expect(@provider.supports[:status]).to be true end it "should support the restart command automatically" do @provider.load_current_resource - expect(@new_resource.supports[:restart]).to be_truthy + expect(@provider.supports[:restart]).to be true end it "should not support the reload command automatically" do @provider.load_current_resource - expect(@new_resource.supports[:reload]).not_to be_truthy + expect(@provider.supports[:reload]).to be_falsey end end diff --git a/spec/unit/provider/service/macosx_spec.rb b/spec/unit/provider/service/macosx_spec.rb index 597845a558..54183bdc3d 100644 --- a/spec/unit/provider/service/macosx_spec.rb +++ b/spec/unit/provider/service/macosx_spec.rb @@ -60,15 +60,15 @@ XML ["Daemon", "Agent"].each do |service_type| ["redis-server", "io.redis.redis-server"].each do |service_name| - ["10.9", "10.10"].each do |platform_version| + ["10.9", "10.10", "10.11"].each do |platform_version| let(:plist) {'/Library/LaunchDaemons/io.redis.redis-server.plist'} let(:session) { StringIO.new } if service_type == 'Agent' let(:plist) {'/Library/LaunchAgents/io.redis.redis-server.plist'} let(:session) {'-S Aqua '} - let(:su_cmd) {'su igor -c'} - if platform_version != "10.10" - let(:su_cmd) {'su -l igor -c'} + let(:su_cmd) {'su -l igor -c'} + if platform_version == "10.9" + let(:su_cmd) {'su igor -c'} end end let(:service_label) {'io.redis.redis-server'} diff --git a/spec/unit/provider/service/openbsd_service_spec.rb b/spec/unit/provider/service/openbsd_service_spec.rb index 1b5206470e..d3c150a14b 100644 --- a/spec/unit/provider/service/openbsd_service_spec.rb +++ b/spec/unit/provider/service/openbsd_service_spec.rb @@ -35,10 +35,12 @@ describe Chef::Provider::Service::Openbsd do node end + let(:supports) { {:status => false} } + let(:new_resource) do new_resource = Chef::Resource::Service.new("sndiod") new_resource.pattern("sndiod") - new_resource.supports({:status => false}) + new_resource.supports(supports) new_resource end @@ -106,9 +108,7 @@ describe Chef::Provider::Service::Openbsd do context "when the service supports status" do let(:status) { double(:stdout => "", :exitstatus => 0) } - before do - new_resource.supports({:status => true}) - end + let(:supports) { { :status => true } } it "should run '/etc/rc.d/service_name status'" do expect(provider).to receive(:shell_out).with("/etc/rc.d/#{new_resource.service_name} check").and_return(status) @@ -305,10 +305,12 @@ describe Chef::Provider::Service::Openbsd do end describe Chef::Provider::Service::Openbsd, "restart_service" do - it "should call 'restart' on the service_name if the resource supports it" do - new_resource.supports({:restart => true}) - expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/rc.d/#{new_resource.service_name} restart") - provider.restart_service() + context "when the new_resource supports restart" do + let(:supports) { { restart: true } } + it "should call 'restart' on the service_name if the resource supports it" do + expect(provider).to receive(:shell_out_with_systems_locale!).with("/etc/rc.d/#{new_resource.service_name} restart") + provider.restart_service() + end end it "should call the restart_command if one has been specified" do diff --git a/spec/unit/provider/service/redhat_spec.rb b/spec/unit/provider/service/redhat_spec.rb index 73cfec8a6f..5aaf54d9f5 100644 --- a/spec/unit/provider/service/redhat_spec.rb +++ b/spec/unit/provider/service/redhat_spec.rb @@ -64,24 +64,76 @@ describe "Chef::Provider::Service::Redhat" do end describe "load current resource" do - it "sets the current enabled status to true if the service is enabled for any run level" do + before do status = double("Status", :exitstatus => 0, :stdout => "" , :stderr => "") - expect(@provider).to receive(:shell_out).with("/sbin/service chef status").and_return(status) + allow(@provider).to receive(:shell_out).with("/sbin/service chef status").and_return(status) + end + + it "sets supports[:status] to true by default" do chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:on 6:off", :stderr => "") expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) - expect(@provider.instance_variable_get("@service_missing")).to be_falsey + expect(@provider.service_missing).to be false @provider.load_current_resource - expect(@current_resource.enabled).to be_truthy + expect(@provider.supports[:status]).to be true + end + + it "lets the user override supports[:status] in the new_resource" do + @new_resource.supports( { status: false } ) + @new_resource.pattern "myservice" + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:on 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + foo_out = double("ps_command", :exitstatus => 0, :stdout => "a line that matches myservice", :stderr => "") + expect(@provider).to receive(:shell_out!).with("foo").and_return(foo_out) + expect(@provider.service_missing).to be false + expect(@provider).not_to receive(:shell_out).with("/sbin/service chef status") + @provider.load_current_resource + expect(@provider.supports[:status]).to be false + end + + it "sets the current enabled status to true if the service is enabled for any run level" do + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:on 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + expect(@provider.service_missing).to be false + @provider.load_current_resource + expect(@current_resource.enabled).to be true end it "sets the current enabled status to false if the regex does not match" do - status = double("Status", :exitstatus => 0, :stdout => "" , :stderr => "") - expect(@provider).to receive(:shell_out).with("/sbin/service chef status").and_return(status) chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:off 2:off 3:off 4:off 5:off 6:off", :stderr => "") expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) - expect(@provider.instance_variable_get("@service_missing")).to be_falsey + expect(@provider.service_missing).to be false expect(@provider.load_current_resource).to eql(@current_resource) - expect(@current_resource.enabled).to be_falsey + expect(@current_resource.enabled).to be false + end + + it "sets the current enabled status to true if the service is enabled at specified run levels" do + @new_resource.run_levels([1, 2]) + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:on 2:on 3:off 4:off 5:off 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + expect(@provider.service_missing).to be false + @provider.load_current_resource + expect(@current_resource.enabled).to be true + expect(@provider.current_run_levels).to eql([1, 2]) + end + + it "sets the current enabled status to false if the service is enabled at a run level it should not" do + @new_resource.run_levels([1, 2]) + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:on 2:on 3:on 4:off 5:off 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + expect(@provider.service_missing).to be false + @provider.load_current_resource + expect(@current_resource.enabled).to be false + expect(@provider.current_run_levels).to eql([1, 2, 3]) + end + + it "sets the current enabled status to false if the service is not enabled at specified run levels" do + @new_resource.run_levels([ 2 ]) + chkconfig = double("Chkconfig", :exitstatus => 0, :stdout => "chef 0:off 1:on 2:off 3:off 4:off 5:off 6:off", :stderr => "") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --list chef", :returns => [0,1]).and_return(chkconfig) + expect(@provider.service_missing).to be false + @provider.load_current_resource + expect(@current_resource.enabled).to be false + expect(@provider.current_run_levels).to eql([1]) end end @@ -144,6 +196,28 @@ describe "Chef::Provider::Service::Redhat" do expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig #{@new_resource.service_name} on") @provider.enable_service end + + it "should call chkconfig to add 'service_name' at specified run_levels" do + allow(@provider).to receive(:run_levels).and_return([1, 2]) + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 12 #{@new_resource.service_name} on") + @provider.enable_service + end + + it "should call chkconfig to add 'service_name' at specified run_levels when run_levels do not match" do + allow(@provider).to receive(:run_levels).and_return([1, 2]) + allow(@provider).to receive(:current_run_levels).and_return([1, 3]) + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 12 #{@new_resource.service_name} on") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 3 #{@new_resource.service_name} off") + @provider.enable_service + end + + it "should call chkconfig to add 'service_name' at specified run_levels if there is an extra run_level" do + allow(@provider).to receive(:run_levels).and_return([1, 2]) + allow(@provider).to receive(:current_run_levels).and_return([1, 2, 3]) + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 12 #{@new_resource.service_name} on") + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 3 #{@new_resource.service_name} off") + @provider.enable_service + end end describe "disable_service" do @@ -151,6 +225,12 @@ describe "Chef::Provider::Service::Redhat" do expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig #{@new_resource.service_name} off") @provider.disable_service end + + it "should call chkconfig to del 'service_name' at specified run_levels" do + allow(@provider).to receive(:run_levels).and_return([1, 2]) + expect(@provider).to receive(:shell_out!).with("/sbin/chkconfig --level 12 #{@new_resource.service_name} off") + @provider.disable_service + end end end diff --git a/spec/unit/provider/service/upstart_service_spec.rb b/spec/unit/provider/service/upstart_service_spec.rb index ca7ce8f930..1c8e304cb7 100644 --- a/spec/unit/provider/service/upstart_service_spec.rb +++ b/spec/unit/provider/service/upstart_service_spec.rb @@ -19,6 +19,10 @@ require 'spec_helper' describe Chef::Provider::Service::Upstart do + let(:shell_out_success) do + double('shell_out_with_systems_locale', :exitstatus => 0, :error? => false) + end + before(:each) do @node =Chef::Node.new @node.name('upstarter') @@ -173,7 +177,7 @@ describe Chef::Provider::Service::Upstart do end it "should run the services status command if one has been specified" do - allow(@provider).to receive(:shell_out!).with("/bin/chefhasmonkeypants status").and_return(0) + allow(@provider).to receive(:shell_out!).with("/bin/chefhasmonkeypants status").and_return(shell_out_success) expect(@current_resource).to receive(:running).with(true) @provider.load_current_resource end @@ -246,7 +250,7 @@ describe Chef::Provider::Service::Upstart do end it "should call '/sbin/start service_name' if no start command is specified" do - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start #{@new_resource.service_name}").and_return(shell_out_success) @provider.start_service() end @@ -261,7 +265,7 @@ describe Chef::Provider::Service::Upstart do @new_resource.parameters({ "OSD_ID" => "2" }) @provider = Chef::Provider::Service::Upstart.new(@new_resource, @run_context) @provider.current_resource = @current_resource - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start rsyslog OSD_ID=2").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start rsyslog OSD_ID=2").and_return(shell_out_success) @provider.start_service() end @@ -274,13 +278,13 @@ describe Chef::Provider::Service::Upstart do it "should call '/sbin/restart service_name' if no restart command is specified" do allow(@current_resource).to receive(:running).and_return(true) - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/restart #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/restart #{@new_resource.service_name}").and_return(shell_out_success) @provider.restart_service() end it "should call '/sbin/start service_name' if restart_service is called for a stopped service" do allow(@current_resource).to receive(:running).and_return(false) - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/start #{@new_resource.service_name}").and_return(shell_out_success) @provider.restart_service() end @@ -293,7 +297,7 @@ describe Chef::Provider::Service::Upstart do it "should call '/sbin/reload service_name' if no reload command is specified" do allow(@current_resource).to receive(:running).and_return(true) - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/reload #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/reload #{@new_resource.service_name}").and_return(shell_out_success) @provider.reload_service() end @@ -306,7 +310,7 @@ describe Chef::Provider::Service::Upstart do it "should call '/sbin/stop service_name' if no stop command is specified" do allow(@current_resource).to receive(:running).and_return(true) - expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/stop #{@new_resource.service_name}").and_return(0) + expect(@provider).to receive(:shell_out_with_systems_locale!).with("/sbin/stop #{@new_resource.service_name}").and_return(shell_out_success) @provider.stop_service() end diff --git a/spec/unit/provider/template/content_spec.rb b/spec/unit/provider/template/content_spec.rb index 4b88a3aea5..3d6e822c00 100644 --- a/spec/unit/provider/template/content_spec.rb +++ b/spec/unit/provider/template/content_spec.rb @@ -23,6 +23,10 @@ describe Chef::Provider::Template::Content do let(:new_resource) do double("Chef::Resource::Template (new)", :cookbook_name => 'openldap', + :recipe_name => 'default', + :source_line => "/Users/lamont/solo/cookbooks/openldap/recipes/default.rb:2:in `from_file'", + :source_line_file => "/Users/lamont/solo/cookbooks/openldap/recipes/default.rb", + :source_line_number => "2", :source => 'openldap_stuff.conf.erb', :local => false, :cookbook => nil, @@ -75,4 +79,41 @@ describe Chef::Provider::Template::Content do expect(IO.read(content.tempfile.path)).to eq("slappiness is a warm gun") end + describe "when using location helpers" do + let(:new_resource) do + double("Chef::Resource::Template (new)", + :cookbook_name => 'openldap', + :recipe_name => 'default', + :source_line => CHEF_SPEC_DATA + "/cookbooks/openldap/recipes/default.rb:2:in `from_file'", + :source_line_file => CHEF_SPEC_DATA + "/cookbooks/openldap/recipes/default.rb", + :source_line_number => "2", + :source => 'helpers.erb', + :local => false, + :cookbook => nil, + :variables => {}, + :inline_helper_blocks => {}, + :inline_helper_modules => [], + :helper_modules => []) + end + + it "creates the template with the rendered content" do + expect(IO.read(content.tempfile.path)).to eql <<EOF +openldap +default +#{CHEF_SPEC_DATA}/cookbooks/openldap/recipes/default.rb:2:in `from_file' +#{CHEF_SPEC_DATA}/cookbooks/openldap/recipes/default.rb +2 +helpers.erb +#{CHEF_SPEC_DATA}/cookbooks/openldap/templates/default/helpers.erb +openldap +default +#{CHEF_SPEC_DATA}/cookbooks/openldap/recipes/default.rb:2:in `from_file' +#{CHEF_SPEC_DATA}/cookbooks/openldap/recipes/default.rb +2 +helpers.erb +#{CHEF_SPEC_DATA}/cookbooks/openldap/templates/default/helpers.erb +EOF + end + + end end diff --git a/spec/unit/provider_resolver_spec.rb b/spec/unit/provider_resolver_spec.rb index e18d69bc19..88df4a20cc 100644 --- a/spec/unit/provider_resolver_spec.rb +++ b/spec/unit/provider_resolver_spec.rb @@ -28,30 +28,37 @@ include Chef::Mixin::ConvertToClassName describe Chef::ProviderResolver do + let(:resource_name) { :service } + let(:provider) { nil } + let(:action) { :start } + let(:node) do node = Chef::Node.new - allow(node).to receive(:[]).with(:os).and_return(os) - allow(node).to receive(:[]).with(:platform_family).and_return(platform_family) - allow(node).to receive(:[]).with(:platform).and_return(platform) - allow(node).to receive(:[]).with(:platform_version).and_return(platform_version) - allow(node).to receive(:is_a?).and_return(Chef::Node) + node.automatic[:os] = os + node.automatic[:platform_family] = platform_family + node.automatic[:platform] = platform + node.automatic[:platform_version] = platform_version + node.automatic[:kernel] = { machine: 'i386' } node end + let(:run_context) { Chef::RunContext.new(node, nil, nil) } let(:provider_resolver) { Chef::ProviderResolver.new(node, resource, action) } + let(:resolved_provider) do + begin + resource ? resource.provider_for_action(action).class : nil + rescue Chef::Exceptions::ProviderNotFound + nil + end + end - let(:action) { :start } - - let(:resolved_provider) { provider_resolver.resolve } - - let(:provider) { nil } - - let(:resource_name) { :service } - - let(:resource) { double(Chef::Resource, provider: provider, resource_name: resource_name) } - - before do - allow(resource).to receive(:is_a?).with(Chef::Resource).and_return(true) + let(:resource) do + resource_class = Chef::ResourceResolver.resolve(resource_name, node: node) + if resource_class + resource = resource_class.new('test', run_context) + resource.provider = provider if provider + end + resource end def self.on_platform(platform, *tags, @@ -83,16 +90,41 @@ describe Chef::ProviderResolver do end def self.expect_providers(**providers) - providers.each do |name, provider| + providers.each do |name, expected| describe name.to_s do let(:resource_name) { name } - if provider - it "resolves to a #{provider}" do - expect(resolved_provider).to eql(provider) + + tags = [] + expected_provider = nil + expected_resource = nil + Array(expected).each do |p| + if p.is_a?(Class) && p <= Chef::Provider + expected_provider = p + elsif p.is_a?(Class) && p <= Chef::Resource + expected_resource = p + else + tags << p + end + end + + if expected_resource && expected_provider + it "'#{name}' resolves to resource #{expected_resource} and provider #{expected_provider}", *tags do + expect(resource.class).to eql(expected_resource) + provider = double(expected_provider, class: expected_provider) + expect(provider).to receive(:action=).with(action) + expect(expected_provider).to receive(:new).with(resource, run_context).and_return(provider) + expect(resolved_provider).to eql(expected_provider) + end + elsif expected_provider + it "'#{name}' resolves to provider #{expected_provider}", *tags do + provider = double(expected_provider) + expect(provider).to receive(:action=).with(action) + expect(expected_provider).to receive(:new).with(resource, run_context).and_return(provider) + expect(resolved_provider).to eql(expected_provider) end else - it "Fails to resolve (since #{name.inspect} is unsupported on #{platform} #{platform_version})" do - expect { resolved_provider }.to raise_error /Cannot find a provider/ + it "'#{name}' fails to resolve (since #{name.inspect} is unsupported on #{platform} #{platform_version})", *tags do + expect(resolved_provider).to be_nil end end end @@ -454,49 +486,48 @@ describe Chef::ProviderResolver do PROVIDERS = { - bash: Chef::Provider::Script, - breakpoint: Chef::Provider::Breakpoint, - chef_gem: Chef::Provider::Package::Rubygems, - cookbook_file: Chef::Provider::CookbookFile, - csh: Chef::Provider::Script, - deploy: Chef::Provider::Deploy::Timestamped, - deploy_revision: Chef::Provider::Deploy::Revision, - directory: Chef::Provider::Directory, - easy_install_package: Chef::Provider::Package::EasyInstall, - erl_call: Chef::Provider::ErlCall, - execute: Chef::Provider::Execute, - file: Chef::Provider::File, - gem_package: Chef::Provider::Package::Rubygems, - git: Chef::Provider::Git, - group: Chef::Provider::Group::Gpasswd, - homebrew_package: Chef::Provider::Package::Homebrew, - http_request: Chef::Provider::HttpRequest, - ifconfig: Chef::Provider::Ifconfig, - link: Chef::Provider::Link, - log: Chef::Provider::Log::ChefLog, - macports_package: Chef::Provider::Package::Macports, - mdadm: Chef::Provider::Mdadm, - mount: Chef::Provider::Mount::Mount, - perl: Chef::Provider::Script, - portage_package: Chef::Provider::Package::Portage, - python: Chef::Provider::Script, - remote_directory: Chef::Provider::RemoteDirectory, - route: Chef::Provider::Route, - rpm_package: Chef::Provider::Package::Rpm, - ruby: Chef::Provider::Script, - ruby_block: Chef::Provider::RubyBlock, - script: Chef::Provider::Script, - subversion: Chef::Provider::Subversion, - template: Chef::Provider::Template, - timestamped_deploy: Chef::Provider::Deploy::Timestamped, - user: Chef::Provider::User::Useradd, - whyrun_safe_ruby_block: Chef::Provider::WhyrunSafeRubyBlock, + bash: [ Chef::Resource::Bash, Chef::Provider::Script ], + breakpoint: [ Chef::Resource::Breakpoint, Chef::Provider::Breakpoint ], + chef_gem: [ Chef::Resource::ChefGem, Chef::Provider::Package::Rubygems ], + cookbook_file: [ Chef::Resource::CookbookFile, Chef::Provider::CookbookFile ], + csh: [ Chef::Resource::Csh, Chef::Provider::Script ], + deploy: [ Chef::Resource::Deploy, Chef::Provider::Deploy::Timestamped ], + deploy_revision: [ Chef::Resource::DeployRevision, Chef::Provider::Deploy::Revision ], + directory: [ Chef::Resource::Directory, Chef::Provider::Directory ], + easy_install_package: [ Chef::Resource::EasyInstallPackage, Chef::Provider::Package::EasyInstall ], + erl_call: [ Chef::Resource::ErlCall, Chef::Provider::ErlCall ], + execute: [ Chef::Resource::Execute, Chef::Provider::Execute ], + file: [ Chef::Resource::File, Chef::Provider::File ], + gem_package: [ Chef::Resource::GemPackage, Chef::Provider::Package::Rubygems ], + git: [ Chef::Resource::Git, Chef::Provider::Git ], + group: [ Chef::Resource::Group, Chef::Provider::Group::Gpasswd ], + homebrew_package: [ Chef::Resource::HomebrewPackage, Chef::Provider::Package::Homebrew ], + http_request: [ Chef::Resource::HttpRequest, Chef::Provider::HttpRequest ], + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], + link: [ Chef::Resource::Link, Chef::Provider::Link ], + log: [ Chef::Resource::Log, Chef::Provider::Log::ChefLog ], + macports_package: [ Chef::Resource::MacportsPackage, Chef::Provider::Package::Macports ], + mdadm: [ Chef::Resource::Mdadm, Chef::Provider::Mdadm ], + mount: [ Chef::Resource::Mount, Chef::Provider::Mount::Mount ], + perl: [ Chef::Resource::Perl, Chef::Provider::Script ], + portage_package: [ Chef::Resource::PortagePackage, Chef::Provider::Package::Portage ], + python: [ Chef::Resource::Python, Chef::Provider::Script ], + remote_directory: [ Chef::Resource::RemoteDirectory, Chef::Provider::RemoteDirectory ], + route: [ Chef::Resource::Route, Chef::Provider::Route ], + ruby: [ Chef::Resource::Ruby, Chef::Provider::Script ], + ruby_block: [ Chef::Resource::RubyBlock, Chef::Provider::RubyBlock ], + script: [ Chef::Resource::Script, Chef::Provider::Script ], + subversion: [ Chef::Resource::Subversion, Chef::Provider::Subversion ], + template: [ Chef::Resource::Template, Chef::Provider::Template ], + timestamped_deploy: [ Chef::Resource::TimestampedDeploy, Chef::Provider::Deploy::Timestamped ], + user: [ Chef::Resource::User, Chef::Provider::User::Useradd ], + whyrun_safe_ruby_block: [ Chef::Resource::WhyrunSafeRubyBlock, Chef::Provider::WhyrunSafeRubyBlock ], # We want to check that these are unsupported: apt_package: nil, bff_package: nil, - dsc_script: nil, dpkg_package: nil, + dsc_script: nil, ips_package: nil, pacman_package: nil, paludis_package: nil, @@ -508,61 +539,62 @@ describe Chef::ProviderResolver do windows_service: nil, "linux" => { - apt_package: Chef::Provider::Package::Apt, - dpkg_package: Chef::Provider::Package::Dpkg, - pacman_package: Chef::Provider::Package::Pacman, - paludis_package: Chef::Provider::Package::Paludis, - rpm_package: Chef::Provider::Package::Rpm, - yum_package: Chef::Provider::Package::Yum, + apt_package: [ Chef::Resource::AptPackage, Chef::Provider::Package::Apt ], + dpkg_package: [ Chef::Resource::DpkgPackage, Chef::Provider::Package::Dpkg ], + pacman_package: [ Chef::Resource::PacmanPackage, Chef::Provider::Package::Pacman ], + paludis_package: [ Chef::Resource::PaludisPackage, Chef::Provider::Package::Paludis ], + rpm_package: [ Chef::Resource::RpmPackage, Chef::Provider::Package::Rpm ], + yum_package: [ Chef::Resource::YumPackage, Chef::Provider::Package::Yum ], "debian" => { - ifconfig: Chef::Provider::Ifconfig::Debian, - package: Chef::Provider::Package::Apt, -# service: Chef::Provider::Service::Debian, + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig::Debian ], + package: [ Chef::Resource::AptPackage, Chef::Provider::Package::Apt ], +# service: [ Chef::Resource::DebianService, Chef::Provider::Service::Debian ], "debian" => { "7.0" => { }, "6.0" => { - ifconfig: Chef::Provider::Ifconfig, -# service: Chef::Provider::Service::Insserv, + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], +# service: [ Chef::Resource::InsservService, Chef::Provider::Service::Insserv ], }, "5.0" => { - ifconfig: Chef::Provider::Ifconfig, + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], }, }, "gcel" => { "3.1.4" => { - ifconfig: Chef::Provider::Ifconfig, + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], }, }, "linaro" => { "3.1.4" => { - ifconfig: Chef::Provider::Ifconfig, + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], }, }, "linuxmint" => { "3.1.4" => { - ifconfig: Chef::Provider::Ifconfig, -# service: Chef::Provider::Service::Upstart, + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], +# service: [ Chef::Resource::UpstartService, Chef::Provider::Service::Upstart ], }, }, "raspbian" => { "3.1.4" => { - ifconfig: Chef::Provider::Ifconfig, + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], }, }, "ubuntu" => { "11.10" => { }, "10.04" => { - ifconfig: Chef::Provider::Ifconfig, + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig ], }, }, }, "arch" => { - package: Chef::Provider::Package::Pacman, + # TODO should be Chef::Resource::PacmanPackage + package: [ Chef::Resource::Package, Chef::Provider::Package::Pacman ], "arch" => { "3.1.4" => { @@ -571,8 +603,8 @@ describe Chef::ProviderResolver do }, "freebsd" => { - group: Chef::Provider::Group::Pw, - user: Chef::Provider::User::Pw, + group: [ Chef::Resource::Group, Chef::Provider::Group::Pw ], + user: [ Chef::Resource::User, Chef::Provider::User::Pw ], "freebsd" => { "3.1.4" => { @@ -580,30 +612,31 @@ describe Chef::ProviderResolver do }, }, "suse" => { - group: Chef::Provider::Group::Gpasswd, + group: [ Chef::Resource::Group, Chef::Provider::Group::Gpasswd ], "suse" => { "12.0" => { }, %w(11.1 11.2 11.3) => { - group: Chef::Provider::Group::Suse, + group: [ Chef::Resource::Group, Chef::Provider::Group::Suse ], }, }, "opensuse" => { -# service: Chef::Provider::Service::Redhat, - package: Chef::Provider::Package::Zypper, - group: Chef::Provider::Group::Usermod, +# service: [ Chef::Resource::RedhatService, Chef::Provider::Service::Redhat ], + package: [ Chef::Resource::ZypperPackage, Chef::Provider::Package::Zypper ], + group: [ Chef::Resource::Group, Chef::Provider::Group::Usermod ], "12.3" => { }, "12.2" => { - group: Chef::Provider::Group::Suse, + group: [ Chef::Resource::Group, Chef::Provider::Group::Suse ], }, }, }, "gentoo" => { - package: Chef::Provider::Package::Portage, - portage_package: Chef::Provider::Package::Portage, -# service: Chef::Provider::Service::Gentoo, + # TODO should be Chef::Resource::PortagePackage + package: [ Chef::Resource::Package, Chef::Provider::Package::Portage ], + portage_package: [ Chef::Resource::PortagePackage, Chef::Provider::Package::Portage ], +# service: [ Chef::Resource::GentooService, Chef::Provider::Service::Gentoo ], "gentoo" => { "3.1.4" => { @@ -612,27 +645,27 @@ describe Chef::ProviderResolver do }, "rhel" => { -# service: Chef::Provider::Service::Systemd, - package: Chef::Provider::Package::Yum, - ifconfig: Chef::Provider::Ifconfig::Redhat, +# service: [ Chef::Resource::SystemdService, Chef::Provider::Service::Systemd ], + package: [ Chef::Resource::YumPackage, Chef::Provider::Package::Yum ], + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig::Redhat ], %w(amazon xcp xenserver ibm_powerkvm cloudlinux parallels) => { "3.1.4" => { -# service: Chef::Provider::Service::Redhat, +# service: [ Chef::Resource::RedhatService, Chef::Provider::Service::Redhat ], }, }, %w(redhat centos scientific oracle) => { "7.0" => { }, "6.0" => { -# service: Chef::Provider::Service::Redhat, +# service: [ Chef::Resource::RedhatService, Chef::Provider::Service::Redhat ], }, }, "fedora" => { "15.0" => { }, "14.0" => { -# service: Chef::Provider::Service::Redhat, +# service: [ Chef::Resource::RedhatService, Chef::Provider::Service::Redhat ], }, }, }, @@ -641,9 +674,9 @@ describe Chef::ProviderResolver do "darwin" => { %w(mac_os_x mac_os_x_server) => { - group: Chef::Provider::Group::Dscl, - package: Chef::Provider::Package::Homebrew, - user: Chef::Provider::User::Dscl, + group: [ Chef::Resource::Group, Chef::Provider::Group::Dscl ], + package: [ Chef::Resource::HomebrewPackage, Chef::Provider::Package::Homebrew ], + user: [ Chef::Resource::User, Chef::Provider::User::Dscl ], "mac_os_x" => { "10.9.2" => { @@ -653,17 +686,17 @@ describe Chef::ProviderResolver do }, "windows" => { - batch: Chef::Provider::Batch, - dsc_script: Chef::Provider::DscScript, - env: Chef::Provider::Env::Windows, - group: Chef::Provider::Group::Windows, - mount: Chef::Provider::Mount::Windows, - package: Chef::Provider::Package::Windows, - powershell_script: Chef::Provider::PowershellScript, - service: Chef::Provider::Service::Windows, - user: Chef::Provider::User::Windows, - windows_package: Chef::Provider::Package::Windows, - windows_service: Chef::Provider::Service::Windows, + batch: [ Chef::Resource::Batch, Chef::Provider::Batch ], + dsc_script: [ Chef::Resource::DscScript, Chef::Provider::DscScript ], + env: [ Chef::Resource::Env, Chef::Provider::Env::Windows ], + group: [ Chef::Resource::Group, Chef::Provider::Group::Windows ], + mount: [ Chef::Resource::Mount, Chef::Provider::Mount::Windows ], + package: [ Chef::Resource::WindowsPackage, Chef::Provider::Package::Windows ], + powershell_script: [ Chef::Resource::PowershellScript, Chef::Provider::PowershellScript ], + service: [ Chef::Resource::WindowsService, Chef::Provider::Service::Windows ], + user: [ Chef::Resource::User, Chef::Provider::User::Windows ], + windows_package: [ Chef::Resource::WindowsPackage, Chef::Provider::Package::Windows ], + windows_service: [ Chef::Resource::WindowsService, Chef::Provider::Service::Windows ], "windows" => { %w(mswin mingw32 windows) => { @@ -674,15 +707,16 @@ describe Chef::ProviderResolver do }, "aix" => { - bff_package: Chef::Provider::Package::Aix, - cron: Chef::Provider::Cron::Aix, - group: Chef::Provider::Group::Aix, - ifconfig: Chef::Provider::Ifconfig::Aix, - mount: Chef::Provider::Mount::Aix, - package: Chef::Provider::Package::Aix, - rpm_package: Chef::Provider::Package::Rpm, - user: Chef::Provider::User::Aix, -# service: Chef::Provider::Service::Aix, + bff_package: [ Chef::Resource::BffPackage, Chef::Provider::Package::Aix ], + cron: [ Chef::Resource::Cron, Chef::Provider::Cron::Aix ], + group: [ Chef::Resource::Group, Chef::Provider::Group::Aix ], + ifconfig: [ Chef::Resource::Ifconfig, Chef::Provider::Ifconfig::Aix ], + mount: [ Chef::Resource::Mount, Chef::Provider::Mount::Aix ], + # TODO should be Chef::Resource::BffPackage + package: [ Chef::Resource::Package, Chef::Provider::Package::Aix ], + rpm_package: [ Chef::Resource::RpmPackage, Chef::Provider::Package::Rpm ], + user: [ Chef::Resource::User, Chef::Provider::User::Aix ], +# service: [ Chef::Resource::AixService, Chef::Provider::Service::Aix ], "aix" => { "aix" => { @@ -696,7 +730,7 @@ describe Chef::ProviderResolver do "hpux" => { "hpux" => { "3.1.4" => { - group: Chef::Provider::Group::Usermod + group: [ Chef::Resource::Group, Chef::Provider::Group::Usermod ] } } } @@ -706,15 +740,15 @@ describe Chef::ProviderResolver do "netbsd" => { "netbsd" => { "3.1.4" => { - group: Chef::Provider::Group::Groupmod, + group: [ Chef::Resource::Group, Chef::Provider::Group::Groupmod ], }, }, }, }, "openbsd" => { - group: Chef::Provider::Group::Usermod, - package: Chef::Provider::Package::Openbsd, + group: [ Chef::Resource::Group, Chef::Provider::Group::Usermod ], + package: [ Chef::Resource::OpenbsdPackage, Chef::Provider::Package::Openbsd ], "openbsd" => { "openbsd" => { @@ -725,15 +759,15 @@ describe Chef::ProviderResolver do }, "solaris2" => { - group: Chef::Provider::Group::Usermod, - ips_package: Chef::Provider::Package::Ips, - package: Chef::Provider::Package::Ips, - mount: Chef::Provider::Mount::Solaris, - solaris_package: Chef::Provider::Package::Solaris, + group: [ Chef::Resource::Group, Chef::Provider::Group::Usermod ], + ips_package: [ Chef::Resource::IpsPackage, Chef::Provider::Package::Ips ], + package: [ Chef::Resource::SolarisPackage, Chef::Provider::Package::Solaris ], + mount: [ Chef::Resource::Mount, Chef::Provider::Mount::Solaris ], + solaris_package: [ Chef::Resource::SolarisPackage, Chef::Provider::Package::Solaris ], "smartos" => { - smartos_package: Chef::Provider::Package::SmartOS, - package: Chef::Provider::Package::SmartOS, + smartos_package: [ Chef::Resource::SmartosPackage, Chef::Provider::Package::SmartOS ], + package: [ Chef::Resource::SmartosPackage, Chef::Provider::Package::SmartOS ], "smartos" => { "3.1.4" => { @@ -744,12 +778,11 @@ describe Chef::ProviderResolver do "solaris2" => { "nexentacore" => { "3.1.4" => { - package: Chef::Provider::Package::Solaris, }, }, "omnios" => { "3.1.4" => { - user: Chef::Provider::User::Solaris, + user: [ Chef::Resource::User, Chef::Provider::User::Solaris ], } }, "openindiana" => { @@ -761,11 +794,11 @@ describe Chef::ProviderResolver do }, }, "solaris2" => { - user: Chef::Provider::User::Solaris, + user: [ Chef::Resource::User, Chef::Provider::User::Solaris ], "5.11" => { + package: [ Chef::Resource::IpsPackage, Chef::Provider::Package::Ips ], }, "5.9" => { - package: Chef::Provider::Package::Solaris, }, }, }, @@ -785,7 +818,8 @@ describe Chef::ProviderResolver do "exherbo" => { "exherbo" => { "3.1.4" => { - package: Chef::Provider::Package::Paludis + # TODO should be Chef::Resource::PaludisPackage + package: [ Chef::Resource::Package, Chef::Provider::Package::Paludis ] } } } diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb index d7a34bc21b..97b88b1732 100644 --- a/spec/unit/provider_spec.rb +++ b/spec/unit/provider_spec.rb @@ -114,9 +114,7 @@ describe Chef::Provider do end it "does not re-load recipes when creating the temporary run context" do - # we actually want to test that RunContext#load is never called, but we - # can't stub all instances of an object with rspec's mocks. :/ - allow(Chef::RunContext).to receive(:new).and_raise("not supposed to happen") + expect_any_instance_of(Chef::RunContext).not_to receive(:load) snitch = Proc.new {temporary_collection = @run_context.resource_collection} @provider.send(:recipe_eval, &snitch) end diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb index ee98e63c1f..ea3ab44c16 100644 --- a/spec/unit/recipe_spec.rb +++ b/spec/unit/recipe_spec.rb @@ -121,6 +121,7 @@ describe Chef::Recipe do it "locate resource for particular platform" do ShaunTheSheep = Class.new(Chef::Resource) + ShaunTheSheep.resource_name :shaun_the_sheep ShaunTheSheep.provides :laughter, :platform => ["television"] node.automatic[:platform] = "television" node.automatic[:platform_version] = "123" @@ -131,6 +132,7 @@ describe Chef::Recipe do it "locate a resource for all platforms" do YourMom = Class.new(Chef::Resource) + YourMom.resource_name :your_mom YourMom.provides :love_and_caring res = recipe.love_and_caring "mommy" expect(res.name).to eql("mommy") @@ -141,7 +143,9 @@ describe Chef::Recipe do before do node.automatic[:platform] = "nbc_sports" Sounders = Class.new(Chef::Resource) + Sounders.resource_name :sounders TottenhamHotspur = Class.new(Chef::Resource) + TottenhamHotspur.resource_name :tottenham_hotspur end after do @@ -149,24 +153,20 @@ describe Chef::Recipe do Object.send(:remove_const, :TottenhamHotspur) end - it "selects one if it is the last declared" do - expect(Chef::Log).not_to receive(:warn) - + it "selects the first one alphabetically" do Sounders.provides :football, platform: "nbc_sports" TottenhamHotspur.provides :football, platform: "nbc_sports" res1 = recipe.football "club world cup" expect(res1.name).to eql("club world cup") - expect(res1).to be_a_kind_of(TottenhamHotspur) + expect(res1).to be_a_kind_of(Sounders) end - it "selects the other one if it is given priority" do - expect(Chef::Log).not_to receive(:warn) - - TottenhamHotspur.provides :football, platform: "nbc_sports" - Sounders.provides :football, platform: "nbc_sports" + it "selects the first one alphabetically even if the declaration order is reversed" do + TottenhamHotspur.provides :football2, platform: "nbc_sports" + Sounders.provides :football2, platform: "nbc_sports" - res1 = recipe.football "club world cup" + res1 = recipe.football2 "club world cup" expect(res1.name).to eql("club world cup") expect(res1).to be_a_kind_of(Sounders) end @@ -405,8 +405,8 @@ describe Chef::Recipe do end it "does not copy the action from the first resource" do - expect(original_resource.action).to eq(:score) - expect(duplicated_resource.action).to eq(:nothing) + expect(original_resource.action).to eq([:score]) + expect(duplicated_resource.action).to eq([:nothing]) end it "does not copy the source location of the first resource" do @@ -501,7 +501,7 @@ describe Chef::Recipe do recipe.from_file(File.join(CHEF_SPEC_DATA, "recipes", "test.rb")) res = recipe.resources(:file => "/etc/nsswitch.conf") expect(res.name).to eql("/etc/nsswitch.conf") - expect(res.action).to eql(:create) + expect(res.action).to eql([:create]) expect(res.owner).to eql("root") expect(res.group).to eql("root") expect(res.mode).to eql(0644) diff --git a/spec/unit/registry_helper_spec.rb b/spec/unit/registry_helper_spec.rb index 036a0834db..b2d0b7b125 100644 --- a/spec/unit/registry_helper_spec.rb +++ b/spec/unit/registry_helper_spec.rb @@ -21,6 +21,7 @@ require 'spec_helper' describe Chef::Provider::RegistryKey do let(:value1) { { :name => "one", :type => :string, :data => "1" } } + let(:value1_upcase_name) { {:name => "ONE", :type => :string, :data => "1"} } let(:key_path) { 'HKCU\Software\OpscodeNumbers' } let(:key) { 'Software\OpscodeNumbers' } let(:key_parent) { 'Software' } @@ -71,7 +72,20 @@ describe Chef::Provider::RegistryKey do expect(@registry).to receive(:data_exists?).with(key_path, value1).and_return(true) @registry.set_value(key_path, value1) end - + it "does nothing if case insensitive key and hive and value exist" do + expect(@registry).to receive(:key_exists!).with(key_path.downcase).and_return(true) + expect(@registry).to receive(:get_hive_and_key).with(key_path.downcase).and_return([@hive_mock, key]) + expect(@registry).to receive(:value_exists?).with(key_path.downcase, value1).and_return(true) + expect(@registry).to receive(:data_exists?).with(key_path.downcase, value1).and_return(true) + @registry.set_value(key_path.downcase, value1) + end + it "does nothing if key and hive and value with a case insensitive name exist" do + expect(@registry).to receive(:key_exists!).with(key_path.downcase).and_return(true) + expect(@registry).to receive(:get_hive_and_key).with(key_path.downcase).and_return([@hive_mock, key]) + expect(@registry).to receive(:value_exists?).with(key_path.downcase, value1_upcase_name).and_return(true) + expect(@registry).to receive(:data_exists?).with(key_path.downcase, value1_upcase_name).and_return(true) + @registry.set_value(key_path.downcase, value1_upcase_name) + end it "updates value if key and hive and value exist, but data is different" do expect(@registry).to receive(:key_exists!).with(key_path).and_return(true) expect(@registry).to receive(:get_hive_and_key).with(key_path).and_return([@hive_mock, key]) diff --git a/spec/unit/resource/breakpoint_spec.rb b/spec/unit/resource/breakpoint_spec.rb index 9c867ebcc7..88ab34d568 100644 --- a/spec/unit/resource/breakpoint_spec.rb +++ b/spec/unit/resource/breakpoint_spec.rb @@ -37,7 +37,7 @@ describe Chef::Resource::Breakpoint do end it "defaults to the break action" do - expect(@breakpoint.action).to eq(:break) + expect(@breakpoint.action).to eq([:break]) end it "names itself after the line number of the file where it's created" do diff --git a/spec/unit/resource/cron_spec.rb b/spec/unit/resource/cron_spec.rb index 743552c1de..0978be6930 100644 --- a/spec/unit/resource/cron_spec.rb +++ b/spec/unit/resource/cron_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Cron do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should accept create or delete for action" do diff --git a/spec/unit/resource/deploy_spec.rb b/spec/unit/resource/deploy_spec.rb index 0403a7ba6b..5b6a452784 100644 --- a/spec/unit/resource/deploy_spec.rb +++ b/spec/unit/resource/deploy_spec.rb @@ -148,10 +148,16 @@ describe Chef::Resource::Deploy do expect(@resource.current_path).to eql("/my/deploy/dir/current") end + it "allows depth to be set via integer" do + expect(@resource.depth).to be_nil + @resource.depth 1 + expect(@resource.depth).to eql(1) + end + it "gives #depth as 5 if shallow clone is true, nil otherwise" do expect(@resource.depth).to be_nil @resource.shallow_clone true - expect(@resource.depth).to eql("5") + expect(@resource.depth).to eql(5) end it "aliases repo as repository" do diff --git a/spec/unit/resource/directory_spec.rb b/spec/unit/resource/directory_spec.rb index c452b2a914..e9e80806db 100644 --- a/spec/unit/resource/directory_spec.rb +++ b/spec/unit/resource/directory_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Directory do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should accept create or delete for action" do diff --git a/spec/unit/resource/dsc_resource_spec.rb b/spec/unit/resource/dsc_resource_spec.rb index ae15f56eaf..06769d86ce 100644 --- a/spec/unit/resource/dsc_resource_spec.rb +++ b/spec/unit/resource/dsc_resource_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::DscResource do } it "has a default action of `:run`" do - expect(dsc_test_resource.action).to eq(:run) + expect(dsc_test_resource.action).to eq([:run]) end it "has an allowed_actions attribute with only the `:run` and `:nothing` attributes" do diff --git a/spec/unit/resource/dsc_script_spec.rb b/spec/unit/resource/dsc_script_spec.rb index 71103ea590..1fa865a2d5 100644 --- a/spec/unit/resource/dsc_script_spec.rb +++ b/spec/unit/resource/dsc_script_spec.rb @@ -29,7 +29,7 @@ describe Chef::Resource::DscScript do Chef::RunContext.new(node, {}, empty_events) } let(:dsc_test_resource) { - Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) + Chef::Resource::DscScript.new(dsc_test_resource_name, dsc_test_run_context) } let(:configuration_code) {'echo "This is supposed to create a configuration document."'} let(:configuration_path) {'c:/myconfigs/formatc.ps1'} @@ -38,7 +38,7 @@ describe Chef::Resource::DscScript do let(:configuration_data_script) { 'c:/myconfigs/data/safedata.psd1' } it "has a default action of `:run`" do - expect(dsc_test_resource.action).to eq(:run) + expect(dsc_test_resource.action).to eq([:run]) end it "has an allowed_actions attribute with only the `:run` and `:nothing` attributes" do @@ -70,6 +70,10 @@ describe Chef::Resource::DscScript do expect(dsc_test_resource.configuration_data_script).to eq(configuration_data_script) end + it "has the ps_credential helper method" do + expect(dsc_test_resource).to respond_to(:ps_credential) + end + context "when calling imports" do let(:module_name) { 'FooModule' } let(:module_name_b) { 'BarModule' } diff --git a/spec/unit/resource/env_spec.rb b/spec/unit/resource/env_spec.rb index 566827a27e..9bee07c593 100644 --- a/spec/unit/resource/env_spec.rb +++ b/spec/unit/resource/env_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Env do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end { :create => false, :delete => false, :modify => false, :flibber => true }.each do |action,bad_value| diff --git a/spec/unit/resource/erl_call_spec.rb b/spec/unit/resource/erl_call_spec.rb index 008d27372a..9abf2e7812 100644 --- a/spec/unit/resource/erl_call_spec.rb +++ b/spec/unit/resource/erl_call_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::ErlCall do end it "should have a default action of run" do - expect(@resource.action).to eql(:run) + expect(@resource.action).to eql([:run]) end it "should accept run as an action" do diff --git a/spec/unit/resource/file/verification_spec.rb b/spec/unit/resource/file/verification_spec.rb index 3609d9d482..6b929789c8 100644 --- a/spec/unit/resource/file/verification_spec.rb +++ b/spec/unit/resource/file/verification_spec.rb @@ -69,12 +69,40 @@ describe Chef::Resource::File::Verification do end context "with a verification command(String)" do + before(:each) do + allow(Chef::Log).to receive(:deprecation).and_return(nil) + end + + def platform_specific_verify_command(variable_name) + if windows? + "if \"#{temp_path}\" == \"%{#{variable_name}}\" (exit 0) else (exit 1)" + else + "test #{temp_path} = %{#{variable_name}}" + end + end + it "substitutes \%{file} with the path" do - test_command = if windows? - "if \"#{temp_path}\" == \"%{file}\" (exit 0) else (exit 1)" - else - "test #{temp_path} = %{file}" - end + test_command = platform_specific_verify_command('file') + v = Chef::Resource::File::Verification.new(parent_resource, test_command, {}) + expect(v.verify(temp_path)).to eq(true) + end + + it "warns about deprecation when \%{file} is used" do + expect(Chef::Log).to receive(:deprecation).with(/%{file} is deprecated/, /verification_spec\.rb/) + test_command = platform_specific_verify_command('file') + Chef::Resource::File::Verification.new(parent_resource, test_command, {}) + .verify(temp_path) + end + + it "does not warn about deprecation when \%{file} is not used" do + expect(Chef::Log).to_not receive(:deprecation) + test_command = platform_specific_verify_command('path') + Chef::Resource::File::Verification.new(parent_resource, test_command, {}) + .verify(temp_path) + end + + it "substitutes \%{path} with the path" do + test_command = platform_specific_verify_command('path') v = Chef::Resource::File::Verification.new(parent_resource, test_command, {}) expect(v.verify(temp_path)).to eq(true) end diff --git a/spec/unit/resource/file_spec.rb b/spec/unit/resource/file_spec.rb index dd20f5f03a..76beaf15e1 100644 --- a/spec/unit/resource/file_spec.rb +++ b/spec/unit/resource/file_spec.rb @@ -29,7 +29,7 @@ describe Chef::Resource::File do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should have a default content of nil" do diff --git a/spec/unit/resource/group_spec.rb b/spec/unit/resource/group_spec.rb index bcf9205f7e..a4029fc911 100644 --- a/spec/unit/resource/group_spec.rb +++ b/spec/unit/resource/group_spec.rb @@ -50,7 +50,7 @@ describe Chef::Resource::Group, "initialize" do end it "should set action to :create" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end %w{create remove modify manage}.each do |action| diff --git a/spec/unit/resource/link_spec.rb b/spec/unit/resource/link_spec.rb index 51221e0472..0246fcd13b 100644 --- a/spec/unit/resource/link_spec.rb +++ b/spec/unit/resource/link_spec.rb @@ -36,7 +36,7 @@ describe Chef::Resource::Link do end it "should have a default action of 'create'" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end { :create => false, :delete => false, :blues => true }.each do |action,bad_value| diff --git a/spec/unit/resource/mdadm_spec.rb b/spec/unit/resource/mdadm_spec.rb index 866309ec5b..6ca99c58e5 100644 --- a/spec/unit/resource/mdadm_spec.rb +++ b/spec/unit/resource/mdadm_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Mdadm do end it "should have a default action of create" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should accept create, assemble, stop as actions" do diff --git a/spec/unit/resource/mount_spec.rb b/spec/unit/resource/mount_spec.rb index ad95c06e04..acce26dcab 100644 --- a/spec/unit/resource/mount_spec.rb +++ b/spec/unit/resource/mount_spec.rb @@ -38,7 +38,7 @@ describe Chef::Resource::Mount do end it "should have a default action of mount" do - expect(@resource.action).to eql(:mount) + expect(@resource.action).to eql([:mount]) end it "should accept mount, umount and remount as actions" do diff --git a/spec/unit/resource/ohai_spec.rb b/spec/unit/resource/ohai_spec.rb index fe29755abf..3bc21a41d2 100644 --- a/spec/unit/resource/ohai_spec.rb +++ b/spec/unit/resource/ohai_spec.rb @@ -34,7 +34,7 @@ describe Chef::Resource::Ohai do end it "should have a default action of create" do - expect(@resource.action).to eql(:reload) + expect(@resource.action).to eql([:reload]) end it "should allow you to set the plugin attribute" do diff --git a/spec/unit/resource/powershell_spec.rb b/spec/unit/resource/powershell_script_spec.rb index 2505c4a3d7..2505c4a3d7 100644 --- a/spec/unit/resource/powershell_spec.rb +++ b/spec/unit/resource/powershell_script_spec.rb diff --git a/spec/unit/resource/registry_key_spec.rb b/spec/unit/resource/registry_key_spec.rb index e2a864d73a..2e2811d026 100644 --- a/spec/unit/resource/registry_key_spec.rb +++ b/spec/unit/resource/registry_key_spec.rb @@ -45,7 +45,7 @@ describe Chef::Resource::RegistryKey, "initialize" do end it "should set action to :create" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end %w{create create_if_missing delete delete_key}.each do |action| diff --git a/spec/unit/resource/route_spec.rb b/spec/unit/resource/route_spec.rb index ffb9304511..ec1d369932 100644 --- a/spec/unit/resource/route_spec.rb +++ b/spec/unit/resource/route_spec.rb @@ -35,7 +35,7 @@ describe Chef::Resource::Route do end it "should have a default action of 'add'" do - expect(@resource.action).to eql(:add) + expect(@resource.action).to eql([:add]) end it "should accept add or delete for action" do diff --git a/spec/unit/resource/ruby_block_spec.rb b/spec/unit/resource/ruby_block_spec.rb index 5d83f7e367..8664564ac5 100644 --- a/spec/unit/resource/ruby_block_spec.rb +++ b/spec/unit/resource/ruby_block_spec.rb @@ -30,8 +30,8 @@ describe Chef::Resource::RubyBlock do expect(@resource).to be_a_kind_of(Chef::Resource::RubyBlock) end - it "should have a default action of 'create'" do - expect(@resource.action).to eql(:run) + it "should have a default action of 'run'" do + expect(@resource.action).to eql([:run]) end it "should have a resource name of :ruby_block" do @@ -46,7 +46,7 @@ describe Chef::Resource::RubyBlock do it "allows the action to be 'create'" do @resource.action :create - expect(@resource.action).to eq(:create) + expect(@resource.action).to eq([:create]) end describe "when it has been initialized with block code" do diff --git a/spec/unit/resource/service_spec.rb b/spec/unit/resource/service_spec.rb index eb6f444e93..b9e3757255 100644 --- a/spec/unit/resource/service_spec.rb +++ b/spec/unit/resource/service_spec.rb @@ -1,7 +1,7 @@ # # Author:: AJ Christensen (<aj@hjksolutions.com>) # Author:: Tyler Cloke (<tyler@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -139,14 +139,14 @@ describe Chef::Resource::Service do expect { @resource.send(attrib, "poop") }.to raise_error(ArgumentError) end - it "should default all the feature support to false" do - support_hash = { :status => false, :restart => false, :reload=> false } + it "should default all the feature support to nil" do + support_hash = { :status => nil, :restart => nil, :reload=> nil } expect(@resource.supports).to eq(support_hash) end it "should allow you to set what features this resource supports as a array" do support_array = [ :status, :restart ] - support_hash = { :status => true, :restart => true, :reload => false } + support_hash = { :status => true, :restart => true, :reload => nil } @resource.supports(support_array) expect(@resource.supports).to eq(support_hash) end diff --git a/spec/unit/resource/user_spec.rb b/spec/unit/resource/user_spec.rb index f05de94fe0..3bf7e6187b 100644 --- a/spec/unit/resource/user_spec.rb +++ b/spec/unit/resource/user_spec.rb @@ -43,7 +43,7 @@ describe Chef::Resource::User, "initialize" do end it "should set action to :create" do - expect(@resource.action).to eql(:create) + expect(@resource.action).to eql([:create]) end it "should set supports[:manage_home] to false" do diff --git a/spec/unit/resource/windows_service_spec.rb b/spec/unit/resource/windows_service_spec.rb index 8866cad1bf..45a295c24e 100644 --- a/spec/unit/resource/windows_service_spec.rb +++ b/spec/unit/resource/windows_service_spec.rb @@ -44,6 +44,6 @@ describe Chef::Resource::WindowsService, "initialize" do it "allows the action to be 'configure_startup'" do resource.action :configure_startup - expect(resource.action).to eq(:configure_startup) + expect(resource.action).to eq([:configure_startup]) end end diff --git a/spec/unit/resource/yum_package_spec.rb b/spec/unit/resource/yum_package_spec.rb index e01b87c580..f24f1e3405 100644 --- a/spec/unit/resource/yum_package_spec.rb +++ b/spec/unit/resource/yum_package_spec.rb @@ -1,6 +1,6 @@ # # Author:: AJ Christensen (<aj@opscode.com>) -# Copyright:: Copyright (c) 2008 Opscode, Inc. +# Copyright:: Copyright (c) 2008-2015 Chef Software, Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -78,3 +78,12 @@ describe Chef::Resource::YumPackage, "allow_downgrade" do expect { @resource.allow_downgrade "monkey" }.to raise_error(ArgumentError) end end + +describe Chef::Resource::YumPackage, "yum_binary" do + let(:resource) { Chef::Resource::YumPackage.new("foo") } + + it "should allow you to specify the yum_binary" do + resource.yum_binary "/usr/bin/yum-something" + expect(resource.yum_binary).to eql("/usr/bin/yum-something") + end +end diff --git a/spec/unit/resource_collection_spec.rb b/spec/unit/resource_collection_spec.rb index b43b012dfc..d52e7e2c26 100644 --- a/spec/unit/resource_collection_spec.rb +++ b/spec/unit/resource_collection_spec.rb @@ -252,7 +252,7 @@ describe Chef::ResourceCollection do expect(json).to match(/instance_vars/) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { rc } end end diff --git a/spec/unit/resource_resolver_spec.rb b/spec/unit/resource_resolver_spec.rb new file mode 100644 index 0000000000..b3bda9d945 --- /dev/null +++ b/spec/unit/resource_resolver_spec.rb @@ -0,0 +1,53 @@ +# +# Author:: Ranjib Dey +# Copyright:: Copyright (c) 2015 Ranjib Dey <ranjib@linux.com>. +# 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/resource_resolver' + + +describe Chef::ResourceResolver do + it '#resolve' do + expect(described_class.resolve(:execute)).to eq(Chef::Resource::Execute) + end + + it '#list' do + expect(described_class.list(:package)).to_not be_empty + end + + context 'instance methods' do + let(:resolver) do + described_class.new(Chef::Node.new, 'execute') + end + + it '#resolve' do + expect(resolver.resolve).to eq Chef::Resource::Execute + end + + it '#list' do + expect(resolver.list).to eq [ Chef::Resource::Execute ] + end + + it '#provided_by? returns true when resource class is in the list' do + expect(resolver.provided_by?(Chef::Resource::Execute)).to be_truthy + end + + it '#provided_by? returns false when resource class is not in the list' do + expect(resolver.provided_by?(Chef::Resource::File)).to be_falsey + end + end +end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 8ba45d9350..b9ba80068b 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -59,8 +59,8 @@ describe Chef::Resource do end describe "when declaring the identity attribute" do - it "has no identity attribute by default" do - expect(Chef::Resource.identity_attr).to be_nil + it "has :name as identity attribute by default" do + expect(Chef::Resource.identity_attr).to eq(:name) end it "sets an identity attribute" do @@ -344,6 +344,7 @@ describe Chef::Resource do expect(r.resource_name).to be_nil expect(r.declared_type).to eq :d end + it "and there are no provides lines, @resource_name is used" do c = Class.new(Chef::Resource) do def initialize(*args, &block) @@ -358,6 +359,20 @@ describe Chef::Resource do expect(r.resource_name).to eq :blah expect(r.declared_type).to eq :d end + + it "and the resource class gets a late-bound name, resource_name is nil" do + c = Class.new(Chef::Resource) do + def self.name + "ResourceSpecNameTest" + end + end + + r = c.new('hi') + r.declared_type = :d + expect(c.resource_name).to be_nil + expect(r.resource_name).to be_nil + expect(r.declared_type).to eq :d + end end it "resource_name without provides is honored" do @@ -416,7 +431,7 @@ describe Chef::Resource do expect(json).to match(/instance_vars/) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @resource } end end @@ -795,21 +810,21 @@ describe Chef::Resource do end it 'adds mappings for a single platform' do - expect(Chef).to receive(:set_resource_priority_array).with( + expect(Chef.resource_handler_map).to receive(:set).with( :dinobot, Chef::Resource::Klz, { platform: ['autobots'] } ) klz.provides :dinobot, platform: ['autobots'] end it 'adds mappings for multiple platforms' do - expect(Chef).to receive(:set_resource_priority_array).with( + expect(Chef.resource_handler_map).to receive(:set).with( :energy, Chef::Resource::Klz, { platform: ['autobots', 'decepticons']} ) klz.provides :energy, platform: ['autobots', 'decepticons'] end it 'adds mappings for all platforms' do - expect(Chef).to receive(:set_resource_priority_array).with( + expect(Chef.resource_handler_map).to receive(:set).with( :tape_deck, Chef::Resource::Klz, {} ) klz.provides :tape_deck @@ -962,4 +977,90 @@ describe Chef::Resource do end end + + describe "#action" do + let(:resource_class) do + Class.new(described_class) do + allowed_actions(%i{one two}) + end + end + let(:resource) { resource_class.new('test', nil) } + subject { resource.action } + + context "with a no action" do + it { is_expected.to eq [:nothing] } + end + + context "with a default action" do + let(:resource_class) do + Class.new(described_class) do + default_action(:one) + end + end + it { is_expected.to eq [:one] } + end + + context "with a symbol action" do + before { resource.action(:one) } + it { is_expected.to eq [:one] } + end + + context "with a string action" do + before { resource.action('two') } + it { is_expected.to eq [:two] } + end + + context "with an array action" do + before { resource.action([:two, :one]) } + it { is_expected.to eq [:two, :one] } + end + + context "with an assignment" do + before { resource.action = :one } + it { is_expected.to eq [:one] } + end + + context "with an array assignment" do + before { resource.action = [:two, :one] } + it { is_expected.to eq [:two, :one] } + end + + context "with an invalid action" do + it { expect { resource.action(:three) }.to raise_error Chef::Exceptions::ValidationFailed } + end + + context "with an invalid assignment action" do + it { expect { resource.action = :three }.to raise_error Chef::Exceptions::ValidationFailed } + end + end + + describe ".default_action" do + let(:default_action) { } + let(:resource_class) do + actions = default_action + Class.new(described_class) do + default_action(actions) if actions + end + end + subject { resource_class.default_action } + + context "with no default actions" do + it { is_expected.to eq [:nothing] } + end + + context "with a symbol default action" do + let(:default_action) { :one } + it { is_expected.to eq [:one] } + end + + context "with a string default action" do + let(:default_action) { 'one' } + it { is_expected.to eq [:one] } + end + + context "with an array default action" do + let(:default_action) { [:two, :one] } + it { is_expected.to eq [:two, :one] } + end + end end diff --git a/spec/unit/role_spec.rb b/spec/unit/role_spec.rb index f120ca6da6..ecc7945a08 100644 --- a/spec/unit/role_spec.rb +++ b/spec/unit/role_spec.rb @@ -217,7 +217,7 @@ describe Chef::Role do end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @role } end end diff --git a/spec/unit/run_context/child_run_context_spec.rb b/spec/unit/run_context/child_run_context_spec.rb new file mode 100644 index 0000000000..63586e459c --- /dev/null +++ b/spec/unit/run_context/child_run_context_spec.rb @@ -0,0 +1,133 @@ +# +# Author:: Adam Jacob (<adam@opscode.com>) +# Author:: Tim Hinderliter (<tim@opscode.com>) +# Author:: Christopher Walters (<cw@opscode.com>) +# Copyright:: Copyright (c) 2008, 2010 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 'support/lib/library_load_order' + +describe Chef::RunContext::ChildRunContext do + context "with a run context with stuff in it" do + let(:chef_repo_path) { File.expand_path(File.join(CHEF_SPEC_DATA, "run_context", "cookbooks")) } + let(:cookbook_collection) { + cl = Chef::CookbookLoader.new(chef_repo_path) + cl.load_cookbooks + Chef::CookbookCollection.new(cl) + } + let(:node) { + node = Chef::Node.new + node.run_list << "test" << "test::one" << "test::two" + node + } + let(:events) { Chef::EventDispatch::Dispatcher.new } + let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) } + + context "and a child run context" do + let(:child) { run_context.create_child } + + it "parent_run_context is set to the parent" do + expect(child.parent_run_context).to eq run_context + end + + it "audits is not the same as the parent" do + expect(child.audits.object_id).not_to eq run_context.audits.object_id + child.audits['hi'] = 'lo' + expect(child.audits['hi']).to eq('lo') + expect(run_context.audits['hi']).not_to eq('lo') + end + + it "resource_collection is not the same as the parent" do + expect(child.resource_collection.object_id).not_to eq run_context.resource_collection.object_id + f = Chef::Resource::File.new('hi', child) + child.resource_collection.insert(f) + expect(child.resource_collection).to include f + expect(run_context.resource_collection).not_to include f + end + + it "immediate_notification_collection is not the same as the parent" do + expect(child.immediate_notification_collection.object_id).not_to eq run_context.immediate_notification_collection.object_id + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_immediately(notification) + expect(child.immediate_notification_collection['file[hi]']).to eq([notification]) + expect(run_context.immediate_notification_collection['file[hi]']).not_to eq([notification]) + end + + it "immediate_notifications is not the same as the parent" do + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_immediately(notification) + expect(child.immediate_notifications(src)).to eq([notification]) + expect(run_context.immediate_notifications(src)).not_to eq([notification]) + end + + it "delayed_notification_collection is not the same as the parent" do + expect(child.delayed_notification_collection.object_id).not_to eq run_context.delayed_notification_collection.object_id + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_delayed(notification) + expect(child.delayed_notification_collection['file[hi]']).to eq([notification]) + expect(run_context.delayed_notification_collection['file[hi]']).not_to eq([notification]) + end + + it "delayed_notifications is not the same as the parent" do + src = Chef::Resource::File.new('hi', child) + dest = Chef::Resource::File.new('argh', child) + notification = Chef::Resource::Notification.new(dest, :create, src) + child.notifies_delayed(notification) + expect(child.delayed_notifications(src)).to eq([notification]) + expect(run_context.delayed_notifications(src)).not_to eq([notification]) + end + + it "create_child creates a child-of-child" do + c = child.create_child + expect(c.parent_run_context).to eq child + end + + context "after load('include::default')" do + before do + run_list = Chef::RunList.new('include::default').expand('_default') + # TODO not sure why we had to do this to get everything to work ... + node.automatic_attrs[:recipes] = [] + child.load(run_list) + end + + it "load_recipe loads into the child" do + expect(child.resource_collection).to be_empty + child.load_recipe("include::includee") + expect(child.resource_collection).not_to be_empty + end + + it "include_recipe loads into the child" do + expect(child.resource_collection).to be_empty + child.include_recipe("include::includee") + expect(child.resource_collection).not_to be_empty + end + + it "load_recipe_file loads into the child" do + expect(child.resource_collection).to be_empty + child.load_recipe_file(File.expand_path("include/recipes/includee.rb", chef_repo_path)) + expect(child.resource_collection).not_to be_empty + end + end + end + end +end diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb index e20ba63b72..99801575ef 100644 --- a/spec/unit/run_context_spec.rb +++ b/spec/unit/run_context_spec.rb @@ -68,6 +68,9 @@ describe Chef::RunContext do "dependency2" => { "version" => "0.0.0", }, + "include" => { + "version" => "0.0.0", + }, "no-default-attr" => { "version" => "0.0.0", }, @@ -84,6 +87,10 @@ describe Chef::RunContext do ) end + it "has a nil parent_run_context" do + expect(run_context.parent_run_context).to be_nil + end + describe "loading cookbooks for a run list" do before do diff --git a/spec/unit/run_list/versioned_recipe_list_spec.rb b/spec/unit/run_list/versioned_recipe_list_spec.rb index 209ac37fc1..9c3ecaa0dd 100644 --- a/spec/unit/run_list/versioned_recipe_list_spec.rb +++ b/spec/unit/run_list/versioned_recipe_list_spec.rb @@ -26,98 +26,165 @@ describe Chef::RunList::VersionedRecipeList do end end + let(:list) { described_class.new } + + let(:versioned_recipes) { [] } + + let(:recipes) { [] } + + before do + recipes.each { |r| list << r } + versioned_recipes.each {|r| list.add_recipe r[:name], r[:version]} + end + describe "add_recipe" do - before(:each) do - @list = Chef::RunList::VersionedRecipeList.new - @list << "apt" - @list << "god" - @list << "apache2" - end + + let(:recipes) { %w[ apt god apache2 ] } it "should append the recipe to the end of the list" do - @list.add_recipe "rails" - expect(@list).to eq(["apt", "god", "apache2", "rails"]) + list.add_recipe "rails" + expect(list).to eq(["apt", "god", "apache2", "rails"]) end it "should not duplicate entries" do - @list.add_recipe "apt" - expect(@list).to eq(["apt", "god", "apache2"]) + list.add_recipe "apt" + expect(list).to eq(["apt", "god", "apache2"]) end it "should allow you to specify a version" do - @list.add_recipe "rails", "1.0.0" - expect(@list).to eq(["apt", "god", "apache2", "rails"]) - expect(@list.with_versions).to include({:name => "rails", :version => "1.0.0"}) + list.add_recipe "rails", "1.0.0" + expect(list).to eq(["apt", "god", "apache2", "rails"]) + expect(list.with_versions).to include({:name => "rails", :version => "1.0.0"}) end it "should allow you to specify a version for a recipe that already exists" do - @list.add_recipe "apt", "1.2.3" - expect(@list).to eq(["apt", "god", "apache2"]) - expect(@list.with_versions).to include({:name => "apt", :version => "1.2.3"}) + list.add_recipe "apt", "1.2.3" + expect(list).to eq(["apt", "god", "apache2"]) + expect(list.with_versions).to include({:name => "apt", :version => "1.2.3"}) end it "should allow you to specify the same version of a recipe twice" do - @list.add_recipe "rails", "1.0.0" - @list.add_recipe "rails", "1.0.0" - expect(@list.with_versions).to include({:name => "rails", :version => "1.0.0"}) + list.add_recipe "rails", "1.0.0" + list.add_recipe "rails", "1.0.0" + expect(list.with_versions).to include({:name => "rails", :version => "1.0.0"}) end it "should allow you to spcify no version, even when a version already exists" do - @list.add_recipe "rails", "1.0.0" - @list.add_recipe "rails" - expect(@list.with_versions).to include({:name => "rails", :version => "1.0.0"}) + list.add_recipe "rails", "1.0.0" + list.add_recipe "rails" + expect(list.with_versions).to include({:name => "rails", :version => "1.0.0"}) end it "should not allow multiple versions of the same recipe" do - @list.add_recipe "rails", "1.0.0" - expect {@list.add_recipe "rails", "0.1.0"}.to raise_error Chef::Exceptions::CookbookVersionConflict + list.add_recipe "rails", "1.0.0" + expect {list.add_recipe "rails", "0.1.0"}.to raise_error Chef::Exceptions::CookbookVersionConflict end end describe "with_versions" do - before(:each) do - @recipes = [ + + let(:versioned_recipes) do + [ {:name => "apt", :version => "1.0.0"}, {:name => "god", :version => nil}, {:name => "apache2", :version => "0.0.1"} ] - @list = Chef::RunList::VersionedRecipeList.new - @recipes.each {|i| @list.add_recipe i[:name], i[:version]} end - it "should return an array of hashes with :name and :version" do - expect(@list.with_versions).to eq(@recipes) + expect(list.with_versions).to eq(versioned_recipes) end it "should retain the same order as the version-less list" do - with_versions = @list.with_versions - @list.each_with_index do |item, index| + with_versions = list.with_versions + list.each_with_index do |item, index| expect(with_versions[index][:name]).to eq(item) end end end describe "with_version_constraints" do - before(:each) do - @recipes = [ - {:name => "apt", :version => "~> 1.2.0"}, - {:name => "god", :version => nil}, - {:name => "apache2", :version => "0.0.1"} - ] - @list = Chef::RunList::VersionedRecipeList.new - @recipes.each {|i| @list.add_recipe i[:name], i[:version]} - @constraints = @recipes.map do |x| - { :name => x[:name], - :version_constraint => Chef::VersionConstraint.new(x[:version]) - } - end + + let(:versioned_recipes) do + [ + {:name => "apt", :version => "~> 1.2.0"}, + {:name => "god", :version => nil}, + {:name => "apache2", :version => "0.0.1"} + ] end + it "should return an array of hashes with :name and :version_constraint" do - @list.with_version_constraints.each do |x| - expect(x).to have_key :name - expect(x[:version_constraint]).not_to be nil + list.with_version_constraints.each_with_index do |recipe_spec, i| + + expected_recipe = versioned_recipes[i] + + expect(recipe_spec[:name]).to eq(expected_recipe[:name]) + expect(recipe_spec[:version_constraint]).to eq(Chef::VersionConstraint.new(expected_recipe[:version])) end end end + + describe "with_fully_qualified_names_and_version_constraints" do + + let(:fq_names) { list.with_fully_qualified_names_and_version_constraints } + + context "with bare cookbook names" do + + let(:recipes) { %w[ apache2 ] } + + it "gives $cookbook_name::default" do + expect(fq_names).to eq( %w[ apache2::default ] ) + end + + end + + context "with qualified recipe names but no versions" do + + let(:recipes) { %w[ mysql::server ] } + + it "returns the qualified recipe names" do + expect(fq_names).to eq( %w[ mysql::server ] ) + end + + end + + context "with unqualified names that have version constraints" do + + let(:versioned_recipes) do + [ + {:name => "apt", :version => "~> 1.2.0"}, + ] + end + + it "gives qualified names with their versions" do + expect(fq_names).to eq([ "apt::default@~> 1.2.0" ]) + end + + it "does not mutate the recipe name" do + expect(fq_names).to eq([ "apt::default@~> 1.2.0" ]) + expect(list).to eq( [ "apt" ] ) + end + + end + + context "with fully qualified names that have version constraints" do + + let(:versioned_recipes) do + [ + {:name => "apt::cacher", :version => "~> 1.2.0"}, + ] + end + + it "gives qualified names with their versions" do + expect(fq_names).to eq([ "apt::cacher@~> 1.2.0" ]) + end + + it "does not mutate the recipe name" do + expect(fq_names).to eq([ "apt::cacher@~> 1.2.0" ]) + expect(list).to eq( [ "apt::cacher" ] ) + end + + end + end + end diff --git a/spec/unit/run_list_spec.rb b/spec/unit/run_list_spec.rb index bf996de8c1..e150579431 100644 --- a/spec/unit/run_list_spec.rb +++ b/spec/unit/run_list_spec.rb @@ -307,7 +307,7 @@ describe Chef::RunList do expect(Chef::JSONCompat.to_json(@run_list)).to eq(Chef::JSONCompat.to_json(["recipe[nagios::client]", "role[production]", "recipe[apache2]"])) end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @run_list } end diff --git a/spec/unit/runner_spec.rb b/spec/unit/runner_spec.rb index 82e57e068c..b30f818da1 100644 --- a/spec/unit/runner_spec.rb +++ b/spec/unit/runner_spec.rb @@ -273,8 +273,8 @@ describe Chef::Runner do expected_message =<<-E Multiple failures occurred: -* FailureProvider::ChefClientFail occurred in delayed notification: failure_resource[explode] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort -* FailureProvider::ChefClientFail occurred in delayed notification: failure_resource[explode again] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort +* FailureProvider::ChefClientFail occurred in delayed notification: [explode] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort +* FailureProvider::ChefClientFail occurred in delayed notification: [explode again] (dynamically defined) had an error: FailureProvider::ChefClientFail: chef had an error of some sort E expect(exception.message).to eq(expected_message) diff --git a/spec/unit/user_spec.rb b/spec/unit/user_spec.rb index 57822df7e3..97cc32eb3e 100644 --- a/spec/unit/user_spec.rb +++ b/spec/unit/user_spec.rb @@ -16,6 +16,11 @@ # limitations under the License. # +# DEPRECATION NOTE +# This code only remains to support users still operating with +# Open Source Chef Server 11 and should be removed once support +# for OSC 11 ends. New development should occur in user_spec.rb. + require 'spec_helper' require 'chef/user' @@ -26,141 +31,98 @@ describe Chef::User do @user = Chef::User.new end - shared_examples_for "string fields with no contraints" do - it "should let you set the public key" do - expect(@user.send(method, "some_string")).to eq("some_string") - end - - it "should return the current public key" do - @user.send(method, "some_string") - expect(@user.send(method)).to eq("some_string") - end - - it "should throw an ArgumentError if you feed it something lame" do - expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) - end - end - - shared_examples_for "boolean fields with no constraints" do - it "should let you set the field" do - expect(@user.send(method, true)).to eq(true) - end - - it "should return the current field value" do - @user.send(method, true) - expect(@user.send(method)).to eq(true) - end - - it "should return the false value when false" do - @user.send(method, false) - expect(@user.send(method)).to eq(false) - end - - it "should throw an ArgumentError if you feed it anything but true or false" do - expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) - end - end - describe "initialize" do it "should be a Chef::User" do expect(@user).to be_a_kind_of(Chef::User) end end - describe "username" do - it "should let you set the username to a string" do - expect(@user.username("ops_master")).to eq("ops_master") + describe "name" do + it "should let you set the name to a string" do + expect(@user.name("ops_master")).to eq("ops_master") end - it "should return the current username" do - @user.username "ops_master" - expect(@user.username).to eq("ops_master") + it "should return the current name" do + @user.name "ops_master" + expect(@user.name).to eq("ops_master") end # It is not feasible to check all invalid characters. Here are a few # that we probably care about. it "should not accept invalid characters" do # capital letters - expect { @user.username "Bar" }.to raise_error(ArgumentError) + expect { @user.name "Bar" }.to raise_error(ArgumentError) # slashes - expect { @user.username "foo/bar" }.to raise_error(ArgumentError) + expect { @user.name "foo/bar" }.to raise_error(ArgumentError) # ? - expect { @user.username "foo?" }.to raise_error(ArgumentError) + expect { @user.name "foo?" }.to raise_error(ArgumentError) # & - expect { @user.username "foo&" }.to raise_error(ArgumentError) + expect { @user.name "foo&" }.to raise_error(ArgumentError) end it "should not accept spaces" do - expect { @user.username "ops master" }.to raise_error(ArgumentError) + expect { @user.name "ops master" }.to raise_error(ArgumentError) end it "should throw an ArgumentError if you feed it anything but a string" do - expect { @user.username Hash.new }.to raise_error(ArgumentError) + expect { @user.name Hash.new }.to raise_error(ArgumentError) end end - describe "boolean fields" do - describe "create_key" do - it_should_behave_like "boolean fields with no constraints" do - let(:method) { :create_key } - end + describe "admin" do + it "should let you set the admin bit" do + expect(@user.admin(true)).to eq(true) end - end - describe "string fields" do - describe "public_key" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :public_key } - end + it "should return the current admin value" do + @user.admin true + expect(@user.admin).to eq(true) end - describe "private_key" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :private_key } - end + it "should default to false" do + expect(@user.admin).to eq(false) end - describe "display_name" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :display_name } - end + it "should throw an ArgumentError if you feed it anything but true or false" do + expect { @user.name Hash.new }.to raise_error(ArgumentError) end + end - describe "first_name" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :first_name } - end + describe "public_key" do + it "should let you set the public key" do + expect(@user.public_key("super public")).to eq("super public") end - describe "middle_name" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :middle_name } - end + it "should return the current public key" do + @user.public_key("super public") + expect(@user.public_key).to eq("super public") end - describe "last_name" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :last_name } - end + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.public_key Hash.new }.to raise_error(ArgumentError) end + end - describe "email" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :email } - end + describe "private_key" do + it "should let you set the private key" do + expect(@user.private_key("super private")).to eq("super private") end - describe "password" do - it_should_behave_like "string fields with no contraints" do - let(:method) { :password } - end + it "should return the private key" do + @user.private_key("super private") + expect(@user.private_key).to eq("super private") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.private_key Hash.new }.to raise_error(ArgumentError) end end describe "when serializing to JSON" do before(:each) do - @user.username("black") + @user.name("black") + @user.public_key("crowes") @json = @user.to_json end @@ -168,62 +130,16 @@ describe Chef::User do expect(@json).to match(/^\{.+\}$/) end - it "includes the username value" do - expect(@json).to include(%q{"username":"black"}) - end - - it "includes the display name when present" do - @user.display_name("get_displayed") - expect(@user.to_json).to include(%{"display_name":"get_displayed"}) - end - - it "does not include the display name if not present" do - expect(@json).not_to include("display_name") - end - - it "includes the first name when present" do - @user.first_name("char") - expect(@user.to_json).to include(%{"first_name":"char"}) - end - - it "does not include the first name if not present" do - expect(@json).not_to include("first_name") - end - - it "includes the middle name when present" do - @user.middle_name("man") - expect(@user.to_json).to include(%{"middle_name":"man"}) - end - - it "does not include the middle name if not present" do - expect(@json).not_to include("middle_name") - end - - it "includes the last name when present" do - @user.last_name("der") - expect(@user.to_json).to include(%{"last_name":"der"}) - end - - it "does not include the last name if not present" do - expect(@json).not_to include("last_name") - end - - it "includes the email when present" do - @user.email("charmander@pokemon.poke") - expect(@user.to_json).to include(%{"email":"charmander@pokemon.poke"}) - end - - it "does not include the email if not present" do - expect(@json).not_to include("email") + it "includes the name value" do + expect(@json).to include(%q{"name":"black"}) end - it "includes the public key when present" do - @user.public_key("crowes") - expect(@user.to_json).to include(%{"public_key":"crowes"}) + it "includes the public key value" do + expect(@json).to include(%{"public_key":"crowes"}) end - it "does not include the public key if not present" do - expect(@json).not_to include("public_key") + it "includes the 'admin' flag" do + expect(@json).to include(%q{"admin":false}) end it "includes the private key when present" do @@ -244,25 +160,18 @@ describe Chef::User do expect(@json).not_to include("password") end - include_examples "to_json equalivent to Chef::JSONCompat.to_json" do + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do let(:jsonable) { @user } end end describe "when deserializing from JSON" do before(:each) do - user = { - "username" => "mr_spinks", - "display_name" => "displayed", - "first_name" => "char", - "middle_name" => "man", - "last_name" => "der", - "email" => "charmander@pokemon.poke", - "password" => "password", + user = { "name" => "mr_spinks", "public_key" => "turtles", "private_key" => "pandas", - "create_key" => false - } + "password" => "password", + "admin" => true } @user = Chef::User.from_json(Chef::JSONCompat.to_json(user)) end @@ -270,277 +179,34 @@ describe Chef::User do expect(@user).to be_a_kind_of(Chef::User) end - it "preserves the username" do - expect(@user.username).to eq("mr_spinks") - end - - it "preserves the display name if present" do - expect(@user.display_name).to eq("displayed") + it "preserves the name" do + expect(@user.name).to eq("mr_spinks") end - it "preserves the first name if present" do - expect(@user.first_name).to eq("char") - end - - it "preserves the middle name if present" do - expect(@user.middle_name).to eq("man") + it "preserves the public key" do + expect(@user.public_key).to eq("turtles") end - it "preserves the last name if present" do - expect(@user.last_name).to eq("der") + it "preserves the admin status" do + expect(@user.admin).to be_truthy end - it "preserves the email if present" do - expect(@user.email).to eq("charmander@pokemon.poke") + it "includes the private key if present" do + expect(@user.private_key).to eq("pandas") end it "includes the password if present" do expect(@user.password).to eq("password") end - it "preserves the public key if present" do - expect(@user.public_key).to eq("turtles") - end - - it "includes the private key if present" do - expect(@user.private_key).to eq("pandas") - end - - it "includes the create key status if not nil" do - expect(@user.create_key).to be_falsey - end end - describe "Versioned API Interactions" do - let(:response_406) { OpenStruct.new(:code => '406') } - let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } - - before (:each) do - @user = Chef::User.new - allow(@user).to receive(:chef_root_rest_v0).and_return(double('chef rest root v0 object')) - allow(@user).to receive(:chef_root_rest_v1).and_return(double('chef rest root v1 object')) - end - - describe "update" do - before do - # populate all fields that are valid between V0 and V1 - @user.username "some_username" - @user.display_name "some_display_name" - @user.first_name "some_first_name" - @user.middle_name "some_middle_name" - @user.last_name "some_last_name" - @user.email "some_email" - @user.password "some_password" - end - - let(:payload) { - { - :username => "some_username", - :display_name => "some_display_name", - :first_name => "some_first_name", - :middle_name => "some_middle_name", - :last_name => "some_last_name", - :email => "some_email", - :password => "some_password" - } - } - - context "when server API V1 is valid on the Chef Server receiving the request" do - context "when the user submits valid data" do - it "properly updates the user" do - expect(@user.chef_root_rest_v1).to receive(:put).with("users/some_username", payload).and_return({}) - @user.update - end - end - end - - context "when server API V1 is not valid on the Chef Server receiving the request" do - let(:payload) { - { - :username => "some_username", - :display_name => "some_display_name", - :first_name => "some_first_name", - :middle_name => "some_middle_name", - :last_name => "some_last_name", - :email => "some_email", - :password => "some_password", - :public_key => "some_public_key" - } - } - - before do - @user.public_key "some_public_key" - allow(@user.chef_root_rest_v1).to receive(:put) - end - - context "when the server returns a 400" do - let(:response_400) { OpenStruct.new(:code => '400') } - let(:exception_400) { Net::HTTPServerException.new("400 Bad Request", response_400) } - - context "when the 400 was due to public / private key fields no longer being supported" do - let(:response_body_400) { '{"error":["Since Server API v1, all keys must be updated via the keys endpoint. "]}' } - - before do - allow(response_400).to receive(:body).and_return(response_body_400) - allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) - end - - it "proceeds with the V0 PUT since it can handle public / private key fields" do - expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) - @user.update - end - - it "does not call server_client_api_version_intersection, since we know to proceed with V0 in this case" do - expect(@user).to_not receive(:server_client_api_version_intersection) - allow(@user.chef_root_rest_v0).to receive(:put).and_return({}) - @user.update - end - end # when the 400 was due to public / private key fields - - context "when the 400 was NOT due to public / private key fields no longer being supported" do - let(:response_body_400) { '{"error":["Some other error. "]}' } - - before do - allow(response_400).to receive(:body).and_return(response_body_400) - allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) - end - - it "will not proceed with the V0 PUT since the original bad request was not key related" do - expect(@user.chef_root_rest_v0).to_not receive(:put).with("users/some_username", payload) - expect { @user.update }.to raise_error(exception_400) - end - - it "raises the original error" do - expect { @user.update }.to raise_error(exception_400) - end - - end - end # when the server returns a 400 - - context "when the server returns a 406" do - # from spec/support/shared/unit/api_versioning.rb - it_should_behave_like "version handling" do - let(:object) { @user } - let(:method) { :update } - let(:http_verb) { :put } - let(:rest_v1) { @user.chef_root_rest_v1 } - end - - context "when the server supports API V0" do - before do - allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) - allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_406) - end - - it "properly updates the user" do - expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) - @user.update - end - end # when the server supports API V0 - end # when the server returns a 406 - - end # when server API V1 is not valid on the Chef Server receiving the request - end # update - - describe "create" do - let(:payload) { - { - :username => "some_username", - :display_name => "some_display_name", - :first_name => "some_first_name", - :last_name => "some_last_name", - :email => "some_email", - :password => "some_password" - } - } - before do - @user.username "some_username" - @user.display_name "some_display_name" - @user.first_name "some_first_name" - @user.last_name "some_last_name" - @user.email "some_email" - @user.password "some_password" - end - - # from spec/support/shared/unit/user_and_client_shared.rb - it_should_behave_like "user or client create" do - let(:object) { @user } - let(:error) { Chef::Exceptions::InvalidUserAttribute } - let(:rest_v0) { @user.chef_root_rest_v0 } - let(:rest_v1) { @user.chef_root_rest_v1 } - let(:url) { "users" } - end - - context "when handling API V1" do - it "creates a new user via the API with a middle_name when it exists" do - @user.middle_name "some_middle_name" - expect(@user.chef_root_rest_v1).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) - @user.create - end - end # when server API V1 is valid on the Chef Server receiving the request - - context "when API V1 is not supported by the server" do - # from spec/support/shared/unit/api_versioning.rb - it_should_behave_like "version handling" do - let(:object) { @user } - let(:method) { :create } - let(:http_verb) { :post } - let(:rest_v1) { @user.chef_root_rest_v1 } - end - end - - context "when handling API V0" do - before do - allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) - allow(@user.chef_root_rest_v1).to receive(:post).and_raise(exception_406) - end - - it "creates a new user via the API with a middle_name when it exists" do - @user.middle_name "some_middle_name" - expect(@user.chef_root_rest_v0).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) - @user.create - end - end # when server API V1 is not valid on the Chef Server receiving the request - - end # create - - # DEPRECATION - # This can be removed after API V0 support is gone - describe "reregister" do - let(:payload) { - { - "username" => "some_username", - } - } - - before do - @user.username "some_username" - end - - context "when server API V0 is valid on the Chef Server receiving the request" do - it "creates a new object via the API" do - expect(@user.chef_root_rest_v0).to receive(:put).with("users/#{@user.username}", payload.merge({"private_key" => true})).and_return({}) - @user.reregister - end - end # when server API V0 is valid on the Chef Server receiving the request - - context "when server API V0 is not supported by the Chef Server" do - # from spec/support/shared/unit/api_versioning.rb - it_should_behave_like "user and client reregister" do - let(:object) { @user } - let(:rest_v0) { @user.chef_root_rest_v0 } - end - end # when server API V0 is not supported by the Chef Server - end # reregister - - end # Versioned API Interactions - describe "API Interactions" do before (:each) do @user = Chef::User.new - @user.username "foobar" - @http_client = double("Chef::REST mock") - allow(Chef::REST).to receive(:new).and_return(@http_client) + @user.name "foobar" + @http_client = double("Chef::ServerAPI mock") + allow(Chef::ServerAPI).to receive(:new).and_return(@http_client) end describe "list" do @@ -552,6 +218,16 @@ describe Chef::User do @osc_inflated_response = { "admin" => @user } end + it "lists all clients on an OSC server" do + allow(@http_client).to receive(:get).with("users").and_return(@osc_response) + expect(Chef::User.list).to eq(@osc_response) + end + + it "inflate all clients on an OSC server" do + allow(@http_client).to receive(:get).with("users").and_return(@osc_response) + expect(Chef::User.list(true)).to eq(@osc_inflated_response) + end + it "lists all clients on an OHC/OPC server" do allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) # We expect that Chef::User.list will give a consistent response @@ -565,15 +241,31 @@ describe Chef::User do end end + describe "create" do + it "creates a new user via the API" do + @user.password "password" + expect(@http_client).to receive(:post).with("users", {:name => "foobar", :admin => false, :password => "password"}).and_return({}) + @user.create + end + end + describe "read" do it "loads a named user from the API" do - expect(@http_client).to receive(:get).with("users/foobar").and_return({"username" => "foobar", "admin" => true, "public_key" => "pubkey"}) + expect(@http_client).to receive(:get).with("users/foobar").and_return({"name" => "foobar", "admin" => true, "public_key" => "pubkey"}) user = Chef::User.load("foobar") - expect(user.username).to eq("foobar") + expect(user.name).to eq("foobar") + expect(user.admin).to eq(true) expect(user.public_key).to eq("pubkey") end end + describe "update" do + it "updates an existing user on via the API" do + expect(@http_client).to receive(:put).with("users/foobar", {:name => "foobar", :admin => false}).and_return({}) + @user.update + end + end + describe "destroy" do it "deletes the specified user via the API" do expect(@http_client).to receive(:delete).with("users/foobar") diff --git a/spec/unit/user_v1_spec.rb b/spec/unit/user_v1_spec.rb new file mode 100644 index 0000000000..8fd370a010 --- /dev/null +++ b/spec/unit/user_v1_spec.rb @@ -0,0 +1,584 @@ +# +# Author:: Steven Danna (steve@opscode.com) +# Copyright:: Copyright (c) 2012 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/user_v1' +require 'tempfile' + +describe Chef::UserV1 do + before(:each) do + @user = Chef::UserV1.new + end + + shared_examples_for "string fields with no contraints" do + it "should let you set the public key" do + expect(@user.send(method, "some_string")).to eq("some_string") + end + + it "should return the current public key" do + @user.send(method, "some_string") + expect(@user.send(method)).to eq("some_string") + end + + it "should throw an ArgumentError if you feed it something lame" do + expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) + end + end + + shared_examples_for "boolean fields with no constraints" do + it "should let you set the field" do + expect(@user.send(method, true)).to eq(true) + end + + it "should return the current field value" do + @user.send(method, true) + expect(@user.send(method)).to eq(true) + end + + it "should return the false value when false" do + @user.send(method, false) + expect(@user.send(method)).to eq(false) + end + + it "should throw an ArgumentError if you feed it anything but true or false" do + expect { @user.send(method, Hash.new) }.to raise_error(ArgumentError) + end + end + + describe "initialize" do + it "should be a Chef::UserV1" do + expect(@user).to be_a_kind_of(Chef::UserV1) + end + end + + describe "username" do + it "should let you set the username to a string" do + expect(@user.username("ops_master")).to eq("ops_master") + end + + it "should return the current username" do + @user.username "ops_master" + expect(@user.username).to eq("ops_master") + end + + # It is not feasible to check all invalid characters. Here are a few + # that we probably care about. + it "should not accept invalid characters" do + # capital letters + expect { @user.username "Bar" }.to raise_error(ArgumentError) + # slashes + expect { @user.username "foo/bar" }.to raise_error(ArgumentError) + # ? + expect { @user.username "foo?" }.to raise_error(ArgumentError) + # & + expect { @user.username "foo&" }.to raise_error(ArgumentError) + end + + + it "should not accept spaces" do + expect { @user.username "ops master" }.to raise_error(ArgumentError) + end + + it "should throw an ArgumentError if you feed it anything but a string" do + expect { @user.username Hash.new }.to raise_error(ArgumentError) + end + end + + describe "boolean fields" do + describe "create_key" do + it_should_behave_like "boolean fields with no constraints" do + let(:method) { :create_key } + end + end + end + + describe "string fields" do + describe "public_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :public_key } + end + end + + describe "private_key" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :private_key } + end + end + + describe "display_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :display_name } + end + end + + describe "first_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :first_name } + end + end + + describe "middle_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :middle_name } + end + end + + describe "last_name" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :last_name } + end + end + + describe "email" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :email } + end + end + + describe "password" do + it_should_behave_like "string fields with no contraints" do + let(:method) { :password } + end + end + end + + describe "when serializing to JSON" do + before(:each) do + @user.username("black") + @json = @user.to_json + end + + it "serializes as a JSON object" do + expect(@json).to match(/^\{.+\}$/) + end + + it "includes the username value" do + expect(@json).to include(%q{"username":"black"}) + end + + it "includes the display name when present" do + @user.display_name("get_displayed") + expect(@user.to_json).to include(%{"display_name":"get_displayed"}) + end + + it "does not include the display name if not present" do + expect(@json).not_to include("display_name") + end + + it "includes the first name when present" do + @user.first_name("char") + expect(@user.to_json).to include(%{"first_name":"char"}) + end + + it "does not include the first name if not present" do + expect(@json).not_to include("first_name") + end + + it "includes the middle name when present" do + @user.middle_name("man") + expect(@user.to_json).to include(%{"middle_name":"man"}) + end + + it "does not include the middle name if not present" do + expect(@json).not_to include("middle_name") + end + + it "includes the last name when present" do + @user.last_name("der") + expect(@user.to_json).to include(%{"last_name":"der"}) + end + + it "does not include the last name if not present" do + expect(@json).not_to include("last_name") + end + + it "includes the email when present" do + @user.email("charmander@pokemon.poke") + expect(@user.to_json).to include(%{"email":"charmander@pokemon.poke"}) + end + + it "does not include the email if not present" do + expect(@json).not_to include("email") + end + + it "includes the public key when present" do + @user.public_key("crowes") + expect(@user.to_json).to include(%{"public_key":"crowes"}) + end + + it "does not include the public key if not present" do + expect(@json).not_to include("public_key") + end + + it "includes the private key when present" do + @user.private_key("monkeypants") + expect(@user.to_json).to include(%q{"private_key":"monkeypants"}) + end + + it "does not include the private key if not present" do + expect(@json).not_to include("private_key") + end + + it "includes the password if present" do + @user.password "password" + expect(@user.to_json).to include(%q{"password":"password"}) + end + + it "does not include the password if not present" do + expect(@json).not_to include("password") + end + + include_examples "to_json equivalent to Chef::JSONCompat.to_json" do + let(:jsonable) { @user } + end + end + + describe "when deserializing from JSON" do + before(:each) do + user = { + "username" => "mr_spinks", + "display_name" => "displayed", + "first_name" => "char", + "middle_name" => "man", + "last_name" => "der", + "email" => "charmander@pokemon.poke", + "password" => "password", + "public_key" => "turtles", + "private_key" => "pandas", + "create_key" => false + } + @user = Chef::UserV1.from_json(Chef::JSONCompat.to_json(user)) + end + + it "should deserialize to a Chef::UserV1 object" do + expect(@user).to be_a_kind_of(Chef::UserV1) + end + + it "preserves the username" do + expect(@user.username).to eq("mr_spinks") + end + + it "preserves the display name if present" do + expect(@user.display_name).to eq("displayed") + end + + it "preserves the first name if present" do + expect(@user.first_name).to eq("char") + end + + it "preserves the middle name if present" do + expect(@user.middle_name).to eq("man") + end + + it "preserves the last name if present" do + expect(@user.last_name).to eq("der") + end + + it "preserves the email if present" do + expect(@user.email).to eq("charmander@pokemon.poke") + end + + it "includes the password if present" do + expect(@user.password).to eq("password") + end + + it "preserves the public key if present" do + expect(@user.public_key).to eq("turtles") + end + + it "includes the private key if present" do + expect(@user.private_key).to eq("pandas") + end + + it "includes the create key status if not nil" do + expect(@user.create_key).to be_falsey + end + end + + describe "Versioned API Interactions" do + let(:response_406) { OpenStruct.new(:code => '406') } + let(:exception_406) { Net::HTTPServerException.new("406 Not Acceptable", response_406) } + + before (:each) do + @user = Chef::UserV1.new + allow(@user).to receive(:chef_root_rest_v0).and_return(double('chef rest root v0 object')) + allow(@user).to receive(:chef_root_rest_v1).and_return(double('chef rest root v1 object')) + end + + describe "update" do + before do + # populate all fields that are valid between V0 and V1 + @user.username "some_username" + @user.display_name "some_display_name" + @user.first_name "some_first_name" + @user.middle_name "some_middle_name" + @user.last_name "some_last_name" + @user.email "some_email" + @user.password "some_password" + end + + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :middle_name => "some_middle_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password" + } + } + + context "when server API V1 is valid on the Chef Server receiving the request" do + context "when the user submits valid data" do + it "properly updates the user" do + expect(@user.chef_root_rest_v1).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + end + end + + context "when server API V1 is not valid on the Chef Server receiving the request" do + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :middle_name => "some_middle_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password", + :public_key => "some_public_key" + } + } + + before do + @user.public_key "some_public_key" + allow(@user.chef_root_rest_v1).to receive(:put) + end + + context "when the server returns a 400" do + let(:response_400) { OpenStruct.new(:code => '400') } + let(:exception_400) { Net::HTTPServerException.new("400 Bad Request", response_400) } + + context "when the 400 was due to public / private key fields no longer being supported" do + let(:response_body_400) { '{"error":["Since Server API v1, all keys must be updated via the keys endpoint. "]}' } + + before do + allow(response_400).to receive(:body).and_return(response_body_400) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) + end + + it "proceeds with the V0 PUT since it can handle public / private key fields" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + + it "does not call server_client_api_version_intersection, since we know to proceed with V0 in this case" do + expect(@user).to_not receive(:server_client_api_version_intersection) + allow(@user.chef_root_rest_v0).to receive(:put).and_return({}) + @user.update + end + end # when the 400 was due to public / private key fields + + context "when the 400 was NOT due to public / private key fields no longer being supported" do + let(:response_body_400) { '{"error":["Some other error. "]}' } + + before do + allow(response_400).to receive(:body).and_return(response_body_400) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_400) + end + + it "will not proceed with the V0 PUT since the original bad request was not key related" do + expect(@user.chef_root_rest_v0).to_not receive(:put).with("users/some_username", payload) + expect { @user.update }.to raise_error(exception_400) + end + + it "raises the original error" do + expect { @user.update }.to raise_error(exception_400) + end + + end + end # when the server returns a 400 + + context "when the server returns a 406" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @user } + let(:method) { :update } + let(:http_verb) { :put } + let(:rest_v1) { @user.chef_root_rest_v1 } + end + + context "when the server supports API V0" do + before do + allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) + allow(@user.chef_root_rest_v1).to receive(:put).and_raise(exception_406) + end + + it "properly updates the user" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/some_username", payload).and_return({}) + @user.update + end + end # when the server supports API V0 + end # when the server returns a 406 + + end # when server API V1 is not valid on the Chef Server receiving the request + end # update + + describe "create" do + let(:payload) { + { + :username => "some_username", + :display_name => "some_display_name", + :first_name => "some_first_name", + :last_name => "some_last_name", + :email => "some_email", + :password => "some_password" + } + } + before do + @user.username "some_username" + @user.display_name "some_display_name" + @user.first_name "some_first_name" + @user.last_name "some_last_name" + @user.email "some_email" + @user.password "some_password" + end + + # from spec/support/shared/unit/user_and_client_shared.rb + it_should_behave_like "user or client create" do + let(:object) { @user } + let(:error) { Chef::Exceptions::InvalidUserAttribute } + let(:rest_v0) { @user.chef_root_rest_v0 } + let(:rest_v1) { @user.chef_root_rest_v1 } + let(:url) { "users" } + end + + context "when handling API V1" do + it "creates a new user via the API with a middle_name when it exists" do + @user.middle_name "some_middle_name" + expect(@user.chef_root_rest_v1).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) + @user.create + end + end # when server API V1 is valid on the Chef Server receiving the request + + context "when API V1 is not supported by the server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "version handling" do + let(:object) { @user } + let(:method) { :create } + let(:http_verb) { :post } + let(:rest_v1) { @user.chef_root_rest_v1 } + end + end + + context "when handling API V0" do + before do + allow(@user).to receive(:server_client_api_version_intersection).and_return([0]) + allow(@user.chef_root_rest_v1).to receive(:post).and_raise(exception_406) + end + + it "creates a new user via the API with a middle_name when it exists" do + @user.middle_name "some_middle_name" + expect(@user.chef_root_rest_v0).to receive(:post).with("users", payload.merge({:middle_name => "some_middle_name"})).and_return({}) + @user.create + end + end # when server API V1 is not valid on the Chef Server receiving the request + + end # create + + # DEPRECATION + # This can be removed after API V0 support is gone + describe "reregister" do + let(:payload) { + { + "username" => "some_username", + } + } + + before do + @user.username "some_username" + end + + context "when server API V0 is valid on the Chef Server receiving the request" do + it "creates a new object via the API" do + expect(@user.chef_root_rest_v0).to receive(:put).with("users/#{@user.username}", payload.merge({"private_key" => true})).and_return({}) + @user.reregister + end + end # when server API V0 is valid on the Chef Server receiving the request + + context "when server API V0 is not supported by the Chef Server" do + # from spec/support/shared/unit/api_versioning.rb + it_should_behave_like "user and client reregister" do + let(:object) { @user } + let(:rest_v0) { @user.chef_root_rest_v0 } + end + end # when server API V0 is not supported by the Chef Server + end # reregister + + end # Versioned API Interactions + + describe "API Interactions" do + before (:each) do + @user = Chef::UserV1.new + @user.username "foobar" + @http_client = double("Chef::REST mock") + allow(Chef::REST).to receive(:new).and_return(@http_client) + end + + describe "list" do + before(:each) do + Chef::Config[:chef_server_url] = "http://www.example.com" + @osc_response = { "admin" => "http://www.example.com/users/admin"} + @ohc_response = [ { "user" => { "username" => "admin" }} ] + allow(Chef::UserV1).to receive(:load).with("admin").and_return(@user) + @osc_inflated_response = { "admin" => @user } + end + + it "lists all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) + # We expect that Chef::UserV1.list will give a consistent response + # so OHC API responses should be transformed to OSC-style output. + expect(Chef::UserV1.list).to eq(@osc_response) + end + + it "inflate all clients on an OHC/OPC server" do + allow(@http_client).to receive(:get).with("users").and_return(@ohc_response) + expect(Chef::UserV1.list(true)).to eq(@osc_inflated_response) + end + end + + describe "read" do + it "loads a named user from the API" do + expect(@http_client).to receive(:get).with("users/foobar").and_return({"username" => "foobar", "admin" => true, "public_key" => "pubkey"}) + user = Chef::UserV1.load("foobar") + expect(user.username).to eq("foobar") + expect(user.public_key).to eq("pubkey") + end + end + + describe "destroy" do + it "deletes the specified user via the API" do + expect(@http_client).to receive(:delete).with("users/foobar") + @user.destroy + end + end + end +end |