├── .gitignore ├── .rubocop.yml ├── .travis.yml ├── .yardopts ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── gemfiles ├── chef-12.16.gemfile ├── chef-12.17.gemfile ├── chef-12.18.gemfile ├── chef-12.19.gemfile ├── chef-12.20.gemfile ├── chef-12.21.gemfile ├── chef-12.gemfile ├── chef-13.0.gemfile ├── chef-13.1.gemfile ├── chef-13.10.gemfile ├── chef-13.2.gemfile ├── chef-13.3.gemfile ├── chef-13.4.gemfile ├── chef-13.5.gemfile ├── chef-13.6.gemfile ├── chef-13.7.gemfile ├── chef-13.8.gemfile ├── chef-13.9.gemfile ├── chef-13.gemfile ├── chef-14.0.gemfile ├── chef-14.1.gemfile ├── chef-14.2.gemfile ├── chef-14.3.gemfile ├── chef-14.4.gemfile ├── chef-14.gemfile └── master.gemfile ├── halite.gemspec ├── lib ├── berkshelf │ ├── halite.rb │ └── locations │ │ └── gem.rb ├── halite.rb └── halite │ ├── berkshelf │ ├── helper.rb │ └── source.rb │ ├── converter.rb │ ├── converter │ ├── chef.rb │ ├── libraries.rb │ ├── metadata.rb │ └── misc.rb │ ├── dependencies.rb │ ├── error.rb │ ├── gem.rb │ ├── helper_base.rb │ ├── rake_helper.rb │ ├── rake_tasks.rb │ ├── spec_helper.rb │ ├── spec_helper │ ├── empty │ │ └── README.md │ ├── patcher.rb │ └── runner.rb │ └── version.rb └── spec ├── converter ├── chef_spec.rb ├── libraries_spec.rb ├── metadata_spec.rb └── misc_spec.rb ├── converter_spec.rb ├── dependencies_spec.rb ├── example_resources ├── custom.rb ├── poise.rb └── simple.rb ├── fixtures ├── cookbooks │ ├── test1 │ │ ├── files │ │ │ └── halite_gem │ │ │ │ ├── test1.rb │ │ │ │ └── test1 │ │ │ │ └── version.rb │ │ ├── libraries │ │ │ └── default.rb │ │ └── metadata.rb │ ├── test2 │ │ ├── attributes.rb │ │ ├── files │ │ │ └── halite_gem │ │ │ │ ├── test2.rb │ │ │ │ └── test2 │ │ │ │ ├── resource.rb │ │ │ │ └── version.rb │ │ ├── libraries │ │ │ └── default.rb │ │ ├── metadata.rb │ │ ├── recipes │ │ │ └── default.rb │ │ └── templates │ │ │ └── default │ │ │ └── conf.erb │ ├── test3 │ │ ├── files │ │ │ └── halite_gem │ │ │ │ ├── test3.rb │ │ │ │ └── test3 │ │ │ │ ├── dsl.rb │ │ │ │ └── version.rb │ │ ├── libraries │ │ │ └── default.rb │ │ ├── metadata.rb │ │ └── recipes │ │ │ └── default.rb │ └── test4 │ │ ├── files │ │ └── halite_gem │ │ │ ├── test4.rb │ │ │ └── test4 │ │ │ └── version.rb │ │ ├── libraries │ │ └── default.rb │ │ └── metadata.rb └── gems │ ├── test1 │ ├── Rakefile │ ├── lib │ │ ├── test1.rb │ │ └── test1 │ │ │ └── version.rb │ └── test1.gemspec │ ├── test2 │ ├── Rakefile │ ├── chef │ │ ├── attributes.rb │ │ ├── recipes │ │ │ └── default.rb │ │ └── templates │ │ │ └── default │ │ │ └── conf.erb │ ├── lib │ │ ├── test2.rb │ │ └── test2 │ │ │ ├── resource.rb │ │ │ └── version.rb │ └── test2.gemspec │ ├── test3 │ ├── Rakefile │ ├── chef │ │ └── recipes │ │ │ └── default.rb │ ├── lib │ │ ├── test3.rb │ │ └── test3 │ │ │ ├── dsl.rb │ │ │ └── version.rb │ └── test3.gemspec │ └── test4 │ ├── Rakefile │ ├── lib │ ├── test4.rb │ └── test4 │ │ └── version.rb │ └── test4.gemspec ├── gem_spec.rb ├── halite_spec.rb ├── integration_spec.rb ├── runner_spec.rb ├── spec_helper.rb └── spec_helper_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | gemfiles/*.lock 3 | coverage/ 4 | **/pkg 5 | .yardoc/ 6 | doc/ 7 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Cop supports --auto-correct. 18 | # Configuration parameters: AlignWith, SupportedStyles, AutoCorrect. 19 | Lint/EndAlignment: 20 | AlignWith: variable 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | cache: bundler 3 | language: ruby 4 | matrix: 5 | include: 6 | - rvm: 2.3.1 7 | gemfile: gemfiles/chef-12.gemfile 8 | - rvm: 2.4.3 9 | gemfile: gemfiles/chef-13.gemfile 10 | - rvm: 2.5.1 11 | gemfile: gemfiles/chef-14.gemfile 12 | - rvm: 2.5.1 13 | gemfile: gemfiles/master.gemfile 14 | - rvm: 2.3.1 15 | gemfile: gemfiles/chef-12.16.gemfile 16 | - rvm: 2.3.1 17 | gemfile: gemfiles/chef-12.17.gemfile 18 | - rvm: 2.3.1 19 | gemfile: gemfiles/chef-12.18.gemfile 20 | - rvm: 2.3.1 21 | gemfile: gemfiles/chef-12.19.gemfile 22 | - rvm: 2.3.1 23 | gemfile: gemfiles/chef-12.20.gemfile 24 | - rvm: 2.3.1 25 | gemfile: gemfiles/chef-12.21.gemfile 26 | - rvm: 2.4.1 27 | gemfile: gemfiles/chef-13.0.gemfile 28 | - rvm: 2.4.1 29 | gemfile: gemfiles/chef-13.1.gemfile 30 | - rvm: 2.4.1 31 | gemfile: gemfiles/chef-13.2.gemfile 32 | - rvm: 2.4.1 33 | gemfile: gemfiles/chef-13.3.gemfile 34 | - rvm: 2.4.2 35 | gemfile: gemfiles/chef-13.4.gemfile 36 | - rvm: 2.4.2 37 | gemfile: gemfiles/chef-13.5.gemfile 38 | - rvm: 2.4.2 39 | gemfile: gemfiles/chef-13.6.gemfile 40 | - rvm: 2.4.3 41 | gemfile: gemfiles/chef-13.7.gemfile 42 | - rvm: 2.4.3 43 | gemfile: gemfiles/chef-13.8.gemfile 44 | - rvm: 2.4.3 45 | gemfile: gemfiles/chef-13.9.gemfile 46 | - rvm: 2.4.3 47 | gemfile: gemfiles/chef-13.10.gemfile 48 | - rvm: 2.5.1 49 | gemfile: gemfiles/chef-14.0.gemfile 50 | - rvm: 2.5.1 51 | gemfile: gemfiles/chef-14.1.gemfile 52 | - rvm: 2.5.1 53 | gemfile: gemfiles/chef-14.2.gemfile 54 | - rvm: 2.5.1 55 | gemfile: gemfiles/chef-14.3.gemfile 56 | - rvm: 2.5.1 57 | gemfile: gemfiles/chef-14.4.gemfile 58 | env: 59 | global: 60 | - USE_SYSTEM_GECODE=true 61 | - secure: bNxYOvi85lnPIPO1qR6NnhfqlREfMXSjmd+xCH01e/gg3x00D+3uyQIotQtglgHRItVIR36GqX70q2g/reudxBsCjfUhH2vDgbtmrWe1ow78rt1xZRlFMQs7JfBpfWKkox5i9mrdzZDekZACA0HUMI1pRM0MYQmkuol/oZw5H4A= 62 | - secure: ecX52GLF7IHCiaPEDzPtztiEmu2mNPfyyPrM7aaNsufat7DdsZqPP9LKpCTOoJC+Jgz9sdbQ6Hf61CeZAsrqZ8sKKdiXUoBUXdcBhvuPdpIbfwq97hjOoqQV7rLSD4EW58fipJU8Rl5P1ijFJizHIyxNB+VWBow3niIMGjIEUSE= 63 | before_install: 64 | - if [[ $BUNDLE_GEMFILE == *master.gemfile ]]; then gem update --system; fi 65 | - gem --version 66 | - gem install bundler -v 1.16.2 67 | - bundle --version 68 | - bundle config --local path ${BUNDLE_PATH:-vendor/bundle} 69 | install: bundle update --jobs=3 --retry=3 70 | script: bundle exec rake integration 71 | 72 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | --plugin classmethods 2 | --embed-mixin ClassMethods 3 | --hide-api private 4 | --markup markdown 5 | --hide-void-return 6 | --tag provides:Provides 7 | --tag action:Actions 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Halite Changelog 2 | 3 | ## v1.8.2 4 | 5 | * Update for newer ChefSpec. 6 | 7 | ## v1.8.1 8 | 9 | * Fix compatibility with new Chef API for unregistering resources. 10 | 11 | ## v1.8.0 12 | 13 | * Give recipes created using the block helper (`recipe do ... end`) a name when 14 | possible. 15 | * Fix support for the Berkshelf plugin on Berks 6.1 and above. 16 | 17 | ## v1.7.0 18 | 19 | * Allow specifying which platforms a cookbook supports via gem metadata: 20 | `spec.metadata['platforms'] = 'ubuntu centos'`. 21 | * Support for automatic ChefSpec matchers in the future. 22 | 23 | ## v1.6.0 24 | 25 | * Chef 13 compatibility. 26 | * Structural supports for gathering development dependencies for cookbook gems. 27 | 28 | ## v1.5.0 29 | 30 | * Set the default spec platform and version even when using the Halite runner 31 | API directly. 32 | * Allow setting the chef_version constraints on a cookbook through a normal 33 | gem dependency on the `chef` gem. 34 | * The default chef_version constraint has been changed from `~> 12.0` to `>= 12` 35 | to allow for better interaction with the upcoming Chef 13 release. 36 | * Bump Stove dependency from 4.x to 5.x. This removes Ruby 2.0 compat. Sorry. 37 | * No longer testing on Ruby 2.2, which happens to be entering security-maintenance 38 | mode today. Don't use it. 39 | 40 | ## v1.4.0 41 | 42 | * Set a default platform and version in ChefSpec because Fauxhai is trying to 43 | deprecate the `chefspec` platform. This may break some tests that relied on 44 | the nil defaults. 45 | 46 | ## v1.3.0 47 | 48 | * Include extended metadata with stove pushes. 49 | * Fix `uninitialized constant Bundler::RemoteSpecification::MatchPlatform` when 50 | using the Berkshelf extension with ChefDK. 51 | 52 | ## v1.2.1 53 | 54 | * Compatibility with Foodcritic 6.0. `issues_url` will be added to the generated 55 | `metadata.rb`. This can be set via `metadata['issues_url']` or auto-detected 56 | if the spec's `homepage` is set to a GitHub project. 57 | * Compatibility with RubyGems 2.2. 58 | 59 | ## v1.2.0 60 | 61 | * Allow passing a `Halite::Gem` object to `Halite.convert`. 62 | * Allow disabling the stove push as part of `rake release` by setting 63 | `$cookbook_push=false` as an environment variable. 64 | * Process `.foodcritic` when running `rake chef:foodcritic`. 65 | 66 | ## v1.1.0 67 | 68 | * Support the new `chef_version` metadata field through gem metadata. Defaults 69 | to `~> 12` for now. 70 | * Make spec helper resources look more like normal Chef resources when in auto 71 | mode. 72 | * Add Halite's synthetic cookbooks to the cookbook compiler too, for 73 | include_recipe and friends. 74 | 75 | ## v1.0.13 76 | 77 | * Additional cookbook metadata to work with Foodcritic 5.1. 78 | 79 | ## v1.0.12 80 | 81 | * Further 12.0 fixes. 82 | 83 | ## v1.0.11 84 | 85 | * Ensure Halite works under Chef 12.0. 86 | 87 | ## v1.0.10 88 | 89 | * Fewer potential pitfalls when using the Halite spec helper in a non-gem 90 | cookbook. Still not 100%, but better. 91 | 92 | ## v1.0.9 93 | 94 | * Additional `StubSpecifications` fixes. 95 | 96 | ## v1.0.8 97 | 98 | * Expose `example_group` inside `SpecHelper`-created resources and providers. 99 | * Handle `StubSpecifications` in the gem environment. 100 | 101 | ## v1.0.7 102 | 103 | * More fixes for Chef 12.4.1. 104 | 105 | ## v1.0.6 106 | 107 | * Minor fix for forward compatibility with 12.4.1+ and the spec helper's auto mode. 108 | 109 | ## v1.0.5 110 | 111 | * Fix the spec helper for Chef <= 12.2. 112 | 113 | ## v1.0.4 114 | 115 | * Fixes to work with Chef 12.4. 116 | 117 | ## v1.0.3 118 | 119 | * Never try to do universe installs of pre-release gems in the Berkshelf extension. 120 | 121 | ## v1.0.2 122 | 123 | * Handle converting cookbooks with pre-release version numbers and other 124 | non-Chef compatible components. 125 | * Include a cookbook's changelog file in converted output. 126 | * Handle OS X's case-insensitivity when converting misc. type files (README, etc). 127 | 128 | ## v1.0.1 129 | 130 | * Fix issues with pre-release version numbers. 131 | 132 | ## v1.0.0 133 | 134 | * Initial release! 135 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | source 'https://rubygems.org/' 18 | 19 | gemspec path: File.expand_path('..', __FILE__) 20 | 21 | def dev_gem(name, path: File.join('..', name)) 22 | path = File.expand_path(File.join('..', path), __FILE__) 23 | if File.exist?(path) 24 | gem name, path: path 25 | end 26 | end 27 | 28 | dev_gem 'poise' 29 | dev_gem 'poise-boiler' 30 | dev_gem 'poise-profiler' 31 | 32 | # Test fixture gems 33 | Dir[File.expand_path('../spec/fixtures/gems/*', __FILE__)].each do |path| 34 | gem File.basename(path), path: path 35 | end 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Halite 2 | 3 | [![Build Status](https://img.shields.io/travis/poise/halite.svg)](https://travis-ci.org/poise/halite) 4 | [![Gem Version](https://img.shields.io/gem/v/halite.svg)](https://rubygems.org/gems/halite) 5 | [![Coverage](https://img.shields.io/codecov/c/github/poise/halite.svg)](https://codecov.io/github/poise/halite) 6 | [![Gemnasium](https://img.shields.io/gemnasium/poise/halite.svg)](https://gemnasium.com/poise/halite) 7 | [![License](https://img.shields.io/badge/license-Apache_2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0) 8 | 9 | Write as a gem, release as a cookbook. 10 | 11 | ## Quick Start 12 | 13 | Create a gem as per normal and add a dependency on `halite`. Add 14 | `require 'halite/rake_tasks'` to your Rakefile. Run `rake build` and the 15 | converted cookbook will be written to `pkg/`. 16 | 17 | All Ruby code in the gem will be converted in to `libraries/` files. You can 18 | add cookbook-specific files by add them to a `chef/` folder in the root of the 19 | gem. 20 | 21 | ## Why? 22 | 23 | Developing cookbooks as gems allows using the full Ruby development ecosystem 24 | and tooling more directly. This includes things like Simplecov for coverage 25 | testing, YARD for documentation, and Gemnasium for dependency monitoring. For 26 | a cookbook that is already mostly library files, this is a natural transition, 27 | with few cookbook-specific pieces to start with. This also allows using Bundler 28 | to manage versions instead of Berkshelf. 29 | 30 | ## Cookbook Dependencies 31 | 32 | To add cookbook dependencies either add them to the gem requirements or use 33 | the `halite_dependencies` metadata field: 34 | 35 | ```ruby 36 | Gem::Specification.new do |spec| 37 | spec.requirements = %w{apache2 mysql} 38 | # or 39 | spec.metadata['halite_dependencies'] = 'php >= 2.0.0, chef-client' 40 | end 41 | ``` 42 | 43 | Additionally if you gem depends on other Halite-based gems those will 44 | automatically converted to cookbook dependencies. 45 | 46 | ## Cookbook Files 47 | 48 | Any files under `chef/` in the gem will be written as is in to the cookbook. 49 | For example you can add a recipe to your gem via `chef/recipes/default.rb`. 50 | 51 | ## Chef Version 52 | 53 | By default cookbooks will be generated with `chef_version '~> 12'` to require 54 | Chef 12.x. This can be overridden using the `halite_chef_version` metadata field: 55 | 56 | ```ruby 57 | Gem::Specification.new do |spec| 58 | spec.metadata['halite_chef_version'] = '>= 12.0.0' 59 | end 60 | ``` 61 | 62 | ## Rake Tasks 63 | 64 | The `halite/rake_tasks` module provides quick defaults. Gem name will be 65 | auto-detected from the `.gemspec` file and the cookbook name will be based 66 | on the gem name. 67 | 68 | ### `rake build` 69 | 70 | The build command will convert the gem to a cookbook and write it to the `pkg/` 71 | folder. 72 | 73 | ### Advanced Usage 74 | 75 | You can also pass custom arguments to the Rake tasks. All parameters are 76 | optional: 77 | 78 | ```ruby 79 | require 'halite/rake_helper' 80 | Halite::RakeHelper.install( 81 | gem_name: 'name', # Name of the gem to convert 82 | base: File.basename(__FILE__), # Base folder for the gem 83 | ) 84 | ``` 85 | 86 | ## Berkshelf Extension 87 | 88 | Halite includes a Berkshelf extension to pull in any gem-based cookbooks that 89 | are available on the system. 90 | 91 | To activate it, include the extension in your `Berksfile`: 92 | 93 | ```ruby 94 | extension 'halite' 95 | ``` 96 | 97 | ## Spec Helper 98 | 99 | Halite includes a set of helpers for RSpec tests. You can enable them in your 100 | `spec_helper.rb`: 101 | 102 | ```ruby 103 | require 'halite/spec_helper' 104 | 105 | RSpec.configure do |config| 106 | config.include Halite::SpecHelper 107 | end 108 | ``` 109 | 110 | ### `recipe` 111 | 112 | Recipes to converge for the test can be defined inline on example groups: 113 | 114 | ```ruby 115 | describe 'cookbook recipe' do 116 | recipe 'myrecipe' 117 | it { is_expected.to create_file('/myfile') } 118 | end 119 | 120 | describe 'inline recipe' do 121 | recipe do 122 | file '/myfile' do 123 | content 'mycontent' 124 | end 125 | end 126 | it { is_expected.to create_file('/myfile') } 127 | end 128 | ``` 129 | 130 | ### `step_into` 131 | 132 | A resource can be added to the list to step in to via the `step_into` helper: 133 | 134 | ```ruby 135 | describe 'mycookbook' do 136 | recipe 'mycookbook::server' 137 | step_into :mycookbook_lwrp 138 | it { is_expected.to ... } 139 | end 140 | ``` 141 | 142 | ### `resource` and `provider` 143 | 144 | For testing mixin-based cookbooks, new resource and provider classes can be 145 | declared on an example group: 146 | 147 | ```ruby 148 | describe MyMixin do 149 | resource(:test_resource) do 150 | include MyMixin 151 | def foo(val=nil) 152 | set_or_return(:foo, val, {}) 153 | end 154 | end 155 | provider(:test_resource) do 156 | def action_run 157 | # ... 158 | end 159 | end 160 | recipe do 161 | test_resource 'test' do 162 | foo 1 163 | action :run 164 | end 165 | end 166 | it { is_expected.to ... } 167 | end 168 | ``` 169 | 170 | These helper resources and providers are only available within the scope of 171 | recipes defined on that example group or groups nested inside it. Helper 172 | resources are automatically `step_into`'d. 173 | 174 | ## Using a Pre-release Version of a Cookbook 175 | 176 | When a Halite-based cookbook is released, a converted copy is generally uploaded 177 | to [the Supermarket](https://supermarket.chef.io/). To use unreleased versions, 178 | you need to pull in the code from git via bundler and then tell the Berkshelf 179 | extension to convert it for you. 180 | 181 | To grab the pre-release gem, add a line like the following to your Gemfile: 182 | 183 | ```ruby 184 | gem 'poise-application', github: 'poise/application' 185 | ``` 186 | 187 | You will need one `gem` line for each Halite-based cookbook you want to use, 188 | possibly including dependencies if you want to use pre-release versions of 189 | those as well. 190 | 191 | Next you need to use Berkshelf to convert the gem to its cookbook form: 192 | 193 | ```ruby 194 | source 'https://supermarket.chef.io/' 195 | extension 'halite' 196 | cookbook 'application', gem: 'poise-application' 197 | ``` 198 | 199 | Again you will need one `cookbook` line per Halite based cookbook you want to 200 | use. Also make sure to check the correct names for the gem and cookbook, they 201 | may not be the same though for other Poise cookbooks they generally follow the 202 | same pattern. 203 | 204 | If you are using something that integrates with Berkshelf like Test-Kitchen or 205 | ChefSpec, this is all you need to do. You could use `berks upload` to push a 206 | converted copy of all cookbooks to a Chef Server, though running pre-release 207 | code in production should be done with great care. 208 | 209 | ## License 210 | 211 | Copyright 2015, Noah Kantrowitz 212 | 213 | Licensed under the Apache License, Version 2.0 (the "License"); 214 | you may not use this file except in compliance with the License. 215 | You may obtain a copy of the License at 216 | 217 | http://www.apache.org/licenses/LICENSE-2.0 218 | 219 | Unless required by applicable law or agreed to in writing, software 220 | distributed under the License is distributed on an "AS IS" BASIS, 221 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 222 | See the License for the specific language governing permissions and 223 | limitations under the License. 224 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'bundler/gem_tasks' 18 | 19 | require 'rspec/core/rake_task' 20 | RSpec::Core::RakeTask.new(:spec) do |t| 21 | t.rspec_opts = [].tap do |a| 22 | a << '--color' 23 | a << "--format #{ENV['CI'] ? 'documentation' : 'Fuubar'}" 24 | a << '--tag ~slow' 25 | a << '--backtrace' if ENV['DEBUG'] 26 | a << "--seed #{ENV['SEED']}" if ENV['SEED'] 27 | end.join(' ') 28 | end 29 | 30 | RSpec::Core::RakeTask.new(:integration) do |t| 31 | t.rspec_opts = [].tap do |a| 32 | a << '--color' 33 | a << "--format #{ENV['CI'] ? 'documentation' : 'Fuubar'}" 34 | a << '--backtrace' if ENV['DEBUG'] 35 | a << "--seed #{ENV['SEED']}" if ENV['SEED'] 36 | end.join(' ') 37 | end 38 | 39 | desc 'Run all tests' 40 | task :test => [:spec] 41 | 42 | task :default => [:test] 43 | -------------------------------------------------------------------------------- /gemfiles/chef-12.16.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 12.16.42' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-12.17.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 12.17.44' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-12.18.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 12.18.31' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-12.19.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 12.19.36' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-12.20.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 12.20.3' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-12.21.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 12.21.31' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-12.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015-2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 12.21' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.0.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.0.118' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.1.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.1.31' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.10.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.10.4' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.2.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.2.20' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.3.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.3.42' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.4.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.4.24' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.5.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.5.3' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.6.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.6.4' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.7.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.7.16' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.8.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.8.0' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.9.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.9.4' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-13.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2017, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 13.10' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-14.0.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 14.0.202' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-14.1.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 14.1.12' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-14.2.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 14.2.0' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-14.3.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 14.3.37' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-14.4.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 14.4.56' 20 | -------------------------------------------------------------------------------- /gemfiles/chef-14.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', '~> 14.4' 20 | -------------------------------------------------------------------------------- /gemfiles/master.gemfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | eval_gemfile File.expand_path('../../Gemfile', __FILE__) 18 | 19 | gem 'chef', github: 'chef/chef' 20 | gem 'ohai', github: 'chef/ohai' 21 | gem 'poise', github: 'poise/poise' 22 | gem 'poise-boiler', github: 'poise/poise-boiler' 23 | gem 'poise-profiler', github: 'poise/poise-profiler' 24 | -------------------------------------------------------------------------------- /halite.gemspec: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | lib = File.expand_path('../lib', __FILE__) 18 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 19 | require 'halite/version' 20 | 21 | Gem::Specification.new do |spec| 22 | spec.name = 'halite' 23 | spec.version = Halite::VERSION 24 | spec.authors = ['Noah Kantrowitz'] 25 | spec.email = %w{noah@coderanger.net} 26 | spec.description = 'A set of helpers to write Chef cookbooks as Ruby gems.' 27 | spec.summary = spec.description 28 | spec.homepage = 'https://github.com/poise/halite' 29 | spec.license = 'Apache 2.0' 30 | 31 | spec.files = `git ls-files`.split($/) 32 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 33 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 34 | spec.require_paths = %w{lib} 35 | 36 | spec.add_dependency 'chef', '>= 12.0', '< 15' 37 | spec.add_dependency 'stove', '~> 5.0' 38 | spec.add_dependency 'bundler' # Used for Bundler.load_gemspec 39 | spec.add_dependency 'thor' # Used for Thor::Shell 40 | 41 | spec.add_development_dependency 'poise', '~> 2.0' 42 | spec.add_development_dependency 'poise-boiler', '~> 1.0' 43 | spec.add_development_dependency 'rspec-command', '~> 1.0' 44 | end 45 | -------------------------------------------------------------------------------- /lib/berkshelf/halite.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Install the Halite source 18 | require 'halite/berkshelf/helper' 19 | ::Halite::Berkshelf::Helper.install 20 | 21 | 22 | # Berkshelf plugin loading. 23 | # @api private 24 | module Berkshelf 25 | # Set up the :gem location type. 26 | autoload :GemLocation, 'berkshelf/locations/gem' 27 | end 28 | -------------------------------------------------------------------------------- /lib/berkshelf/locations/gem.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'halite' 18 | 19 | 20 | module Berkshelf 21 | # Berkshelf location plugin to install via Halite gems. 22 | # 23 | # @since 1.0.0 24 | # @api private 25 | class GemLocation < BaseLocation 26 | attr_reader :gem_name 27 | 28 | def initialize(*args) 29 | super 30 | @gem_name = options[:gem] 31 | end 32 | 33 | 34 | # Always force the re-install. 35 | # 36 | # @see BaseLocation#installed? 37 | def installed? 38 | false 39 | end 40 | 41 | # Convert the gem. 42 | # 43 | # @see BaseLocation#install 44 | def install 45 | cache_path.rmtree if cache_path.exist? 46 | cache_path.mkpath 47 | Halite.convert(gem_name, cache_path) 48 | validate_cached!(cache_path) 49 | end 50 | 51 | # @see BaseLocation#cached_cookbook 52 | def cached_cookbook 53 | if cache_path.join('metadata.rb').exist? 54 | @cached_cookbook ||= CachedCookbook.from_path(cache_path) 55 | else 56 | nil 57 | end 58 | end 59 | 60 | # Lockfile rendering for Halite gem cookbooks. 61 | def to_lock 62 | " gem: #{gem_name}\n" 63 | end 64 | 65 | def ==(other) 66 | other.is_a?(GemLocation) && other.gem_name == gem_name 67 | end 68 | 69 | def to_s 70 | "gem from #{gem_name}" 71 | end 72 | 73 | def inspect 74 | "#" 75 | end 76 | 77 | # The path to the converted gem. 78 | # 79 | # @return [Pathname] 80 | def cache_path 81 | @cache_path ||= Pathname.new(Berkshelf.berkshelf_path).join('.cache', 'halite', dependency.name) 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/halite.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | # Library to convert a Ruby gem to a Chef cookbook. 19 | # 20 | # @since 1.0.0 21 | # @see convert 22 | module Halite 23 | autoload :Converter, 'halite/converter' 24 | autoload :Dependencies, 'halite/dependencies' 25 | autoload :Error, 'halite/error' 26 | autoload :Gem, 'halite/gem' 27 | autoload :SpecHelper, 'halite/spec_helper' 28 | autoload :VERSION, 'halite/version' 29 | 30 | # Convert a Ruby gem to a Chef cookbook. 31 | # 32 | # @param gem_name [String, Gem::Specification] Gem to convert. 33 | # @param base_path [String] Output path. 34 | # @return [void] 35 | # @example Converting a gem by name 36 | # Halite.convert('mygem', 'dest') 37 | # @example Converting a gem from a loaded gemspec 38 | # Halite.convert(Bundler.load_gemspec('mygem.gemspec'), 'dest') 39 | def self.convert(gem_name, base_path) 40 | gem_data = if gem_name.is_a?(Gem) 41 | gem_name 42 | else 43 | Gem.new(gem_name) 44 | end 45 | Converter.write(gem_data, base_path) 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/halite/berkshelf/helper.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'halite' 18 | require 'halite/berkshelf/source' 19 | require 'berkshelf/berksfile' 20 | require 'berkshelf/downloader' 21 | 22 | 23 | module Halite 24 | module Berkshelf 25 | # Helper class to monkeypatch global gem support in to Berkshelf. 26 | # 27 | # @since 1.0.0 28 | # @api private 29 | class Helper 30 | def self.install(*args) 31 | new(*args).install 32 | end 33 | 34 | def initialize 35 | end 36 | 37 | # Install the two patches. 38 | # 39 | # @return [void] 40 | def install 41 | # Switch this to use Module#prepend at some point when I stop caring about Ruby 1.9. 42 | ::Berkshelf::Berksfile.class_exec do 43 | old_sources = instance_method(:sources) 44 | define_method(:sources) do 45 | original_sources = begin 46 | old_sources.bind(self).call 47 | rescue ::Berkshelf::NoAPISourcesDefined 48 | # We don't care, there will be a source 49 | [] 50 | end 51 | # Make sure we never add two halite sources. 52 | original_sources.reject {|s| s.is_a?(::Halite::Berkshelf::Source) } + [::Halite::Berkshelf::Source.new(self)] 53 | end 54 | end 55 | 56 | # Inject support for the :halite location type 57 | ::Berkshelf::Downloader.class_exec do 58 | old_try_download = instance_method(:try_download) 59 | define_method(:try_download) do |source, name, version| 60 | remote_cookbook = source.cookbook(name, version) 61 | if remote_cookbook && remote_cookbook.location_type == :halite 62 | tmp_dir = Dir.mktmpdir 63 | Halite.convert(remote_cookbook.location_path, tmp_dir) 64 | tmp_dir 65 | else 66 | old_try_download.bind(self).call(source, name, version) 67 | end 68 | end 69 | end 70 | 71 | end 72 | end 73 | 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/halite/berkshelf/source.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'halite/gem' 18 | require 'berkshelf/source' 19 | require 'berkshelf/api_client/remote_cookbook' 20 | 21 | 22 | module Halite 23 | module Berkshelf 24 | # Berkshelf global source to find all Halite cookbooks in the current 25 | # gems environment. 26 | # 27 | # @since 1.0.0 28 | # @api private 29 | class Source < ::Berkshelf::Source 30 | def initialize(berksfile) 31 | # 6.1+ mode. 32 | super(berksfile, {halite: 'halite://'}) 33 | rescue ArgumentError 34 | # Legacy mode. 35 | super('halite://') 36 | end 37 | 38 | def build_universe 39 | # Scan all gems 40 | ::Gem::Specification.stubs.map do |spec| 41 | Gem.new(spec) 42 | end.select do |cook| 43 | # Make sure this is a cookbook, and that it isn't a pre-release. 44 | cook.is_halite_cookbook? && cook.version == cook.cookbook_version 45 | end.map do |cook| 46 | # Build a fake "remote" cookbook 47 | ::Berkshelf::APIClient::RemoteCookbook.new( 48 | cook.cookbook_name, 49 | cook.cookbook_version, 50 | { 51 | location_type: 'halite', 52 | location_path: cook.name, 53 | dependencies: cook.cookbook_dependencies.inject({}) {|memo, dep| memo[dep.name] = dep.requirement; memo }, 54 | }, 55 | ) 56 | end 57 | end 58 | 59 | # Show "... from Halite gems" when installing. 60 | def to_s 61 | "Halite gems" 62 | end 63 | alias :uri :to_s 64 | end 65 | 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /lib/halite/converter.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | module Halite 19 | # (see Converter.write) 20 | module Converter 21 | autoload :Chef, 'halite/converter/chef' 22 | autoload :Libraries, 'halite/converter/libraries' 23 | autoload :Metadata, 'halite/converter/metadata' 24 | autoload :Misc, 'halite/converter/misc' 25 | 26 | # Convert a cookbook gem to a normal Chef cookbook. 27 | # 28 | # @since 1.0.0 29 | # @param gem_data [Halite::Gem] Gem to convert. 30 | # @param output_path [String] Output path. 31 | # @return [void] 32 | # @example 33 | # Halite::Converter.write(Halite::Gem.new(gemspec), 'dest') 34 | def self.write(gem_data, output_path) 35 | Chef.write(gem_data, output_path) 36 | Libraries.write(gem_data, output_path) 37 | Metadata.write(gem_data, output_path) 38 | Misc.write(gem_data, output_path) 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/halite/converter/chef.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'fileutils' 18 | 19 | 20 | module Halite 21 | module Converter 22 | # Converter module for cookbook-specific files. These are copied verbatim 23 | # from the chef/ directory in the gem. 24 | # 25 | # @since 1.0.0 26 | # @api private 27 | module Chef 28 | # Copy all files in the chef/ directory in the gem. 29 | # 30 | # @param gem_data [Halite::Gem] Gem to generate from. 31 | # @param output_path [String] Output path for the cookbook. 32 | # @return [void] 33 | def self.write(gem_data, output_path) 34 | gem_data.each_file('chef') do |path, rel_path| 35 | dir_path = File.dirname(rel_path) 36 | FileUtils.mkdir_p(File.join(output_path, dir_path)) unless dir_path == '.' 37 | FileUtils.copy(path, File.join(output_path, rel_path), preserve: true) 38 | end 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/halite/converter/libraries.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'fileutils' 18 | 19 | 20 | module Halite 21 | module Converter 22 | # Converter methods for gem library code (ex. files under lib/). 23 | # 24 | # @since 1.0.0 25 | # @api private 26 | module Libraries 27 | # Generate the bootstrap code for the Chef cookbook. 28 | # 29 | # @param gem_data [Halite::Gem] Gem to generate from. 30 | # @param entry_points [Array] Zero or more entry points to be 31 | # automatically loaded. 32 | # @return [String] 33 | def self.generate_bootstrap(gem_data, entry_points) 34 | ''.tap do |buf| 35 | buf << gem_data.license_header 36 | buf << <<-EOH 37 | raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load] 38 | $LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__) 39 | EOH 40 | entry_points.each do |entry_point| 41 | buf << "require #{entry_point.inspect}\n" 42 | end 43 | end 44 | end 45 | 46 | # Copy all library code to the files/halite_gem/ directory in the cookbook. 47 | # 48 | # @param gem_data [Halite::Gem] Gem to generate from. 49 | # @param output_path [String] Output path for the cookbook. 50 | # @return [void] 51 | def self.write_libraries(gem_data, output_path) 52 | dest_path = File.join(output_path, 'files', 'halite_gem') 53 | FileUtils.mkdir_p(dest_path) 54 | gem_data.each_library_file do |path, rel_path| 55 | dir_path = File.dirname(rel_path) 56 | FileUtils.mkdir_p(File.join(dest_path, dir_path)) unless dir_path == '.' 57 | FileUtils.copy(path, File.join(dest_path, rel_path), preserve: true) 58 | end 59 | end 60 | 61 | # Find any default entry points (files name cheftie.rb) in the gem. 62 | # 63 | # @param gem_data [Halite::Gem] Gem to generate from. 64 | # @return [Array] 65 | def self.find_default_entry_points(gem_data) 66 | [].tap do |entry_points| 67 | gem_data.each_library_file do |_path, rel_path| 68 | if File.basename(rel_path) == 'cheftie.rb' 69 | # Trim the .rb for cleanliness. 70 | entry_points << rel_path[0..-4] 71 | end 72 | end 73 | end 74 | end 75 | 76 | # Create the bootstrap code in the cookbook. 77 | # 78 | # @param gem_data [Halite::Gem] Gem to generate from. 79 | # @param output_path [String] Output path for the cookbook. 80 | # @param entry_point [String, Array] Entry point(s) for the 81 | # bootstrap. These are require paths that will be loaded automatically 82 | # during a Chef converge. 83 | # @return [void] 84 | def self.write_bootstrap(gem_data, output_path, entry_point=nil) 85 | # Default entry point. 86 | entry_point ||= gem_data.spec.metadata['halite_entry_point'] || find_default_entry_points(gem_data) 87 | # Parse and cast. 88 | entry_point = Array(entry_point).map {|s| s.split }.flatten 89 | # Write bootstrap file. 90 | lib_path = File.join(output_path, 'libraries') 91 | FileUtils.mkdir_p(lib_path) 92 | IO.write(File.join(lib_path, 'default.rb'), generate_bootstrap(gem_data, entry_point)) 93 | end 94 | 95 | # Write out the library code for the cookbook. 96 | # 97 | # @param gem_data [Halite::Gem] Gem to generate from. 98 | # @param output_path [String] Output path for the cookbook. 99 | # @return [void] 100 | def self.write(gem_data, output_path, entry_point=nil) 101 | write_libraries(gem_data, output_path) 102 | write_bootstrap(gem_data, output_path, entry_point) 103 | end 104 | 105 | end 106 | end 107 | end 108 | -------------------------------------------------------------------------------- /lib/halite/converter/metadata.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | module Halite 19 | module Converter 20 | # Converter module to create the metadata.rb for the cookbook. 21 | # 22 | # @since 1.0.0 23 | # @api private 24 | module Metadata 25 | # Generate a cookbook metadata file. 26 | # 27 | # @param gem_data [Halite::Gem] Gem to generate from. 28 | # @return [String] 29 | def self.generate(gem_data) 30 | ''.tap do |buf| 31 | buf << gem_data.license_header 32 | buf << "name #{gem_data.cookbook_name.inspect}\n" 33 | buf << "version #{gem_data.cookbook_version.inspect}\n" 34 | if gem_data.spec.description && !gem_data.spec.description.empty? 35 | buf << "description #{gem_data.spec.description.inspect}\n" 36 | end 37 | if readme_path = gem_data.find_misc_path('Readme') # rubocop:disable Lint/AssignmentInCondition 38 | buf << "long_description #{IO.read(readme_path).inspect}\n" 39 | end 40 | buf << "maintainer #{gem_data.spec.authors.join(', ').inspect}\n" unless gem_data.spec.authors.empty? 41 | buf << "maintainer_email #{Array(gem_data.spec.email).join(',').inspect}\n" if gem_data.spec.email 42 | buf << "source_url #{gem_data.spec.homepage.inspect} if defined?(source_url)\n" if gem_data.spec.homepage 43 | buf << "issues_url #{gem_data.issues_url.inspect} if defined?(issues_url)\n" if gem_data.issues_url 44 | buf << "license #{gem_data.spec.licenses.join(', ').inspect}\n" unless gem_data.spec.licenses.empty? 45 | gem_data.cookbook_dependencies.each do |dep| 46 | buf << "depends #{dep.name.inspect}" 47 | buf << ", #{dep.requirement.inspect}" if dep.requirement != '>= 0' 48 | buf << "\n" 49 | end 50 | buf << "chef_version #{gem_data.chef_version_requirement.map(&:inspect).join(', ')} if defined?(chef_version)\n" 51 | gem_data.platforms.each do |platform| 52 | buf << "supports #{platform.map(&:inspect).join(', ')}\n" 53 | end 54 | end 55 | end 56 | 57 | # Write out a cookbook metadata file. 58 | # 59 | # @param gem_data [Halite::Gem] Gem to generate from. 60 | # @param output_path [String] Output path for the cookbook. 61 | # @return [void] 62 | def self.write(gem_data, output_path) 63 | IO.write(File.join(output_path, 'metadata.rb'), generate(gem_data)) 64 | end 65 | 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/halite/converter/misc.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'fileutils' 18 | 19 | 20 | module Halite 21 | module Converter 22 | # Converter module for miscellanous project-level files like README.md 23 | # and LICENSE.txt. 24 | # 25 | # @since 1.0.0 26 | # @api private 27 | module Misc 28 | # Copy miscellaneous project-level files. 29 | # 30 | # @param gem_data [Halite::Gem] Gem to generate from. 31 | # @param output_path [String] Output path for the cookbook. 32 | # @return [void] 33 | def self.write(gem_data, output_path) 34 | %w{Readme License Copying Contributing Changelog}.each do |name| 35 | if path = gem_data.find_misc_path(name) # rubocop:disable Lint/AssignmentInCondition 36 | FileUtils.copy(path, File.join(output_path, File.basename(path)), preserve: true) 37 | end 38 | end 39 | end 40 | 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/halite/dependencies.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'halite/error' 18 | 19 | 20 | module Halite 21 | # Error class for invalid dependencies. 22 | # 23 | # @since 1.0.0 24 | class InvalidDependencyError < Error; end 25 | 26 | # Methods to extract cookbook dependencies from a gem. 27 | # 28 | # @since 1.0.0 29 | module Dependencies 30 | Dependency = Struct.new(:name, :requirement, :type, :spec) do 31 | def ==(other) 32 | self.name == other.name && \ 33 | self.requirement == other.requirement && \ 34 | self.type == other.type 35 | end 36 | 37 | def cookbook 38 | Gem.new(spec) if spec 39 | end 40 | end 41 | 42 | # Extract the cookbook dependencies from a gem specification. 43 | # 44 | # @since 1.6.0 Added development parameter. 45 | # @param spec [Gem::Specification] Gem to extract from. 46 | # @param development [Boolean] If true, consider development depencies. 47 | # @return [Array] 48 | def self.extract(spec, development: false) 49 | deps = [] 50 | deps += clean_and_tag(extract_from_requirements(spec), :requirements) 51 | deps += clean_and_tag(extract_from_metadata(spec), :metadata) 52 | deps += clean_and_tag(extract_from_dependencies(spec, development: development), :dependencies) 53 | deps 54 | end 55 | 56 | def self.extract_from_requirements(spec) 57 | # Simple dependencies in the requirements array. 58 | spec.requirements 59 | end 60 | 61 | def self.extract_from_metadata(spec) 62 | # This will only work on Rubygems 2.0 or higher I think, gee thats just too bad. 63 | # The metadata can only be a single string, so split on comma. 64 | spec.metadata.fetch('halite_dependencies', '').split(/,/) 65 | end 66 | 67 | def self.extract_from_dependencies(spec, development: false) 68 | # Find any gem dependencies that are cookbooks in disguise. 69 | spec.dependencies.select do |dep| 70 | (development || dep.type == :runtime) && Gem.new(dep).is_halite_cookbook? 71 | end.map do |dep| 72 | gem = Gem.new(dep) 73 | [gem.cookbook_name] + dep.requirements_list + [gem.spec] 74 | end 75 | end 76 | 77 | def self.clean_and_tag(deps, tag) 78 | deps.map do |dep| 79 | dep = clean(dep) 80 | Dependency.new(dep[0], dep[1], tag, dep[2]) 81 | end 82 | end 83 | 84 | def self.clean(dep) 85 | # Convert to an array of strings, remove the spec to be re-added later. 86 | dep = Array(dep) 87 | spec = if dep.last.is_a?(::Gem::Specification) 88 | dep.pop 89 | end 90 | dep = Array(dep).map {|obj| obj.is_a?(::Gem::Specification) ? obj : obj.to_s.strip } 91 | # Unpack single strings like 'foo >= 1.0' 92 | dep = dep.first.split(/\s+/, 2) if dep.length == 1 93 | # Default version constraint to match rubygems behavior when sourcing from simple strings 94 | dep << '>= 0' if dep.length == 1 95 | raise InvalidDependencyError.new("Chef only supports a single version constraint on each dependency: #{dep}") if dep.length > 2 # ಠ_ಠ 96 | dep[1] = clean_requirement(dep[1]) 97 | # Re-add the spec 98 | dep << spec if spec 99 | dep 100 | end 101 | 102 | def self.clean_requirement(req) 103 | req = ::Gem::Requirement.create(req) 104 | req.requirements[0][1] = clean_version(req.requirements[0][1]) 105 | req.to_s 106 | end 107 | 108 | def self.clean_version(ver) 109 | segments = ver.segments.dup 110 | # Various ways Chef differs from Rubygems. 111 | # Strip any pre-release tags in the version. 112 | segments = segments.take_while {|s| s.to_s =~ /^\d+$/ } 113 | # Must be x or x.y or x.y.z. 114 | raise InvalidDependencyError.new("Chef only supports two or three version segments: #{ver}") if segments.length < 1 || segments.length > 3 115 | # If x, convert to x.0 because Chef requires two segments. 116 | segments << 0 if segments.length == 1 117 | # Convert 0.0 or 0.0.0 to just 0. 118 | segments = [0] if segments.all? {|s| s == 0 } 119 | ::Gem::Version.new(segments.join('.')) 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /lib/halite/error.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | module Halite 19 | # Base class for Halite library errors. 20 | # 21 | # @since 1.0.0 22 | class Error < ::Exception 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/halite/gem.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # Because Chef 12.0 never got a backport of https://github.com/chef/chef/commit/04ba9182cda51b79630aab2918bbc6bba2d99c23 18 | # and requiring chef/cookbook_version below can trigger the bug if singleton 19 | # already loaded. Remove this when dropping support for 12.0. 20 | require 'singleton' 21 | 22 | begin 23 | require 'bundler' # Pull in the bundler top-level because of missing requires. 24 | require 'bundler/stub_specification' 25 | rescue LoadError 26 | # Bundler too old. 27 | end 28 | require 'chef/cookbook_version' 29 | 30 | require 'halite/dependencies' 31 | require 'halite/error' 32 | 33 | 34 | module Halite 35 | # A model for a gem/cookbook within Halite. 36 | # 37 | # @since 1.0.0 38 | # @example 39 | # g = Halite::Gem.new('chef-mycookbook', '1.1.0') 40 | # puts(g.cookbook_name) #=> mycookbook 41 | class Gem 42 | attr_reader :name 43 | 44 | # name can be either a string name, Gem::Dependency, or Gem::Specification 45 | # @param name [String, Gem::Dependency, Gem::Specification] 46 | def initialize(name, version=nil) 47 | # Allow passing a Dependency by just grabbing its spec. 48 | name = dependency_to_spec(name) if name.is_a?(::Gem::Dependency) 49 | # Stubs don't load enough data for us, grab the real spec. RIP IOPS. 50 | name = name.to_spec if name.is_a?(::Gem::StubSpecification) || (defined?(Bundler::StubSpecification) && name.is_a?(Bundler::StubSpecification)) 51 | if name.is_a?(::Gem::Specification) 52 | raise Error.new("Cannot pass version when using an explicit specficiation") if version 53 | @spec = name 54 | @name = spec.name 55 | else 56 | @name = name 57 | @version = version 58 | raise Error.new("Gem #{name}#{version ? " v#{version}" : ''} not found") unless spec 59 | end 60 | end 61 | 62 | def spec 63 | @spec ||= dependency_to_spec(::Gem::Dependency.new(@name, ::Gem::Requirement.new(@version))) 64 | end 65 | 66 | def version 67 | spec.version.to_s 68 | end 69 | 70 | def cookbook_name 71 | if spec.metadata.include?('halite_name') 72 | spec.metadata['halite_name'] 73 | else 74 | spec.name.gsub(/(^(chef|cookbook)[_-])|([_-](chef|cookbook))$/, '') 75 | end 76 | end 77 | 78 | # Version of the gem sanitized for Chef. This means no non-numeric tags and 79 | # only three numeric components. 80 | # 81 | # @return [String] 82 | def cookbook_version 83 | if match = version.match(/^(\d+\.\d+\.(\d+)?)/) 84 | match[1] 85 | else 86 | raise Halite::Error.new("Unable to parse #{version.inspect} as a Chef cookbook version") 87 | end 88 | end 89 | 90 | # Path to the .gemspec for this gem. This is different from 91 | # Gem::Specification#spec_file because the Rubygems API is shit and just 92 | # assumes the file layout matches normal, which is not the case with Bundler 93 | # and path or git sources. 94 | # 95 | # @return [String] 96 | def spec_file 97 | File.join(spec.full_gem_path, spec.name + '.gemspec') 98 | end 99 | 100 | # License header extacted from the gemspec. Suitable for inclusion in other 101 | # Ruby source files. 102 | # 103 | # @return [String] 104 | def license_header 105 | IO.readlines(spec_file).take_while { |line| line.strip.empty? || line.strip.start_with?('#') }.join('') 106 | end 107 | 108 | # URL to the issue tracker for this project. 109 | # 110 | # @return [String, nil] 111 | def issues_url 112 | if spec.metadata['issues_url'] 113 | spec.metadata['issues_url'] 114 | elsif spec.homepage =~ /^http(s)?:\/\/(www\.)?github\.com/ 115 | spec.homepage.chomp('/') + '/issues' 116 | end 117 | end 118 | 119 | # Platform support to be used in the Chef metadata. 120 | # 121 | # @return [Array>] 122 | def platforms 123 | raw_platforms = spec.metadata.fetch('platforms', '').strip 124 | case raw_platforms 125 | when '' 126 | [] 127 | when 'any', 'all', '*' 128 | # Based on `ls lib/fauxhai/platforms | xargs echo`. 129 | %w{aix amazon arch centos chefspec debian dragonfly4 fedora freebsd gentoo 130 | ios_xr mac_os_x nexus omnios openbsd opensuse oracle raspbian redhat 131 | slackware smartos solaris2 suse ubuntu windows}.map {|p| [p] } 132 | when /,/ 133 | # Comma split mode. String looks like "name, name constraint, name constraint" 134 | raw_platforms.split(/\s*,\s*/).map {|p| p.split(/\s+/, 2) } 135 | else 136 | # Whitepace split mode, assume no constraints. 137 | raw_platforms.split(/\s+/).map {|p| [p] } 138 | end 139 | end 140 | 141 | # Iterate over all the files in the gem, with an optional prefix. Each 142 | # element in the iterable will be [full_path, relative_path], where 143 | # relative_path is relative to the prefix or gem path. 144 | # 145 | # @param prefix_paths [String, Array, nil] Option prefix paths. 146 | # @param block [Proc] Callable for iteration. 147 | # @return [Array>] 148 | # @example 149 | # gem_data.each_file do |full_path, rel_path| 150 | # # ... 151 | # end 152 | def each_file(prefix_paths=nil, &block) 153 | globs = if prefix_paths 154 | Array(prefix_paths).map {|path| File.join(spec.full_gem_path, path) } 155 | else 156 | [spec.full_gem_path] 157 | end 158 | [].tap do |files| 159 | globs.each do |glob| 160 | Dir[File.join(glob, '**', '*')].each do |path| 161 | next unless File.file?(path) 162 | val = [path, path[glob.length+1..-1]] 163 | block.call(*val) if block 164 | files << val 165 | end 166 | end 167 | # Make sure the order is stable for my tests. Probably overkill, I think 168 | # Dir#[] sorts already. 169 | files.sort! 170 | end 171 | end 172 | 173 | # Special case of the {#each_file} the gem's require paths. 174 | # 175 | # @param block [Proc] Callable for iteration. 176 | # @return [Array>] 177 | def each_library_file(&block) 178 | each_file(spec.require_paths, &block) 179 | end 180 | 181 | def cookbook_dependencies(development: false) 182 | Dependencies.extract(spec, development: development) 183 | end 184 | 185 | # Is this gem really a cookbook? (anything that depends directly on halite and doesn't have the ignore flag) 186 | def is_halite_cookbook? 187 | spec.dependencies.any? {|subdep| subdep.name == 'halite'} && !spec.metadata.include?('halite_ignore') 188 | end 189 | 190 | # Create a Chef::CookbookVersion object that represents this gem. This can 191 | # be injected in to Chef to simulate the cookbook being available. 192 | # 193 | # @return [Chef::CookbookVersion] 194 | # @example 195 | # run_context.cookbook_collection[gem.cookbook_name] = gem.as_cookbook_version 196 | def as_cookbook_version 197 | # Put this in a local variable for a closure below. 198 | path = spec.full_gem_path 199 | Chef::CookbookVersion.new(cookbook_name, File.join(path, 'chef')).tap do |c| 200 | # Use CookbookVersion#files_for as a feature test for ManifestV2. This 201 | # can be changed to ::Gem::Requirement.create('>= 13').satisfied_by?(::Gem::Version.create(Chef::VERSION)) 202 | # once https://github.com/chef/chef/pull/5929 is merged. 203 | if defined?(c.files_for) 204 | c.all_files = each_file('chef').map(&:first) 205 | else 206 | c.attribute_filenames = each_file('chef/attributes').map(&:first) 207 | c.file_filenames = each_file('chef/files').map(&:first) 208 | c.recipe_filenames = each_file('chef/recipes').map(&:first) 209 | c.template_filenames = each_file('chef/templates').map(&:first) 210 | end 211 | # Haxx, rewire the filevendor for this cookbook to look up in our folder. 212 | # This is touching two different internal interfaces, but ¯\_(ツ)_/¯ 213 | c.send(:file_vendor).define_singleton_method(:get_filename) do |filename| 214 | File.join(path, 'chef', filename) 215 | end 216 | # Store the true root for use in other tools. 217 | c.define_singleton_method(:halite_root) { path } 218 | end 219 | end 220 | 221 | # Search for a file like README.md or LICENSE.txt in the gem. 222 | # 223 | # @param name [String] Basename to search for. 224 | # @return [String, Array] 225 | # @example 226 | # gem.misc_file('Readme') => /path/to/readme.txt 227 | def find_misc_path(name) 228 | [name, name.upcase, name.downcase].each do |base| 229 | ['.md', '', '.txt', '.html'].each do |suffix| 230 | path = File.join(spec.full_gem_path, base+suffix) 231 | return path if File.exist?(path) && Dir.entries(File.dirname(path)).include?(File.basename(path)) 232 | end 233 | end 234 | # Didn't find anything 235 | nil 236 | end 237 | 238 | # Figure out which version of Chef this cookbook requires. Returns an array 239 | # of Gem::Requirement-style string like `~> 12.0`. 240 | # 241 | # @return [Array] 242 | def chef_version_requirement 243 | if spec.metadata['halite_chef_version'] 244 | # Manually overridden by gem metadata, use that. 245 | [spec.metadata['halite_chef_version']] 246 | elsif dep = spec.dependencies.find {|inner_dep| inner_dep.name == 'chef' && !inner_dep.requirement.none? } 247 | # Parse through the dependencies looking for something like `spec.add_dependency 'chef', '>= 12.1''`. 248 | dep.requirement.as_list 249 | else 250 | ['>= 12'] 251 | end 252 | end 253 | 254 | private 255 | 256 | # Find a spec given a dependency. 257 | # 258 | # @since 1.0.1 259 | # @param dep [Gem::Dependency] Dependency to solve. 260 | # @return [Gem::Specificiation] 261 | def dependency_to_spec(dep) 262 | # #to_spec doesn't allow prereleases unless the requirement is 263 | # for a prerelease. Just use the last valid spec if possible. 264 | spec = dep.to_spec || dep.to_specs.last 265 | raise Error.new("Cannot find a gem to satisfy #{dep}") unless spec 266 | spec 267 | rescue ::Gem::LoadError => ex 268 | raise Error.new("Cannot find a gem to satisfy #{dep}: #{ex}") 269 | end 270 | end 271 | end 272 | -------------------------------------------------------------------------------- /lib/halite/helper_base.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | # Much inspiration from Bundler's GemHelper. Thanks! 19 | require 'bundler' 20 | require 'thor' 21 | require 'thor/shell' 22 | 23 | require 'halite/error' 24 | 25 | 26 | module Halite 27 | # Base class for helpers like Rake tasks. 28 | # 29 | # @api semipublic 30 | # @since 1.0.0 31 | class HelperBase 32 | # Class method helper to install the tasks. 33 | # 34 | # @param args Arguments to be passed to {#initialize}. 35 | # @return [void] 36 | # @example 37 | # MyApp::RakeHelper.install(gem_name: 'otherapp') 38 | def self.install(*args) 39 | new(*args).install 40 | end 41 | 42 | # Name of the gem to use in these Rake tasks. 43 | # @return [String] 44 | attr_reader :gem_name 45 | 46 | # Base folder of the gem. 47 | # @return [String] 48 | attr_reader :base 49 | 50 | # Helper options. 51 | # @return [Hash] 52 | attr_reader :options 53 | 54 | # @param gem_name [String] Name of the gem to use in these Rake tasks. 55 | # @param base [String] Base folder of the gem. 56 | # @options options [Boolean] no_color Forcibly disable using colors in the output. 57 | def initialize(gem_name: nil, base: nil, **options) 58 | @base = base || if defined?(::Rake) && ::Rake.original_dir 59 | ::Rake.original_dir 60 | else 61 | Dir.pwd 62 | end # rubocop:disable Lint/EndAlignment 63 | @gem_name = gem_name || find_gem_name(@base) 64 | @options = options 65 | end 66 | 67 | # Subclass hoook to provide the actual tasks or other helpers to install. 68 | # 69 | # @return [void] 70 | # @example 71 | # def install 72 | # extend Rake::DSL 73 | # desc 'My awesome task' 74 | # task 'mytask' do 75 | # # ... 76 | # end 77 | # end 78 | def install 79 | raise NotImplementedError 80 | end 81 | 82 | private 83 | 84 | # Return a Thor::Shell object based on output settings. 85 | # 86 | # @return [Thor::Shell::Basic] 87 | # @example 88 | # shell.say('Operation completed', :green) 89 | def shell 90 | @shell ||= if options[:no_color] || !STDOUT.tty? 91 | Thor::Shell::Basic 92 | else 93 | Thor::Base.shell 94 | end.new 95 | end 96 | 97 | # Search a directory for a .gemspec file to determine the gem name. 98 | # Returns nil if no gemspec is found. 99 | # 100 | # @param base [String] Folder to search. 101 | # @return [String, nil] 102 | def find_gem_name(base) 103 | spec = Dir[File.join(base, '*.gemspec')].first 104 | File.basename(spec, '.gemspec') if spec 105 | end 106 | 107 | # Gem specification for the current gem. 108 | # 109 | # @return [Gem::Specification] 110 | def gemspec 111 | @gemspec ||= begin 112 | raise Error.new("Unable to automatically determine gem name from specs in #{base}. Please set the gem name via #{self.class.name}.install_tasks(gem_name: 'name')") unless gem_name 113 | g = Bundler.load_gemspec(File.join(base, gem_name+'.gemspec')) 114 | # This is returning the path it would be in if installed normally, 115 | # override so we get the local path. Also for reasons that are entirely 116 | # beyond me, #tap makes Gem::Specification flip out so do it old-school. 117 | g.full_gem_path = base 118 | g 119 | end 120 | end 121 | 122 | # Cookbook model for the current gem. 123 | # 124 | # @return [Halite::Gem] 125 | def cookbook 126 | require 'halite/gem' 127 | @cookbook ||= Halite::Gem.new(gemspec) 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/halite/rake_helper.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'tmpdir' 18 | 19 | require 'chef/version' 20 | 21 | require 'halite' 22 | require 'halite/error' 23 | require 'halite/helper_base' 24 | 25 | 26 | module Halite 27 | # Helper class to install Halite rake tasks. 28 | # 29 | # @since 1.0.0 30 | # @example Rakefile 31 | # require 'halite/rake_helper' 32 | # Halite::RakeHelper.install 33 | class RakeHelper < HelperBase 34 | # Install all Rake tasks. 35 | # 36 | # @return [void] 37 | def install 38 | extend Rake::DSL 39 | # Core Halite tasks 40 | unless options[:no_gem] 41 | desc "Convert #{gemspec.name}-#{gemspec.version} to a cookbook in the pkg directory" 42 | task 'chef:build' do 43 | build_cookbook 44 | end 45 | 46 | desc "Push #{gemspec.name}-#{gemspec.version} to Supermarket" 47 | task 'chef:release' => ['chef:build'] do 48 | release_cookbook(pkg_path) 49 | end 50 | 51 | # Patch the core gem tasks to run ours too 52 | task 'build' => ['chef:build'] 53 | task 'release' => ['chef:release'] 54 | else 55 | desc "Push #{gem_name} to Supermarket" 56 | task 'chef:release' do 57 | release_cookbook(base) 58 | end 59 | end 60 | 61 | # Foodcritic doesn't have a config file, so just always try to add it. 62 | unless options[:no_foodcritic] 63 | install_foodcritic 64 | end 65 | 66 | # If a .kitchen.yml exists, install the Test Kitchen tasks. 67 | unless options[:no_kitchen] || !File.exist?(File.join(@base, '.kitchen.yml')) 68 | install_kitchen 69 | end 70 | end 71 | 72 | private 73 | 74 | def pkg_path 75 | @pkg_path ||= File.join(base, 'pkg', "#{gemspec.name}-#{gemspec.version}") 76 | end 77 | 78 | def install_foodcritic 79 | require 'foodcritic' 80 | 81 | desc 'Run Foodcritic linter' 82 | task 'chef:foodcritic' do 83 | foodcritic_cmd = "foodcritic --chef-version #{Chef::VERSION} --epic-fail any --tags ~FC054" 84 | foodcritic_config = File.join(base, '.foodcritic') 85 | if File.exist?(foodcritic_config) 86 | IO.readlines(foodcritic_config).each do |line| 87 | foodcritic_cmd << " --tags #{line.strip}" 88 | end 89 | end 90 | foodcritic_cmd << " '%{path}'" 91 | if options[:no_gem] 92 | sh(foodcritic_cmd % {path: base}) 93 | else 94 | Dir.mktmpdir('halite_test') do |path| 95 | Halite.convert(gemspec, path) 96 | sh(foodcritic_cmd % {path: path}) 97 | end 98 | end 99 | end 100 | 101 | add_test_task('chef:foodcritic') 102 | rescue LoadError 103 | task 'chef:foodcritic' do 104 | raise "Foodcritic is not available. You can use Halite::RakeHelper.install_tasks(no_foodcritic: true) to disable it." 105 | end 106 | end 107 | 108 | def install_kitchen 109 | desc 'Run all Test Kitchen tests' 110 | task 'chef:kitchen' do 111 | if ENV['KITCHEN_CONCURRENCY'] 112 | sh(*%W{kitchen test -d always -c #{ENV['KITCHEN_CONCURRENCY']}}) 113 | else 114 | sh(*%w{kitchen test -d always}) 115 | end 116 | end 117 | 118 | add_test_task('chef:kitchen') 119 | end 120 | 121 | def add_test_task(name) 122 | # Only set a description if the task doesn't already exist 123 | desc 'Run all tests' unless Rake.application.lookup('test') 124 | task :test => [name] 125 | end 126 | 127 | def build_cookbook 128 | # Make sure pkg/name-version exists and is empty 129 | FileUtils.mkdir_p(pkg_path) 130 | remove_files_in_folder(pkg_path) 131 | Halite.convert(gem_name, pkg_path) 132 | shell.say("#{gemspec.name} #{gemspec.version} converted to pkg/#{gemspec.name}-#{gemspec.version}/.", :green) 133 | end 134 | 135 | def release_cookbook(path) 136 | # To match the behavior of Bundler's release task w.r.t $gem_push. 137 | return if %w{n no nil false off 0}.include?(ENV['cookbook_push'].to_s.downcase) 138 | Dir.chdir(path) do 139 | sh('stove --no-git --extended-metadata') 140 | shell.say("Pushed #{gemspec.name} #{gemspec.version} to supermarket.chef.io.", :green) unless options[:no_gem] 141 | end 142 | end 143 | 144 | # Remove everything in a path, but not the directory itself 145 | def remove_files_in_folder(base_path) 146 | existing_files = Dir.glob(File.join(base_path, '**', '*'), File::FNM_DOTMATCH).map {|path| File.expand_path(path)}.uniq.reverse # expand_path just to normalize foo/. -> foo 147 | existing_files.delete(base_path) # Don't remove the base 148 | # Fuck FileUtils, it is a confusing pile of fail for remove*/rm* 149 | existing_files.each do |path| 150 | if File.file?(path) 151 | File.unlink(path) 152 | elsif File.directory?(path) 153 | Dir.unlink(path) 154 | else 155 | # Because paranoia 156 | raise Error.new("Unknown type of file at '#{path}', possible symlink deletion attack") 157 | end 158 | end 159 | end 160 | end 161 | end 162 | -------------------------------------------------------------------------------- /lib/halite/rake_tasks.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'halite/rake_helper' 18 | ::Halite::RakeHelper.install 19 | -------------------------------------------------------------------------------- /lib/halite/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # This must come first to ensure ChefSpec can patch in for matcher loading. 18 | require 'chefspec' 19 | 20 | # Fix load ordering bug in Chef 12.0.1. Remove this when dropping support for 12.0. 21 | require 'chef/providers' 22 | 23 | require 'chef/node' 24 | require 'chef/provider' 25 | require 'chef/resource' 26 | 27 | 28 | module Halite 29 | # A helper module for RSpec tests of resource-based cookbooks. 30 | # 31 | # @since 1.0.0 32 | # @example 33 | # describe MyMixin do 34 | # resource(:my_thing) do 35 | # include Poise 36 | # include MyMixin 37 | # action(:install) 38 | # attribute(:path, kind_of: String, default: '/etc/thing') 39 | # end 40 | # provider(:my_thing) do 41 | # include Poise 42 | # def action_install 43 | # file new_resource.path do 44 | # content new_resource.my_mixin 45 | # end 46 | # end 47 | # end 48 | # recipe do 49 | # my_thing 'test' 50 | # end 51 | # 52 | # it { is_expected.to create_file('/etc/thing').with(content: 'mixin stuff') } 53 | # end 54 | module SpecHelper 55 | autoload :Patcher, 'halite/spec_helper/patcher' 56 | autoload :Runner, 'halite/spec_helper/runner' 57 | extend RSpec::SharedContext 58 | def self.described_class; nil; end 59 | include ChefSpec::API 60 | 61 | # @!attribute [r] step_into 62 | # Resource names to step in to when running this example. 63 | # @see https://github.com/sethvargo/chefspec#testing-lwrps 64 | # @return [Array] 65 | # @example 66 | # before do 67 | # step_into << :my_lwrp 68 | # end 69 | let(:step_into) { [] } 70 | # @!attribute [r] default_attributes 71 | # Hash to use as default-level node attributes for this example. 72 | # @return [Hash] 73 | # @example 74 | # before do 75 | # default_attributes['myapp']['url'] = 'http://testserver' 76 | # end 77 | let(:default_attributes) { Hash.new } 78 | # @!attribute [r] normal_attributes 79 | # Hash to use as normal-level node attributes for this example. 80 | # @return [Hash] 81 | # @see #default_attributes 82 | let(:normal_attributes) { Hash.new } 83 | # @!attribute [r] override_attributes 84 | # Hash to use as override-level node attributes for this example. 85 | # @return [Hash] 86 | # @see #default_attributes 87 | let(:override_attributes) { Hash.new } 88 | # @todo docs 89 | let(:halite_gemspec) { nil } 90 | # @!attribute [r] chefspec_options 91 | # Options hash for the ChefSpec runner instance. 92 | # @return [Hash] 93 | # @example Enable Fauxhai attributes 94 | # let(:chefspec_options) { {platform: 'ubuntu', version: '12.04'} } 95 | let(:chefspec_options) { Hash.new } 96 | 97 | # Cross-link the Halite and ChefSpec data. 98 | let(:chefspec_platform) do 99 | chefspec_options[:platform] || 'ubuntu' 100 | end 101 | let(:chefspec_platform_version) do 102 | chefspec_options[:version] 103 | end 104 | 105 | # Merge in extra options data. 106 | def chef_runner_options 107 | super.tap do |options| 108 | options[:halite_gemspec] = halite_gemspec 109 | # And some legacy data. 110 | options[:default_attributes].update(default_attributes) 111 | options[:normal_attributes].update(normal_attributes) 112 | options[:override_attributes].update(override_attributes) 113 | options.update(chefspec_options) 114 | end 115 | end 116 | 117 | # Custom runner class. 118 | def chef_runner_class 119 | Halite::SpecHelper::Runner 120 | end 121 | 122 | # Don't try to converge any recipes, even by default. 123 | let(:chef_run) do 124 | chef_runner.converge 125 | end 126 | 127 | # An alias for slightly more semantic meaning, just forces the lazy #subject 128 | # to run. 129 | # 130 | # @see http://www.relishapp.com/rspec/rspec-core/v/3-2/docs/subject/explicit-subject RSpec's subject helper 131 | # @example 132 | # describe 'my recipe' do 133 | # recipe 'my_recipe' 134 | # it { run_chef } 135 | # end 136 | def run_chef 137 | chef_run 138 | end 139 | 140 | # Return a helper-defined resource. 141 | # 142 | # @param name [Symbol] Name of the resource. 143 | # @return [Class] 144 | # @example 145 | # subject { resource(:my_resource) } 146 | def resource(name) 147 | self.class.resources[name.to_sym] 148 | end 149 | 150 | # Return a helper-defined provider. 151 | # 152 | # @param name [Symbol] Name of the provider. 153 | # @return [Class] 154 | # @example 155 | # subject { provider(:my_provider) } 156 | def provider(name) 157 | self.class.providers[name.to_sym] 158 | end 159 | 160 | # @!classmethods 161 | module ClassMethods 162 | # Define a recipe to be run via ChefSpec and used as the subject of this 163 | # example group. You can specify either a single recipe block or 164 | # one-or-more recipe names. 165 | # 166 | # @param recipe_names [Array] Recipe names to converge for this test. 167 | # @param block [Proc] Recipe to converge for this test. 168 | # @param subject [Boolean] If true, this recipe should be the subject of 169 | # this test. 170 | # @example Using a recipe block 171 | # describe 'my recipe' do 172 | # recipe do 173 | # ruby_block 'test' 174 | # end 175 | # it { is_expected.to run_ruby_block('test') } 176 | # end 177 | # @example Using external recipes 178 | # describe 'my recipe' do 179 | # recipe 'my_recipe' 180 | # it { is_expected.to run_ruby_block('test') } 181 | # end 182 | def recipe(*recipe_names, subject: true, &block) 183 | # Keep the actual logic in a let in case I want to define the subject as something else 184 | let(:chef_run) { recipe_names.empty? ? chef_runner.converge_block(&block) : chef_runner.converge(*recipe_names) } 185 | subject { chef_run } if subject 186 | end 187 | 188 | # Configure ChefSpec to step in to a resource/provider. This will also 189 | # automatically create ChefSpec matchers for the resource. 190 | # 191 | # @overload step_into(name) 192 | # @param name [String, Symbol] Name of the resource in snake-case. 193 | # @overload step_into(resource, resource_name) 194 | # @param resource [Class] Resource class to step in to. 195 | # @param resource_name [String, Symbol, nil] Name of the given resource in snake-case. 196 | # @example 197 | # describe 'my_lwrp' do 198 | # step_into(:my_lwrp) 199 | # recipe do 200 | # my_lwrp 'test' 201 | # end 202 | # it { is_expected.to run_ruby_block('test') } 203 | # end 204 | def step_into(name=nil, resource_name=nil, unwrap_notifying_block: true) 205 | return super() if name.nil? 206 | resource_class = if name.is_a?(Class) 207 | name 208 | elsif resources[name.to_sym] 209 | # Handle cases where the resource has defined via a helper with 210 | # step_into:false but a nested example wants to step in. 211 | resources[name.to_sym] 212 | else 213 | # Won't see platform/os specific resources but not sure how to fix 214 | # that. I need the class here for the matcher creation below. 215 | Chef::Resource.resource_for_node(name.to_sym, Chef::Node.new) 216 | end 217 | resource_name ||= if resource_class.respond_to?(:resource_name) 218 | resource_class.resource_name 219 | else 220 | Chef::Mixin::ConvertToClassName.convert_to_snake_case(resource_class.name.split('::').last) 221 | end 222 | 223 | # Patch notifying_block from Poise::Provider to just run directly. 224 | # This is not a great solution but it is better than nothing for right 225 | # now. In the future this should maybe do an internal converge but using 226 | # ChefSpec somehow? 227 | if unwrap_notifying_block 228 | old_provider_for_action = resource_class.instance_method(:provider_for_action) 229 | resource_class.send(:define_method, :provider_for_action) do |*args| 230 | old_provider_for_action.bind(self).call(*args).tap do |provider| 231 | if provider.respond_to?(:notifying_block, true) 232 | provider.define_singleton_method(:notifying_block) do |&block| 233 | block.call 234 | end 235 | end 236 | end 237 | end 238 | end 239 | 240 | # Add to the let variable passed in to ChefSpec. 241 | super(resource_name) 242 | end 243 | 244 | # Define a resource class for use in an example group. By default the 245 | # :run action will be set as the default. 246 | # 247 | # @param name [Symbol] Name for the resource in snake-case. 248 | # @param options [Hash] Resource options. 249 | # @option options [Class, Symbol] :parent (Chef::Resource) Parent class 250 | # for the resource. If a symbol is given, it corresponds to another 251 | # resource defined via this helper. 252 | # @option options [Boolean] :auto (true) Set the resource name correctly 253 | # and use :run as the default action. 254 | # @param block [Proc] Body of the resource class. Optional. 255 | # @example 256 | # describe MyMixin do 257 | # resource(:my_resource) do 258 | # include Poise 259 | # attribute(:path, kind_of: String) 260 | # end 261 | # provider(:my_resource) 262 | # recipe do 263 | # my_resource 'test' do 264 | # path '/tmp' 265 | # end 266 | # end 267 | # it { is_expected.to run_my_resource('test').with(path: '/tmp') } 268 | # end 269 | def resource(name, auto: true, parent: Chef::Resource, step_into: true, unwrap_notifying_block: true, patch: true, defined_at: caller[0], &block) 270 | parent = resources[parent] if parent.is_a?(Symbol) 271 | raise Halite::Error.new("Parent class for #{name} is not a class: #{parent.inspect}") unless parent.is_a?(Class) 272 | # Pull out the example group for use in the class. 273 | example_group = self 274 | # Create the resource class. 275 | resource_class = Class.new(parent) do 276 | # Make the anonymous class pretend to have a name. 277 | define_singleton_method(:name) do 278 | 'Chef::Resource::' + Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s) 279 | end 280 | 281 | # Helper for debugging, shows where the class was defined. 282 | define_singleton_method(:halite_defined_at) do 283 | defined_at 284 | end 285 | 286 | # Create magic delegators for various metadata. 287 | { 288 | example_group: example_group, 289 | described_class: example_group.metadata[:described_class], 290 | }.each do |key, value| 291 | define_method(key) { value } 292 | define_singleton_method(key) { value } 293 | end 294 | 295 | # Evaluate the class body. 296 | class_exec(&block) if block 297 | 298 | # Optional initialization steps. Disable for special unicorn tests. 299 | if auto 300 | # Fill in a :run action by default. 301 | old_init = instance_method(:initialize) 302 | define_method(:initialize) do |*args| 303 | old_init.bind(self).call(*args) 304 | # Fill in the resource name because I know it, but don't 305 | # overwrite because a parent might have done this already. 306 | @resource_name = name.to_sym 307 | # ChefSpec doesn't seem to work well with action :nothing 308 | if Array(@action) == [:nothing] 309 | @action = :run 310 | @allowed_actions |= [:run] 311 | end 312 | if defined?(self.class.default_action) && Array(self.class.default_action) == [:nothing] 313 | self.class.default_action(:run) 314 | end 315 | end 316 | end 317 | end 318 | 319 | # Try to set the resource name for 12.4+. 320 | if defined?(resource_class.resource_name) 321 | resource_class.resource_name(name) 322 | end 323 | 324 | # Clean up any global registration that happens on class compile. 325 | Patcher.post_create_cleanup(name, resource_class) if patch 326 | 327 | # Store for use up with the parent system 328 | halite_helpers[:resources][name.to_sym] = resource_class 329 | 330 | # Automatically step in to our new resource 331 | step_into(resource_class, name, unwrap_notifying_block: unwrap_notifying_block) if step_into 332 | 333 | around do |ex| 334 | if patch && resource(name) == resource_class 335 | # We haven't been overridden from a nested scope. 336 | Patcher.patch(name, resource_class) { ex.run } 337 | else 338 | ex.run 339 | end 340 | end 341 | end 342 | 343 | # Define a provider class for use in an example group. By default a :run 344 | # action will be created, load_current_resource will be defined as a 345 | # no-op, and the RSpec matchers will be available inside the provider. 346 | # 347 | # @param name [Symbol] Name for the provider in snake-case. 348 | # @param options [Hash] Provider options. 349 | # @option options [Class, Symbol] :parent (Chef::Provider) Parent class 350 | # for the provider. If a symbol is given, it corresponds to another 351 | # resource defined via this helper. 352 | # @option options [Boolean] :auto (true) Create action_run and 353 | # load_current_resource. 354 | # @option options [Boolean] :rspec (true) Include RSpec matchers in the 355 | # provider. 356 | # @param block [Proc] Body of the provider class. Optional. 357 | # @example 358 | # describe MyMixin do 359 | # resource(:my_resource) 360 | # provider(:my_resource) do 361 | # include Poise 362 | # def action_run 363 | # ruby_block 'test' 364 | # end 365 | # end 366 | # recipe do 367 | # my_resource 'test' 368 | # end 369 | # it { is_expected.to run_my_resource('test') } 370 | # it { is_expected.to run_ruby_block('test') } 371 | # end 372 | def provider(name, auto: true, rspec: true, parent: Chef::Provider, patch: true, defined_at: caller[0], &block) 373 | parent = providers[parent] if parent.is_a?(Symbol) 374 | raise Halite::Error.new("Parent class for #{name} is not a class: #{parent.inspect}") unless parent.is_a?(Class) 375 | # Pull out the example group for use in the class. 376 | example_group = self 377 | # Create the provider class. 378 | provider_class = Class.new(parent) do 379 | # Pull in RSpec expectations. 380 | if rspec 381 | include RSpec::Matchers 382 | include RSpec::Mocks::ExampleMethods 383 | end 384 | 385 | if auto 386 | # Default blank impl to avoid error. 387 | def load_current_resource 388 | end 389 | 390 | # Blank action because I do that so much. 391 | def action_run 392 | end 393 | end 394 | 395 | # Make the anonymous class pretend to have a name. 396 | define_singleton_method(:name) do 397 | 'Chef::Provider::' + Chef::Mixin::ConvertToClassName.convert_to_class_name(name.to_s) 398 | end 399 | 400 | # Helper for debugging, shows where the class was defined. 401 | define_singleton_method(:halite_defined_at) do 402 | defined_at 403 | end 404 | 405 | # Create magic delegators for various metadata. 406 | { 407 | example_group: example_group, 408 | described_class: example_group.metadata[:described_class], 409 | }.each do |key, value| 410 | define_method(key) { value } 411 | define_singleton_method(key) { value } 412 | end 413 | 414 | # Evaluate the class body. 415 | class_exec(&block) if block 416 | end 417 | 418 | # Clean up any global registration that happens on class compile. 419 | Patcher.post_create_cleanup(name, provider_class) if patch 420 | 421 | # Store for use up with the parent system 422 | halite_helpers[:providers][name.to_sym] = provider_class 423 | 424 | around do |ex| 425 | if patch && provider(name) == provider_class 426 | # We haven't been overridden from a nested scope. 427 | Patcher.patch(name, provider_class) { ex.run } 428 | else 429 | ex.run 430 | end 431 | end 432 | end 433 | 434 | def included(klass) 435 | super 436 | klass.extend ClassMethods 437 | end 438 | 439 | # Storage for helper-defined resources and providers to find them for 440 | # parent lookups if needed. 441 | # 442 | # @api private 443 | # @return [Hash>] 444 | def halite_helpers 445 | @halite_helpers ||= {resources: {}, providers: {}} 446 | end 447 | 448 | # Find all helper-defined resources in the current context and parents. 449 | # 450 | # @api private 451 | # @return [Hash] 452 | def resources 453 | ([self] + parent_groups).reverse.inject({}) do |memo, group| 454 | begin 455 | memo.merge(group.halite_helpers[:resources] || {}) 456 | rescue NoMethodError 457 | memo 458 | end 459 | end 460 | end 461 | 462 | # Find all helper-defined providers in the current context and parents. 463 | # 464 | # @api private 465 | # @return [Hash] 466 | def providers 467 | ([self] + parent_groups).reverse.inject({}) do |memo, group| 468 | begin 469 | memo.merge(group.halite_helpers[:providers] || {}) 470 | rescue NoMethodError 471 | memo 472 | end 473 | end 474 | end 475 | end 476 | 477 | extend ClassMethods 478 | end 479 | 480 | # Method version of SpecHelper module. Used to inject a gem data to load 481 | # a synthetic cookbook during testing. 482 | # 483 | # @see Halite::SpecHelper 484 | # @param gemspec [Gem::Specification] Gem spec to load as cookbook. 485 | def self.SpecHelper(gemspec) 486 | # Create a new anonymous module 487 | mod = Module.new 488 | 489 | # Fake the name 490 | def mod.name 491 | super || 'Halite::SpecHelper' 492 | end 493 | 494 | mod.define_singleton_method(:included) do |klass| 495 | super(klass) 496 | # Pull in the main helper to cover most of the needed logic 497 | klass.class_exec do 498 | include Halite::SpecHelper 499 | let(:halite_gemspec) { gemspec } 500 | end 501 | end 502 | 503 | mod 504 | end 505 | end 506 | -------------------------------------------------------------------------------- /lib/halite/spec_helper/empty/README.md: -------------------------------------------------------------------------------- 1 | This folder is intentionally empty except for this README. It is used by Halite::SpecHelper::Runner. 2 | -------------------------------------------------------------------------------- /lib/halite/spec_helper/patcher.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/resource' 18 | require 'chef/version' 19 | 20 | 21 | module Halite 22 | module SpecHelper 23 | # Utility methods to patch a resource or provider class in to Chef for the 24 | # duration of a block. 25 | # 26 | # @since 1.0.0 27 | # @api private 28 | module Patcher 29 | # Patch a class in to Chef for the duration of a block. 30 | # 31 | # @param name [String, Symbol] Name to create in snake-case (eg. :my_name). 32 | # @param klass [Class] Class to patch in. 33 | # @param block [Proc] Block to execute while the patch is available. 34 | # @return [void] 35 | def self.patch(name, klass, &block) 36 | patch_handler_map(name, klass) do 37 | patch_recipe_dsl(name, klass) do 38 | block.call 39 | end 40 | end 41 | end 42 | 43 | # Perform any post-class-creation cleanup tasks to deal with compile time 44 | # global registrations. 45 | # 46 | # @since 1.0.4 47 | # @param name [String, Symbol] Name of the class that was created in 48 | # snake-case (eg. :my_name). 49 | # @param klass [Class] Newly created class. 50 | # @return [void] 51 | def self.post_create_cleanup(name, klass) 52 | # Remove from DSL. 53 | Chef::DSL::Resources.remove_resource_dsl(name) if defined?(Chef::DSL::Resources.remove_resource_dsl) 54 | # Remove from the handler map. 55 | {handler: handler_map_for(klass)}.each do |type, map| 56 | if map 57 | # Make sure we add name in there too because anonymous classes don't 58 | # get a handler map registration by default. 59 | removed_keys = remove_from_node_map(map, klass) | [name.to_sym] 60 | # This ivar is used down in #patch_*_map to re-add the correct 61 | # keys based on the class definition. 62 | klass.instance_variable_set(:"@halite_original_#{type}_keys", removed_keys) 63 | end 64 | end 65 | end 66 | 67 | # Patch a resource in to Chef's recipe DSL for the duration of a code 68 | # block. This is a no-op before Chef 12.4. 69 | # 70 | # @param name [Symbol] Name to patch in. 71 | # @param klass [Class] Resource class to patch in. 72 | # @param block [Proc] Block to execute while the patch is available. 73 | # @return [void] 74 | def self.patch_recipe_dsl(name, klass, &block) 75 | return block.call unless defined?(Chef::DSL::Resources.add_resource_dsl) && klass < Chef::Resource 76 | begin 77 | Chef::DSL::Resources.add_resource_dsl(name) 78 | block.call 79 | ensure 80 | Chef::DSL::Resources.remove_resource_dsl(name) 81 | end 82 | end 83 | 84 | # Patch a class in to the correct handler map for the duration of a code 85 | # block. This is a no-op before Chef 12.4.1. 86 | # 87 | # @since 1.0.7 88 | # @param name [Symbol] Name to patch in. 89 | # @param klass [Class] Resource or provider class to patch in. 90 | # @param block [Proc] Block to execute while the patch is available. 91 | # @return [void] 92 | def self.patch_handler_map(name, klass, &block) 93 | handler_map = handler_map_for(klass) 94 | return block.call unless handler_map 95 | begin 96 | klass.instance_variable_get(:@halite_original_handler_keys).each do |key| 97 | handler_map.set(key, klass) 98 | end 99 | block.call 100 | ensure 101 | remove_from_node_map(handler_map, klass) 102 | end 103 | end 104 | 105 | private 106 | 107 | # Find the global handler map for a class. 108 | # 109 | # @since 1.0.7 110 | # @param klass [Class] Resource or provider class to look up. 111 | # @return [nil, Chef::Platform::ResourceHandlerMap, Chef::Platform::ProviderHandlerMap] 112 | def self.handler_map_for(klass) 113 | if defined?(Chef.resource_handler_map) && klass < Chef::Resource 114 | Chef.resource_handler_map 115 | elsif defined?(Chef.provider_handler_map) && klass < Chef::Provider 116 | Chef.provider_handler_map 117 | end 118 | end 119 | 120 | # Remove a value from a Chef::NodeMap. Returns the keys that were removed. 121 | # 122 | # @since 1.0.4 123 | # @param node_map [Chef::NodeMap] Node map to remove from. 124 | # @param value [Object] Value to remove. 125 | # @return [Array] 126 | def self.remove_from_node_map(node_map, value) 127 | # Chef sometime after 13.7.16 supports Chef::NodeMap#delete_class 128 | return node_map.delete_class(value).keys if node_map.respond_to?(:delete_class) 129 | 130 | # Sigh. 131 | removed_keys = [] 132 | # 12.4.1+ switched this to a private accessor and lazy init. 133 | map = if node_map.respond_to?(:map, true) 134 | node_map.send(:map) 135 | else 136 | node_map.instance_variable_get(:@map) 137 | end 138 | map.each do |key, matchers| 139 | matchers.delete_if do |matcher| 140 | # in 13.7.16 the :value key in the hash was renamed to :klass 141 | vkey = matcher.key?(:klass) ? :klass : :value 142 | 143 | # In 12.4+ this value is an array of classes, before that it is the class. 144 | if matcher[vkey].is_a?(Array) 145 | matcher[vkey].include?(value) 146 | else 147 | matcher[vkey] == value 148 | end && removed_keys << key # Track removed keys in a hacky way. 149 | end 150 | # Clear empty matchers entirely. 151 | map.delete(key) if matchers.empty? 152 | end 153 | removed_keys 154 | end 155 | 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/halite/spec_helper/runner.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/mixin/shell_out' # ಠ_ಠ Missing upstream require 18 | require 'chef/recipe' 19 | require 'chefspec/mixins/normalize' # ಠ_ಠ Missing upstream require 20 | require 'chefspec/solo_runner' 21 | 22 | require 'halite/error' 23 | require 'halite/gem' 24 | 25 | 26 | module Halite 27 | module SpecHelper 28 | # ChefSpec runner class with Halite customizations. This adds attribute 29 | # options, Halite synthetic cookbook injection, and block-based recipes. 30 | # 31 | # @since 1.0.0 32 | class Runner < ChefSpec::SoloRunner 33 | def initialize(options={}) 34 | # Store the gemspec for later use 35 | @halite_gemspec = options[:halite_gemspec] 36 | super 37 | end 38 | 39 | def preload! 40 | end 41 | 42 | def converge(*args, &block) 43 | super(*args) do |node| 44 | add_halite_cookbooks(node, @halite_gemspec) if @halite_gemspec 45 | block.call(node) if block 46 | end 47 | end 48 | 49 | private 50 | 51 | def add_halite_cookbooks(node, gemspecs) 52 | Array(gemspecs).each do |gemspec| 53 | gem_data = Halite::Gem.new(gemspec) 54 | # Catch any dependency loops. 55 | next if run_context.cookbook_collection.include?(gem_data.cookbook_name) && run_context.cookbook_collection[gem_data.cookbook_name].respond_to?(:halite_root) 56 | run_context.cookbook_collection[gem_data.cookbook_name] = gem_data.as_cookbook_version 57 | gem_data.cookbook_dependencies.each do |dep| 58 | add_halite_cookbooks(node, dep.spec) if dep.spec 59 | end 60 | # Add to the compiler for RunContext#unreachable_cookbook? 61 | cookbook_order = run_context.instance_variable_get(:@cookbook_compiler).cookbook_order 62 | name_sym = gem_data.cookbook_name.to_sym 63 | cookbook_order << name_sym unless cookbook_order.include?(name_sym) 64 | # Load attributes if any. 65 | gem_data.each_file('chef/attributes') do |_full_path, rel_path| 66 | raise Halite::Error.new("Chef does not support nested attribute files: #{rel_path}") if rel_path.include?(File::SEPARATOR) 67 | name = File.basename(rel_path, '.rb') 68 | node.include_attribute("#{gem_data.cookbook_name}::#{name}") 69 | end 70 | end 71 | end 72 | 73 | # Override the normal cookbook loading behavior. 74 | def cookbook 75 | if @halite_gemspec 76 | halite_gem = Halite::Gem.new(Array(@halite_gemspec).first) 77 | Chef::Cookbook::Metadata.new.tap do |metadata| 78 | metadata.name(halite_gem.cookbook_name) 79 | end 80 | else 81 | super 82 | end 83 | end 84 | 85 | # Don't try to autodetect the calling cookbook. 86 | def calling_cookbook_path(*args) 87 | File.expand_path('../empty', __FILE__) 88 | end 89 | 90 | # Inject a better chefspec_cookbook_root option. 91 | def apply_chef_config! 92 | super 93 | if @halite_gemspec 94 | Chef::Config[:chefspec_cookbook_root] = Array(@halite_gemspec).first.full_gem_path 95 | end 96 | end 97 | 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /lib/halite/version.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | 18 | module Halite 19 | # Halite version. 20 | VERSION = '1.8.3.pre' 21 | end 22 | -------------------------------------------------------------------------------- /spec/converter/chef_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe Halite::Converter::Chef do 20 | describe '#write' do 21 | let(:files) { [] } 22 | let(:gem_data) do 23 | instance_double('Halite::Gem').tap do |d| 24 | allow(d).to receive(:each_file) do |&block| 25 | files.each {|path| block.call(File.join('/source', path), path) } 26 | end 27 | end 28 | end 29 | subject { described_class.write(gem_data, '/test') } 30 | 31 | context 'with a single file' do 32 | let(:files) { ['recipes/default.rb'] } 33 | 34 | it do 35 | expect(FileUtils).to receive(:mkdir_p).with('/test/recipes') 36 | expect(FileUtils).to receive(:copy).with('/source/recipes/default.rb', '/test/recipes/default.rb', preserve: true) 37 | subject 38 | end 39 | end # /context with a single file 40 | 41 | context 'with multiple files' do 42 | let(:files) { ['attributes.rb', 'recipes/default.rb', 'templates/default/conf.erb'] } 43 | 44 | it do 45 | expect(FileUtils).to receive(:mkdir_p).with('/test/recipes') 46 | expect(FileUtils).to receive(:mkdir_p).with('/test/templates/default') 47 | expect(FileUtils).to receive(:copy).with('/source/attributes.rb', '/test/attributes.rb', preserve: true) 48 | expect(FileUtils).to receive(:copy).with('/source/recipes/default.rb', '/test/recipes/default.rb', preserve: true) 49 | expect(FileUtils).to receive(:copy).with('/source/templates/default/conf.erb', '/test/templates/default/conf.erb', preserve: true) 50 | subject 51 | end 52 | end # /context with multiple files 53 | end # /describe #write 54 | end 55 | -------------------------------------------------------------------------------- /spec/converter/libraries_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | require 'halite/converter/libraries' 19 | 20 | describe Halite::Converter::Libraries do 21 | describe '#generate_bootstrap' do 22 | let(:gem_data) { instance_double('Halite::Gem', license_header: '') } 23 | let(:entry_points) { [] } 24 | subject { described_class.generate_bootstrap(gem_data, entry_points) } 25 | 26 | context 'with defaults' do 27 | it { is_expected.to eq <<-EOH } 28 | raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load] 29 | $LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__) 30 | EOH 31 | end # /context with defaults 32 | 33 | context 'with a license header' do 34 | before do 35 | allow(gem_data).to receive(:license_header).and_return("# Copyright me.\n") 36 | end 37 | it { is_expected.to eq <<-EOH } 38 | # Copyright me. 39 | raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load] 40 | $LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__) 41 | EOH 42 | end 43 | 44 | context 'with entry points' do 45 | let(:entry_points) { %w{mygem/one mygem/two} } 46 | it { is_expected.to eq <<-EOH } 47 | raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load] 48 | $LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__) 49 | require "mygem/one" 50 | require "mygem/two" 51 | EOH 52 | end # /context with entry points 53 | end # /describe #generate_bootstrap 54 | 55 | describe '#write_libraries' do 56 | let(:library_files) { [] } 57 | let(:gem_data) do 58 | instance_double('Halite::Gem').tap do |d| 59 | allow(d).to receive(:each_library_file) do |&block| 60 | library_files.each {|path| block.call(File.join('/source', path), path) } 61 | end 62 | end 63 | end 64 | subject { described_class.write_libraries(gem_data, '/test') } 65 | 66 | context 'with a single file' do 67 | let(:library_files) { %w{mygem.rb} } 68 | it do 69 | expect(FileUtils).to receive(:mkdir_p).with('/test/files/halite_gem') 70 | expect(FileUtils).to receive(:copy).with('/source/mygem.rb', '/test/files/halite_gem/mygem.rb', preserve: true) 71 | subject 72 | end 73 | end # /context with a single file 74 | 75 | context 'with a multiple files' do 76 | let(:library_files) { %w{mygem.rb mygem/one.rb mygem/two.rb} } 77 | it do 78 | expect(FileUtils).to receive(:mkdir_p).with('/test/files/halite_gem') 79 | expect(FileUtils).to receive(:mkdir_p).with('/test/files/halite_gem/mygem').twice 80 | expect(FileUtils).to receive(:copy).with('/source/mygem.rb', '/test/files/halite_gem/mygem.rb', preserve: true) 81 | expect(FileUtils).to receive(:copy).with('/source/mygem/one.rb', '/test/files/halite_gem/mygem/one.rb', preserve: true) 82 | expect(FileUtils).to receive(:copy).with('/source/mygem/two.rb', '/test/files/halite_gem/mygem/two.rb', preserve: true) 83 | subject 84 | end 85 | end # /context with a multiple files 86 | end # /describe #write_libraries 87 | 88 | describe '#find_default_entry_points' do 89 | let(:library_files) { [] } 90 | let(:gem_data) do 91 | instance_double('Halite::Gem').tap do |d| 92 | allow(d).to receive(:each_library_file) do |&block| 93 | library_files.each {|path| block.call(File.join('/source', path), path) } 94 | end 95 | end 96 | end 97 | subject { described_class.find_default_entry_points(gem_data) } 98 | 99 | context 'with no default entry points' do 100 | let(:library_files) { %w{mygem.rb} } 101 | it { is_expected.to eq [] } 102 | end # /context with no default entry points 103 | 104 | context 'with a single entry point' do 105 | let(:library_files) { %w{mygem.rb mygem/cheftie.rb} } 106 | it { is_expected.to eq ['mygem/cheftie'] } 107 | end # /context with a single entry point 108 | 109 | context 'with multiple entry points' do 110 | let(:library_files) { %w{mygem.rb mygem/cheftie.rb mygem/other.rb mygem/other/cheftie.rb} } 111 | it { is_expected.to eq ['mygem/cheftie', 'mygem/other/cheftie'] } 112 | end # /context with multiple entry points 113 | end # /describe #find_default_entry_points 114 | 115 | describe '#write_bootstrap' do 116 | let(:entry_point) { nil } 117 | let(:spec) { instance_double('Gem::Specification', metadata: {})} 118 | let(:gem_data) { instance_double('Halite::Gem', spec: spec) } 119 | let(:output) { double('output sentinel') } 120 | subject { described_class.write_bootstrap(gem_data, '/test', entry_point) } 121 | 122 | context 'with defaults' do 123 | it do 124 | expect(described_class).to receive(:find_default_entry_points).with(gem_data).and_return([]) 125 | expect(FileUtils).to receive(:mkdir_p).with('/test/libraries') 126 | expect(described_class).to receive(:generate_bootstrap).with(gem_data, []).and_return(output) 127 | expect(IO).to receive(:write).with('/test/libraries/default.rb', output) 128 | subject 129 | end 130 | end # /context with defaults 131 | 132 | context 'with an explicit entry point' do 133 | let(:entry_point) { 'mygem/one' } 134 | it do 135 | expect(FileUtils).to receive(:mkdir_p).with('/test/libraries') 136 | expect(described_class).to receive(:generate_bootstrap).with(gem_data, ['mygem/one']).and_return(output) 137 | expect(IO).to receive(:write).with('/test/libraries/default.rb', output) 138 | subject 139 | end 140 | end # /context with an explicit entry point 141 | 142 | context 'with metadata entry points' do 143 | it do 144 | allow(spec).to receive(:metadata).and_return({'halite_entry_point' => 'mygem/one mygem/two'}) 145 | expect(FileUtils).to receive(:mkdir_p).with('/test/libraries') 146 | expect(described_class).to receive(:generate_bootstrap).with(gem_data, ['mygem/one', 'mygem/two']).and_return(output) 147 | expect(IO).to receive(:write).with('/test/libraries/default.rb', output) 148 | subject 149 | end 150 | end # /context with metadata entry points 151 | end # /describe #write_bootstrap 152 | 153 | describe '#write' do 154 | it do 155 | expect(described_class).to receive(:write_libraries) 156 | expect(described_class).to receive(:write_bootstrap) 157 | described_class.write(nil, nil) 158 | end 159 | end # /describe #write 160 | end 161 | -------------------------------------------------------------------------------- /spec/converter/metadata_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe Halite::Converter::Metadata do 20 | describe '#generate' do 21 | let(:gem_name) { 'mygem' } 22 | let(:cookbook_name) { gem_name } 23 | let(:version) { '1.0.0' } 24 | let(:cookbook_version) { version } 25 | let(:issues_url) { nil } 26 | let(:cookbook_dependencies) { [] } 27 | let(:gem_metadata) { {} } 28 | let(:chef_version_requirement) { ['>= 12'] } 29 | let(:spec) do 30 | instance_double('Gem::Specification', 31 | author: nil, 32 | authors: [], 33 | description: '', 34 | email: nil, 35 | homepage: nil, 36 | license: nil, 37 | licenses: [], 38 | metadata: gem_metadata, 39 | ) 40 | end 41 | let(:gem_data) do 42 | instance_double('Halite::Gem', 43 | cookbook_dependencies: cookbook_dependencies.map {|dep| Halite::Dependencies::Dependency.new(*dep) }, 44 | cookbook_name: cookbook_name, 45 | find_misc_path: nil, 46 | license_header: '', 47 | name: gem_name, 48 | spec: spec, 49 | version: version, 50 | cookbook_version: cookbook_version, 51 | issues_url: issues_url, 52 | chef_version_requirement: chef_version_requirement, 53 | platforms: [], 54 | ) 55 | end 56 | subject { described_class.generate(gem_data) } 57 | 58 | context 'with simple data' do 59 | it { is_expected.to eq <<-EOH } 60 | name "mygem" 61 | version "1.0.0" 62 | chef_version ">= 12" if defined?(chef_version) 63 | EOH 64 | end # /context with simple data 65 | 66 | context 'with a license header' do 67 | before do 68 | allow(gem_data).to receive(:license_header).and_return("# header\n") 69 | end 70 | it { is_expected.to eq <<-EOH } 71 | # header 72 | name "mygem" 73 | version "1.0.0" 74 | chef_version ">= 12" if defined?(chef_version) 75 | EOH 76 | end # /context with a license header 77 | 78 | context 'with one dependency' do 79 | let(:cookbook_dependencies) { [['other', '>= 0']] } 80 | it { is_expected.to eq <<-EOH } 81 | name "mygem" 82 | version "1.0.0" 83 | depends "other" 84 | chef_version ">= 12" if defined?(chef_version) 85 | EOH 86 | end # /context with one dependency 87 | 88 | context 'with two dependencies' do 89 | let(:cookbook_dependencies) { [['other', '~> 1.0'], ['another', '~> 2.0.0']] } 90 | it { is_expected.to eq <<-EOH } 91 | name "mygem" 92 | version "1.0.0" 93 | depends "other", "~> 1.0" 94 | depends "another", "~> 2.0.0" 95 | chef_version ">= 12" if defined?(chef_version) 96 | EOH 97 | end # /context with two dependencies 98 | 99 | context 'with a description' do 100 | before do 101 | allow(spec).to receive(:description).and_return('My awesome library!') 102 | end 103 | 104 | it { is_expected.to eq <<-EOH } 105 | name "mygem" 106 | version "1.0.0" 107 | description "My awesome library!" 108 | chef_version ">= 12" if defined?(chef_version) 109 | EOH 110 | end # /context with a description 111 | 112 | context 'with a readme' do 113 | before do 114 | allow(gem_data).to receive(:find_misc_path).and_return('/source/README.md') 115 | allow(IO).to receive(:read).with('/source/README.md').and_return("My awesome readme!\nCopyright me.\n") 116 | end 117 | 118 | it { is_expected.to eq <<-EOH } 119 | name "mygem" 120 | version "1.0.0" 121 | long_description "My awesome readme!\\nCopyright me.\\n" 122 | chef_version ">= 12" if defined?(chef_version) 123 | EOH 124 | end # /context with a readme 125 | 126 | context 'with a chef_version' do 127 | let(:chef_version_requirement) { ['>= 0'] } 128 | 129 | it { is_expected.to eq <<-EOH } 130 | name "mygem" 131 | version "1.0.0" 132 | chef_version ">= 0" if defined?(chef_version) 133 | EOH 134 | end # /context with a chef_version 135 | 136 | context 'with a complex chef_version' do 137 | let(:chef_version_requirement) { ['< 15', '>= 12.5'] } 138 | 139 | it { is_expected.to eq <<-EOH } 140 | name "mygem" 141 | version "1.0.0" 142 | chef_version "< 15", ">= 12.5" if defined?(chef_version) 143 | EOH 144 | end # /context with a complex chef_version 145 | 146 | context 'with an issues_url' do 147 | let(:issues_url) { 'http://issues' } 148 | 149 | it { is_expected.to eq <<-EOH } 150 | name "mygem" 151 | version "1.0.0" 152 | issues_url "http://issues" if defined?(issues_url) 153 | chef_version ">= 12" if defined?(chef_version) 154 | EOH 155 | end # /context with an issues_url 156 | 157 | context 'with platforms' do 158 | before do 159 | allow(gem_data).to receive(:platforms).and_return([%w{ubuntu}, %w{debian}, %w{redhat}]) 160 | end 161 | 162 | it { is_expected.to eq <<-EOH } 163 | name "mygem" 164 | version "1.0.0" 165 | chef_version ">= 12" if defined?(chef_version) 166 | supports "ubuntu" 167 | supports "debian" 168 | supports "redhat" 169 | EOH 170 | end # /context with platforms 171 | 172 | context 'with complex platforms' do 173 | before do 174 | allow(gem_data).to receive(:platforms).and_return([['ubuntu', '>= 16.04'], ['debian', '>= 6', '< 9'], %w{redhat}]) 175 | end 176 | 177 | it { is_expected.to eq <<-EOH } 178 | name "mygem" 179 | version "1.0.0" 180 | chef_version ">= 12" if defined?(chef_version) 181 | supports "ubuntu", ">= 16.04" 182 | supports "debian", ">= 6", "< 9" 183 | supports "redhat" 184 | EOH 185 | end # /context with complex platforms 186 | end # /describe #generate 187 | 188 | describe '#write' do 189 | let(:output) { double('output sentinel') } 190 | before { allow(described_class).to receive(:generate).and_return(output) } 191 | 192 | it 'should write out metadata' do 193 | expect(IO).to receive(:write).with('/test/metadata.rb', output) 194 | described_class.write(nil, '/test') 195 | end 196 | end # /describe #write 197 | end 198 | -------------------------------------------------------------------------------- /spec/converter/misc_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe Halite::Converter::Misc do 20 | describe '#write' do 21 | let(:files) { [] } 22 | let(:gem_data) do 23 | instance_double('Halite::Gem').tap do |d| 24 | allow(d).to receive(:find_misc_path) do |name| 25 | if files.include?(name) 26 | "/source/#{name}" 27 | else 28 | nil 29 | end 30 | end 31 | end 32 | end 33 | subject { described_class.write(gem_data, '/test') } 34 | 35 | context 'with no files' do 36 | it do 37 | expect(FileUtils).to_not receive(:copy) 38 | subject 39 | end 40 | end # /context with no files 41 | 42 | context 'with a Readme' do 43 | let(:files) { 'Readme' } 44 | 45 | it do 46 | expect(FileUtils).to receive(:copy).with('/source/Readme', '/test/Readme', preserve: true) 47 | subject 48 | end 49 | end # /context with a Readme 50 | 51 | context 'with multiple files' do 52 | let(:files) { %w{Readme License} } 53 | 54 | it 'writes out a README' do 55 | expect(FileUtils).to receive(:copy).with('/source/Readme', '/test/Readme', preserve: true) 56 | expect(FileUtils).to receive(:copy).with('/source/License', '/test/License', preserve: true) 57 | subject 58 | end 59 | end # /context with multiple files 60 | end # /describe #write 61 | end 62 | -------------------------------------------------------------------------------- /spec/converter_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe Halite::Converter do 20 | describe '#write' do 21 | it do 22 | expect(Halite::Converter::Metadata).to receive(:write) 23 | expect(Halite::Converter::Libraries).to receive(:write) 24 | expect(Halite::Converter::Chef).to receive(:write) 25 | expect(Halite::Converter::Misc).to receive(:write) 26 | described_class.write(nil, '/test') 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/dependencies_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | require 'halite/dependencies' 19 | 20 | describe Halite::Dependencies do 21 | def fake_gem(name='name', version='1.0.0', &block) 22 | Gem::Specification.new do |s| 23 | s.name = name 24 | s.version = Gem::Version.new(version) 25 | block.call(s) if block 26 | end 27 | end 28 | 29 | let(:gem_stubs) do 30 | [ 31 | fake_gem('gem1'), 32 | fake_gem('gem2') {|s| s.requirements << 'dep2' }, 33 | fake_gem('gem3') {|s| s.add_dependency 'halite' }, 34 | ] 35 | end 36 | 37 | before do 38 | allow(Gem::Specification).to receive(:stubs).and_return(gem_stubs) 39 | allow(Gem::Specification).to receive(:stubs_for) do |name| 40 | gem_stubs.select {|stub| stub.name == name } 41 | end 42 | end 43 | 44 | describe '#extract_from_requirements' do 45 | let(:requirements) { [] } 46 | subject { described_class.extract_from_requirements(fake_gem {|s| s.requirements += requirements }) } 47 | 48 | context 'with []' do 49 | it { is_expected.to eq [] } 50 | end 51 | 52 | context 'with [req1]' do 53 | let(:requirements) { ['req1'] } 54 | it { is_expected.to eq ['req1'] } 55 | end 56 | 57 | context 'with [req1, req2]' do 58 | let(:requirements) { ['req1', 'req2'] } 59 | it { is_expected.to eq ['req1', 'req2'] } 60 | end 61 | end # /describe #extract_from_requirements 62 | 63 | 64 | describe '#extract_from_metadata' do 65 | let(:metadata) { nil } 66 | subject { described_class.extract_from_metadata(fake_gem {|s| s.metadata = {'halite_dependencies' => metadata} if metadata }) } 67 | 68 | context 'with no metadata' do 69 | it { is_expected.to eq [] } 70 | end 71 | 72 | context 'with req1' do 73 | let(:metadata) { 'req1' } 74 | it { is_expected.to eq ['req1'] } 75 | end 76 | 77 | context 'with req1,req2' do 78 | let(:metadata) { 'req1,req2' } 79 | it { is_expected.to eq ['req1', 'req2'] } 80 | end 81 | end # /describe #extract_from_metadata 82 | 83 | describe '#extract_from_dependencies' do 84 | let(:gemspec) { } 85 | subject { described_class.extract_from_dependencies(gemspec) } 86 | 87 | context 'with a halite-ish dependency' do 88 | let(:gemspec) { fake_gem {|s| s.add_dependency 'gem3' } } 89 | it { is_expected.to eq [['gem3', '>= 0', gem_stubs[2]]] } 90 | end 91 | 92 | context 'with a development dependency' do 93 | let(:gemspec) { fake_gem {|s| s.add_development_dependency 'gem3' } } 94 | it { is_expected.to eq [] } 95 | end 96 | 97 | context 'with a non-halite dependency' do 98 | let(:gemspec) { fake_gem {|s| s.add_development_dependency 'gem1' } } 99 | it { is_expected.to eq [] } 100 | end 101 | 102 | context 'with development mode' do 103 | subject { described_class.extract_from_dependencies(gemspec, development: true) } 104 | 105 | context 'with a halite-ish dependency' do 106 | let(:gemspec) { fake_gem {|s| s.add_dependency 'gem3' } } 107 | it { is_expected.to eq [['gem3', '>= 0', gem_stubs[2]]] } 108 | end 109 | 110 | context 'with a development dependency' do 111 | let(:gemspec) { fake_gem {|s| s.add_development_dependency 'gem3' } } 112 | it { is_expected.to eq [['gem3', '>= 0', gem_stubs[2]]] } 113 | end 114 | 115 | context 'with a non-halite dependency' do 116 | let(:gemspec) { fake_gem {|s| s.add_development_dependency 'gem1' } } 117 | it { is_expected.to eq [] } 118 | end 119 | end # /context with development mode 120 | end # /describe #extract_from_dependencies 121 | 122 | describe '#clean' do 123 | let(:dependency) { nil } 124 | subject { described_class.clean(dependency) } 125 | before { allow(described_class).to receive(:clean_requirement) {|arg| arg.to_s } } 126 | 127 | context 'with name' do 128 | let(:dependency) { 'name' } 129 | it { is_expected.to eq ['name', '>= 0'] } 130 | end 131 | 132 | context 'with name 1.0.0' do 133 | let(:dependency) { 'name 1.0.0' } 134 | it { is_expected.to eq ['name', '1.0.0'] } 135 | end 136 | 137 | context 'with name = 1.0.0' do 138 | let(:dependency) { 'name = 1.0.0' } 139 | it { is_expected.to eq ['name', '= 1.0.0'] } 140 | end 141 | 142 | context 'with [name]' do 143 | let(:dependency) { ['name'] } 144 | it { is_expected.to eq ['name', '>= 0'] } 145 | end 146 | 147 | context 'with [name, = 1.0.0]' do 148 | let(:dependency) { ['name', '= 1.0.0'] } 149 | it { is_expected.to eq ['name', '= 1.0.0'] } 150 | end 151 | 152 | context 'with [name, = 1.0.0, = 1.0.0]' do 153 | let(:dependency) { ['name', '= 1.0.0', '= 1.0.0'] } 154 | it { expect { subject }.to raise_error Halite::InvalidDependencyError } 155 | end 156 | end # /describe #clean 157 | 158 | describe '#clean_requirement' do 159 | let(:requirement) { nil } 160 | subject { described_class.clean_requirement(requirement) } 161 | before { allow(described_class).to receive(:clean_version) {|arg| arg } } 162 | 163 | context 'with = 1.0.0' do 164 | let(:requirement) { '= 1.0.0' } 165 | it { is_expected.to eq '= 1.0.0' } 166 | end 167 | 168 | context 'with 1.0.0' do 169 | let(:requirement) { '1.0.0' } 170 | it { is_expected.to eq '= 1.0.0' } 171 | end 172 | 173 | context 'with = 1.0.0' do 174 | let(:requirement) { '= 1.0.0' } 175 | it { is_expected.to eq '= 1.0.0' } 176 | end 177 | 178 | context 'with =1.0.0' do 179 | let(:requirement) { '=1.0.0' } 180 | it { is_expected.to eq '= 1.0.0' } 181 | end 182 | 183 | context 'with ~> 1.0.0' do 184 | let(:requirement) { '~> 1.0.0' } 185 | it { is_expected.to eq '~> 1.0.0' } 186 | end 187 | 188 | context 'with >= 1.0.0' do 189 | let(:requirement) { '>= 1.0.0' } 190 | it { is_expected.to eq '>= 1.0.0' } 191 | end 192 | 193 | context 'with <= 1.0.0' do 194 | let(:requirement) { '<= 1.0.0' } 195 | it { is_expected.to eq '<= 1.0.0' } 196 | end 197 | end # /describe #clean_requirement 198 | 199 | describe '#clean_version' do 200 | let(:version) { nil } 201 | subject { described_class.clean_version(::Gem::Version.new(version)).to_s } 202 | 203 | context 'with 1.0.0' do 204 | let(:version) { '1.0.0' } 205 | it { is_expected.to eq '1.0.0' } 206 | end 207 | 208 | context 'with 1.0' do 209 | let(:version) { '1.0' } 210 | it { is_expected.to eq '1.0' } 211 | end 212 | 213 | context 'with 1' do 214 | let(:version) { '1' } 215 | it { is_expected.to eq '1.0' } 216 | end 217 | 218 | context 'with 0' do 219 | let(:version) { '0' } 220 | it { is_expected.to eq '0' } 221 | end 222 | 223 | context 'with 0.0' do 224 | let(:version) { '0.0' } 225 | it { is_expected.to eq '0' } 226 | end 227 | 228 | context 'with 1.0.a' do 229 | let(:version) { '1.0.a' } 230 | it { is_expected.to eq '1.0' } 231 | end 232 | 233 | context 'with 2.3.rc.0' do 234 | let(:version) { '2.3.rc.0' } 235 | it { is_expected.to eq '2.3' } 236 | end 237 | 238 | context 'with 1.2.3.4' do 239 | let(:version) { '1.2.3.4' } 240 | it { expect { subject }.to raise_error Halite::InvalidDependencyError } 241 | end 242 | 243 | context 'with 1.2.3' do 244 | let(:version) { '1.2.3' } 245 | it { is_expected.to eq '1.2.3' } 246 | end 247 | end # /describe #clean_version 248 | end 249 | -------------------------------------------------------------------------------- /spec/example_resources/custom.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/resource' 18 | require 'chef/provider' 19 | 20 | 21 | # A `halite_test_custom` resource for use in Halite's unit tests. 22 | class HaliteTestCustom < Chef::Resource 23 | resource_name(:halite_test_custom) 24 | action(:run) do 25 | ruby_block new_resource.name do 26 | block { } 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/example_resources/poise.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/resource' 18 | require 'chef/provider' 19 | require 'poise' 20 | 21 | 22 | # A `halite_test_poise` resource for use in Halite's unit tests. 23 | module HaliteTestPoise 24 | class Resource < Chef::Resource 25 | include Poise 26 | provides(:halite_test_poise) 27 | actions(:run) 28 | end 29 | 30 | class Provider < Chef::Provider 31 | include Poise 32 | provides(:halite_test_poise) 33 | 34 | def action_run 35 | notifying_block do 36 | ruby_block new_resource.name do 37 | block { } 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/example_resources/simple.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'chef/resource' 18 | require 'chef/provider' 19 | 20 | 21 | # A `halite_test_simple` resource for use in Halite's unit tests. 22 | class Chef 23 | class Resource::HaliteTestSimple < Resource 24 | resource_name(:halite_test_simple) 25 | # actions(:run) 26 | default_action(:run) 27 | end 28 | 29 | class Provider::HaliteTestSimple < Provider 30 | provides(:halite_test_simple) 31 | include Chef::DSL::Recipe 32 | 33 | def load_current_resource 34 | end 35 | 36 | def action_run 37 | ruby_block new_resource.name do 38 | block { } 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test1/files/halite_gem/test1.rb: -------------------------------------------------------------------------------- 1 | module Test1 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test1/files/halite_gem/test1/version.rb: -------------------------------------------------------------------------------- 1 | module Test1 2 | VERSION = '1.2.3' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test1/libraries/default.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Awesome license 3 | raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load] 4 | $LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__) 5 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test1/metadata.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Awesome license 3 | name "test1" 4 | version "1.2.3" 5 | maintainer "Noah Kantrowitz" 6 | maintainer_email "noah@coderanger.net" 7 | source_url "http://example.com/" if defined?(source_url) 8 | license "Apache 2.0" 9 | chef_version ">= 12" if defined?(chef_version) 10 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test2/attributes.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poise/halite/9ae174e6b7c5d4674f3301394e14567fa89a8b3e/spec/fixtures/cookbooks/test2/attributes.rb -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test2/files/halite_gem/test2.rb: -------------------------------------------------------------------------------- 1 | require 'test2/resource' 2 | 3 | module Test2 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test2/files/halite_gem/test2/resource.rb: -------------------------------------------------------------------------------- 1 | require 'chef/resource' 2 | 3 | module Test2 4 | class Resource < Chef::Resource 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test2/files/halite_gem/test2/version.rb: -------------------------------------------------------------------------------- 1 | module Test2 2 | VERSION = '4.5.6' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test2/libraries/default.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load] 3 | $LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__) 4 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test2/metadata.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | name "test2" 3 | version "4.5.6" 4 | maintainer "Noah Kantrowitz" 5 | maintainer_email "noah@coderanger.net" 6 | source_url "http://example.com/" if defined?(source_url) 7 | license "Apache 2.0" 8 | depends "testdep" 9 | chef_version ">= 12" if defined?(chef_version) 10 | supports "ubuntu" 11 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test2/recipes/default.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poise/halite/9ae174e6b7c5d4674f3301394e14567fa89a8b3e/spec/fixtures/cookbooks/test2/recipes/default.rb -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test2/templates/default/conf.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poise/halite/9ae174e6b7c5d4674f3301394e14567fa89a8b3e/spec/fixtures/cookbooks/test2/templates/default/conf.erb -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test3/files/halite_gem/test3.rb: -------------------------------------------------------------------------------- 1 | require 'test3/dsl' 2 | 3 | module Test3 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test3/files/halite_gem/test3/dsl.rb: -------------------------------------------------------------------------------- 1 | require 'test2/version' 2 | 3 | module Test3 4 | module DSL 5 | def test3_method 6 | "!!!!!!!!!!test3#{Test2::VERSION}" 7 | end 8 | end 9 | end 10 | 11 | class Chef 12 | class Recipe 13 | include Test3::DSL 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test3/files/halite_gem/test3/version.rb: -------------------------------------------------------------------------------- 1 | module Test3 2 | VERSION = '7.8.9' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test3/libraries/default.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load] 3 | $LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__) 4 | require "test3/dsl" 5 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test3/metadata.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | name "test3" 3 | version "7.8.9" 4 | maintainer "Noah Kantrowitz" 5 | maintainer_email "noah@coderanger.net" 6 | source_url "http://example.com/" if defined?(source_url) 7 | license "Apache 2.0" 8 | depends "test2", "~> 4.5.6" 9 | chef_version ">= 3" if defined?(chef_version) 10 | supports "ubuntu", ">= 16.04" 11 | supports "redhat" 12 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test3/recipes/default.rb: -------------------------------------------------------------------------------- 1 | log test3_method 2 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test4/files/halite_gem/test4.rb: -------------------------------------------------------------------------------- 1 | module Test4 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test4/files/halite_gem/test4/version.rb: -------------------------------------------------------------------------------- 1 | module Test4 2 | VERSION = '2.3.1.rc.1' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test4/libraries/default.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load] 3 | $LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__) 4 | -------------------------------------------------------------------------------- /spec/fixtures/cookbooks/test4/metadata.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | name "test4" 3 | version "2.3.1" 4 | maintainer "Noah Kantrowitz" 5 | maintainer_email "noah@coderanger.net" 6 | source_url "http://example.com/" if defined?(source_url) 7 | issues_url "http://issues" if defined?(issues_url) 8 | license "Apache 2.0" 9 | chef_version "< 99", ">= 1" if defined?(chef_version) 10 | supports "ubuntu" 11 | supports "debian" 12 | supports "centos" 13 | supports "redhat" 14 | supports "fedora" 15 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test1/Rakefile: -------------------------------------------------------------------------------- 1 | require 'halite/rake_tasks' 2 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test1/lib/test1.rb: -------------------------------------------------------------------------------- 1 | module Test1 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test1/lib/test1/version.rb: -------------------------------------------------------------------------------- 1 | module Test1 2 | VERSION = '1.2.3' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test1/test1.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # Awesome license 3 | lib = File.expand_path('../lib', __FILE__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'test1/version' 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = 'test1' 9 | spec.version = Test1::VERSION 10 | spec.authors = ['Noah Kantrowitz'] 11 | spec.email = %w{noah@coderanger.net} 12 | spec.description = %q|| 13 | spec.summary = %q|| 14 | spec.homepage = 'http://example.com/' 15 | spec.license = 'Apache 2.0' 16 | 17 | spec.files = `cd #{File.expand_path('..', __FILE__)} && git ls-files`.split($/) 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = %w{lib} 21 | 22 | spec.add_dependency 'halite' 23 | 24 | spec.add_development_dependency 'rake' 25 | end 26 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test2/Rakefile: -------------------------------------------------------------------------------- 1 | require 'halite/rake_tasks' 2 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test2/chef/attributes.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poise/halite/9ae174e6b7c5d4674f3301394e14567fa89a8b3e/spec/fixtures/gems/test2/chef/attributes.rb -------------------------------------------------------------------------------- /spec/fixtures/gems/test2/chef/recipes/default.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poise/halite/9ae174e6b7c5d4674f3301394e14567fa89a8b3e/spec/fixtures/gems/test2/chef/recipes/default.rb -------------------------------------------------------------------------------- /spec/fixtures/gems/test2/chef/templates/default/conf.erb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poise/halite/9ae174e6b7c5d4674f3301394e14567fa89a8b3e/spec/fixtures/gems/test2/chef/templates/default/conf.erb -------------------------------------------------------------------------------- /spec/fixtures/gems/test2/lib/test2.rb: -------------------------------------------------------------------------------- 1 | require 'test2/resource' 2 | 3 | module Test2 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test2/lib/test2/resource.rb: -------------------------------------------------------------------------------- 1 | require 'chef/resource' 2 | 3 | module Test2 4 | class Resource < Chef::Resource 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test2/lib/test2/version.rb: -------------------------------------------------------------------------------- 1 | module Test2 2 | VERSION = '4.5.6' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test2/test2.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'test2/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'test2' 8 | spec.version = Test2::VERSION 9 | spec.authors = ['Noah Kantrowitz'] 10 | spec.email = %w{noah@coderanger.net} 11 | spec.description = %q|| 12 | spec.summary = %q|| 13 | spec.homepage = 'http://example.com/' 14 | spec.license = 'Apache 2.0' 15 | spec.metadata['platforms'] = 'ubuntu' 16 | 17 | spec.files = `cd #{File.expand_path('..', __FILE__)} && git ls-files`.split($/) 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = %w{lib} 21 | 22 | spec.add_dependency 'halite' 23 | spec.add_development_dependency 'rake' 24 | spec.requirements = %w{testdep} 25 | end 26 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test3/Rakefile: -------------------------------------------------------------------------------- 1 | require 'halite/rake_tasks' 2 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test3/chef/recipes/default.rb: -------------------------------------------------------------------------------- 1 | log test3_method 2 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test3/lib/test3.rb: -------------------------------------------------------------------------------- 1 | require 'test3/dsl' 2 | 3 | module Test3 4 | end 5 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test3/lib/test3/dsl.rb: -------------------------------------------------------------------------------- 1 | require 'test2/version' 2 | 3 | module Test3 4 | module DSL 5 | def test3_method 6 | "!!!!!!!!!!test3#{Test2::VERSION}" 7 | end 8 | end 9 | end 10 | 11 | class Chef 12 | class Recipe 13 | include Test3::DSL 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test3/lib/test3/version.rb: -------------------------------------------------------------------------------- 1 | module Test3 2 | VERSION = '7.8.9' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test3/test3.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'test3/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'test3' 8 | spec.version = Test3::VERSION 9 | spec.authors = ['Noah Kantrowitz'] 10 | spec.email = %w{noah@coderanger.net} 11 | spec.description = %q|| 12 | spec.summary = %q|| 13 | spec.homepage = 'http://example.com/' 14 | spec.license = 'Apache 2.0' 15 | spec.metadata['halite_entry_point'] = 'test3/dsl' 16 | spec.metadata['halite_chef_version'] = '>= 3' 17 | spec.metadata['platforms'] = 'ubuntu >= 16.04, redhat' 18 | 19 | spec.files = `cd #{File.expand_path('..', __FILE__)} && git ls-files`.split($/) 20 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 21 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 22 | spec.require_paths = %w{lib} 23 | 24 | spec.add_dependency 'halite' 25 | spec.add_dependency 'test2', '~> 4.5.6' 26 | spec.add_development_dependency 'rake' 27 | end 28 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test4/Rakefile: -------------------------------------------------------------------------------- 1 | require 'halite/rake_tasks' 2 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test4/lib/test4.rb: -------------------------------------------------------------------------------- 1 | module Test4 2 | end 3 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test4/lib/test4/version.rb: -------------------------------------------------------------------------------- 1 | module Test4 2 | VERSION = '2.3.1.rc.1' 3 | end 4 | -------------------------------------------------------------------------------- /spec/fixtures/gems/test4/test4.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'test4/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'test4' 8 | spec.version = Test4::VERSION 9 | spec.authors = ['Noah Kantrowitz'] 10 | spec.email = %w{noah@coderanger.net} 11 | spec.description = %q|| 12 | spec.summary = %q|| 13 | spec.homepage = 'http://example.com/' 14 | spec.license = 'Apache 2.0' 15 | spec.metadata['issues_url'] = 'http://issues' 16 | spec.metadata['platforms'] = 'ubuntu debian centos redhat fedora' 17 | 18 | spec.files = `cd #{File.expand_path('..', __FILE__)} && git ls-files`.split($/) 19 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 20 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 21 | spec.require_paths = %w{lib} 22 | 23 | spec.add_dependency 'halite' 24 | spec.add_dependency 'chef', '>= 1', '< 99' 25 | 26 | spec.add_development_dependency 'rake' 27 | spec.add_development_dependency 'test2', '~> 4.5.6' 28 | end 29 | -------------------------------------------------------------------------------- /spec/gem_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | require 'halite/gem' 19 | require 'halite/version' 20 | 21 | describe Halite::Gem do 22 | subject { described_class.new(gem_name, gem_version) } 23 | let(:gem_name) { 'halite' } 24 | let(:gem_version) { nil } 25 | 26 | # Because the default uses .inspect which is spammy. 27 | RSpec::Matchers.define :quiet_be_a do |expected| 28 | match {|actual| actual.is_a?(expected) } 29 | description { "be a #{expected.name}" } 30 | end 31 | 32 | context 'when loading halite' do 33 | its(:name) { is_expected.to eq 'halite' } 34 | its(:cookbook_name) { is_expected.to eq 'halite' } 35 | its(:version) { is_expected.to eq Halite::VERSION } 36 | its(:spec) { is_expected.to quiet_be_a Gem::Specification } 37 | its(:issues_url) { is_expected.to eq 'https://github.com/poise/halite/issues' } 38 | its(:platforms) { is_expected.to eq [] } 39 | end 40 | 41 | context 'when loading halite with a version' do 42 | let(:gem_version) { Halite::VERSION } 43 | its(:name) { is_expected.to eq 'halite' } 44 | its(:cookbook_name) { is_expected.to eq 'halite' } 45 | its(:version) { is_expected.to eq Halite::VERSION } 46 | its(:spec) { is_expected.to quiet_be_a Gem::Specification } 47 | end 48 | 49 | context 'when loading rspec' do 50 | let(:gem_name) { 'rspec' } 51 | its(:name) { is_expected.to eq 'rspec' } 52 | its(:spec) { is_expected.to quiet_be_a Gem::Specification } 53 | its(:is_halite_cookbook?) { is_expected.to be_falsey } 54 | end 55 | 56 | context 'when loading test1' do 57 | let(:gem_name) { 'test1' } 58 | its(:name) { is_expected.to eq 'test1' } 59 | its(:cookbook_name) { is_expected.to eq 'test1' } 60 | its(:version) { is_expected.to eq '1.2.3' } 61 | its(:cookbook_version) { is_expected.to eq '1.2.3' } 62 | its(:license_header) { is_expected.to eq "# coding: utf-8\n# Awesome license\n" } 63 | its(:each_library_file) { is_expected.to eq [ 64 | [File.expand_path('../fixtures/gems/test1/lib/test1.rb', __FILE__), 'test1.rb'], 65 | [File.expand_path('../fixtures/gems/test1/lib/test1/version.rb', __FILE__), 'test1/version.rb'], 66 | ] } 67 | its(:cookbook_dependencies) { is_expected.to eq [] } 68 | its(:is_halite_cookbook?) { is_expected.to be_truthy } 69 | its(:issues_url) { is_expected.to be_nil } 70 | its(:chef_version_requirement) { is_expected.to eq ['>= 12'] } 71 | its(:platforms) { is_expected.to eq [] } 72 | 73 | describe '#each_file' do 74 | context 'with no prefixes' do 75 | it 'returns all files' do 76 | expect(subject.each_file).to eq [ 77 | [File.expand_path('../fixtures/gems/test1/Rakefile', __FILE__), 'Rakefile'], 78 | [File.expand_path('../fixtures/gems/test1/lib/test1.rb', __FILE__), 'lib/test1.rb'], 79 | [File.expand_path('../fixtures/gems/test1/lib/test1/version.rb', __FILE__), 'lib/test1/version.rb'], 80 | [File.expand_path('../fixtures/gems/test1/test1.gemspec', __FILE__), 'test1.gemspec'], 81 | ] 82 | end 83 | end 84 | 85 | context 'with a prefix that overlaps a filename' do 86 | it 'returns only files in that folder' do 87 | expect(subject.each_file('lib/test1')).to eq [ 88 | [File.expand_path('../fixtures/gems/test1/lib/test1/version.rb', __FILE__), 'version.rb'], 89 | ] 90 | end 91 | end 92 | end 93 | end # /context when loading test1 94 | 95 | context 'when loading test2' do 96 | let(:gem_name) { 'test2' } 97 | its(:name) { is_expected.to eq 'test2' } 98 | its(:cookbook_name) { is_expected.to eq 'test2' } 99 | its(:version) { is_expected.to eq '4.5.6' } 100 | its(:cookbook_version) { is_expected.to eq '4.5.6' } 101 | its(:license_header) { is_expected.to eq "# coding: utf-8\n" } 102 | its(:each_library_file) { is_expected.to eq [ 103 | [File.expand_path('../fixtures/gems/test2/lib/test2.rb', __FILE__), 'test2.rb'], 104 | [File.expand_path('../fixtures/gems/test2/lib/test2/resource.rb', __FILE__), 'test2/resource.rb'], 105 | [File.expand_path('../fixtures/gems/test2/lib/test2/version.rb', __FILE__), 'test2/version.rb'], 106 | ] } 107 | its(:cookbook_dependencies) { is_expected.to eq [Halite::Dependencies::Dependency.new('testdep', '>= 0', :requirements)] } 108 | its(:is_halite_cookbook?) { is_expected.to be_truthy } 109 | its(:issues_url) { is_expected.to be_nil } 110 | its(:chef_version_requirement) { is_expected.to eq ['>= 12'] } 111 | its(:platforms) { is_expected.to eq [%w{ubuntu}] } 112 | 113 | describe '#each_file' do 114 | context 'with no prefixes' do 115 | it 'returns all files' do 116 | expect(subject.each_file).to eq [ 117 | [File.expand_path('../fixtures/gems/test2/Rakefile', __FILE__), 'Rakefile'], 118 | [File.expand_path('../fixtures/gems/test2/chef/attributes.rb', __FILE__), 'chef/attributes.rb'], 119 | [File.expand_path('../fixtures/gems/test2/chef/recipes/default.rb', __FILE__), 'chef/recipes/default.rb'], 120 | [File.expand_path('../fixtures/gems/test2/chef/templates/default/conf.erb', __FILE__), 'chef/templates/default/conf.erb'], 121 | [File.expand_path('../fixtures/gems/test2/lib/test2.rb', __FILE__), 'lib/test2.rb'], 122 | [File.expand_path('../fixtures/gems/test2/lib/test2/resource.rb', __FILE__), 'lib/test2/resource.rb'], 123 | [File.expand_path('../fixtures/gems/test2/lib/test2/version.rb', __FILE__), 'lib/test2/version.rb'], 124 | [File.expand_path('../fixtures/gems/test2/test2.gemspec', __FILE__), 'test2.gemspec'], 125 | ] 126 | end 127 | end 128 | 129 | context 'with a prefix of chef' do 130 | it 'returns only files in that folder' do 131 | expect(subject.each_file('chef')).to eq [ 132 | [File.expand_path('../fixtures/gems/test2/chef/attributes.rb', __FILE__), 'attributes.rb'], 133 | [File.expand_path('../fixtures/gems/test2/chef/recipes/default.rb', __FILE__), 'recipes/default.rb'], 134 | [File.expand_path('../fixtures/gems/test2/chef/templates/default/conf.erb', __FILE__), 'templates/default/conf.erb'], 135 | ] 136 | end 137 | end 138 | end 139 | end # /context when loading test2 140 | 141 | context 'when loading test3' do 142 | let(:gem_name) { 'test3' } 143 | its(:cookbook_name) { is_expected.to eq 'test3' } 144 | its(:cookbook_dependencies) { is_expected.to eq [Halite::Dependencies::Dependency.new('test2', '~> 4.5.6', :dependencies)] } 145 | its(:is_halite_cookbook?) { is_expected.to be_truthy } 146 | its(:issues_url) { is_expected.to be_nil } 147 | its(:chef_version_requirement) { is_expected.to eq [">= 3"] } 148 | its(:platforms) { is_expected.to eq [['ubuntu', '>= 16.04'], %w{redhat}] } 149 | end # /context when loading test3 150 | 151 | context 'when loading test4' do 152 | let(:gem_name) { 'test4' } 153 | its(:cookbook_name) { is_expected.to eq 'test4' } 154 | its(:cookbook_dependencies) { is_expected.to eq [] } 155 | it { expect(subject.cookbook_dependencies(development: true)).to eq [Halite::Dependencies::Dependency.new('test2', '~> 4.5.6', :dependencies)] } 156 | its(:version) { is_expected.to eq '2.3.1.rc.1' } 157 | its(:cookbook_version) { is_expected.to eq '2.3.1' } 158 | its(:issues_url) { is_expected.to eq 'http://issues' } 159 | its(:chef_version_requirement) { is_expected.to eq ['< 99', '>= 1'] } 160 | its(:platforms) { is_expected.to eq [%w{ubuntu}, %w{debian}, %w{centos}, %w{redhat}, %w{fedora}] } 161 | end # /context when loading test4 162 | 163 | context 'when loading a Gem::Dependency' do 164 | let(:dependency) do 165 | reqs = [] 166 | reqs << Gem::Requirement.create(gem_version) if gem_version 167 | Gem::Dependency.new(gem_name, *reqs) 168 | end 169 | subject { described_class.new(dependency) } 170 | 171 | context 'test1' do 172 | let(:gem_name) { 'test1' } 173 | its(:version) { is_expected.to eq '1.2.3' } 174 | end # /context test1 175 | 176 | context 'test1 > 1.2.0' do 177 | let(:gem_name) { 'test1' } 178 | let(:gem_version) { '> 1.2.0' } 179 | its(:version) { is_expected.to eq '1.2.3' } 180 | end # /context test1 > 1.2.0 181 | 182 | context 'test1 > 1.3.0' do 183 | let(:gem_name) { 'test1' } 184 | let(:gem_version) { '> 1.3.0' } 185 | it { expect { subject }.to raise_error Halite::Error } 186 | end # /context test1 > 1.3.0 187 | 188 | context 'test1 ~> 1.2' do 189 | let(:gem_name) { 'test1' } 190 | let(:gem_version) { '~> 1.2' } 191 | its(:version) { is_expected.to eq '1.2.3' } 192 | end # /context test1 ~> 1.2 193 | 194 | context 'test4 ~> 2.3' do 195 | let(:gem_name) { 'test4' } 196 | let(:gem_version) { '~> 2.3' } 197 | its(:version) { is_expected.to eq '2.3.1.rc.1' } 198 | end # /context test4 ~> 2.3 199 | end # /context when loading a Gem::Dependency 200 | 201 | describe '#cookbook_name' do 202 | let(:metadata) { {} } 203 | subject { described_class.new(Gem::Specification.new {|s| s.name = gem_name; s.metadata.update(metadata) }).cookbook_name } 204 | 205 | context 'with a gem named mygem' do 206 | let(:gem_name) { 'mygem' } 207 | it { is_expected.to eq 'mygem' } 208 | end 209 | 210 | context 'with a gem with an override' do 211 | let(:metadata) { {'halite_name' => 'other' } } 212 | it { is_expected.to eq 'other' } 213 | end 214 | 215 | context 'with a gem named chef-mygem' do 216 | let(:gem_name) { 'chef-mygem' } 217 | it { is_expected.to eq 'mygem' } 218 | end 219 | 220 | context 'with a gem named cookbook-mygem' do 221 | let(:gem_name) { 'cookbook-mygem' } 222 | it { is_expected.to eq 'mygem' } 223 | end 224 | 225 | context 'with a gem named mygem-chef' do 226 | let(:gem_name) { 'mygem-chef' } 227 | it { is_expected.to eq 'mygem' } 228 | end 229 | 230 | context 'with a gem named mygem-cookbook' do 231 | let(:gem_name) { 'mygem-cookbook' } 232 | it { is_expected.to eq 'mygem' } 233 | end 234 | 235 | context 'with a gem named chef-mygem-cookbook' do 236 | let(:gem_name) { 'chef-mygem-cookbook' } 237 | it { is_expected.to eq 'mygem' } 238 | end 239 | 240 | context 'with a gem named mycompany-mygem' do 241 | let(:gem_name) { 'mycompany-mygem' } 242 | it { is_expected.to eq 'mycompany-mygem' } 243 | end 244 | end # /describe #cookbook_name 245 | 246 | describe '#platforms' do 247 | let(:platforms) { '' } 248 | let(:metadata) { {'platforms' => platforms} } 249 | subject { described_class.new(Gem::Specification.new {|s| s.metadata.update(metadata) }).platforms } 250 | 251 | context 'with no metadata' do 252 | let(:metadata) { {} } 253 | it { is_expected.to eq [] } 254 | end # /context with no metadata 255 | 256 | context 'with ""' do 257 | let(:platforms) { '' } 258 | it { is_expected.to eq [] } 259 | end # /context with "" 260 | 261 | context 'with " "' do 262 | let(:platforms) { ' ' } 263 | it { is_expected.to eq [] } 264 | end # /context with " " 265 | 266 | context 'with "name1"' do 267 | let(:platforms) { 'name1' } 268 | it { is_expected.to eq [%w{name1}] } 269 | end # /context with "name1" 270 | 271 | context 'with "name1 "' do 272 | let(:platforms) { 'name1 ' } 273 | it { is_expected.to eq [%w{name1}] } 274 | end # /context with "name1 " 275 | 276 | context 'with " name1"' do 277 | let(:platforms) { ' name1' } 278 | it { is_expected.to eq [%w{name1}] } 279 | end # /context with " name1" 280 | 281 | context 'with "name1 name2"' do 282 | let(:platforms) { 'name1 name2' } 283 | it { is_expected.to eq [%w{name1}, %w{name2}] } 284 | end # /context with "name1 name2" 285 | 286 | context 'with "name1, name2"' do 287 | let(:platforms) { 'name1, name2' } 288 | it { is_expected.to eq [%w{name1}, %w{name2}] } 289 | end # /context with "name1, name2" 290 | 291 | context 'with "name1 >= 1.0, name2"' do 292 | let(:platforms) { 'name1 >= 1.0, name2' } 293 | it { is_expected.to eq [['name1', '>= 1.0'], %w{name2}] } 294 | end # /context with "name1 >= 1.0, name2" 295 | 296 | context 'with "name1 >= 1.0, name2 <= 2.0"' do 297 | let(:platforms) { 'name1 >= 1.0, name2 <= 2.0' } 298 | it { is_expected.to eq [['name1', '>= 1.0'], ['name2', '<= 2.0']] } 299 | end # /context with "name1 >= 1.0, name2 <= 2.0" 300 | 301 | context 'with "any"' do 302 | let(:platforms) { 'any' } 303 | it { is_expected.to eq [%w{aix}, %w{amazon}, %w{arch}, %w{centos}, %w{chefspec}, %w{debian}, %w{dragonfly4}, %w{fedora}, %w{freebsd}, %w{gentoo}, %w{ios_xr}, %w{mac_os_x}, %w{nexus}, %w{omnios}, %w{openbsd}, %w{opensuse}, %w{oracle}, %w{raspbian}, %w{redhat}, %w{slackware}, %w{smartos}, %w{solaris2}, %w{suse}, %w{ubuntu}, %w{windows}] } 304 | end # /context with "any" 305 | 306 | context 'with "all"' do 307 | let(:platforms) { 'all' } 308 | it { is_expected.to eq [%w{aix}, %w{amazon}, %w{arch}, %w{centos}, %w{chefspec}, %w{debian}, %w{dragonfly4}, %w{fedora}, %w{freebsd}, %w{gentoo}, %w{ios_xr}, %w{mac_os_x}, %w{nexus}, %w{omnios}, %w{openbsd}, %w{opensuse}, %w{oracle}, %w{raspbian}, %w{redhat}, %w{slackware}, %w{smartos}, %w{solaris2}, %w{suse}, %w{ubuntu}, %w{windows}] } 309 | end # /context with "all" 310 | 311 | context 'with "*"' do 312 | let(:platforms) { '*' } 313 | it { is_expected.to eq [%w{aix}, %w{amazon}, %w{arch}, %w{centos}, %w{chefspec}, %w{debian}, %w{dragonfly4}, %w{fedora}, %w{freebsd}, %w{gentoo}, %w{ios_xr}, %w{mac_os_x}, %w{nexus}, %w{omnios}, %w{openbsd}, %w{opensuse}, %w{oracle}, %w{raspbian}, %w{redhat}, %w{slackware}, %w{smartos}, %w{solaris2}, %w{suse}, %w{ubuntu}, %w{windows}] } 314 | end # /context with "*" 315 | end # /describe #platforms 316 | end 317 | -------------------------------------------------------------------------------- /spec/halite_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | describe Halite do 20 | describe '#convert' do 21 | let(:fake_gem) do 22 | double('Halite::Gem').tap do |g| 23 | allow(g).to receive(:is_a?).and_return(false) 24 | allow(g).to receive(:is_a?).with(Halite::Gem).and_return(true) 25 | end 26 | end 27 | 28 | context 'with a gem name' do 29 | before do 30 | allow(Halite::Gem).to receive(:new).with('mygem').and_return(fake_gem) 31 | end 32 | 33 | it do 34 | expect(Halite::Converter).to receive(:write).with(fake_gem, '/path') 35 | described_class.convert('mygem', '/path') 36 | end 37 | end # /context with a gem name 38 | 39 | context 'with a Gem object' do 40 | it do 41 | expect(Halite::Converter).to receive(:write).with(fake_gem, '/path') 42 | described_class.convert(fake_gem, '/path') 43 | end 44 | end # /context with a Gem object 45 | end # /describe #convert 46 | end 47 | -------------------------------------------------------------------------------- /spec/integration_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | require 'rspec-command' 19 | 20 | describe 'integration tests' do 21 | include RSpecCommand 22 | include RSpecCommand::Rake 23 | let(:extra_gems) { [] } 24 | let(:recipes) { [] } 25 | let(:expect_output) { } 26 | 27 | shared_examples 'an integration test' do |gem_name, version, stub_cookbooks=[]| 28 | describe 'Halite.convert' do 29 | subject { Halite.convert(gem_name, temp_path) } 30 | it { is_expected.to match_fixture("cookbooks/#{gem_name}") } 31 | end # /describe Halite.convert 32 | 33 | describe 'run it with chef-solo', slow: true do 34 | file('solo.rb') { "cookbook_path '#{temp_path}'" } 35 | file 'runner/metadata.rb', "name 'runner'\ndepends '#{gem_name}'" 36 | file 'runner/recipes/default.rb', '' 37 | # Write out a stub cookbooks for dependency testing. 38 | stub_cookbooks.each do |(stub_name, stub_version)| 39 | file "#{stub_name}/metadata.rb", "name '#{stub_name}'\nversion '#{stub_version}'" 40 | end 41 | before do 42 | ([gem_name] + extra_gems).each do |name| 43 | cookbook_path = File.join(temp_path, name) 44 | Dir.mkdir(cookbook_path) 45 | Halite.convert(name, cookbook_path) 46 | end 47 | end 48 | command { "chef-solo -l debug -c solo.rb -o #{(['runner']+recipes).map{|r| "recipe[#{r}]"}.join(',')}" } 49 | 50 | it do 51 | # Force the command to run at least once. 52 | subject 53 | Array(expect_output).each do |output| 54 | expect(subject.stdout).to include(output), "'#{output}' not found in the output of #{subject.command}:\n#{subject.stdout}" 55 | end 56 | end 57 | end # /describe run it with chef-solo 58 | 59 | describe 'run rake chef:build' do 60 | fixture_file "gems/#{gem_name}" 61 | rake_task 'chef:build' 62 | it { is_expected.to match_fixture("cookbooks/#{gem_name}", "pkg/#{gem_name}-#{version}") } 63 | end # /describe run rake chef:build 64 | end # /shared_examples an integration test 65 | 66 | context 'with test1 gem', integration: true do 67 | it_should_behave_like 'an integration test', 'test1', '1.2.3' 68 | end # /context with test1 gem 69 | 70 | context 'with test2 gem', integration: true do 71 | it_should_behave_like 'an integration test', 'test2', '4.5.6', [['testdep', '1.0.0']] 72 | end # /context with test2 gem 73 | 74 | context 'with test3 gem', integration: true do 75 | let(:extra_gems) { ['test2'] } 76 | let(:recipes) { ['test3'] } 77 | let(:expect_output) { '!!!!!!!!!!test34.5.6' } 78 | it_should_behave_like 'an integration test', 'test3', '7.8.9', [['testdep', '1.0.0']] 79 | end # /context with test3 gem 80 | 81 | context 'with test4 gem', integration: true do 82 | it_should_behave_like 'an integration test', 'test4', '2.3.1.rc.1' 83 | end # /context with test1 gem 84 | end 85 | -------------------------------------------------------------------------------- /spec/runner_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | # This file is not named spec/spec_helper/runner_spec.rb because that seems confusing. 18 | 19 | require 'spec_helper' 20 | 21 | require 'halite/spec_helper/runner' 22 | 23 | describe Halite::SpecHelper::Runner do 24 | let(:options) { Hash.new } 25 | subject { described_class.new(options.merge(platform: 'ubuntu')) } 26 | 27 | describe '#initialize' do 28 | before { subject.converge } 29 | 30 | context 'with a simple option' do 31 | let(:options) { {dry_run: true} } 32 | its(:dry_run?) { is_expected.to be_truthy } 33 | end # /context with a simple option 34 | 35 | context 'with default_attributes' do 36 | let(:options) { {default_attributes: {halite: 'test'}} } 37 | it { expect(subject.node.role_default['halite']).to eq('test') } 38 | it { expect(subject.node['halite']).to eq('test') } 39 | end # /context with default_attributes 40 | 41 | context 'with normal_attributes' do 42 | let(:options) { {normal_attributes: {halite: 'test'}} } 43 | it { expect(subject.node.normal['halite']).to eq('test') } 44 | it { expect(subject.node['halite']).to eq('test') } 45 | end # /context with normal_attributes 46 | 47 | context 'with override_attributes' do 48 | let(:options) { {override_attributes: {halite: 'test'}} } 49 | it { expect(subject.node.role_override['halite']).to eq('test') } 50 | it { expect(subject.node['halite']).to eq('test') } 51 | end # /context with override_attributes 52 | end # /describe #initialize 53 | 54 | describe '#converge' do 55 | let(:options) { {dry_run: true} } 56 | 57 | it do 58 | expect(subject.node.run_list).to receive(:add).with('test') 59 | subject.converge('test') 60 | end 61 | end # /describe #converge 62 | 63 | describe '#converge_block' do 64 | it do 65 | sentinel = [false] 66 | subject.converge_block { sentinel[0] = true } 67 | expect(sentinel[0]).to eq(true) 68 | end 69 | end # /describe #converge_block 70 | end 71 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'poise_boiler/helpers/spec_helper' 18 | PoiseBoiler::Helpers::SpecHelper.install(no_halite: true) 19 | require 'halite' 20 | require 'chefspec' 21 | require 'example_resources/simple' 22 | require 'example_resources/poise' 23 | require 'example_resources/custom' 24 | -------------------------------------------------------------------------------- /spec/spec_helper_spec.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015, Noah Kantrowitz 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | require 'spec_helper' 18 | 19 | require 'halite/spec_helper' 20 | 21 | describe Halite::SpecHelper do 22 | include Halite::SpecHelper 23 | 24 | describe '#recipe' do 25 | context 'with a block' do 26 | recipe do 27 | ruby_block 'test' 28 | end 29 | 30 | it { is_expected.to run_ruby_block('test') } 31 | end # /context with a block 32 | 33 | context 'with a recipe' do 34 | let(:chefspec_options) { {dry_run: true} } 35 | recipe 'test' 36 | 37 | it do 38 | expect(chef_runner).to receive(:converge).with('test') 39 | chef_run 40 | end 41 | end # /context with a recipe 42 | 43 | context 'with subject:false' do 44 | subject { 42 } 45 | recipe(subject: false) do 46 | ruby_block 'test' 47 | end 48 | 49 | it { is_expected.to eq 42 } 50 | end # /context with subject:false 51 | end # /describe #recipe 52 | 53 | describe '#resource' do 54 | subject { resource(:halite_test) } 55 | 56 | context 'with defaults' do 57 | resource(:halite_test) 58 | it { is_expected.to be_a(Class) } 59 | it { is_expected.to be < Chef::Resource } 60 | its(:resource_name) { is_expected.to eq :halite_test } if defined?(Chef::Resource.resource_name) 61 | it { expect(subject.new('test', nil).resource_name).to eq(:halite_test) } 62 | it { expect(Array(subject.new('test', nil).action)).to eq([:run]) } 63 | it { expect(subject.new('test', nil).allowed_actions).to eq([:nothing, :run]) } 64 | end # /context with defaults 65 | 66 | context 'with auto:false' do 67 | resource(:halite_test, auto: false) 68 | it { is_expected.to be_a(Class) } 69 | it { is_expected.to be < Chef::Resource } 70 | # #resource_name was added upstream in 12.4 so ignore this test there. 71 | it { expect(subject.new('test', nil).resource_name).to be_nil } unless defined?(Chef::Resource.resource_name) 72 | it { expect(Array(subject.new('test', nil).action)).to eq [:nothing] } 73 | it { expect(subject.new('test', nil).allowed_actions).to eq [:nothing] } 74 | end # /context with auto:false 75 | 76 | context 'with a parent' do 77 | resource(:halite_test, parent: Chef::Resource::File) 78 | it { is_expected.to be_a(Class) } 79 | it { is_expected.to be < Chef::Resource } 80 | it { is_expected.to be < Chef::Resource::File } 81 | its(:resource_name) { is_expected.to eq :halite_test } if defined?(Chef::Resource.resource_name) 82 | it { expect(subject.new('test', nil).resource_name).to eq(:halite_test) } 83 | end # /context with a parent 84 | 85 | context 'with a helper-defined parent' do 86 | resource(:halite_parent) 87 | resource(:halite_test, parent: :halite_parent) 88 | it { is_expected.to be_a(Class) } 89 | it { is_expected.to be < Chef::Resource } 90 | it { is_expected.to be < resource('halite_parent') } 91 | its(:resource_name) { is_expected.to eq :halite_test } if defined?(Chef::Resource.resource_name) 92 | it { expect(subject.new('test', nil).resource_name).to eq(:halite_test) } 93 | end # /context with a helper-defined parent 94 | 95 | context 'with a helper-defined parent in an enclosing context' do 96 | resource(:halite_parent) 97 | context 'inner' do 98 | resource(:halite_test, parent: :halite_parent) 99 | it { is_expected.to be_a(Class) } 100 | it { is_expected.to be < Chef::Resource } 101 | it { is_expected.to be < resource(:halite_parent) } 102 | end 103 | end # /context with a helper-defined parent in an enclosing context 104 | 105 | # Long name is long but ¯\_(ツ)_/¯ 106 | context 'regression test for finding the wrong parent in a sibling context' do 107 | resource(:halite_parent) do 108 | def value 109 | :parent 110 | end 111 | end 112 | subject { resource(:halite_test).new('test', nil).value } 113 | 114 | context 'sibling' do 115 | resource(:halite_parent) do 116 | def value 117 | :sibling 118 | end 119 | end 120 | # Sanity check that this still works. Important test is below. 121 | resource(:halite_test, parent: :halite_parent) 122 | it { is_expected.to eq(:sibling) } 123 | end 124 | 125 | context 'inner' do 126 | # The actual regression test. 127 | resource(:halite_test, parent: :halite_parent) 128 | it { is_expected.to eq(:parent) } 129 | end 130 | end # /context regression test for finding the wrong parent in a sibling context 131 | 132 | context 'with step_into:false' do 133 | resource(:halite_test, step_into: false) 134 | provider(:halite_test) do 135 | def action_run 136 | ruby_block 'inner' 137 | end 138 | end 139 | recipe do 140 | halite_test 'test' 141 | end 142 | # Have to create this because step_into normally handles that 143 | def run_halite_test(resource_name) 144 | ChefSpec::Matchers::ResourceMatcher.new(:halite_test, :run, resource_name) 145 | end 146 | 147 | it { is_expected.to run_halite_test('test') } 148 | it { is_expected.to_not run_ruby_block('inner') } 149 | end # /context with step_into:false 150 | 151 | describe 'with magic helpers' do 152 | resource(:halite_test) 153 | 154 | context 'on the class' do 155 | its(:example_group) { is_expected.to be_truthy } 156 | its(:described_class) { is_expected.to eq Halite::SpecHelper } 157 | end # /context on the class 158 | 159 | context 'on the instance' do 160 | subject { super().new('test', nil) } 161 | its(:example_group) { is_expected.to be_truthy } 162 | its(:described_class) { is_expected.to eq Halite::SpecHelper } 163 | end # /context on the instance 164 | end # /describe with magic helpers 165 | end # /describe #resource 166 | 167 | describe '#provider' do 168 | subject { provider(:halite_test) } 169 | 170 | context 'with defaults' do 171 | provider(:halite_test) 172 | it { is_expected.to be_a(Class) } 173 | it { is_expected.to be < Chef::Provider } 174 | its(:instance_methods) { are_expected.to include(:action_run) } 175 | end # /context with defaults 176 | 177 | context 'with magic helpers' do 178 | provider(:halite_test) 179 | 180 | context 'on the class' do 181 | its(:example_group) { is_expected.to be_truthy } 182 | its(:described_class) { is_expected.to eq Halite::SpecHelper } 183 | end # /context on the class 184 | 185 | context 'on the instance' do 186 | subject { super().new(double(name: 'test', cookbook_name: 'test', resource_name: :halite_test), nil) } 187 | its(:example_group) { is_expected.to be_truthy } 188 | its(:described_class) { is_expected.to eq Halite::SpecHelper } 189 | end # /context on the instance 190 | end # /describe with magic helpers 191 | end # /describe #provider 192 | 193 | describe '#step_into' do 194 | context 'with a simple HWRP' do 195 | recipe do 196 | halite_test_simple 'test' 197 | end 198 | 199 | context 'with step_into' do 200 | step_into(:halite_test_simple) 201 | it { is_expected.to run_halite_test_simple('test') } 202 | it { is_expected.to run_ruby_block('test') } 203 | it { expect(chef_run.halite_test_simple('test')).to be_a(Chef::Resource) } 204 | end # /context with step_into 205 | 206 | context 'without step_into' do 207 | # Can't use the nice matcher because step_into is what would create that. 208 | it { is_expected.to ChefSpec::Matchers::ResourceMatcher.new('halite_test_simple', 'run', 'test') } 209 | it { is_expected.to_not run_ruby_block('test') } 210 | end # /context without step_into 211 | end # /context with a simple HWRP 212 | 213 | context 'with a Poise resource' do 214 | recipe do 215 | halite_test_poise 'test' 216 | end 217 | 218 | context 'with step_into' do 219 | step_into(:halite_test_poise) 220 | it { is_expected.to run_halite_test_poise('test') } 221 | it { is_expected.to run_ruby_block('test') } 222 | it { expect(chef_run.halite_test_poise('test')).to be_a(Chef::Resource) } 223 | end # /context with step_into 224 | 225 | context 'without step_into' do 226 | it { is_expected.to run_halite_test_poise('test') } 227 | it { is_expected.to_not run_ruby_block('test') } 228 | it { expect(chef_run.halite_test_poise('test')).to be_a(Chef::Resource) } 229 | end # /context without step_into 230 | end # /context with a Poise resource 231 | 232 | context 'with a custom resource' do 233 | recipe do 234 | halite_test_custom 'test' 235 | end 236 | 237 | context 'with step_into' do 238 | step_into(:halite_test_custom) 239 | it { is_expected.to run_halite_test_custom('test') } 240 | it { is_expected.to run_ruby_block('test') } 241 | it { expect(chef_run.halite_test_custom('test')).to be_a(Chef::Resource) } 242 | end # /context with step_into 243 | 244 | context 'without step_into' do 245 | it { is_expected.to run_halite_test_custom('test') } 246 | it { is_expected.to_not run_ruby_block('test') } 247 | it { expect(chef_run.halite_test_custom('test')).to be_a(Chef::Resource) } 248 | end # /context without step_into 249 | end # /context with a custom resource 250 | end # /describe #step_into 251 | 252 | describe 'patcher' do 253 | #let(:chefspec_options) { {log_level: :debug} } 254 | resource(:halite_test) 255 | subject do 256 | chef_runner.converge 257 | resource(:halite_test).new('test', chef_runner.run_context).provider_for_action(:run) 258 | end 259 | 260 | context 'with a provider in scope' do 261 | provider(:halite_test) 262 | # Basically it just shouldn't raise an error. 263 | it { is_expected.to be_truthy } 264 | end # /context with a provider in scope 265 | 266 | context 'with a named provider in scope' do 267 | provider(:halite_test_other) do 268 | provides(:halite_test) 269 | end 270 | # Basically it just shouldn't raise an error. 271 | it { is_expected.to be_truthy } 272 | end # /context with a named provider in scope 273 | 274 | context 'without a provider in scope' do 275 | # 12.4.1+ uses Chef::Exceptions::ProviderNotFound, before that ArgumentError. 276 | it { expect { subject }.to raise_error (defined?(Chef::Exceptions::ProviderNotFound) ? Chef::Exceptions::ProviderNotFound : ArgumentError) } 277 | end # /context without a provider in scope 278 | end # /describe patcher 279 | end 280 | --------------------------------------------------------------------------------