diff options
68 files changed, 2353 insertions, 846 deletions
@@ -8,6 +8,7 @@ InstalledFiles _yardoc coverage doc/ +binstubs/ lib/bundler/man pkg rdoc diff --git a/.travis.yml b/.travis.yml index c0b640e..4994db3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,18 +1,34 @@ -rvm: 2.0 +rvm: 2.1 gemfile: Gemfile +# This prevents testing branches that are created just for PRs +branches: + only: + - master + +# Early warning system to catch if Rubygems breaks something +before_install: + - gem update --system + - gem install bundler + sudo: false -script: bundle exec rake pedant +script: + - bundle update + - bundle exec rake pedant matrix: include: - - rvm: 2.0 - rvm: 2.1 + env: PEDANT_KNIFE_TESTS=true PEDANT_ALLOW_RVM=1 - rvm: 2.1 env: SINGLE_ORG=true - rvm: 2.1 env: CHEF_FS=true - rvm: 2.1 + env: + - CHEF_FS=true + - "GEMFILE_MOD=\"gem 'chef', github: 'chef/chef'\"" + - rvm: 2.1 env: FILE_STORE=true - rvm: 2.1 script: bundle exec rake chef_spec @@ -21,11 +37,3 @@ matrix: script: bundle exec rake spec env: TEST=rake_spec - allow_failures: - - rvm: 2.1 - gemfile: gemfiles/latest-chef.gemfile - script: bundle exec rake chef_spec - enc: TEST=chef_spec_latest -# - rvm: 2.1.1 -# gemfile: gemfiles/berkshelf.gemfile -# script: bundle exec rake berkshelf_spec diff --git a/CHANGELOG.md b/CHANGELOG.md index 43b3f44..8627141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,252 +1,572 @@ -Chef Zero CHANGELOG -=================== +# Change Log -# 4.2.2 +## [4.7.1](https://github.com/chef/chef-zero/tree/4.7.1) (2016-07-07) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.7.0...4.7.1) -* [PR#133](https://github.com/chef/chef-zero/pull/133): Fix RSpec - helpers to use `chef_zero_opts` from let binding. -* [PR#131](https://github.com/chef/chef-zero/pull/131): Adding new - `server_on_port` method to the socketless server map. +**Implemented enhancements:** -# 4.2.1 +- Downgrade info log message to debug [\#221](https://github.com/chef/chef-zero/pull/221) ([stanhu](https://github.com/stanhu)) -* [PR#125](https://github.com/chef/chef-zero/pull/125): Don't polute - global chef_server configs when running RSpec +## [v4.7.0](https://github.com/chef/chef-zero/tree/v4.7.0) (2016-06-30) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.6.2...v4.7.0) -# 4.2.0 +**Implemented enhancements:** -* [PR#124](https://github.com/chef/chef-zero/pull/124): Bump ffi-yajl - dependency -* [PR#119](https://github.com/chef/chef-zero/pull/119): Add - :organization and :data_scope options to RSpec support method - `with_chef_server` +- Add external\_authentication\_uid to actors endpoint for querying [\#217](https://github.com/chef/chef-zero/pull/217) ([kmacgugan](https://github.com/kmacgugan)) -# 4.1.0 +**Merged pull requests:** -* [PR#121](https://github.com/chef/chef-zero/pull/121): Add Socketless - mode. -* [**Phil Dibowitz**](https://github.com/jaymzh): - Added support for /version +- Depend on rack \< 2 to restore Ruby 2.1 compat [\#219](https://github.com/chef/chef-zero/pull/219) ([tas50](https://github.com/tas50)) -# 4.0 (2/11/2014) +## [v4.6.2](https://github.com/chef/chef-zero/tree/v4.6.2) (2016-04-28) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.6.1...v4.6.2) -- Add policyfile endpoints -- Remove Ruby 1.8 and 1.9 support +**Fixed bugs:** -# 3.2 (9/26/2014) +- Log responses only at debug log level [\#216](https://github.com/chef/chef-zero/pull/216) ([stevendanna](https://github.com/stevendanna)) -* removed 'json' gem dependency, replaced it with 'ffi-yajl' +## [v4.6.1](https://github.com/chef/chef-zero/tree/v4.6.1) (2016-04-14) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.6.0...v4.6.1) -# 3.1.3 (9/3/2014) +**Fixed bugs:** -* fixes for running Chef local mode in multi-org mode +- Actually merge key data in user PUT response [\#214](https://github.com/chef/chef-zero/pull/214) ([jkeiser](https://github.com/jkeiser)) +- Fix users endpoint in OSC compat mode to use a data store URL [\#213](https://github.com/chef/chef-zero/pull/213) ([jkeiser](https://github.com/jkeiser)) -# 3.1.2 (8/29/2014) +## [v4.6.0](https://github.com/chef/chef-zero/tree/v4.6.0) (2016-04-14) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.5.0...v4.6.0) -* add default to rspec for cookbooks -* add /organizations/NAME/organization/_acl as an alias for /organizations/NAME/organizations/_acl +**Implemented enhancements:** -# 3.1.1 (8/28/2014) +- Enable listening on more than one address [\#208](https://github.com/chef/chef-zero/pull/208) ([jaymzh](https://github.com/jaymzh)) +- Implemented GET /orgs/ORG/users/USER/keys\(/key\) endpoint recently added to server. [\#205](https://github.com/chef/chef-zero/pull/205) ([tylercloke](https://github.com/tylercloke)) +- Implement APIv1 behaviors [\#201](https://github.com/chef/chef-zero/pull/201) ([danielsdeleo](https://github.com/danielsdeleo)) +- Make user and client keys endpoints pass Pedant specs [\#199](https://github.com/chef/chef-zero/pull/199) ([jrunning](https://github.com/jrunning)) +- fix necessary for metadata gem [\#197](https://github.com/chef/chef-zero/pull/197) ([lamont-granquist](https://github.com/lamont-granquist)) -* fix minor bug with unknown container acls +**Fixed bugs:** -# 3.1 (8/28/2014) +- Fix bugs related to Array vs Enumerator vs Port for options\[:port/host\]. [\#212](https://github.com/chef/chef-zero/pull/212) ([tylercloke](https://github.com/tylercloke)) -* New rspec data directives: organization, acl, group, container -* Fix organizations POST to honor full_name -* Fixes for enterprise rspec data loading -* Fix invites not removing the invite when user is forcibly added to an org +## [v4.5.0](https://github.com/chef/chef-zero/tree/v4.5.0) (2016-01-29) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.4.2...v4.5.0) -# 3.0 (7/22/2014) +**Merged pull requests:** -* Enterprise Chef support (organizations, ACLs, groups, much more) -* SSL support (@sawanoboly) +- Run chef-zero against master Chef in travis [\#195](https://github.com/chef/chef-zero/pull/195) ([jkeiser](https://github.com/jkeiser)) +- Make ACLs for policies/policy\_groups/cookbook\_artifacts work [\#194](https://github.com/chef/chef-zero/pull/194) ([jkeiser](https://github.com/jkeiser)) +- Return 410 on /controls so we stop skipping that pedant spec. [\#192](https://github.com/chef/chef-zero/pull/192) ([randomcamel](https://github.com/randomcamel)) +- Enable container specs. [\#191](https://github.com/chef/chef-zero/pull/191) ([randomcamel](https://github.com/randomcamel)) +- Enable headers pedant tests [\#190](https://github.com/chef/chef-zero/pull/190) ([danielsdeleo](https://github.com/danielsdeleo)) +- Enable knife pedant tests [\#189](https://github.com/chef/chef-zero/pull/189) ([danielsdeleo](https://github.com/danielsdeleo)) +- Start running policy and cookbook artifact tests [\#187](https://github.com/chef/chef-zero/pull/187) ([jkeiser](https://github.com/jkeiser)) -# 2.2 (6/18/2014) +## [v4.4.2](https://github.com/chef/chef-zero/tree/v4.4.2) (2016-01-15) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.4.1...v4.4.2) -* allow port ranges to be passed in as enumerables, which will be tried in sequence until one works: `ChefZero::Server.new(:port => 80.upto(100))` +**Merged pull requests:** -# 2.1.5 (6/2/2014) +- Make hoovering and deleting parent dir work everywhere for cookbook\_artifacts [\#186](https://github.com/chef/chef-zero/pull/186) ([jkeiser](https://github.com/jkeiser)) +- Explain why omnibus/authz/authN/validation checks are skipped [\#185](https://github.com/chef/chef-zero/pull/185) ([danielsdeleo](https://github.com/danielsdeleo)) -* fix issue with :single_org => <value> not being honored +## [v4.4.1](https://github.com/chef/chef-zero/tree/v4.4.1) (2016-01-14) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.4.0...v4.4.1) -# 2.1.4 (5/27/2014) +**Merged pull requests:** -* fix issue with global Thread.exit_on_exception being set +- Only test master branch and PRs [\#184](https://github.com/chef/chef-zero/pull/184) ([danielsdeleo](https://github.com/danielsdeleo)) +- Internal orgs appears to be unused in oc-chef-pedant [\#183](https://github.com/chef/chef-zero/pull/183) ([danielsdeleo](https://github.com/danielsdeleo)) +- Fix cookbook\_artifact rspec [\#182](https://github.com/chef/chef-zero/pull/182) ([jkeiser](https://github.com/jkeiser)) +- Point chef-server back to master [\#180](https://github.com/chef/chef-zero/pull/180) ([thommay](https://github.com/thommay)) +- Ignore the universe endpoint tests in pedant [\#176](https://github.com/chef/chef-zero/pull/176) ([thommay](https://github.com/thommay)) -# 2.1.3 (5/27/2014) +## [v4.4.0](https://github.com/chef/chef-zero/tree/v4.4.0) (2015-12-11) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.3.2...v4.4.0) -* rspec: default port to 8900 to not conflict with normal default port -* rspec: when chef_zero_opts is set, check if current server has those options before continuing +**Closed issues:** -# 2.1.2 (5/27/2014) +- Please bump hashie version if possible [\#97](https://github.com/chef/chef-zero/issues/97) -* fix build_uri (and thus cookbook downloads) +**Merged pull requests:** -# 2.1.1 (5/26/2014) +- ChefZero::RSpec support for cookbook\_artifacts. [\#179](https://github.com/chef/chef-zero/pull/179) ([randomcamel](https://github.com/randomcamel)) +- /cookbook\_artifacts support for in-memory and FILE\_STORE backends \(not ChefFS\) [\#178](https://github.com/chef/chef-zero/pull/178) ([randomcamel](https://github.com/randomcamel)) +- Update and refactor policy and policy\_groups endpoints [\#177](https://github.com/chef/chef-zero/pull/177) ([jkeiser](https://github.com/jkeiser)) +- Point at master of oc-chef-pedant and chef [\#174](https://github.com/chef/chef-zero/pull/174) ([stevendanna](https://github.com/stevendanna)) +- Upgrade pedant, and enable running in ChefFS mode [\#173](https://github.com/chef/chef-zero/pull/173) ([randomcamel](https://github.com/randomcamel)) +- Implement the /policies and /policy\_groups API routes [\#172](https://github.com/chef/chef-zero/pull/172) ([randomcamel](https://github.com/randomcamel)) +- Add gemspec files to allow bundler to run from the gem [\#169](https://github.com/chef/chef-zero/pull/169) ([ksubrama](https://github.com/ksubrama)) -* flip defaults off in V1ToV2Adapater, allowing most chef tests to pass against 2.1.1 +## [v4.3.2](https://github.com/chef/chef-zero/tree/v4.3.2) (2015-09-30) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.3.1...v4.3.2) -# 2.1 (5/26/2014) +## [v4.3.1](https://github.com/chef/chef-zero/tree/v4.3.1) (2015-09-30) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.3.0...v4.3.1) -* **Multi-tenancy!** If you set :single_org => nil when starting the server, you will gain /organizations/* at the beginning of all URLs. Internally, all endpoints are rooted at /organizations/ORG anyway, there is just a translation that goes on to add /organizations/single_org to the URL when someone hits chef-zero. -* Fixes to support chef-zero local mode passing pedant +**Fixed bugs:** -# 2.0.2 (1/20/2014) +- chefspec client creation test broken by \#117 [\#165](https://github.com/chef/chef-zero/issues/165) +- Translate admin="true" to admin=true [\#166](https://github.com/chef/chef-zero/pull/166) ([jkeiser](https://github.com/jkeiser)) -* Fix a series of typos in the README -* Read JSON, not a file path in `from_json` -* Fix IPV6 support -* Remove moneta as a dependency +## [v4.3.0](https://github.com/chef/chef-zero/tree/v4.3.0) (2015-09-02) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.2.3...v4.3.0) -# 2.0.1 (1/3/2014) +**Implemented enhancements:** -* Make playground items more semantic -* Fix an issue where an incorrect number of parameters was passed in `environments/NAME/nodes` endpoint -* Fix an issue where the `data_store` was not yet initialized in the server +- Allow Hashie to float to 3.x \(no need to be so specific\) [\#164](https://github.com/chef/chef-zero/pull/164) ([jkeiser](https://github.com/jkeiser)) +- Server api version [\#155](https://github.com/chef/chef-zero/pull/155) ([andrewjamesbrown](https://github.com/andrewjamesbrown)) +- Add /organizations/NAME/nodes/NAME/\_identifiers endpoint [\#152](https://github.com/chef/chef-zero/pull/152) ([andrewjamesbrown](https://github.com/andrewjamesbrown)) +- CS12 Support [\#117](https://github.com/chef/chef-zero/pull/117) ([marcparadise](https://github.com/marcparadise)) -# 2.0.0 (12/17/2013) +**Fixed bugs:** -* Remove Puma (and `--socket` option) -* Use a cleaner threading approach -* Implement a better `running?` check +- Adding back logic to delete the association request when adding a user to an org \(as well as adding the user to the groups\) [\#160](https://github.com/chef/chef-zero/pull/160) ([tyler-ball](https://github.com/tyler-ball)) +- Switch to pedant in chef-server repo [\#151](https://github.com/chef/chef-zero/pull/151) ([andrewjamesbrown](https://github.com/andrewjamesbrown)) +- Update gem dependencies [\#146](https://github.com/chef/chef-zero/pull/146) ([andrewjamesbrown](https://github.com/andrewjamesbrown)) +- Remove dependency on chef [\#140](https://github.com/chef/chef-zero/pull/140) ([terceiro](https://github.com/terceiro)) -# 1.7.3 +**Merged pull requests:** -* (Backport) Read JSON, not a file path in `from_json` +- Autogenerated changelog [\#163](https://github.com/chef/chef-zero/pull/163) ([jkeiser](https://github.com/jkeiser)) -# 1.6.3 +## [v4.2.3](https://github.com/chef/chef-zero/tree/v4.2.3) (2015-06-19) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.2.2...v4.2.3) -* (Backport) Read JSON, not a file path in `from_json` +**Merged pull requests:** -# 1.5.5 +- Make server\_scope: :context work again [\#143](https://github.com/chef/chef-zero/pull/143) ([jkeiser](https://github.com/jkeiser)) -* Fix issue with - in term (name:a-b) +## [v4.2.2](https://github.com/chef/chef-zero/tree/v4.2.2) (2015-05-18) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.2.1...v4.2.2) -# 1.5.4 +**Merged pull requests:** -* Fix issue where run_lists in format cookbook::recipe@version do not depsolve +- Update version and changelog for 4.2.2 [\#134](https://github.com/chef/chef-zero/pull/134) ([danielsdeleo](https://github.com/danielsdeleo)) +- Access server opts in example context not describe context [\#133](https://github.com/chef/chef-zero/pull/133) ([danielsdeleo](https://github.com/danielsdeleo)) +- Adding `server\_on\_port` method to socketless server map [\#131](https://github.com/chef/chef-zero/pull/131) ([tyler-ball](https://github.com/tyler-ball)) +- Ignore .ruby-version [\#98](https://github.com/chef/chef-zero/pull/98) ([raskchanky](https://github.com/raskchanky)) -# 1.5.3 +## [v4.2.1](https://github.com/chef/chef-zero/tree/v4.2.1) (2015-04-07) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.2.0...v4.2.1) -* Add Server: chef-zero header to response +**Merged pull requests:** -# 1.5.2 +- Don't pollute global Chef server options [\#125](https://github.com/chef/chef-zero/pull/125) ([jkeiser](https://github.com/jkeiser)) -* Fix a couple of search query issues (make parentheses and NOT term:value work) +## [v4.2.0](https://github.com/chef/chef-zero/tree/v4.2.0) (2015-04-06) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.1.0...v4.2.0) -# 1.5.1 +**Merged pull requests:** -* Add Unix domain socket support (e.g. chef-zero --socket /tmp/chef-zero.sock) (stevendanna) +- bump ffi-yajl dep [\#124](https://github.com/chef/chef-zero/pull/124) ([lamont-granquist](https://github.com/lamont-granquist)) +- Add :organization and :data\_scope options to with\_chef\_server [\#119](https://github.com/chef/chef-zero/pull/119) ([jkeiser](https://github.com/jkeiser)) -# 1.5 +## [v4.1.0](https://github.com/chef/chef-zero/tree/v4.1.0) (2015-04-01) +[Full Changelog](https://github.com/chef/chef-zero/compare/v4.0...v4.1.0) -* Add -d option for daemon mode (sethvargo) -* Fix bug with cookbook metadata.rb files that rely on __FILE__ +**Merged pull requests:** -# 1.4 +- Socketless Requests [\#121](https://github.com/chef/chef-zero/pull/121) ([danielsdeleo](https://github.com/danielsdeleo)) +- Partially Revert 1b2a6e5f107254cce8200a4750035b30265ae0c8 [\#120](https://github.com/chef/chef-zero/pull/120) ([danielsdeleo](https://github.com/danielsdeleo)) +- Policyfile Revision ID Validation. [\#116](https://github.com/chef/chef-zero/pull/116) ([danielsdeleo](https://github.com/danielsdeleo)) +- Use a Chef version compatible with chef-zero 4.x [\#115](https://github.com/chef/chef-zero/pull/115) ([danielsdeleo](https://github.com/danielsdeleo)) +- Support /version; fix some global URIs [\#113](https://github.com/chef/chef-zero/pull/113) ([jaymzh](https://github.com/jaymzh)) -* Run with downgraded Puma 1.6 in order to work on Windows (2.x doesn't yet) +## [v4.0](https://github.com/chef/chef-zero/tree/v4.0) (2015-02-11) +[Full Changelog](https://github.com/chef/chef-zero/compare/v3.2.1...v4.0) -# 1.3 +**Closed issues:** -* Fix bug with search when JSON contains the same key in different places +- Upgrading to chef-client 12 packages appear to break on TK's chef-zero provisioner [\#108](https://github.com/chef/chef-zero/issues/108) +- The local\_mode on chef 12.0.0.alpha.2 can't create object to local filesystem. [\#99](https://github.com/chef/chef-zero/issues/99) -# 1.2.1 +**Merged pull requests:** -* Fix search when JSON contains integers +- Policyfile get/set API [\#111](https://github.com/chef/chef-zero/pull/111) ([danielsdeleo](https://github.com/danielsdeleo)) -# 1.2 +## [v3.2.1](https://github.com/chef/chef-zero/tree/v3.2.1) (2014-11-26) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.2.1...v3.2.1) -* Allow rspec users to specify cookbook NAME, VERSION, { :frozen => true } -* Documentation fix +**Closed issues:** -# 1.1.3 +- missing `else`? [\#102](https://github.com/chef/chef-zero/issues/102) -* Return better defaults for cookbooks -* Support /cookbook_versions?cookbook_versions=... query parameter -* Fix server crash when cookbook has multiple identical checksums +**Merged pull requests:** -# 1.1.2 +- Version bump for 3.2.1. [\#105](https://github.com/chef/chef-zero/pull/105) ([sersut](https://github.com/sersut)) +- fix: should set https to rack.url\_scheme \#87 [\#104](https://github.com/chef/chef-zero/pull/104) ([sawanoboly](https://github.com/sawanoboly)) +- Add option for logging to a file. [\#103](https://github.com/chef/chef-zero/pull/103) ([jaymzh](https://github.com/jaymzh)) +- add CORS header [\#96](https://github.com/chef/chef-zero/pull/96) ([smith](https://github.com/smith)) -* Allow rspec users to specify the same data twice (overwrites) +## [v2.2.1](https://github.com/chef/chef-zero/tree/v2.2.1) (2014-10-08) +[Full Changelog](https://github.com/chef/chef-zero/compare/v3.2...v2.2.1) -# 1.1.1 +## [v3.2](https://github.com/chef/chef-zero/tree/v3.2) (2014-09-27) +[Full Changelog](https://github.com/chef/chef-zero/compare/v3.1.3...v3.2) -* Fix broken rspec functionality (jkeiser, reset) +**Closed issues:** -# 1.1 +- chef-zero not copying data\_bags into cache [\#83](https://github.com/chef/chef-zero/issues/83) +- Wrong generated client keys with Ruby 1.9.2 [\#65](https://github.com/chef/chef-zero/issues/65) -* Create plugin system to allow other storage besides memory +**Merged pull requests:** -# 1.0.1 +- Removing 'json' gem dependency, replacing with 'ffi-yajl' [\#93](https://github.com/chef/chef-zero/pull/93) ([tyler-ball](https://github.com/tyler-ball)) -* Fix depsolver crash with frozen version strings (sethvargo) +## [v3.1.3](https://github.com/chef/chef-zero/tree/v3.1.3) (2014-09-04) +[Full Changelog](https://github.com/chef/chef-zero/compare/v3.1.2...v3.1.3) -# 1.0 +**Merged pull requests:** -* Increased testing of server +- Pass base URI to V1 data store only in true single org mode [\#89](https://github.com/chef/chef-zero/pull/89) ([jkeiser](https://github.com/jkeiser)) -# 0.9.13 +## [v3.1.2](https://github.com/chef/chef-zero/tree/v3.1.2) (2014-08-29) +[Full Changelog](https://github.com/chef/chef-zero/compare/v3.1.1...v3.1.2) -* Remove extra require of 'thin' so rspec users don't get broke +## [v3.1.1](https://github.com/chef/chef-zero/tree/v3.1.1) (2014-08-29) +[Full Changelog](https://github.com/chef/chef-zero/compare/v3.1...v3.1.1) -# 0.9.12 +## [v3.1](https://github.com/chef/chef-zero/tree/v3.1) (2014-08-29) +[Full Changelog](https://github.com/chef/chef-zero/compare/v3.0...v3.1) -* Switch from thin to puma (sethvargo) +## [v3.0](https://github.com/chef/chef-zero/tree/v3.0) (2014-08-22) +[Full Changelog](https://github.com/chef/chef-zero/compare/v3.0.0.rc.1...v3.0) -# 0.9.11 +**Implemented enhancements:** -* Support full cookbook metadata.rb syntax, including "depends" +- Make Enterprise Chef pass oc-chef-pedant [\#73](https://github.com/chef/chef-zero/issues/73) +- Add Enterprise Chef users endpoints [\#72](https://github.com/chef/chef-zero/issues/72) +- Add Enterprise Chef containers endpoints [\#71](https://github.com/chef/chef-zero/issues/71) +- Add Enterprise Chef groups endpoints [\#70](https://github.com/chef/chef-zero/issues/70) +- Add Enterprise Chef acls endpoints [\#69](https://github.com/chef/chef-zero/issues/69) +- Add Enterprise Chef organizations endpoints [\#68](https://github.com/chef/chef-zero/issues/68) -# 0.9.10 +**Closed issues:** -* Add -d flag to print debug output (sethvargo) +- SSL support [\#86](https://github.com/chef/chef-zero/issues/86) -# 0.9.9 +## [v3.0.0.rc.1](https://github.com/chef/chef-zero/tree/v3.0.0.rc.1) (2014-08-22) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.2...v3.0.0.rc.1) -* Remove chef as a dependency so we can run on jruby (reset) -* Server assumes json is acceptable if Accept header is not sent (stevendanna) +**Closed issues:** -# 0.9.8 +- Vagrant on windows fails to load Chef-Zero due to Chef dependencies [\#55](https://github.com/chef/chef-zero/issues/55) -* Support runlists with a::b in them in depsolver +**Merged pull requests:** -# 0.9.7 +- Support ssl \(--\[no-\]ssl option\) [\#87](https://github.com/chef/chef-zero/pull/87) ([sawanoboly](https://github.com/sawanoboly)) +- Get enterprise chef-zero passing oc-chef-pedant [\#84](https://github.com/chef/chef-zero/pull/84) ([jkeiser](https://github.com/jkeiser)) +- waffle.io Badge [\#74](https://github.com/chef/chef-zero/pull/74) ([waffle-iron](https://github.com/waffle-iron)) -* Return file URLs and other important things in depsolver response +## [v2.2](https://github.com/chef/chef-zero/tree/v2.2) (2014-06-18) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.5...v2.2) -# 0.9.6 +**Merged pull requests:** -* Make 404 a JSON response +- Allow server to try multiple ports [\#67](https://github.com/chef/chef-zero/pull/67) ([jkeiser](https://github.com/jkeiser)) -# 0.9.5 +## [v2.1.5](https://github.com/chef/chef-zero/tree/v2.1.5) (2014-06-03) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.4...v2.1.5) -* Fix crash in 405 error response generator -* Add ability to verify request/response pairs from rspec api +**Merged pull requests:** -# 0.9.4 +- Honor :single\_org =\> 'orgname' parameter everywhere [\#66](https://github.com/chef/chef-zero/pull/66) ([jkeiser](https://github.com/jkeiser)) -* Ruby 1.8.7 support +## [v2.1.4](https://github.com/chef/chef-zero/tree/v2.1.4) (2014-05-28) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.3...v2.1.4) -# 0.9.3 +## [v2.1.3](https://github.com/chef/chef-zero/tree/v2.1.3) (2014-05-27) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.2...v2.1.3) -* rspec fixes: - - Faster (0 retries) - - Work with more than one test - - Allow tags on when_the_chef_server -* make 500 response return actual exception info +## [v2.1.2](https://github.com/chef/chef-zero/tree/v2.1.2) (2014-05-27) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.1.1...v2.1.2) -# 0.9.2 +## [v2.1.1](https://github.com/chef/chef-zero/tree/v2.1.1) (2014-05-27) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.1...v2.1.1) -* Speed increase for rspec (only start server once) -* Support CTRL+C when running rspec chef-zero tests +## [v2.1](https://github.com/chef/chef-zero/tree/v2.1) (2014-05-26) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.6.3...v2.1) -# 0.9.1 +**Closed issues:** -* Switch from webrick -> thin -* Bugfixes +- Is there a good way to detect if cookbook is being run using chef-zero? [\#63](https://github.com/chef/chef-zero/issues/63) +- Not loading roles [\#61](https://github.com/chef/chef-zero/issues/61) +- How to handle changing url of ChefZero during chef-client run [\#59](https://github.com/chef/chef-zero/issues/59) +- data\_bag\_item fails in definition block executions [\#58](https://github.com/chef/chef-zero/issues/58) -# 0.9 +**Merged pull requests:** -* Initial code-complete release with working server +- Add multi-tenancy support and chef local mode tests [\#64](https://github.com/chef/chef-zero/pull/64) ([jkeiser](https://github.com/jkeiser)) + +## [v1.6.3](https://github.com/chef/chef-zero/tree/v1.6.3) (2014-02-10) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.7.3...v1.6.3) + +**Closed issues:** + +- chef-zero 404'ing on data bag items [\#56](https://github.com/chef/chef-zero/issues/56) +- Errno::ENAMETOOLONG [\#53](https://github.com/chef/chef-zero/issues/53) +- Backport fix for Isssue \#48 to the 1.x line [\#51](https://github.com/chef/chef-zero/issues/51) +- Search strange behavior [\#42](https://github.com/chef/chef-zero/issues/42) + +## [v1.7.3](https://github.com/chef/chef-zero/tree/v1.7.3) (2014-01-22) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.0.2...v1.7.3) + +**Closed issues:** + +- search query fails when using parenthesis [\#38](https://github.com/chef/chef-zero/issues/38) + +## [v2.0.2](https://github.com/chef/chef-zero/tree/v2.0.2) (2014-01-21) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.0.1...v2.0.2) + +**Closed issues:** + +- Parsing metadata blows up on evaluating metadata.json. [\#48](https://github.com/chef/chef-zero/issues/48) +- depsolve endpoint needs informative error messages [\#32](https://github.com/chef/chef-zero/issues/32) + +**Merged pull requests:** + +- Read JSON, not a file path [\#52](https://github.com/chef/chef-zero/pull/52) ([sethvargo](https://github.com/sethvargo)) +- Fix IPV6 support [\#50](https://github.com/chef/chef-zero/pull/50) ([sethvargo](https://github.com/sethvargo)) +- Fix typo [\#47](https://github.com/chef/chef-zero/pull/47) ([gregkare](https://github.com/gregkare)) + +## [v2.0.1](https://github.com/chef/chef-zero/tree/v2.0.1) (2014-01-03) +[Full Changelog](https://github.com/chef/chef-zero/compare/v2.0.0...v2.0.1) + +**Merged pull requests:** + +- Fix clear data when no data was added to chef zero [\#46](https://github.com/chef/chef-zero/pull/46) ([alex-slynko](https://github.com/alex-slynko)) +- Fix an issue with an incorrect number of parameters passed to build\_uri [\#45](https://github.com/chef/chef-zero/pull/45) ([sethvargo](https://github.com/sethvargo)) +- Make playground items more semantic [\#44](https://github.com/chef/chef-zero/pull/44) ([sethvargo](https://github.com/sethvargo)) + +## [v2.0.0](https://github.com/chef/chef-zero/tree/v2.0.0) (2013-12-17) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.7.2...v2.0.0) + +**Closed issues:** + +- Not found returned for all roles [\#34](https://github.com/chef/chef-zero/issues/34) + +**Merged pull requests:** + +- Remove puma and fork a subprocess [\#43](https://github.com/chef/chef-zero/pull/43) ([sethvargo](https://github.com/sethvargo)) + +## [v1.7.2](https://github.com/chef/chef-zero/tree/v1.7.2) (2013-11-19) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.7.1...v1.7.2) + +**Merged pull requests:** + +- Make Server Options Configurable in RSpec Helper [\#41](https://github.com/chef/chef-zero/pull/41) ([danielsdeleo](https://github.com/danielsdeleo)) + +## [v1.7.1](https://github.com/chef/chef-zero/tree/v1.7.1) (2013-11-03) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.7...v1.7.1) + +**Implemented enhancements:** + +- Find a pure-Ruby server solution [\#36](https://github.com/chef/chef-zero/issues/36) + +## [v1.7](https://github.com/chef/chef-zero/tree/v1.7) (2013-11-02) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.7.beta.1...v1.7) + +## [v1.7.beta.1](https://github.com/chef/chef-zero/tree/v1.7.beta.1) (2013-11-02) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.6.2...v1.7.beta.1) + +**Merged pull requests:** + +- improve depsolver error message [\#40](https://github.com/chef/chef-zero/pull/40) ([lamont-granquist](https://github.com/lamont-granquist)) + +## [v1.6.2](https://github.com/chef/chef-zero/tree/v1.6.2) (2013-10-11) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.6.1...v1.6.2) + +## [v1.6.1](https://github.com/chef/chef-zero/tree/v1.6.1) (2013-10-11) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.6...v1.6.1) + +**Closed issues:** + +- \[\* TO \*\] search syntax doesn't work [\#37](https://github.com/chef/chef-zero/issues/37) + +**Merged pull requests:** + +- Range queries with smoke test [\#35](https://github.com/chef/chef-zero/pull/35) ([mattgleeson](https://github.com/mattgleeson)) + +## [v1.6](https://github.com/chef/chef-zero/tree/v1.6) (2013-09-13) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.6...v1.6) + +## [v1.5.6](https://github.com/chef/chef-zero/tree/v1.5.6) (2013-09-13) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.5...v1.5.6) + +## [v1.5.5](https://github.com/chef/chef-zero/tree/v1.5.5) (2013-08-08) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.4...v1.5.5) + +**Closed issues:** + +- Incorrect search behavior when a query parameter contains a '-' character [\#31](https://github.com/chef/chef-zero/issues/31) +- databag.save throws 405 "Method not allowed" [\#29](https://github.com/chef/chef-zero/issues/29) +- How can I validate if I am running in Chef-Zero mode? [\#28](https://github.com/chef/chef-zero/issues/28) +- License missing from gemspec [\#26](https://github.com/chef/chef-zero/issues/26) + +**Merged pull requests:** + +- fixup end\_range typos [\#30](https://github.com/chef/chef-zero/pull/30) ([mattgleeson](https://github.com/mattgleeson)) +- Add license to gemspec \(fixes \#26\) [\#27](https://github.com/chef/chef-zero/pull/27) ([sethvargo](https://github.com/sethvargo)) + +## [v1.5.4](https://github.com/chef/chef-zero/tree/v1.5.4) (2013-07-12) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.3...v1.5.4) + +**Closed issues:** + +- Issue with [\#25](https://github.com/chef/chef-zero/issues/25) +- Typo error in README [\#24](https://github.com/chef/chef-zero/issues/24) +- Add a CHANGELOG [\#11](https://github.com/chef/chef-zero/issues/11) + +## [v1.5.3](https://github.com/chef/chef-zero/tree/v1.5.3) (2013-06-28) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.2...v1.5.3) + +**Closed issues:** + +- Chef Zero should set a server header for identification [\#23](https://github.com/chef/chef-zero/issues/23) +- Data bag search of the form "x:y AND NOT z:w" fails [\#22](https://github.com/chef/chef-zero/issues/22) + +## [v1.5.2](https://github.com/chef/chef-zero/tree/v1.5.2) (2013-06-27) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.5.1...v1.5.2) + +## [v1.5.1](https://github.com/chef/chef-zero/tree/v1.5.1) (2013-06-19) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.5...v1.5.1) + +**Closed issues:** + +- Developing Vagrant-Chef-Zero [\#21](https://github.com/chef/chef-zero/issues/21) + +**Merged pull requests:** + +- Allow chef-zero to listen on Unix domain socket. [\#20](https://github.com/chef/chef-zero/pull/20) ([stevendanna](https://github.com/stevendanna)) + +## [v1.5](https://github.com/chef/chef-zero/tree/v1.5) (2013-06-12) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.4...v1.5) + +**Merged pull requests:** + +- Support daemon mode [\#19](https://github.com/chef/chef-zero/pull/19) ([sethvargo](https://github.com/sethvargo)) + +## [v1.4](https://github.com/chef/chef-zero/tree/v1.4) (2013-06-07) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.4.0.alpha...v1.4) + +**Merged pull requests:** + +- Downgrade Puma so Chef Zero runs on Windows [\#18](https://github.com/chef/chef-zero/pull/18) ([sethvargo](https://github.com/sethvargo)) + +## [v1.4.0.alpha](https://github.com/chef/chef-zero/tree/v1.4.0.alpha) (2013-06-07) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.3...v1.4.0.alpha) + +**Closed issues:** + +- Searching for a node with an exact name fails [\#15](https://github.com/chef/chef-zero/issues/15) + +## [v1.3](https://github.com/chef/chef-zero/tree/v1.3) (2013-06-06) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.2.1...v1.3) + +**Closed issues:** + +- Searching is Broken [\#14](https://github.com/chef/chef-zero/issues/14) + +## [v1.2.1](https://github.com/chef/chef-zero/tree/v1.2.1) (2013-06-05) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.2...v1.2.1) + +**Closed issues:** + +- Allow seeding of Data [\#13](https://github.com/chef/chef-zero/issues/13) + +## [v1.2](https://github.com/chef/chef-zero/tree/v1.2) (2013-06-03) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.1.3...v1.2) + +**Merged pull requests:** + +- Fix usage info to be consistent with latest version [\#12](https://github.com/chef/chef-zero/pull/12) ([andrewgross](https://github.com/andrewgross)) + +## [v1.1.3](https://github.com/chef/chef-zero/tree/v1.1.3) (2013-05-30) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.1.2...v1.1.3) + +## [v1.1.2](https://github.com/chef/chef-zero/tree/v1.1.2) (2013-05-29) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.1.1...v1.1.2) + +## [v1.1.1](https://github.com/chef/chef-zero/tree/v1.1.1) (2013-05-28) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.1...v1.1.1) + +**Merged pull requests:** + +- fix undefined method .list on CookbookData [\#10](https://github.com/chef/chef-zero/pull/10) ([reset](https://github.com/reset)) + +## [v1.1](https://github.com/chef/chef-zero/tree/v1.1) (2013-05-28) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.0.1...v1.1) + +## [v1.0.1](https://github.com/chef/chef-zero/tree/v1.0.1) (2013-05-20) +[Full Changelog](https://github.com/chef/chef-zero/compare/v1.0...v1.0.1) + +**Merged pull requests:** + +- Fix frozen version [\#9](https://github.com/chef/chef-zero/pull/9) ([sethvargo](https://github.com/sethvargo)) + +## [v1.0](https://github.com/chef/chef-zero/tree/v1.0) (2013-05-20) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.13...v1.0) + +## [v0.9.13](https://github.com/chef/chef-zero/tree/v0.9.13) (2013-05-20) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.12...v0.9.13) + +## [v0.9.12](https://github.com/chef/chef-zero/tree/v0.9.12) (2013-05-20) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.11...v0.9.12) + +**Merged pull requests:** + +- Move to Puma [\#8](https://github.com/chef/chef-zero/pull/8) ([sethvargo](https://github.com/sethvargo)) + +## [v0.9.11](https://github.com/chef/chef-zero/tree/v0.9.11) (2013-05-19) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.9...v0.9.11) + +**Closed issues:** + +- no port found in cookbook url - can't download cookbooks unless listening on port 80 [\#5](https://github.com/chef/chef-zero/issues/5) + +**Merged pull requests:** + +- Add '-d' flag [\#7](https://github.com/chef/chef-zero/pull/7) ([sethvargo](https://github.com/sethvargo)) + +## [v0.9.9](https://github.com/chef/chef-zero/tree/v0.9.9) (2013-05-14) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.8...v0.9.9) + +**Closed issues:** + +- HTTP 500 Returned when Accept Header isn't set. [\#3](https://github.com/chef/chef-zero/issues/3) +- knife plugin? [\#2](https://github.com/chef/chef-zero/issues/2) +- Cannot give a specific recipe from chef-client [\#1](https://github.com/chef/chef-zero/issues/1) + +**Merged pull requests:** + +- Remove dependency on Chef gem \(adds JRuby support\) [\#6](https://github.com/chef/chef-zero/pull/6) ([reset](https://github.com/reset)) +- Assume application/json is acceptable if no Accept header was sent. [\#4](https://github.com/chef/chef-zero/pull/4) ([stevendanna](https://github.com/stevendanna)) + +## [v0.9.8](https://github.com/chef/chef-zero/tree/v0.9.8) (2013-04-29) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.7...v0.9.8) + +## [v0.9.7](https://github.com/chef/chef-zero/tree/v0.9.7) (2013-04-17) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.6...v0.9.7) + +## [v0.9.6](https://github.com/chef/chef-zero/tree/v0.9.6) (2013-04-17) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.5...v0.9.6) + +## [v0.9.5](https://github.com/chef/chef-zero/tree/v0.9.5) (2013-01-21) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.4...v0.9.5) + +## [v0.9.4](https://github.com/chef/chef-zero/tree/v0.9.4) (2013-01-18) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.3...v0.9.4) + +## [v0.9.3](https://github.com/chef/chef-zero/tree/v0.9.3) (2013-01-12) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.2...v0.9.3) + +## [v0.9.2](https://github.com/chef/chef-zero/tree/v0.9.2) (2012-12-31) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9.1...v0.9.2) + +## [v0.9.1](https://github.com/chef/chef-zero/tree/v0.9.1) (2012-12-24) +[Full Changelog](https://github.com/chef/chef-zero/compare/v0.9...v0.9.1) + +## [v0.9](https://github.com/chef/chef-zero/tree/v0.9) (2012-12-24) + + +\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*
\ No newline at end of file @@ -1,9 +1,20 @@ source 'https://rubygems.org' gemspec -gem 'rest-client', :github => 'chef/rest-client' +# gem 'rest-client', :github => 'chef/rest-client' -gem 'chef-pedant', :github => 'chef/chef-pedant', :tag => '1.0.46' +gem 'oc-chef-pedant', :github => 'chef/chef-server' -gem 'chef', :github => 'opscode/chef', :tag => '12.2.1' +group :changelog do + gem "github_changelog_generator" +end +# bundler resolve failure on "rspec_junit_formatter" +# gem 'chef-pedant', :github => 'opscode/chef-pedant', :ref => "server-cli-option" + +# gem 'chef', :github => 'chef/chef', :branch => 'jk/policies-acls' + +if ENV['GEMFILE_MOD'] + puts "GEMFILE_MOD: #{ENV['GEMFILE_MOD']}" + instance_eval(ENV['GEMFILE_MOD']) +end @@ -1,8 +1,13 @@ -[![Stories in Ready](https://badge.waffle.io/chef/chef-zero.png?label=ready&title=Ready)](https://waffle.io/chef/chef-zero)[![Stories in Progress](https://badge.waffle.io/chef/chef-zero.png?label=in+progress&title=In+Progress)](https://waffle.io/chef/chef-zero) Chef Zero ========= +[![Status](https://travis-ci.org/chef/chef-zero.svg?branch=master)](https://travis-ci.org/chef/chef-zero) +[![Gem Version](https://badge.fury.io/rb/chef-zero.svg)](http://badge.fury.io/rb/chef-zero) +[![Stories in Ready](https://badge.waffle.io/chef/chef-zero.png?label=ready&title=Ready)](https://waffle.io/chef/chef-zero) +[![Stories in Progress](https://badge.waffle.io/chef/chef-zero.png?label=in+progress&title=In+Progress)](https://waffle.io/chef/chef-zero) + + Description ----------- Chef Zero is a simple, easy-install, in-memory Chef server that can be useful @@ -3,21 +3,43 @@ require 'bundler/gem_tasks' require 'chef_zero/version' +def run_oc_pedant(env={}) + ENV.update(env) + require File.expand_path('spec/run_oc_pedant') +end + +ENV_DOCS = <<END +Environment: + - RSPEC_OPTS Options to pass to RSpec + e.g. RSPEC_OPTS="--fail-fast --profile 5" + - PEDANT_OPTS Options to pass to oc-chef-pedant + e.g. PEDANT_OPTS="--focus-keys --skip-users" + - LOG_LEVEL Set the log level (default: warn) + e.g. LOG_LEVEL=debug +END + task :default => :pedant -desc "run specs" +desc "Run specs" task :spec do system('rspec spec/*_spec.rb') end -desc "run pedant" -task :pedant do - require File.expand_path('spec/run_pedant') +desc "Run oc-chef-pedant\n\n#{ENV_DOCS}" +task :pedant => :oc_pedant + +desc "Run oc-chef-pedant with CHEF_FS set\n\n#{ENV_DOCS}" +task :cheffs do + run_oc_pedant('CHEF_FS' => 'yes') +end + +desc "Run oc-chef-pedant with FILE_STORE set\n\n#{ENV_DOCS}" +task :filestore do + run_oc_pedant('FILE_STORE' => 'yes') end -desc "run oc pedant" task :oc_pedant do - require File.expand_path('spec/run_oc_pedant') + run_oc_pedant end task :chef_spec do @@ -29,3 +51,16 @@ task :berkshelf_spec do gem_path = Bundler.environment.specs['berkshelf'].first.full_gem_path system("cd #{gem_path} && thor spec:ci") end + +begin + require "github_changelog_generator/task" + + GitHubChangelogGenerator::RakeTask.new :changelog do |config| + config.future_release = ChefZero::VERSION + config.enhancement_labels = "enhancement,Enhancement,New Feature,Feature".split(",") + config.bug_labels = "bug,Bug,Improvement,Upstream Bug".split(",") + config.exclude_labels = "duplicate,question,invalid,wontfix,no_changelog,Exclude From Changelog,Question,Discussion".split(",") + end +rescue LoadError + puts "github_changelog_generator is not available. gem install github_changelog_generator to generate changelogs" +end diff --git a/bin/chef-zero b/bin/chef-zero index 54739bc..03b6d01 100755 --- a/bin/chef-zero +++ b/bin/chef-zero @@ -31,7 +31,8 @@ OptionParser.new do |opts| opts.banner = "Usage: chef-zero [ARGS]" opts.on("-H", "--host HOST", "Host to bind to (default: 127.0.0.1)") do |value| - options[:host] = value + options[:host] ||= [] + options[:host] << value end opts.on("-p", "--port PORT", "Port to listen on (e.g. 8889, or 8500-8600 or 8885,8888)") do |value| @@ -55,6 +56,11 @@ OptionParser.new do |opts| options[:log_file] = value end + opts.on("--enterprise", "Whether to run in enterprise mode") do |value| + options[:single_org] = nil + options[:osc_compat] = false + end + opts.on("--multi-org", "Whether to run in multi-org mode") do |value| options[:single_org] = nil end diff --git a/chef-zero.gemspec b/chef-zero.gemspec index 8538040..8114a81 100644 --- a/chef-zero.gemspec +++ b/chef-zero.gemspec @@ -8,21 +8,28 @@ Gem::Specification.new do |s| s.summary = 'Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes' s.description = s.summary s.author = 'John Keiser' - s.email = 'jkeiser@opscode.com' - s.homepage = 'http://www.opscode.com' + s.email = 'jkeiser@chef.io' + s.homepage = 'http://www.chef.io' s.license = 'Apache 2.0' - s.add_dependency 'mixlib-log', '~> 1.3' - s.add_dependency 'hashie', '~> 2.0' + s.required_ruby_version = ">= 2.1.0" + + s.add_dependency 'mixlib-log', '~> 1.3' + s.add_dependency 'hashie', '>= 2.0', '< 4.0' s.add_dependency 'uuidtools', '~> 2.1' - s.add_dependency 'ffi-yajl', '>= 1.1', '< 3.0' - s.add_dependency 'rack' + s.add_dependency 'ffi-yajl', '~> 2.2' + s.add_dependency 'rack', '< 2' # 2.0 requires Ruby 2.2+ + s.add_development_dependency 'pry' + s.add_development_dependency 'pry-byebug' + s.add_development_dependency 'pry-stack_explorer' s.add_development_dependency 'rake' s.add_development_dependency 'rspec' + s.add_development_dependency 'chef' s.bindir = 'bin' s.executables = ['chef-zero'] s.require_path = 'lib' - s.files = %w(LICENSE README.md Rakefile) + Dir.glob('{lib,spec}/**/*') + s.files = %w(LICENSE README.md Gemfile Rakefile) + Dir.glob('*.gemspec') + + Dir.glob('{lib,spec}/**/*', File::FNM_DOTMATCH).reject {|f| File.directory?(f) } end diff --git a/gemfiles/berkshelf.gemfile b/gemfiles/berkshelf.gemfile deleted file mode 100644 index ca99e91..0000000 --- a/gemfiles/berkshelf.gemfile +++ /dev/null @@ -1,15 +0,0 @@ -source 'https://rubygems.org' - -gemspec :path => "../" - -gem 'berkshelf', :github => 'berkshelf' - -# development dependencies of berkshelf -gem 'aruba', '~> 0.5' -gem 'fuubar', '~> 1.1' -gem 'rake', '~> 0.9' -gem 'rspec', '~> 2.13' -gem 'spork', '~> 0.9' -gem 'test-kitchen', '~> 1.2' -gem 'webmock', '~> 1.11' -gem 'yard', '~> 0.8' diff --git a/gemfiles/latest-chef.gemfile b/gemfiles/latest-chef.gemfile deleted file mode 100644 index ba98724..0000000 --- a/gemfiles/latest-chef.gemfile +++ /dev/null @@ -1,5 +0,0 @@ -source 'https://rubygems.org' - -gemspec :path => "../" - -gem 'chef', :github => 'opscode/chef' diff --git a/gemfiles/latest-pedant.gemfile b/gemfiles/latest-pedant.gemfile deleted file mode 100644 index f5fdb86..0000000 --- a/gemfiles/latest-pedant.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source 'https://rubygems.org' - -gemspec :path => "../" - -gem 'rest-client', :git => 'git://github.com/opscode/rest-client.git' -gem 'chef-pedant', :github => 'opscode/chef-pedant' -gem 'chef', '>= 11.0' diff --git a/gemfiles/no-pedant.gemfile b/gemfiles/no-pedant.gemfile deleted file mode 100644 index 7784f18..0000000 --- a/gemfiles/no-pedant.gemfile +++ /dev/null @@ -1,4 +0,0 @@ -source 'https://rubygems.org' - -gemspec :path => "../" - diff --git a/gemfiles/oc-chef-pedant.gemfile b/gemfiles/oc-chef-pedant.gemfile deleted file mode 100644 index 26324c3..0000000 --- a/gemfiles/oc-chef-pedant.gemfile +++ /dev/null @@ -1,7 +0,0 @@ -source 'https://rubygems.org' -gemspec :path => '../' - -gem 'rest-client', :github => 'opscode/rest-client', :branch => 'lcg/1.6.7-version-lying' -gem 'chef-pedant', :github => 'opscode/chef-pedant', :ref => '81f3b4ecbc09d04950f2819b38a6a8f906ada2a7' -gem 'oc-chef-pedant', :git => 'git@github.com:opscode/oc-chef-pedant', :ref => '3c0eb31f1e49aa947b81ad51387b7a68adbc5f91' -gem 'chef', :github => 'opscode/chef' diff --git a/lib/chef_zero.rb b/lib/chef_zero.rb index 354ab07..94633d4 100644 --- a/lib/chef_zero.rb +++ b/lib/chef_zero.rb @@ -1,6 +1,9 @@ module ChefZero require 'chef_zero/log' + MIN_API_VERSION = 0 + MAX_API_VERSION = 1 + CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIDMzCCApygAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnjELMAkGA1UEBhMCVVMx\nEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxFjAUBgNVBAoM\nDU9wc2NvZGUsIEluYy4xHDAaBgNVBAsME0NlcnRpZmljYXRlIFNlcnZpY2UxMjAw\nBgNVBAMMKW9wc2NvZGUuY29tL2VtYWlsQWRkcmVzcz1hdXRoQG9wc2NvZGUuY29t\nMB4XDTEyMTEyMTAwMzQyMVoXDTIyMTExOTAwMzQyMVowgZsxEDAOBgNVBAcTB1Nl\nYXR0bGUxEzARBgNVBAgTCldhc2hpbmd0b24xCzAJBgNVBAYTAlVTMRwwGgYDVQQL\nExNDZXJ0aWZpY2F0ZSBTZXJ2aWNlMRYwFAYDVQQKEw1PcHNjb2RlLCBJbmMuMS8w\nLQYDVQQDFCZVUkk6aHR0cDovL29wc2NvZGUuY29tL0dVSURTL3VzZXJfZ3VpZDCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLDmPbR71bS2esZlZh/HfC6\n0azXFjl2677wq2ovk9xrUb0Ui4ZLC66TqQ9C/RBzOjXU4TRf3hgPTqvlCgHusl0d\nIcLCrsSl6kPEhJpYWWfRoroIAwf82A9yLQekhqXZEXu5EKkwoUMqyF6m0ZCasaE1\ny8niQxdLAsk3ady/CGQlFqHTPKFfU5UASR2LRtYC1MCIvJHDFRKAp9kPJbQo9P37\nZ8IU7cDudkZFgNLmDixlWsh7C0ghX8fgAlj1P6FgsFufygam973k79GhIP54dELB\nc0S6E8ekkRSOXU9jX/IoiXuFglBvFihAdhvED58bMXzj2AwXUyeAlxItnvs+NVUC\nAwEAATANBgkqhkiG9w0BAQUFAAOBgQBkFZRbMoywK3hb0/X7MXmPYa7nlfnd5UXq\nr2n32ettzZNmEPaI2d1j+//nL5qqhOlrWPS88eKEPnBOX/jZpUWOuAAddnrvFzgw\nrp/C2H7oMT+29F+5ezeViLKbzoFYb4yECHBoi66IFXNae13yj7taMboBeUmE664G\nTB/MZpRr8g==\n-----END CERTIFICATE-----\n" PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0sOY9tHvVtLZ6xmVmH8d\n8LrRrNcWOXbrvvCrai+T3GtRvRSLhksLrpOpD0L9EHM6NdThNF/eGA9Oq+UKAe6y\nXR0hwsKuxKXqQ8SEmlhZZ9GiuggDB/zYD3ItB6SGpdkRe7kQqTChQyrIXqbRkJqx\noTXLyeJDF0sCyTdp3L8IZCUWodM8oV9TlQBJHYtG1gLUwIi8kcMVEoCn2Q8ltCj0\n/ftnwhTtwO52RkWA0uYOLGVayHsLSCFfx+ACWPU/oWCwW5/KBqb3veTv0aEg/nh0\nQsFzRLoTx6SRFI5dT2Nf8iiJe4WCUG8WKEB2G8QPnxsxfOPYDBdTJ4CXEi2e+z41\nVQIDAQAB\n-----END PUBLIC KEY-----\n" PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0sOY9tHvVtLZ6xmVmH8d8LrRrNcWOXbrvvCrai+T3GtRvRSL\nhksLrpOpD0L9EHM6NdThNF/eGA9Oq+UKAe6yXR0hwsKuxKXqQ8SEmlhZZ9GiuggD\nB/zYD3ItB6SGpdkRe7kQqTChQyrIXqbRkJqxoTXLyeJDF0sCyTdp3L8IZCUWodM8\noV9TlQBJHYtG1gLUwIi8kcMVEoCn2Q8ltCj0/ftnwhTtwO52RkWA0uYOLGVayHsL\nSCFfx+ACWPU/oWCwW5/KBqb3veTv0aEg/nh0QsFzRLoTx6SRFI5dT2Nf8iiJe4WC\nUG8WKEB2G8QPnxsxfOPYDBdTJ4CXEi2e+z41VQIDAQABAoIBAALhqbW2KQ+G0nPk\nZacwFbi01SkHx8YBWjfCEpXhEKRy0ytCnKW5YO+CFU2gHNWcva7+uhV9OgwaKXkw\nKHLeUJH1VADVqI4Htqw2g5mYm6BPvWnNsjzpuAp+BR+VoEGkNhj67r9hatMAQr0I\nitTvSH5rvd2EumYXIHKfz1K1SegUk1u1EL1RcMzRmZe4gDb6eNBs9Sg4im4ybTG6\npPIytA8vBQVWhjuAR2Tm+wZHiy0Az6Vu7c2mS07FSX6FO4E8SxWf8idaK9ijMGSq\nFvIS04mrY6XCPUPUC4qm1qNnhDPpOr7CpI2OO98SqGanStS5NFlSFXeXPpM280/u\nfZUA0AECgYEA+x7QUnffDrt7LK2cX6wbvn4mRnFxet7bJjrfWIHf+Rm0URikaNma\nh0/wNKpKBwIH+eHK/LslgzcplrqPytGGHLOG97Gyo5tGAzyLHUWBmsNkRksY2sPL\nuHq6pYWJNkqhnWGnIbmqCr0EWih82x/y4qxbJYpYqXMrit0wVf7yAgkCgYEA1twI\ngFaXqesetTPoEHSQSgC8S4D5/NkdriUXCYb06REcvo9IpFMuiOkVUYNN5d3MDNTP\nIdBicfmvfNELvBtXDomEUD8ls1UuoTIXRNGZ0VsZXu7OErXCK0JKNNyqRmOwcvYL\nJRqLfnlei5Ndo1lu286yL74c5rdTLs/nI2p4e+0CgYB079ZmcLeILrmfBoFI8+Y/\ngJLmPrFvXBOE6+lRV7kqUFPtZ6I3yQzyccETZTDvrnx0WjaiFavUPH27WMjY01S2\nTMtO0Iq1MPsbSrglO1as8MvjB9ldFcvp7gy4Q0Sv6XT0yqJ/S+vo8Df0m+H4UBpU\nf5o6EwBSd/UQxwtZIE0lsQKBgQCswfjX8Eg8KL/lJNpIOOE3j4XXE9ptksmJl2sB\njxDnQYoiMqVO808saHVquC/vTrpd6tKtNpehWwjeTFuqITWLi8jmmQ+gNTKsC9Gn\n1Pxf2Gb67PqnEpwQGln+TRtgQ5HBrdHiQIi+5am+gnw89pDrjjO5rZwhanAo6KPJ\n1zcPNQKBgQDxFu8v4frDmRNCVaZS4f1B6wTrcMrnibIDlnzrK9GG6Hz1U7dDv8s8\nNf4UmeMzDXjlPWZVOvS5+9HKJPdPj7/onv8B2m18+lcgTTDJBkza7R1mjL1Cje/Z\nKcVGsryKN6cjE7yCDasnA7R2rVBV/7NWeJV77bmzT5O//rW4yIfUIg==\n-----END RSA PRIVATE KEY-----\n" diff --git a/lib/chef_zero/chef_data/acl_path.rb b/lib/chef_zero/chef_data/acl_path.rb index c783a1a..52b43d4 100644 --- a/lib/chef_zero/chef_data/acl_path.rb +++ b/lib/chef_zero/chef_data/acl_path.rb @@ -13,7 +13,8 @@ module ChefZero # specified on X, they are not inherited from X's parent # - stop adding pivotal to acls (he already has access to what he needs) module AclPath - ORG_DATA_TYPES = %w(clients cookbooks containers data environments groups nodes roles sandboxes) + ORG_DATA_TYPES = %w(clients cookbook_artifacts cookbooks containers data environments groups + nodes policies policy_groups roles sandboxes) TOP_DATA_TYPES = %w(containers organizations users) # ACL data paths for a partition are: diff --git a/lib/chef_zero/chef_data/cookbook_data.rb b/lib/chef_zero/chef_data/cookbook_data.rb index aed625b..e690fde 100644 --- a/lib/chef_zero/chef_data/cookbook_data.rb +++ b/lib/chef_zero/chef_data/cookbook_data.rb @@ -108,6 +108,11 @@ module ChefZero cookbook_arg(:replacing, cookbook, version_constraints) end + def gem(*opts) + self[:gems] ||= [] + self[:gems] << opts + end + def recipe(recipe, description) self[:recipes][recipe] = description end diff --git a/lib/chef_zero/chef_data/data_normalizer.rb b/lib/chef_zero/chef_data/data_normalizer.rb index f7e87a6..e819f1d 100644 --- a/lib/chef_zero/chef_data/data_normalizer.rb +++ b/lib/chef_zero/chef_data/data_normalizer.rb @@ -8,17 +8,18 @@ module ChefZero def self.normalize_acls(acls) ChefData::DefaultCreator::PERMISSIONS.each do |perm| acls[perm] ||= {} - acls[perm]['actors'] ||= [] + (acls[perm]['actors'] ||= []).uniq! # this gets doubled sometimes, for reasons. acls[perm]['groups'] ||= [] end acls end - def self.normalize_client(client, name) + def self.normalize_client(client, name, orgname = nil) client['name'] ||= name - client['admin'] ||= false - client['admin'] = !!client['admin'] - client['public_key'] ||= PUBLIC_KEY + client['clientname'] ||= name + client['admin'] = !!client['admin'] if client.key?('admin') + client['public_key'] = PUBLIC_KEY unless client.key?('public_key') + client['orgname'] ||= orgname client['validator'] ||= false client['validator'] = !!client['validator'] client['json_class'] ||= "Chef::ApiClient" @@ -35,7 +36,7 @@ module ChefZero def self.normalize_user(user, name, identity_keys, osc_compat, method=nil) user[identity_keys.first] ||= name - user['public_key'] ||= PUBLIC_KEY + user['public_key'] = PUBLIC_KEY unless user.key?('public_key') user['admin'] ||= false user['admin'] = !!user['admin'] user['openid'] ||= nil @@ -78,7 +79,8 @@ module ChefZero data_bag_item end - def self.normalize_cookbook(endpoint, org_prefix, cookbook, name, version, base_uri, method) + def self.normalize_cookbook(endpoint, org_prefix, cookbook, name, version, base_uri, method, + is_cookbook_artifact=false) # TODO I feel dirty if method != 'PUT' cookbook.each_pair do |key, value| @@ -91,24 +93,28 @@ module ChefZero end end cookbook['name'] ||= "#{name}-#{version}" - # TODO this feels wrong, but the real chef server doesn't expand this default - # cookbook['version'] ||= version - cookbook['cookbook_name'] ||= name + # TODO it feels wrong, but the real chef server doesn't expand 'version', so we don't either. + cookbook['frozen?'] ||= false cookbook['metadata'] ||= {} cookbook['metadata']['version'] ||= version - # Sad to not be expanding defaults just because Chef doesn't :( - # cookbook['metadata']['name'] ||= name - # cookbook['metadata']['description'] ||= "A fabulous new cookbook" + + # defaults set by the client and not the Server: + # metadata[name, description, maintainer, maintainer_email, license] + cookbook['metadata']['long_description'] ||= "" - # cookbook['metadata']['maintainer'] ||= "YOUR_COMPANY_NAME" - # cookbook['metadata']['maintainer_email'] ||= "YOUR_EMAIL" - # cookbook['metadata']['license'] ||= "none" cookbook['metadata']['dependencies'] ||= {} cookbook['metadata']['attributes'] ||= {} cookbook['metadata']['recipes'] ||= {} end - cookbook['json_class'] ||= 'Chef::CookbookVersion' + + if is_cookbook_artifact + cookbook.delete('json_class') + else + cookbook['cookbook_name'] ||= name + cookbook['json_class'] ||= 'Chef::CookbookVersion' + end + cookbook['chef_type'] ||= 'cookbook_version' if method == 'MIN' cookbook['metadata'].delete('attributes') @@ -165,6 +171,20 @@ module ChefZero node end + def self.normalize_policy(policy, name, revision) + policy['name'] ||= name + policy['revision_id'] ||= revision + policy['run_list'] ||= [] + policy['cookbook_locks'] ||= {} + policy + end + + def self.normalize_policy_group(policy_group, name) + policy_group[name] ||= 'name' + policy_group['policies'] ||= {} + policy_group + end + def self.normalize_organization(org, name) org['name'] ||= name org['full_name'] ||= name diff --git a/lib/chef_zero/chef_data/default_creator.rb b/lib/chef_zero/chef_data/default_creator.rb index 60b495a..957018c 100644 --- a/lib/chef_zero/chef_data/default_creator.rb +++ b/lib/chef_zero/chef_data/default_creator.rb @@ -148,6 +148,7 @@ module ChefZero DEFAULT_ORG_SPINE = { 'clients' => {}, + 'cookbook_artifacts' => {}, 'cookbooks' => {}, 'data' => {}, 'environments' => %w(_default), @@ -155,12 +156,14 @@ module ChefZero 'checksums' => {} }, 'nodes' => {}, + 'policies' => {}, + 'policy_groups' => {}, 'roles' => {}, 'sandboxes' => {}, 'users' => {}, 'org' => {}, - 'containers' => %w(clients containers cookbooks data environments groups nodes roles sandboxes), + 'containers' => %w(clients containers cookbook_artifacts cookbooks data environments groups nodes policies policy_groups roles sandboxes), 'groups' => %w(admins billing-admins clients users), 'association_requests' => {} } @@ -279,7 +282,7 @@ module ChefZero 'delete' => { 'groups' => %w(admins) }, 'grant' => { 'groups' => %w(admins) }, } - when 'containers/cookbooks', 'containers/environments', 'containers/roles' + when 'containers/environments', 'containers/roles', 'containers/policy_groups', 'containers/policies' { 'create' => { 'groups' => %w(admins users) }, 'read' => { 'groups' => %w(admins users clients) }, @@ -287,7 +290,7 @@ module ChefZero 'delete' => { 'groups' => %w(admins users) }, 'grant' => { 'groups' => %w(admins) }, } - when 'containers/cookbooks', 'containers/data' + when 'containers/cookbooks', 'containers/cookbook_artifacts', 'containers/data' { 'create' => { 'groups' => %w(admins users clients) }, 'read' => { 'groups' => %w(admins users clients) }, @@ -378,11 +381,12 @@ module ChefZero # Non-default containers do not get superusers added to them, # because reasons. unless path.size == 4 && path[0] == 'organizations' && path[2] == 'containers' && !exists?(path) - owners |= superusers + owners += superusers end end - owners.uniq + # we don't de-dup this list, because pedant expects to see ["pivotal", "pivotal"] in some cases. + owners end def default_acl(acl_path, acl={}) @@ -436,7 +440,7 @@ module ChefZero when 4 return path[0] == 'organizations' && ( (path[2] == 'acls' && path[1] != 'root') || - %w(cookbooks data).include?(path[2])) + %w(cookbooks cookbook_artifacts data policies policy_groups).include?(path[2])) else return false end diff --git a/lib/chef_zero/data_store/data_error.rb b/lib/chef_zero/data_store/data_error.rb index 9822a6b..b392e58 100644 --- a/lib/chef_zero/data_store/data_error.rb +++ b/lib/chef_zero/data_store/data_error.rb @@ -19,13 +19,14 @@ module ChefZero module DataStore class DataError < StandardError + attr_reader :path, :cause + def initialize(path, cause = nil) @path = path @cause = cause + path_for_msg = path.nil? ? "nil" : "/#{path.join('/')}" + super "Data path: #{path_for_msg}" end - - attr_reader :path - attr_reader :cause end end end diff --git a/lib/chef_zero/data_store/default_facade.rb b/lib/chef_zero/data_store/default_facade.rb index 0d4cf34..c941322 100644 --- a/lib/chef_zero/data_store/default_facade.rb +++ b/lib/chef_zero/data_store/default_facade.rb @@ -62,7 +62,7 @@ module ChefZero end options_hash = options.last.is_a?(Hash) ? options.last : {} - default_creator.created(path + [ name ], options_hash[:requestor], options.include?(:recursive)) + default_creator.created(path + [ name ], options_hash[:requestor], options.include?(:create_dir)) end def get(path, request=nil) diff --git a/lib/chef_zero/endpoints/actor_default_key_endpoint.rb b/lib/chef_zero/endpoints/actor_default_key_endpoint.rb new file mode 100644 index 0000000..3be1475 --- /dev/null +++ b/lib/chef_zero/endpoints/actor_default_key_endpoint.rb @@ -0,0 +1,77 @@ +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # ActorDefaultKeyEndpoint + # + # This class handles DELETE/GET/PUT requests for client/user default public + # keys, i.e. requests with identity key "default". All others are handled + # by ActorKeyEndpoint. + # + # Default public keys are stored with the actor (client or user) instead of + # under user/client_keys. Handling those in a separate endpoint offloads + # the branching logic onto the router rather than branching in every + # endpoint method (`if request.rest_path[-1] == "default" ...`). + # + # /users/USER/keys/default + # /organizations/ORG/clients/CLIENT/keys/default + class ActorDefaultKeyEndpoint < RestBase + DEFAULT_PUBLIC_KEY_NAME = "default".freeze + + def get(request) + # 404 if actor doesn't exist + actor_data = get_actor_data(request) + key_data = default_public_key_from_actor(actor_data) + + # 404 if the actor doesn't have a default key + if key_data["public_key"].nil? + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + end + + json_response(200, default_public_key_from_actor(actor_data)) + end + + def delete(request) + path = actor_path(request) + actor_data = get_actor_data(request) # 404 if actor doesn't exist + + default_public_key = delete_actor_default_public_key!(request, path, actor_data) + json_response(200, default_public_key) + end + + def put(request) + # 404 if actor doesn't exist + actor_data = get_actor_data(request) + + new_public_key = parse_json(request.body)["public_key"] + actor_data["public_key"] = new_public_key + + set_data(request, actor_path(request), to_json(actor_data)) + end + + private + + def actor_path(request) + return request.rest_path[0..3] if request.rest_path[2] == "clients" + request.rest_path[0..1] + end + + def get_actor_data(request) + path = actor_path(request) + parse_json(get_data(request, path)) + end + + def default_public_key_from_actor(actor_data) + { "name" => DEFAULT_PUBLIC_KEY_NAME, + "public_key" => actor_data["public_key"], + "expiration_date" => "infinity" } + end + + def delete_actor_default_public_key!(request, path, actor_data) + new_actor_data = actor_data.merge("public_key" => nil) + set_data(request, path, to_json(new_actor_data)) + default_public_key_from_actor(actor_data) + end + end + end +end diff --git a/lib/chef_zero/endpoints/actor_endpoint.rb b/lib/chef_zero/endpoints/actor_endpoint.rb index 342619c..dd2caf2 100644 --- a/lib/chef_zero/endpoints/actor_endpoint.rb +++ b/lib/chef_zero/endpoints/actor_endpoint.rb @@ -8,8 +8,19 @@ module ChefZero # /organizations/ORG/users/NAME # /users/NAME class ActorEndpoint < RestObjectEndpoint + + def get(request) + result = super + user_data = parse_json(result[2]) + + user_data.delete("public_key") unless request.api_v0? + + json_response(200, user_data) + end + def delete(request) result = super + if request.rest_path[0] == 'users' list_data(request, [ 'organizations' ]).each do |org| begin @@ -18,12 +29,15 @@ module ChefZero end end end + + delete_actor_keys!(request) result end def put(request) # Find out if we're updating the public key. request_body = FFI_Yajl::Parser.parse(request.body, :create_additions => false) + if request_body['public_key'].nil? # If public_key is null, then don't overwrite it. Weird patchiness. body_modified = true @@ -33,18 +47,18 @@ module ChefZero end # Generate private_key if requested. - if request_body.has_key?('private_key') + if request_body.key?('private_key') body_modified = true - if request_body['private_key'] + + if request_body.delete('private_key') private_key, public_key = server.gen_key_pair updating_public_key = true request_body['public_key'] = public_key end - request_body.delete('private_key') end - # Save request - request.body = FFI_Yajl::Encoder.encode(request_body, :pretty => true) if body_modified + # Put modified body back in `request.body` + request.body = to_json(request_body) if body_modified # PUT /clients is patchy request.body = patch_request_body(request) @@ -53,21 +67,29 @@ module ChefZero # Inject private_key into response, delete public_key/password if applicable if result[0] == 200 || result[0] == 201 + client_or_user_name = identity_key_value(request) || request.rest_path[-1] + + if is_rename?(request) + rename_keys!(request, client_or_user_name) + end + if request.rest_path[0] == 'users' - key = nil - identity_keys.each do |identity_key| - key ||= request_body[identity_key] - end - key ||= request.rest_path[-1] response = { - 'uri' => build_uri(request.base_uri, [ 'users', key ]) + 'uri' => build_uri(request.base_uri, [ 'users', client_or_user_name ]) } else - response = FFI_Yajl::Parser.parse(result[2], :create_additions => false) + response = parse_json(result[2]) end - response['private_key'] = private_key if private_key - response.delete('public_key') if !updating_public_key && request.rest_path[2] == 'users' + + if client?(request) + response['private_key'] = private_key ? private_key : false + else + response['private_key'] = private_key if private_key + response.delete('public_key') unless updating_public_key + end + response.delete('password') + json_response(result[0], response) else result @@ -75,13 +97,87 @@ module ChefZero end def populate_defaults(request, response_json) - response = FFI_Yajl::Parser.parse(response_json, :create_additions => false) - if request.rest_path[2] == 'clients' - response = ChefData::DataNormalizer.normalize_client(response, request.rest_path[3]) + response = parse_json(response_json) + + populated_response = + if client?(request) + ChefData::DataNormalizer.normalize_client( + response, + response["name"] || request.rest_path[-1], + request.rest_path[1] + ) + else + ChefData::DataNormalizer.normalize_user( + response, + response["username"] || request.rest_path[-1], + identity_keys, + server.options[:osc_compat], + request.method + ) + end + + to_json(populated_response) + end + + private + + # Move key data to new path + def rename_keys!(request, new_client_or_user_name) + orig_keys_path = keys_path_base(request) + new_keys_path = orig_keys_path.dup + .tap {|path| path[-2] = new_client_or_user_name } + + key_names = list_data_or_else(request, orig_keys_path, nil) + return unless key_names # No keys to move + + key_names.each do |key_name| + # Get old data + orig_path = [ *orig_keys_path, key_name ] + data = get_data(request, orig_path, :data_store_exceptions) + + # Copy data to new path + create_data( + request, + new_keys_path, key_name, + data, + :create_dir + ) + end + + # Delete original data + delete_data_dir(request, orig_keys_path, :recursive, :data_store_exceptions) + end + + def delete_actor_keys!(request) + path = keys_path_base(request)[0..-2] + delete_data_dir(request, path, :recursive, :data_store_exceptions) + rescue DataStore::DataNotFoundError + end + + def client?(request, rest_path=nil) + rest_path ||= request.rest_path + request.rest_path[2] == "clients" + end + + # Return the data store keys path for the request client or user, e.g. + # + # /organizations/ORG/clients/CLIENT -> /organizations/ORG/client_keys/CLIENT/keys + # /organizations/ORG/users/USER -> /organizations/ORG/user_keys/USER/keys + # /users/USER -> /user_keys/USER + # + def keys_path_base(request, client_or_user_name=nil) + rest_path = (rest_path || request.rest_path).dup + rest_path = rest_path.dup + case rest_path[-2] + when "users" + rest_path[-2] = "user_keys" + when "clients" + rest_path[-2] = "client_keys" else - response = ChefData::DataNormalizer.normalize_user(response, request.rest_path[3], identity_keys, server.options[:osc_compat], request.method) + raise "Unexpected URL #{rest_path.join("/")}: cannot determine key path" end - FFI_Yajl::Encoder.encode(response, :pretty => true) + rest_path << "keys" + rest_path end end end diff --git a/lib/chef_zero/endpoints/actor_key_endpoint.rb b/lib/chef_zero/endpoints/actor_key_endpoint.rb new file mode 100644 index 0000000..f2b65ed --- /dev/null +++ b/lib/chef_zero/endpoints/actor_key_endpoint.rb @@ -0,0 +1,62 @@ +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # ActorKeyEndpoint + # + # This class handles DELETE/GET/PUT requests for all client/user keys + # **except** default public keys, i.e. requests with identity key + # "default". Those are handled by ActorDefaultKeyEndpoint. See that class + # for more information. + # + # /users/USER/keys/NAME + # /organizations/ORG/clients/CLIENT/keys/NAME + class ActorKeyEndpoint < RestBase + def get(request) + validate_actor!(request) + key_path = data_path(request) + already_json_response(200, get_data(request, key_path)) + end + + def delete(request) + validate_actor!(request) # 404 if actor doesn't exist + + key_path = data_path(request) + data = get_data(request, key_path) + delete_data(request, key_path) + + already_json_response(200, data) + end + + def put(request) + validate_actor!(request) # 404 if actor doesn't exist + set_data(request, data_path(request), request.body) + end + + private + + # Returns the keys data store path, which is the same as + # `request.rest_path` except with "client_keys" instead of "clients" or + # "user_keys" instead of "users." + def data_path(request) + request.rest_path.dup.tap do |path| + if client?(request) + path[2] = "client_keys" + else + path[0] = "user_keys" + end + end + end + + # Raises RestErrorResponse (404) if actor doesn't exist + def validate_actor!(request) + actor_path = request.rest_path[ client?(request) ? 0..3 : 0..1 ] + get_data(request, actor_path) + end + + def client?(request) + request.rest_path[2] == "clients" + end + end + end +end diff --git a/lib/chef_zero/endpoints/actor_keys_endpoint.rb b/lib/chef_zero/endpoints/actor_keys_endpoint.rb new file mode 100644 index 0000000..f3624d6 --- /dev/null +++ b/lib/chef_zero/endpoints/actor_keys_endpoint.rb @@ -0,0 +1,129 @@ +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # /users/USER/keys + # /organizations/ORG/clients/CLIENT/keys + class ActorKeysEndpoint < RestBase + DEFAULT_PUBLIC_KEY_NAME = "default" + DATE_FORMAT = "%FT%TZ" # e.g. 2015-12-24T21:00:00Z + + def get(request, alt_uri_root=nil) + path = data_path(request) + + # Get actor or 404 if it doesn't exist + actor_json = get_data(request, actor_path(request)) + + key_names = list_data_or_else(request, path, []) + key_names.unshift(DEFAULT_PUBLIC_KEY_NAME) if actor_has_default_public_key?(actor_json) + + result = key_names.map do |key_name| + list_key(request, [ *path, key_name ], alt_uri_root) + end + + json_response(200, result) + end + + def post(request) + request_body = parse_json(request.body) + + # Try loading the client or user so a 404 is returned if it doesn't exist + actor_json = get_data(request, actor_path(request)) + + generate_keys = request_body["public_key"].nil? + + if generate_keys + private_key, public_key = server.gen_key_pair + else + public_key = request_body['public_key'] + end + + key_name = request_body["name"] + + if key_name == DEFAULT_PUBLIC_KEY_NAME + store_actor_default_public_key!(request, actor_json, public_key) + else + store_actor_public_key!(request, key_name, public_key, request_body["expiration_date"]) + end + + response_body = { "uri" => key_uri(request, key_name) } + response_body["private_key"] = private_key if generate_keys + + json_response(201, response_body, + headers: { "Location" => response_body["uri"] }) + end + + private + + def store_actor_public_key!(request, name, public_key, expiration_date) + data = to_json( + "name" => name, + "public_key" => public_key, + "expiration_date" => expiration_date + ) + + create_data(request, data_path(request), name, data, :create_dir) + end + + def store_actor_default_public_key!(request, actor_json, public_key) + actor_data = parse_json(actor_json) + + if actor_data["public_key"] + raise RestErrorResponse.new(409, "Object already exists: #{key_uri(request, DEFAULT_PUBLIC_KEY_NAME)}") + end + + actor_data["public_key"] = public_key + set_data(request, actor_path(request), to_json(actor_data)) + + end + + # Returns the keys data store path, which is the same as + # `request.rest_path` except with "user_keys" instead of "users" or + # "client_keys" instead of "clients." + def data_path(request) + request.rest_path.dup.tap do |path| + if client?(request) + path[2] = "client_keys" + else + path[0] = "user_keys" + end + end + end + + def list_key(request, data_path, alt_uri_root=nil) + key_name, expiration_date = + if data_path[-1] == DEFAULT_PUBLIC_KEY_NAME + [ DEFAULT_PUBLIC_KEY_NAME, "infinity" ] + else + parse_json(get_data(request, data_path)) + .values_at("name", "expiration_date") + end + + expired = expiration_date != "infinity" && + DateTime.now > DateTime.strptime(expiration_date, DATE_FORMAT) + + { "name" => key_name, + "uri" => key_uri(request, key_name, alt_uri_root), + "expired" => expired } + end + + def client?(request) + request.rest_path[2] == "clients" + end + + def key_uri(request, key_name, alt_uri_root=nil) + uri_root = alt_uri_root.nil? ? request.rest_path : alt_uri_root + build_uri(request.base_uri, [ *uri_root, key_name ]) + end + + def actor_path(request) + return request.rest_path[0..3] if client?(request) + request.rest_path[0..1] + end + + def actor_has_default_public_key?(actor_json) + !!parse_json(actor_json)["public_key"] + end + end + end +end diff --git a/lib/chef_zero/endpoints/actors_endpoint.rb b/lib/chef_zero/endpoints/actors_endpoint.rb index c6c676f..6297aed 100644 --- a/lib/chef_zero/endpoints/actors_endpoint.rb +++ b/lib/chef_zero/endpoints/actors_endpoint.rb @@ -8,30 +8,25 @@ module ChefZero def get(request) response = super(request) - if request.query_params['email'] - results = FFI_Yajl::Parser.parse(response[2], :create_additions => false) - new_results = {} - results.each do |name, url| - record = get_data(request, request.rest_path + [ name ], :nil) - if record - record = FFI_Yajl::Parser.parse(record, :create_additions => false) - new_results[name] = url if record['email'] == request.query_params['email'] - end - end - response[2] = FFI_Yajl::Encoder.encode(new_results, :pretty => true) + # apply query filters: if one applies, stop processing rest + # (precendence matches chef-server: https://github.com/chef/chef-server/blob/268a0c9/src/oc_erchef/apps/chef_objects/src/chef_user.erl#L554-L559) + if value = request.query_params['external_authentication_uid'] + response[2] = filter('external_authentication_uid', value, request, response[2]) + elsif value = request.query_params['email'] + response[2] = filter('email', value, request, response[2]) end if request.query_params['verbose'] - results = FFI_Yajl::Parser.parse(response[2], :create_additions => false) + results = parse_json(response[2]) results.each do |name, url| record = get_data(request, request.rest_path + [ name ], :nil) if record - record = FFI_Yajl::Parser.parse(record, :create_additions => false) + record = parse_json(record) record = ChefData::DataNormalizer.normalize_user(record, name, identity_keys, server.options[:osc_compat]) results[name] = record end end - response[2] = FFI_Yajl::Encoder.encode(results, :pretty => true) + response[2] = to_json(results) end response end @@ -39,26 +34,67 @@ module ChefZero def post(request) # First, find out if the user actually posted a public key. If not, make # one. - request_body = FFI_Yajl::Parser.parse(request.body, :create_additions => false) + request_body = parse_json(request.body) public_key = request_body['public_key'] - if !public_key + + skip_key_create = !request.api_v0? && !request_body["create_key"] + + if !public_key && !skip_key_create private_key, public_key = server.gen_key_pair request_body['public_key'] = public_key - request.body = FFI_Yajl::Encoder.encode(request_body, :pretty => true) + request.body = to_json(request_body) + elsif skip_key_create + request_body['public_key'] = nil + request.body = to_json(request_body) end result = super(request) if result[0] == 201 # If we generated a key, stuff it in the response. - response = FFI_Yajl::Parser.parse(result[2], :create_additions => false) - response['private_key'] = private_key if private_key - response['public_key'] = public_key unless request.rest_path[0] == 'users' + user_data = parse_json(result[2]) + + key_data = {} + key_data['private_key'] = private_key if private_key + key_data['public_key'] = public_key unless request.rest_path[0] == 'users' + + response = + if request.api_v0? + user_data.merge!(key_data) + elsif skip_key_create && !public_key + user_data + else + actor_name = request_body["name"] || request_body["username"] || request_body["clientname"] + + relpath_to_default_key = [ actor_name, "keys", "default" ] + key_data["uri"] = build_uri(request.base_uri, request.rest_path + relpath_to_default_key) + key_data["public_key"] = public_key + key_data["name"] = "default" + key_data["expiration_date"] = "infinity" + user_data["chef_key"] = key_data + user_data + end + json_response(201, response) else result end end + + private + + def filter(key, value, request, resp) + results = parse_json(resp) + new_results = {} + results.each do |name, url| + record = get_data(request, request.rest_path + [ name ], :nil) + if record + record = parse_json(record) + new_results[name] = url if record[key] == value + end + end + to_json(new_results) + end end end end diff --git a/lib/chef_zero/endpoints/containers_endpoint.rb b/lib/chef_zero/endpoints/containers_endpoint.rb index 3f7af87..8a4220f 100644 --- a/lib/chef_zero/endpoints/containers_endpoint.rb +++ b/lib/chef_zero/endpoints/containers_endpoint.rb @@ -8,6 +8,18 @@ module ChefZero def initialize(server) super(server, %w(id containername)) end + + # create a container. + # input: {"containername"=>"new-container", "containerpath"=>"/"} + def post(request) + data = parse_json(request.body) + # if they don't match, id wins. + container_name = data["id"] || data["containername"] + container_path_suffix = data["containerpath"].split("/").reject { |o| o.empty? } + create_data(request, request.rest_path, container_name, to_json({}), :create_dir) + + json_response(201, { uri: build_uri(request.base_uri, request.rest_path + container_path_suffix + [container_name]) }) + end end end end diff --git a/lib/chef_zero/endpoints/controls_endpoint.rb b/lib/chef_zero/endpoints/controls_endpoint.rb new file mode 100644 index 0000000..5790d7f --- /dev/null +++ b/lib/chef_zero/endpoints/controls_endpoint.rb @@ -0,0 +1,15 @@ +module ChefZero + module Endpoints + # /organizations/ORG/controls + class ControlsEndpoint < RestBase + # ours is not to wonder why; ours is but to make the pedant specs pass. + def get(request) + error(410, "Server says 410, chef-zero says 410.") + end + + def post(request) + error(410, "Server says 410, chef-zero says 410.") + end + end + end +end diff --git a/lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb b/lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb new file mode 100644 index 0000000..e17fea2 --- /dev/null +++ b/lib/chef_zero/endpoints/cookbook_artifact_endpoint.rb @@ -0,0 +1,24 @@ +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + class CookbookArtifactEndpoint < RestBase + # GET /organizations/ORG/cookbook_artifacts/COOKBOOK + def get(request) + cookbook_name = request.rest_path.last + cookbook_url = build_uri(request.base_uri, request.rest_path) + response_data = {} + versions = [] + + list_data(request).each do |identifier| + artifact_url = build_uri(request.base_uri, request.rest_path + [cookbook_name, identifier]) + versions << { url: artifact_url, identifier: identifier } + end + + response_data[cookbook_name] = { url: cookbook_url, versions: versions } + + return json_response(200, response_data) + end + end + end +end diff --git a/lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb b/lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb new file mode 100644 index 0000000..b70e2c4 --- /dev/null +++ b/lib/chef_zero/endpoints/cookbook_artifact_identifier_endpoint.rb @@ -0,0 +1,68 @@ +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + class CookbookArtifactIdentifierEndpoint < ChefZero::Endpoints::CookbookVersionEndpoint + # these endpoints are almost, but not quite, not entirely unlike the corresponding /cookbooks endpoints. + # it could all be refactored for maximum reuse, but they're short REST methods with well-defined + # behavioral specs (pedant), so there's not a huge benefit. + + # GET /organizations/ORG/cookbook_artifacts/NAME/IDENTIFIER + def get(request) + cookbook_data = normalize(request, parse_json(get_data(request))) + return json_response(200, cookbook_data) + end + + # PUT /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER + def put(request) + if exists_data?(request) + return error(409, "Cookbooks cannot be modified, and a cookbook with this identifier already exists.") + end + + set_data(request, nil, request.body, :create_dir) + + return already_json_response(201, request.body) + end + + # DELETE /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER + def delete(request) + begin + doomed_cookbook_json = get_data(request) + identified_cookbook_data = normalize(request, parse_json(doomed_cookbook_json)) + delete_data(request) + + # go through the recipes and delete stuff in the file store. + hoover_unused_checksums(get_checksums(doomed_cookbook_json), request) + + # if this was the last revision, delete the directory so future requests will 404, instead of + # returning 200 with an empty list. + # Last one out turns out the lights: delete /organizations/ORG/cookbooks/COOKBOOK if it no longer has versions + cookbook_path = request.rest_path[0..3] + if exists_data_dir?(request, cookbook_path) && list_data(request, cookbook_path).size == 0 + delete_data_dir(request, cookbook_path) + end + + json_response(200, identified_cookbook_data) + rescue RestErrorResponse => ex + if ex.response_code == 404 + error(404, "not_found") + else + raise + end + end + end + + private + + def make_file_store_path(rest_path, recipe) + rest_path.first(2) + ["file_store", "checksums", recipe["checksum"]] + end + + def normalize(request, cookbook_artifact_data) + ChefData::DataNormalizer.normalize_cookbook(self, request.rest_path[0..1], + cookbook_artifact_data, request.rest_path[3], request.rest_path[4], + request.base_uri, request.method, true) + end + end + end +end diff --git a/lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb b/lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb new file mode 100644 index 0000000..d9fdb20 --- /dev/null +++ b/lib/chef_zero/endpoints/cookbook_artifacts_endpoint.rb @@ -0,0 +1,34 @@ +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + class CookbookArtifactsEndpoint < RestBase + # GET /organizations/ORG/cookbook_artifacts + def get(request) + data = {} + + artifacts = begin + list_data(request) + rescue Exception => e + if e.response_code == 404 + return already_json_response(200, "{}") + end + end + + artifacts.each do |cookbook_artifact| + cookbook_url = build_uri(request.base_uri, request.rest_path + [cookbook_artifact]) + + versions = [] + list_data(request, request.rest_path + [cookbook_artifact]).each do |identifier| + artifact_url = build_uri(request.base_uri, request.rest_path + [cookbook_artifact, identifier]) + versions << { url: artifact_url, identifier: identifier } + end + + data[cookbook_artifact] = { url: cookbook_url, versions: versions } + end + + return json_response(200, data) + end + end + end +end diff --git a/lib/chef_zero/endpoints/cookbook_version_endpoint.rb b/lib/chef_zero/endpoints/cookbook_version_endpoint.rb index 8dad508..5502ba0 100644 --- a/lib/chef_zero/endpoints/cookbook_version_endpoint.rb +++ b/lib/chef_zero/endpoints/cookbook_version_endpoint.rb @@ -58,8 +58,8 @@ module ChefZero deleted_cookbook = get_data(request) response = super(request) - cookbook_name = request.rest_path[3] - cookbook_path = request.rest_path[0..1] + ['cookbooks', cookbook_name] + # Last one out turns out the lights: delete /organizations/ORG/cookbooks/NAME if it no longer has versions + cookbook_path = request.rest_path[0..3] if exists_data_dir?(request, cookbook_path) && list_data(request, cookbook_path).size == 0 delete_data_dir(request, cookbook_path) end @@ -86,10 +86,19 @@ module ChefZero private def hoover_unused_checksums(deleted_checksums, request) - data_store.list(request.rest_path[0..1] + ['cookbooks']).each do |cookbook_name| - data_store.list(request.rest_path[0..1] + ['cookbooks', cookbook_name]).each do |version| - cookbook = data_store.get(request.rest_path[0..1] + ['cookbooks', cookbook_name, version], request) - deleted_checksums = deleted_checksums - get_checksums(cookbook) + %w(cookbooks cookbook_artifacts).each do |cookbook_type| + begin + cookbooks = data_store.list(request.rest_path[0..1] + [cookbook_type]) + rescue ChefZero::DataStore::DataNotFoundError + # Not all chef versions support cookbook_artifacts + raise unless cookbook_type == "cookbook_artifacts" + cookbooks = [] + end + cookbooks.each do |cookbook_name| + data_store.list(request.rest_path[0..1] + [cookbook_type, cookbook_name]).each do |version| + cookbook = data_store.get(request.rest_path[0..1] + [cookbook_type, cookbook_name, version], request) + deleted_checksums = deleted_checksums - get_checksums(cookbook) + end end end deleted_checksums.each do |checksum| diff --git a/lib/chef_zero/endpoints/dummy_endpoint.rb b/lib/chef_zero/endpoints/dummy_endpoint.rb new file mode 100644 index 0000000..fe16a7e --- /dev/null +++ b/lib/chef_zero/endpoints/dummy_endpoint.rb @@ -0,0 +1,31 @@ + +# pedant makes a couple of Solr-related calls from its search_utils.rb file that we can't work around (e.g. +# with monkeypatching). the necessary Pedant::Config values are set in run_oc_pedant.rb. --cdoherty +module ChefZero + module Endpoints + class DummyEndpoint < RestBase + # called by #direct_solr_query, once each for roles, nodes, and data bag items. each RSpec example makes + # 3 calls, with the expected sequence of return values [0, 1, 0]. + def get(request) + + # this could be made less brittle, but if things change to have more than 3 cycles, we should really + # be notified by a spec failure. + @mock_values ||= ([0, 1, 0] * 3).map { |val| make_response(val) } + + retval = @mock_values.shift + json_response(200, retval) + end + + # called by #force_solr_commit in pedant's , which doesn't check the return value. + def post(request) + # sure thing! + json_response(200, { message: "This dummy POST endpoint didn't do anything." }) + end + + def make_response(value) + { "response" => { "numFound" => value } } + end + end + end +end + diff --git a/lib/chef_zero/endpoints/node_endpoint.rb b/lib/chef_zero/endpoints/node_endpoint.rb index 223ec9f..f2bb8ba 100644 --- a/lib/chef_zero/endpoints/node_endpoint.rb +++ b/lib/chef_zero/endpoints/node_endpoint.rb @@ -6,6 +6,20 @@ module ChefZero module Endpoints # /nodes/ID class NodeEndpoint < RestObjectEndpoint + def put(request) + data = parse_json(request.body) + + if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"]) + return error(400, "Field 'policy_name' invalid", :pretty => false) + end + + if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"]) + return error(400, "Field 'policy_group' invalid", :pretty => false) + end + + super(request) + end + def populate_defaults(request, response_json) node = FFI_Yajl::Parser.parse(response_json, :create_additions => false) node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3]) diff --git a/lib/chef_zero/endpoints/node_identifiers_endpoint.rb b/lib/chef_zero/endpoints/node_identifiers_endpoint.rb new file mode 100644 index 0000000..9f89a98 --- /dev/null +++ b/lib/chef_zero/endpoints/node_identifiers_endpoint.rb @@ -0,0 +1,22 @@ +require 'ffi_yajl' +require 'chef_zero/rest_base' +require 'uuidtools' + +module ChefZero + module Endpoints + # /organizations/NAME/nodes/NAME/_identifiers + class NodeIdentifiersEndpoint < RestBase + def get(request) + if get_data(request, request.rest_path[0..3]) + result = { + :id => UUIDTools::UUID.parse_raw(request.rest_path[0..4].to_s).to_s.gsub('-',''), + :authz_id => '0'*32, + :org_id => UUIDTools::UUID.parse_raw(request.rest_path[0..1].to_s).to_s.gsub('-','') } + json_response(200, result) + else + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + end + end + end + end +end diff --git a/lib/chef_zero/endpoints/nodes_endpoint.rb b/lib/chef_zero/endpoints/nodes_endpoint.rb new file mode 100644 index 0000000..8b9d852 --- /dev/null +++ b/lib/chef_zero/endpoints/nodes_endpoint.rb @@ -0,0 +1,35 @@ +require 'ffi_yajl' +require 'chef_zero/endpoints/rest_object_endpoint' +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + # /nodes + class NodesEndpoint < RestListEndpoint + + def post(request) + # /nodes validation + if request.rest_path.last == "nodes" + data = parse_json(request.body) + + if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"]) + return error(400, "Field 'policy_name' invalid", :pretty => false) + end + + if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"]) + return error(400, "Field 'policy_group' invalid", :pretty => false) + end + end + + super(request) + end + + def populate_defaults(request, response_json) + node = FFI_Yajl::Parser.parse(response_json, :create_additions => false) + node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3]) + FFI_Yajl::Encoder.encode(node, :pretty => true) + end + end + end +end + diff --git a/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb b/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb index 608d548..72b0e4d 100644 --- a/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb +++ b/lib/chef_zero/endpoints/organization_association_requests_endpoint.rb @@ -21,8 +21,9 @@ module ChefZero def get(request) orgname = request.rest_path[1] - result = list_data(request).map { |username| { "id" => "#{username}-#{orgname}", 'username' => username } } - json_response(200, result) + ChefZero::Endpoints::OrganizationUserBase.get(self, request) do |username| + { "id" => "#{username}-#{orgname}", 'username' => username } + end end end end diff --git a/lib/chef_zero/endpoints/organization_endpoint.rb b/lib/chef_zero/endpoints/organization_endpoint.rb index bc5c3f0..a5512db 100644 --- a/lib/chef_zero/endpoints/organization_endpoint.rb +++ b/lib/chef_zero/endpoints/organization_endpoint.rb @@ -16,18 +16,23 @@ module ChefZero new_org.each do |key, value| org[key] = value end - org = FFI_Yajl::Encoder.encode(org, :pretty => true) + save_org = FFI_Yajl::Encoder.encode(org, :pretty => true) if new_org['name'] != request.rest_path[-1] # This is a rename return error(400, "Cannot rename org #{request.rest_path[-1]} to #{new_org['name']}: rename not supported for orgs") end - set_data(request, request.rest_path + [ 'org' ], org) - json_response(200, "uri" => "#{build_uri(request.base_uri, request.rest_path)}") + set_data(request, request.rest_path + [ 'org' ], save_org) + json_response(200, { + "uri" => "#{build_uri(request.base_uri, request.rest_path)}", + "name" => org['name'], + "org_type" => org['org_type'], + "full_name" => org['full_name'] + }) end def delete(request) org = get_data(request, request.rest_path + [ 'org' ]) - delete_data_dir(request, request.rest_path) + delete_data_dir(request, request.rest_path, :recursive) already_json_response(200, populate_defaults(request, org)) end diff --git a/lib/chef_zero/endpoints/organization_user_base.rb b/lib/chef_zero/endpoints/organization_user_base.rb new file mode 100644 index 0000000..d4ccf44 --- /dev/null +++ b/lib/chef_zero/endpoints/organization_user_base.rb @@ -0,0 +1,15 @@ +require 'ffi_yajl' +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + module OrganizationUserBase + + def self.get(obj, request, &block) + result = obj.list_data(request).map(&block) + obj.json_response(200, result) + end + + end + end +end diff --git a/lib/chef_zero/endpoints/organization_user_default_key_endpoint.rb b/lib/chef_zero/endpoints/organization_user_default_key_endpoint.rb new file mode 100644 index 0000000..953edc1 --- /dev/null +++ b/lib/chef_zero/endpoints/organization_user_default_key_endpoint.rb @@ -0,0 +1,16 @@ +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # GET /organizations/ORG/users/USER/keys/default + class OrganizationUserDefaultKeyEndpoint < RestBase + def get(request) + # 404 if it doesn't exist + get_data(request, request.rest_path[0..3]) + # Just use the /users/USER/keys/default endpoint + request.rest_path = request.rest_path[2..-1] + ActorDefaultKeyEndpoint.new(server).get(request) + end + end + end +end diff --git a/lib/chef_zero/endpoints/organization_user_endpoint.rb b/lib/chef_zero/endpoints/organization_user_endpoint.rb index ef922d4..8fd97d4 100644 --- a/lib/chef_zero/endpoints/organization_user_endpoint.rb +++ b/lib/chef_zero/endpoints/organization_user_endpoint.rb @@ -20,29 +20,7 @@ module ChefZero json_response(200, ChefData::DataNormalizer.normalize_user(user, request.rest_path[3], ['username'], server.options[:osc_compat])) end - def post(request) - orgname = request.rest_path[1] - username = request.rest_path[3] - - users = get_data(request, [ 'organizations', orgname, 'groups', 'users' ]) - users = FFI_Yajl::Parser.parse(users, :create_additions => false) - - create_data(request, [ 'organizations', orgname, 'users' ], username, '{}') - - # /organizations/ORG/association_requests/USERNAME-ORG - begin - delete_data(request, [ 'organizations', orgname, 'association_requests', username], :data_store_exceptions) - rescue DataStore::DataNotFoundError - end - - # Add the user to the users group if it isn't already there - if !users['users'] || !users['users'].include?(username) - users['users'] ||= [] - users['users'] |= [ username ] - set_data(request, [ 'organizations', orgname, 'groups', 'users' ], FFI_Yajl::Encoder.encode(users, :pretty => true)) - end - json_response(200, {}) - end + # Note: post to a named org user is not permitted, alllow invalid method handling (405) end end end diff --git a/lib/chef_zero/endpoints/organization_user_key_endpoint.rb b/lib/chef_zero/endpoints/organization_user_key_endpoint.rb new file mode 100644 index 0000000..e0c114c --- /dev/null +++ b/lib/chef_zero/endpoints/organization_user_key_endpoint.rb @@ -0,0 +1,17 @@ +require 'chef_zero/rest_base' +require 'chef_zero/endpoints/actor_keys_endpoint' + +module ChefZero + module Endpoints + # GET /organizations/ORG/users/USER/keys/NAME + class OrganizationUserKeyEndpoint < RestBase + def get(request) + # 404 if not a member of the org + get_data(request, request.rest_path[0..3]) + # Just use the /users/USER/keys endpoint + request.rest_path = request.rest_path[2..-1] + ActorKeyEndpoint.new(server).get(request) + end + end + end +end diff --git a/lib/chef_zero/endpoints/organization_user_keys_endpoint.rb b/lib/chef_zero/endpoints/organization_user_keys_endpoint.rb new file mode 100644 index 0000000..96a84fe --- /dev/null +++ b/lib/chef_zero/endpoints/organization_user_keys_endpoint.rb @@ -0,0 +1,17 @@ +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # GET /organizations/ORG/users/USER/keys + class OrganizationUserKeysEndpoint < RestBase + def get(request) + # 404 if it doesn't exist + get_data(request, request.rest_path[0..3]) + # Just use the /users/USER/keys/key endpoint + original_path = request.rest_path + request.rest_path = request.rest_path[2..-1] + ActorKeysEndpoint.new(server).get(request, original_path) + end + end + end +end diff --git a/lib/chef_zero/endpoints/organization_users_endpoint.rb b/lib/chef_zero/endpoints/organization_users_endpoint.rb index ed70cf6..861c670 100644 --- a/lib/chef_zero/endpoints/organization_users_endpoint.rb +++ b/lib/chef_zero/endpoints/organization_users_endpoint.rb @@ -1,13 +1,42 @@ require 'ffi_yajl' require 'chef_zero/rest_base' +require 'chef_zero/endpoints/organization_user_base' module ChefZero module Endpoints # /organizations/ORG/users class OrganizationUsersEndpoint < RestBase + def post(request) + orgname = request.rest_path[1] + json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) + username = json['username'] + + if exists_data?(request, [ 'organizations', orgname, 'users', username ]) + raise RestErrorResponse.new(409, "User #{username} is already in organization #{orgname}") + end + + users = get_data(request, [ 'organizations', orgname, 'groups', 'users' ]) + users = FFI_Yajl::Parser.parse(users, :create_additions => false) + + create_data(request, request.rest_path, username, '{}') + + # /organizations/ORG/association_requests/USERNAME-ORG + begin + delete_data(request, [ 'organizations', orgname, 'association_requests', username], :data_store_exceptions) + rescue DataStore::DataNotFoundError + end + + # Add the user to the users group if it isn't already there + if !users['users'] || !users['users'].include?(username) + users['users'] ||= [] + users['users'] |= [ username ] + set_data(request, [ 'organizations', orgname, 'groups', 'users' ], FFI_Yajl::Encoder.encode(users, :pretty => true)) + end + json_response(201, { "uri" => build_uri(request.base_uri, request.rest_path + [ username ]) }) + end + def get(request) - result = list_data(request).map { |username| { "user" => { "username" => username } } } - json_response(200, result) + ChefZero::Endpoints::OrganizationUserBase.get(self, request) { |username| { "user" => { "username" => username } } } end end end diff --git a/lib/chef_zero/endpoints/organizations_endpoint.rb b/lib/chef_zero/endpoints/organizations_endpoint.rb index 88816e8..41bf03b 100644 --- a/lib/chef_zero/endpoints/organizations_endpoint.rb +++ b/lib/chef_zero/endpoints/organizations_endpoint.rb @@ -17,8 +17,11 @@ module ChefZero def post(request) contents = FFI_Yajl::Parser.parse(request.body, :create_additions => false) name = contents['name'] + full_name = contents['full_name'] if name.nil? error(400, "Must specify 'name' in JSON") + elsif full_name.nil? + error(400, "Must specify 'full_name' in JSON") elsif exists_data_dir?(request, request.rest_path + [ name ]) error(409, "Organization already exists") else @@ -43,8 +46,12 @@ module ChefZero set_data(request, validator_path, validator) end + json_response(201, { "uri" => "#{build_uri(request.base_uri, org_path)}", + "name" => name, + "org_type" => org["org_type"], + "full_name" => full_name, "clientname" => validator_name, "private_key" => private_key }) diff --git a/lib/chef_zero/endpoints/policies_endpoint.rb b/lib/chef_zero/endpoints/policies_endpoint.rb index 83d503c..37493da 100644 --- a/lib/chef_zero/endpoints/policies_endpoint.rb +++ b/lib/chef_zero/endpoints/policies_endpoint.rb @@ -1,154 +1,26 @@ -require 'ffi_yajl' - -require 'chef/version_class' -require 'chef/exceptions' - -require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/chef_data/data_normalizer' module ChefZero module Endpoints - # /policies/:group/:name - class PoliciesEndpoint < RestObjectEndpoint - def initialize(server) - super(server, 'id') - end - + # /organizations/ORG/policies + class PoliciesEndpoint < RestBase + # GET /organizations/ORG/policies def get(request) - already_json_response(200, get_data(request)) - end - - # Right now we're allowing PUT to create. - def put(request) - error = validate(request) - return error if error - - code = - if data_store.exists?(request.rest_path) - set_data(request, request.rest_path, request.body, :data_store_exceptions) - 200 - else - name = request.rest_path[4] - data_store.create(request.rest_path[0..3], name, request.body, :create_dir) - 201 - end - already_json_response(code, request.body) - end - - def delete(request) - result = get_data(request, request.rest_path) - delete_data(request, request.rest_path, :data_store_exceptions) - already_json_response(200, result) - end + response_data = {} + policy_names = list_data(request) + policy_names.each do |policy_name| + policy_path = request.rest_path + [policy_name] + policy_uri = build_uri(request.base_uri, policy_path) + revisions = list_data(request, policy_path + ["revisions"]) - private - - def validate(request) - req_object = validate_json(request.body) - validate_revision_id(request, req_object) || - validate_name(request, req_object) || - validate_run_list(req_object) || - validate_each_run_list_item(req_object) || - validate_cookbook_locks_collection(req_object) || - validate_each_cookbook_locks_item(req_object) - end - - def validate_json(request_body) - FFI_Yajl::Parser.parse(request_body) - # TODO: rescue parse error, return 400 - # error(400, "Must specify #{identity_keys.map { |k| k.inspect }.join(' or ')} in JSON") - end - - def validate_revision_id(request, req_object) - if !req_object.key?("revision_id") - error(400, "Field 'revision_id' missing") - elsif req_object["revision_id"].empty? - error(400, "Field 'revision_id' invalid") - elsif req_object["revision_id"].size > 255 - error(400, "Field 'revision_id' invalid") - elsif req_object["revision_id"] !~ /^[\-[:alnum:]_\.\:]+$/ - error(400, "Field 'revision_id' invalid") + response_data[policy_name] = { + uri: policy_uri, + revisions: hashify_list(revisions) + } end - end - def validate_name(request, req_object) - if !req_object.key?("name") - error(400, "Field 'name' missing") - elsif req_object["name"] != (uri_policy_name = URI.decode(request.rest_path[4])) - error(400, "Field 'name' invalid : #{uri_policy_name} does not match #{req_object["name"]}") - elsif req_object["name"].size > 255 - error(400, "Field 'name' invalid") - elsif req_object["name"] !~ /^[\-[:alnum:]_\.\:]+$/ - error(400, "Field 'name' invalid") - end - end - - def validate_run_list(req_object) - if !req_object.key?("run_list") - error(400, "Field 'run_list' missing") - elsif !req_object["run_list"].kind_of?(Array) - error(400, "Field 'run_list' is not a valid run list") - end + return json_response(200, response_data) end - - def validate_each_run_list_item(req_object) - req_object["run_list"].each do |run_list_item| - if res_400 = validate_run_list_item(run_list_item) - return res_400 - end - end - nil - end - - def validate_run_list_item(run_list_item) - if !run_list_item.kind_of?(String) - error(400, "Field 'run_list' is not a valid run list") - elsif run_list_item !~ /\Arecipe\[[^\s]+::[^\s]+\]\Z/ - error(400, "Field 'run_list' is not a valid run list") - end - end - - def validate_cookbook_locks_collection(req_object) - if !req_object.key?("cookbook_locks") - error(400, "Field 'cookbook_locks' missing") - elsif !req_object["cookbook_locks"].kind_of?(Hash) - error(400, "Field 'cookbook_locks' invalid") - end - end - - def validate_each_cookbook_locks_item(req_object) - req_object["cookbook_locks"].each do |cookbook_name, lock| - if res_400 = validate_cookbook_locks_item(cookbook_name, lock) - return res_400 - end - end - nil - end - - def validate_cookbook_locks_item(cookbook_name, lock) - if !lock.kind_of?(Hash) - error(400, "cookbook_lock entries must be a JSON object") - elsif !lock.key?("identifier") - error(400, "Field 'identifier' missing") - elsif lock["identifier"].size > 255 - error(400, "Field 'identifier' invalid") - elsif !lock.key?("version") - error(400, "Field 'version' missing") - elsif lock.key?("dotted_decimal_identifier") - unless valid_version?(lock["dotted_decimal_identifier"]) - error(400, "Field 'dotted_decimal_identifier' is not a valid version") - end - end - end - - def valid_version?(version_string) - Chef::Version.new(version_string) - true - rescue Chef::Exceptions::InvalidCookbookVersion - false - end - end end end - diff --git a/lib/chef_zero/endpoints/policy_endpoint.rb b/lib/chef_zero/endpoints/policy_endpoint.rb new file mode 100644 index 0000000..d8c1bc8 --- /dev/null +++ b/lib/chef_zero/endpoints/policy_endpoint.rb @@ -0,0 +1,24 @@ +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + # /organizations/ORG/policies/NAME + class PolicyEndpoint < RestBase + # GET /organizations/ORG/policies/NAME + def get(request) + revisions = list_data(request, request.rest_path + ["revisions"]) + data = { revisions: hashify_list(revisions) } + return json_response(200, data) + end + + # DELETE /organizations/ORG/policies/NAME + def delete(request) + revisions = list_data(request, request.rest_path + ["revisions"]) + data = { revisions: hashify_list(revisions) } + + delete_data_dir(request, nil, :recursive) + return json_response(200, data) + end + end + end +end diff --git a/lib/chef_zero/endpoints/policy_group_endpoint.rb b/lib/chef_zero/endpoints/policy_group_endpoint.rb new file mode 100644 index 0000000..54732c8 --- /dev/null +++ b/lib/chef_zero/endpoints/policy_group_endpoint.rb @@ -0,0 +1,46 @@ +require 'ffi_yajl' +require 'chef_zero/rest_base' +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + # /organizations/ORG/policy_groups/NAME + class PolicyGroupEndpoint < RestBase + + # GET /organizations/ORG/policy_groups/NAME + def get(request) + data = { + uri: build_uri(request.base_uri, request.rest_path), + policies: get_policy_group_policies(request) + } + json_response(200, data) + end + + # build a hash of {"some_policy_name"=>{"revision_id"=>"909c26701e291510eacdc6c06d626b9fa5350d25"}} + def get_policy_group_policies(request) + policies_revisions = {} + + policies_path = request.rest_path + ["policies"] + policy_names = list_data(request, policies_path) + policy_names.each do |policy_name| + revision = parse_json(get_data(request, policies_path + [policy_name])) + policies_revisions[policy_name] = { revision_id: revision} + end + + policies_revisions + end + + # DELETE /organizations/ORG/policy_groups/NAME + def delete(request) + policy_group_policies = get_policy_group_policies(request) + delete_data_dir(request, nil, :recursive) + + data = { + uri: build_uri(request.base_uri, request.rest_path), + policies: policy_group_policies + } + json_response(200, data) + end + end + end +end diff --git a/lib/chef_zero/endpoints/policy_group_policy_endpoint.rb b/lib/chef_zero/endpoints/policy_group_policy_endpoint.rb new file mode 100644 index 0000000..d227905 --- /dev/null +++ b/lib/chef_zero/endpoints/policy_group_policy_endpoint.rb @@ -0,0 +1,84 @@ +require 'ffi_yajl' +require 'chef_zero/rest_base' +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + # /organizations/ORG/policy_groups/GROUP/policies/NAME + # + # in the data store, this REST path actually stores the revision ID of ${policy_name} that's currently + # associated with ${policy_group}. + class PolicyGroupPolicyEndpoint < RestBase + + # GET /organizations/ORG/policy_groups/GROUP/policies/NAME + def get(request) + policy_name = request.rest_path[5] + + # fetch /organizations/{organization}/policies/{policy_name}/revisions/{revision_id} + revision_id = parse_json(get_data(request)) + result = get_data(request, request.rest_path[0..1] + + ["policies", policy_name, "revisions", revision_id]) + result = ChefData::DataNormalizer.normalize_policy(parse_json(result), policy_name, revision_id) + json_response(200, result) + end + + # Create or update the policy document for the given policy group and policy name. If no policy group + # with the given name exists, it will be created. If no policy with the given revision_id exists, it + # will be created from the document in the request body. If a policy with that revision_id exists, the + # Chef Server simply associates that revision id with the given policy group. When successful, the + # document that was created or updated is returned. + + ## MANDATORY FIELDS AND FORMATS + # * `revision_id`: String; Must be < 255 chars, matches /^[\-[:alnum:]_\.\:]+$/ + # * `name`: String; Must match name in URI; Must be <= 255 chars, matches /^[\-[:alnum:]_\.\:]+$/ + # * `run_list`: Array + # * `run_list[i]`: Fully Qualified Recipe Run List Item + # * `cookbook_locks`: JSON Object + # * `cookbook_locks(key)`: CookbookName + # * `cookbook_locks[item]`: JSON Object, mandatory keys: "identifier", "dotted_decimal_identifier" + # * `cookbook_locks[item]["identifier"]`: varchar(255) ? + # * `cookbook_locks[item]["dotted_decimal_identifier"]` ChefCompatibleVersionNumber + + # PUT /organizations/ORG/policy_groups/GROUP/policies/NAME + def put(request) + policyfile_data = parse_json(request.body) + policy_name = request.rest_path[5] + revision_id = policyfile_data["revision_id"] + + # If the policy revision being submitted does not exist, create it. + # Storage: /organizations/ORG/policies/POLICY/revisions/REVISION + policyfile_path = request.rest_path[0..1] + ["policies", policy_name, "revisions", revision_id] + if !exists_data?(request, policyfile_path) + create_data(request, policyfile_path[0..-2], revision_id, request.body, :create_dir) + end + + # if named policy exists and the given revision ID exists, associate the revision ID with the policy + # group. + # Storage: /organizations/ORG/policies/POLICY/revisions/REVISION + response_code = exists_data?(request) ? 200 : 201 + set_data(request, nil, to_json(revision_id), :create, :create_dir) + + already_json_response(response_code, request.body) + end + + # DELETE /organizations/ORG/policy_groups/GROUP/policies/NAME + def delete(request) + # Save the existing association. + current_revision_id = parse_json(get_data(request)) + + # delete the association. + delete_data(request) + + # return the full policy document at the no-longer-associated revision. + policy_name = request.rest_path[5] + policy_path = request.rest_path[0..1] + ["policies", policy_name, + "revisions", current_revision_id] + + + full_policy_doc = parse_json(get_data(request, policy_path)) + full_policy_doc = ChefData::DataNormalizer.normalize_policy(full_policy_doc, policy_name, current_revision_id) + return json_response(200, full_policy_doc) + end + end + end +end diff --git a/lib/chef_zero/endpoints/policy_groups_endpoint.rb b/lib/chef_zero/endpoints/policy_groups_endpoint.rb new file mode 100644 index 0000000..f17db8d --- /dev/null +++ b/lib/chef_zero/endpoints/policy_groups_endpoint.rb @@ -0,0 +1,38 @@ +require 'ffi_yajl' +require 'chef_zero/rest_base' +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + # /organizations/ORG/policy_groups + # + # in the data store, this REST path actually stores the revision ID of ${policy_name} that's currently + # associated with ${policy_group}. + class PolicyGroupsEndpoint < RestBase + # GET /organizations/ORG/policy_groups + def get(request) + # each policy group has policies and associated revisions under + # /policy_groups/{group name}/policies/{policy name}. + response_data = {} + list_data(request).each do |group_name| + group_path = request.rest_path + [group_name] + policy_list = list_data(request, group_path + ["policies"]) + + # build the list of policies with their revision ID associated with this policy group. + policies = {} + policy_list.each do |policy_name| + revision_id = parse_json(get_data(request, group_path + ["policies", policy_name])) + policies[policy_name] = { revision_id: revision_id } + end + + response_data[group_name] = { + uri: build_uri(request.base_uri, group_path) + } + response_data[group_name][:policies] = policies unless policies.empty? + end + + json_response(200, response_data) + end + end + end +end diff --git a/lib/chef_zero/endpoints/policy_revision_endpoint.rb b/lib/chef_zero/endpoints/policy_revision_endpoint.rb new file mode 100644 index 0000000..6a77d26 --- /dev/null +++ b/lib/chef_zero/endpoints/policy_revision_endpoint.rb @@ -0,0 +1,23 @@ +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + # /organizations/ORG/policies/NAME/revisions/REVISION + class PolicyRevisionEndpoint < RestBase + # GET /organizations/ORG/policies/NAME/revisions/REVISION + def get(request) + data = parse_json(get_data(request)) + data = ChefData::DataNormalizer.normalize_policy(data, request.rest_path[3], request.rest_path[5]) + return json_response(200, data) + end + + # DELETE /organizations/ORG/policies/NAME/revisions/REVISION + def delete(request) + policyfile_data = parse_json(get_data(request)) + policyfile_data = ChefData::DataNormalizer.normalize_policy(policyfile_data, request.rest_path[3], request.rest_path[5]) + delete_data(request) + return json_response(200, policyfile_data) + end + end + end +end diff --git a/lib/chef_zero/endpoints/policy_revisions_endpoint.rb b/lib/chef_zero/endpoints/policy_revisions_endpoint.rb new file mode 100644 index 0000000..7c20a24 --- /dev/null +++ b/lib/chef_zero/endpoints/policy_revisions_endpoint.rb @@ -0,0 +1,15 @@ +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + # /organizations/ORG/policies/NAME/revisions + class PolicyRevisionsEndpoint < RestBase + # POST /organizations/ORG/policies/NAME/revisions + def post(request) + policyfile_data = parse_json(request.body) + create_data(request, request.rest_path, policyfile_data["revision_id"], request.body, :create_dir) + return already_json_response(201, request.body) + end + end + end +end diff --git a/lib/chef_zero/endpoints/principal_endpoint.rb b/lib/chef_zero/endpoints/principal_endpoint.rb index 8cad07c..64c6986 100644 --- a/lib/chef_zero/endpoints/principal_endpoint.rb +++ b/lib/chef_zero/endpoints/principal_endpoint.rb @@ -8,16 +8,20 @@ module ChefZero class PrincipalEndpoint < RestBase def get(request) name = request.rest_path[-1] + # If /organizations/ORG/users/NAME exists, use this user (only org members have precedence over clients). hey are an org member. json = get_data(request, request.rest_path[0..1] + [ 'users', name ], :nil) if json type = 'user' org_member = true else + # If /organizations/ORG/clients/NAME exists, use the client. json = get_data(request, request.rest_path[0..1] + [ 'clients', name ], :nil) if json type = 'client' org_member = true else + # If there is no client with that name, check for a user (/users/NAME) and return that with + # org_member = false. json = get_data(request, [ 'users', name ], :nil) if json type = 'user' @@ -26,13 +30,22 @@ module ChefZero end end if json - json_response(200, { + principal_data = { 'name' => name, 'type' => type, 'public_key' => FFI_Yajl::Parser.parse(json)['public_key'] || PUBLIC_KEY, 'authz_id' => '0'*32, 'org_member' => org_member - }) + } + + response_data = + if request.api_v0? + principal_data + else + { "principals" => [ principal_data ] } + end + + json_response(200, response_data) else error(404, 'Principal not found') end diff --git a/lib/chef_zero/endpoints/rest_object_endpoint.rb b/lib/chef_zero/endpoints/rest_object_endpoint.rb index 9e978b4..7e839c0 100644 --- a/lib/chef_zero/endpoints/rest_object_endpoint.rb +++ b/lib/chef_zero/endpoints/rest_object_endpoint.rb @@ -21,12 +21,11 @@ module ChefZero def put(request) # We grab the old body to trigger a 404 if it doesn't exist old_body = get_data(request) - request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) - key = identity_keys.map { |k| request_json[k] }.select { |v| v }.first - key ||= request.rest_path[-1] + # If it's a rename, check for conflict and delete the old value - rename = key != request.rest_path[-1] - if rename + if is_rename?(request) + key = identity_key_value(request) + begin create_data(request, request.rest_path[0..-2], key, request.body, :data_store_exceptions) rescue DataStore::DataAlreadyExistsError @@ -56,8 +55,23 @@ module ChefZero return FFI_Yajl::Encoder.encode(merged_json, :pretty => true) end end + request.body end + + private + + # Get the value of the (first existing) identity key from the request body or nil + def identity_key_value(request) + request_json = parse_json(request.body) + identity_keys.map { |k| request_json[k] }.compact.first + end + + # Does this request change the value of the identity key? + def is_rename?(request) + return false unless key = identity_key_value(request) + key != request.rest_path[-1] + end end end end diff --git a/lib/chef_zero/endpoints/search_endpoint.rb b/lib/chef_zero/endpoints/search_endpoint.rb index 3a093be..a9ad2bf 100644 --- a/lib/chef_zero/endpoints/search_endpoint.rb +++ b/lib/chef_zero/endpoints/search_endpoint.rb @@ -10,13 +10,15 @@ module ChefZero # /search/INDEX class SearchEndpoint < RestBase def get(request) - results = search(request) + orgname = request.rest_path[1] + results = search(request, orgname) results['rows'] = results['rows'].map { |name,uri,value,search_value| value } json_response(200, results) end def post(request) - full_results = search(request) + orgname = request.rest_path[1] + full_results = search(request, orgname) keys = FFI_Yajl::Parser.parse(request.body, :create_additions => false) partial_results = full_results['rows'].map do |name, uri, doc, search_value| data = {} @@ -45,10 +47,10 @@ module ChefZero private - def search_container(request, index) + def search_container(request, index, orgname) relative_parts, normalize_proc = case index when 'client' - [ ['clients'], Proc.new { |client, name| ChefData::DataNormalizer.normalize_client(client, name) } ] + [ ['clients'], Proc.new { |client, name| ChefData::DataNormalizer.normalize_client(client, name, orgname) } ] when 'node' [ ['nodes'], Proc.new { |node, name| ChefData::DataNormalizer.normalize_node(node, name) } ] when 'environment' @@ -92,7 +94,7 @@ module ChefZero end end - def search(request) + def search(request, orgname = nil) # Extract parameters index = request.rest_path[3] query_string = request.query_params['q'] || '*:*' @@ -104,7 +106,7 @@ module ChefZero rows = rows.to_i if rows # Get the search container - container, expander = search_container(request, index) + container, expander = search_container(request, index, orgname) # Search! result = [] diff --git a/lib/chef_zero/endpoints/server_api_version_endpoint.rb b/lib/chef_zero/endpoints/server_api_version_endpoint.rb new file mode 100644 index 0000000..8ddeaba --- /dev/null +++ b/lib/chef_zero/endpoints/server_api_version_endpoint.rb @@ -0,0 +1,14 @@ +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # /server_api_version + class ServerAPIVersionEndpoint < RestBase + API_VERSION = 1 + def get(request) + json_response(200, {"min_api_version"=>MIN_API_VERSION, "max_api_version"=>MAX_API_VERSION}, + request_version: request.api_version, response_version: API_VERSION) + end + end + end +end diff --git a/lib/chef_zero/endpoints/user_organizations_endpoint.rb b/lib/chef_zero/endpoints/user_organizations_endpoint.rb index b6decb9..3eb13c4 100644 --- a/lib/chef_zero/endpoints/user_organizations_endpoint.rb +++ b/lib/chef_zero/endpoints/user_organizations_endpoint.rb @@ -13,7 +13,7 @@ module ChefZero result = result.map do |orgname| org = get_data(request, [ 'organizations', orgname, 'org' ]) org = FFI_Yajl::Parser.parse(org, :create_additions => false) - ChefData::DataNormalizer.normalize_organization(org, orgname) + { "organization" => ChefData::DataNormalizer.normalize_organization(org, orgname) } end json_response(200, result) end diff --git a/lib/chef_zero/rest_base.rb b/lib/chef_zero/rest_base.rb index 48d423a..71f6f15 100644 --- a/lib/chef_zero/rest_base.rb +++ b/lib/chef_zero/rest_base.rb @@ -5,6 +5,9 @@ require 'chef_zero/chef_data/acl_path' module ChefZero class RestBase + DEFAULT_REQUEST_VERSION = 0 + DEFAULT_RESPONSE_VERSION = 0 + def initialize(server) @server = server end @@ -15,7 +18,36 @@ module ChefZero server.data_store end + def check_api_version(request) + return if request.api_version.nil? # Not present in headers + version = request.api_version.to_i + + unless version.to_s == request.api_version.to_s # Version is not an Integer + return json_response(406, + { "username" => request.requestor }, + request_version: -1, response_version: -1 + ) + end + + if version > MAX_API_VERSION || version < MIN_API_VERSION + response = { + "error" => "invalid-x-ops-server-api-version", + "message" => "Specified version #{version} not supported", + "min_api_version" => MIN_API_VERSION, + "max_api_version" => MAX_API_VERSION + } + + return json_response(406, + response, + request_version: version, response_version: -1 + ) + end + end + def call(request) + response = check_api_version(request) + return response unless response.nil? + method = request.method.downcase.to_sym if !self.respond_to?(method) accept_methods = [:get, :put, :post, :delete].select { |m| self.respond_to?(m) } @@ -29,7 +61,7 @@ module ChefZero begin self.send(method, request) rescue RestErrorResponse => e - ChefZero::Log.debug("#{e.inspect}\n#{e.backtrace.join("\n")}") + ChefZero::Log.info("#{e.inspect}\n#{e.backtrace.join("\n")}") error(e.response_code, e.error) end end @@ -82,7 +114,7 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end @@ -101,7 +133,7 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end @@ -120,7 +152,7 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end end @@ -133,13 +165,13 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}") end rescue DataStore::DataAlreadyExistsError if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") + raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}") end end end @@ -152,13 +184,13 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}") end rescue DataStore::DataAlreadyExistsError if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") + raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}") end end end @@ -173,20 +205,60 @@ module ChefZero data_store.exists_dir?(rest_path) end - def error(response_code, error) - json_response(response_code, {"error" => [error]}) + def error(response_code, error, opts={}) + json_response(response_code, { "error" => [ error ] }, opts) end - def json_response(response_code, json) - already_json_response(response_code, FFI_Yajl::Encoder.encode(json, :pretty => true)) + # Serializes `data` to JSON and returns an Array with the + # response code, HTTP headers and JSON body. + # + # @param [Fixnum] response_code HTTP response code + # @param [Hash] data The data for the response body as a Hash + # @param [Hash] options + # @option options [Hash] :headers (see #already_json_response) + # @option options [Boolean] :pretty (true) Pretty-format the JSON + # @option options [Fixnum] :request_version (see #already_json_response) + # @option options [Fixnum] :response_version (see #already_json_response) + # + # @return (see #already_json_response) + # + def json_response(response_code, data, options={}) + options = { pretty: true }.merge(options) + do_pretty_json = !!options.delete(:pretty) # make sure we have a proper Boolean. + json = FFI_Yajl::Encoder.encode(data, pretty: do_pretty_json) + already_json_response(response_code, json, options) end def text_response(response_code, text) [response_code, {"Content-Type" => "text/plain"}, text] end - def already_json_response(response_code, json_text) - [response_code, {"Content-Type" => "application/json"}, json_text] + # Returns an Array with the response code, HTTP headers, and JSON body. + # + # @param [Fixnum] response_code The HTTP response code + # @param [String] json_text The JSON body for the response + # @param [Hash] options + # @option options [Hash] :headers ({}) HTTP headers (may override default headers) + # @option options [Fixnum] :request_version (0) Request API version + # @option options [Fixnum] :response_version (0) Response API version + # + # @return [Array(Fixnum, Hash{String => String}, String)] + # + def already_json_response(response_code, json_text, options={}) + version_header = FFI_Yajl::Encoder.encode( + "min_version" => MIN_API_VERSION.to_s, + "max_version" => MAX_API_VERSION.to_s, + "request_version" => options[:request_version] || DEFAULT_REQUEST_VERSION.to_s, + "response_version" => options[:response_version] || DEFAULT_RESPONSE_VERSION.to_s + ) + + headers = { + "Content-Type" => "application/json", + "X-Ops-Server-API-Version" => version_header + } + headers.merge!(options[:headers]) if options[:headers] + + [ response_code, headers, json_text ] end # To be called from inside rest endpoints @@ -195,12 +267,12 @@ module ChefZero # Strip off /organizations/chef if we are in single org mode if rest_path[0..1] != [ 'organizations', server.options[:single_org] ] raise "Unexpected URL #{rest_path[0..1]} passed to build_uri in single org mode" - else - "#{base_uri}/#{rest_path[2..-1].join('/')}" end - else - "#{base_uri}/#{rest_path.join('/')}" + + return self.class.build_uri(base_uri, rest_path[2..-1]) end + + self.class.build_uri(base_uri, rest_path) end def self.build_uri(base_uri, rest_path) @@ -210,5 +282,39 @@ module ChefZero def populate_defaults(request, response) response end + + def parse_json(json) + FFI_Yajl::Parser.parse(json, create_additions: false) + end + + def to_json(data) + FFI_Yajl::Encoder.encode(data, :pretty => true) + end + + def get_data_or_else(request, path, or_else_value) + if exists_data?(request, path) + parse_json(get_data(request, path)) + else + or_else_value + end + end + + def list_data_or_else(request, path, or_else_value) + if exists_data_dir?(request, path) + list_data(request, path) + else + or_else_value + end + end + + def hashify_list(list) + list.reduce({}) { |acc, obj| acc.merge( obj => {} ) } + end + + def policy_name_invalid?(name) + !name.is_a?(String) || + name.size > 255 || + name =~ /[+ !]/ + end end end diff --git a/lib/chef_zero/rest_error_response.rb b/lib/chef_zero/rest_error_response.rb index e75d427..8859650 100644 --- a/lib/chef_zero/rest_error_response.rb +++ b/lib/chef_zero/rest_error_response.rb @@ -1,11 +1,11 @@ module ChefZero class RestErrorResponse < StandardError + attr_reader :response_code, :error + def initialize(response_code, error) @response_code = response_code @error = error + super "#{response_code}: #{error}" end - - attr_reader :response_code - attr_reader :error end end diff --git a/lib/chef_zero/rest_request.rb b/lib/chef_zero/rest_request.rb index e79af7f..c12ea31 100644 --- a/lib/chef_zero/rest_request.rb +++ b/lib/chef_zero/rest_request.rb @@ -2,6 +2,9 @@ require 'rack/request' module ChefZero class RestRequest + + ZERO = "0".freeze + def initialize(env, rest_base_prefix = []) @env = env @rest_base_prefix = rest_base_prefix @@ -11,13 +14,27 @@ module ChefZero attr_accessor :rest_base_prefix def base_uri - @base_uri ||= "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{env['SCRIPT_NAME']}" + # Load balancer awareness + if env['HTTP_X_FORWARDED_PROTO'] + scheme = env['HTTP_X_FORWARDED_PROTO'] + else + scheme = env['rack.url_scheme'] + end + @base_uri ||= "#{scheme}://#{env['HTTP_HOST']}#{env['SCRIPT_NAME']}" end def base_uri=(value) @base_uri = value end + def api_version + @env['HTTP_X_OPS_SERVER_API_VERSION'] || ZERO + end + + def api_v0? + api_version == ZERO + end + def requestor @env['HTTP_X_OPS_USERID'] end @@ -30,6 +47,10 @@ module ChefZero @rest_path ||= rest_base_prefix + env['PATH_INFO'].split('/').select { |part| part != "" } end + def rest_path=(rest_path) + @rest_path = rest_path + end + def body=(body) @body = body end diff --git a/lib/chef_zero/rest_router.rb b/lib/chef_zero/rest_router.rb index f2770d3..a93af8b 100644 --- a/lib/chef_zero/rest_router.rb +++ b/lib/chef_zero/rest_router.rb @@ -1,3 +1,5 @@ +require 'pp' + module ChefZero class RestRouter def initialize(routes) @@ -15,24 +17,18 @@ module ChefZero attr_accessor :not_found def call(request) - begin - ChefZero::Log.debug(request) - ChefZero::Log.debug(request.body) if request.body - - clean_path = "/" + request.rest_path.join("/") - - response = find_endpoint(clean_path).call(request) - ChefZero::Log.debug([ - "", - "--- RESPONSE (#{response[0]}) ---", - response[2], - "--- END RESPONSE ---", - ].join("\n")) - return response - rescue - ChefZero::Log.error("#{$!.inspect}\n#{$!.backtrace.join("\n")}") - [500, {"Content-Type" => "text/plain"}, "Exception raised! #{$!.inspect}\n#{$!.backtrace.join("\n")}"] + log_request(request) + + clean_path = "/" + request.rest_path.join("/") + + find_endpoint(clean_path).call(request).tap do |response| + log_response(response) end + rescue => ex + exception = "#{ex.inspect}\n#{ex.backtrace.join("\n")}" + + ChefZero::Log.error(exception) + [ 500, { "Content-Type" => "text/plain" }, "Exception raised! #{exception}" ] end private @@ -41,5 +37,36 @@ module ChefZero _, endpoint = routes.find { |route, endpoint| route.match(clean_path) } endpoint || not_found end + + def log_request(request) + ChefZero::Log.debug do + "#{request.method} /#{request.rest_path.join("/")}".tap do |msg| + next unless request.method =~ /^(POST|PUT)$/ + + if request.body.nil? || request.body.empty? + msg << " (no body)" + else + msg << [ + "", + "--- #{request.method} BODY ---", + request.body.chomp, + "--- END #{request.method} BODY ---" + ].join("\n") + end + end + end + + ChefZero::Log.debug { request.pretty_inspect } + end + + def log_response(response) + ChefZero::Log.debug { + [ "", + "--- RESPONSE (#{response[0]}) ---", + response[2].chomp, + "--- END RESPONSE ---", + ].join("\n") + } + end end end diff --git a/lib/chef_zero/rspec.rb b/lib/chef_zero/rspec.rb index b41a944..8867f37 100644 --- a/lib/chef_zero/rspec.rb +++ b/lib/chef_zero/rspec.rb @@ -46,26 +46,28 @@ module ChefZero extend WhenTheChefServerClassMethods include WhenTheChefServerInstanceMethods - @@chef_server_options = { port: 8900, signals: false, log_requests: true, server_scope: :each } + # Take the passed-in options - def self.chef_server_options - @@chef_server_options - end + define_singleton_method(:chef_server_options) { + @chef_server_options ||= begin + _chef_server_options = { port: 8900, signals: false, log_requests: true } + _chef_server_options = _chef_server_options.merge(tags.last) if tags.last.is_a?(Hash) + _chef_server_options = _chef_server_options.freeze + end + } + # Merge in chef_server_options from let(:chef_server_options) def chef_server_options - self.class.chef_server_options + chef_server_options = self.class.chef_server_options.dup + chef_server_options = chef_server_options.merge(chef_zero_opts) if self.respond_to?(:chef_zero_opts) + chef_server_options end - old_chef_server_url = nil - old_node_name = nil - old_client_key = nil - before chef_server_options[:server_scope] do - chef_server_options = self.chef_server_options - chef_server_options = chef_server_options.merge(chef_zero_opts) if self.respond_to?(:chef_zero_opts) - chef_server_options = chef_server_options.merge(tags.last) if tags.last.is_a?(Hash) - - Log.debug("Starting Chef server with options #{chef_server_options}") + if chef_server_options[:server_scope] != self.class.chef_server_options[:server_scope] + raise "server_scope: #{chef_server_options[:server_scope]} will not be honored: it can only be set on when_the_chef_server!" + end + Log.info("Starting Chef server with options #{chef_server_options}") ChefZero::RSpec.set_server_options(chef_server_options) @@ -74,9 +76,9 @@ module ChefZero end if defined?(Chef::Config) - old_chef_server_url = Chef::Config.chef_server_url - old_node_name = Chef::Config.node_name - old_client_key = Chef::Config.client_key + @old_chef_server_url = Chef::Config.chef_server_url + @old_node_name = Chef::Config.node_name + @old_client_key = Chef::Config.client_key if chef_server_options[:organization] Chef::Config.chef_server_url = "#{ChefZero::RSpec.server.url}/organizations/#{chef_server_options[:organization]}" else @@ -90,9 +92,9 @@ module ChefZero if defined?(Chef::Config) after chef_server_options[:server_scope] do - Chef::Config.chef_server_url = old_chef_server_url - Chef::Config.node_name = old_node_name - Chef::Config.client_key = old_client_key + Chef::Config.chef_server_url = @old_chef_server_url + Chef::Config.node_name = @old_node_name + Chef::Config.client_key = @old_client_key end end @@ -123,6 +125,10 @@ module ChefZero end end + def cookbook_artifact(name, identifier, data = {}, &block) + before(chef_server_options[:server_scope]) { cookbook_artifact(name, identifier, data, &block) } + end + def data_bag(name, data, &block) before(chef_server_options[:server_scope]) { data_bag(name, data, &block) } end @@ -147,6 +153,14 @@ module ChefZero before(chef_server_options[:server_scope]) { org_member(*usernames) } end + def policy(name, data, &block) + before(chef_server_options[:server_scope]) { policy(name, data, &block) } + end + + def policy_group(name, data, &block) + before(chef_server_options[:server_scope]) { policy_group(name, data, &block) } + end + def role(name, data, &block) before(chef_server_options[:server_scope]) { role(name, data, &block) } end @@ -201,6 +215,8 @@ module ChefZero def cookbook(name, version, data = {}, options = {}, &block) with_object_path("cookbooks/#{name}") do + # If you didn't specify metadata.rb, we generate it for you. If you + # explicitly set it to nil, that means you don't want it at all. if data.has_key?('metadata.rb') if data['metadata.rb'].nil? data.delete('metadata.rb') @@ -213,6 +229,22 @@ module ChefZero end end + def cookbook_artifact(name, identifier, data = {}, &block) + with_object_path("cookbook_artifacts/#{name}") do + # If you didn't specify metadata.rb, we generate it for you. If you + # explicitly set it to nil, that means you don't want it at all. + if data.has_key?('metadata.rb') + if data['metadata.rb'].nil? + data.delete('metadata.rb') + end + else + data['metadata.rb'] = "name #{name.inspect}" + end + ChefZero::RSpec.server.load_data({ 'cookbook_artifacts' => { "#{name}-#{identifier}" => data } }, current_org) + instance_eval(&block) if block_given? + end + end + def data_bag(name, data, &block) with_object_path("data/#{name}") do ChefZero::RSpec.server.load_data({ 'data' => { name => data }}, current_org) @@ -249,6 +281,20 @@ module ChefZero ChefZero::RSpec.server.load_data({ 'members' => usernames }, current_org) end + def policy(name, version, data, &block) + with_object_path("policies/#{name}") do + ChefZero::RSpec.server.load_data({ 'policies' => { name => { version => data } } }, current_org) + instance_eval(&block) if block_given? + end + end + + def policy_group(name, data, &block) + with_object_path("policy_groups/#{name}") do + ChefZero::RSpec.server.load_data({ 'policy_groups' => { name => data } }, current_org) + instance_eval(&block) if block_given? + end + end + def role(name, data, &block) with_object_path("roles/#{name}") do ChefZero::RSpec.server.load_data({ 'roles' => { name => data } }, current_org) diff --git a/lib/chef_zero/server.rb b/lib/chef_zero/server.rb index 9cf7b39..19744b1 100644 --- a/lib/chef_zero/server.rb +++ b/lib/chef_zero/server.rb @@ -40,13 +40,24 @@ require 'chef_zero/endpoints/rest_list_endpoint' require 'chef_zero/endpoints/authenticate_user_endpoint' require 'chef_zero/endpoints/acls_endpoint' require 'chef_zero/endpoints/acl_endpoint' -require 'chef_zero/endpoints/actors_endpoint' require 'chef_zero/endpoints/actor_endpoint' +require 'chef_zero/endpoints/actors_endpoint' +require 'chef_zero/endpoints/actor_key_endpoint' +require 'chef_zero/endpoints/organization_user_key_endpoint' +require 'chef_zero/endpoints/organization_user_default_key_endpoint' +require 'chef_zero/endpoints/organization_user_keys_endpoint' +require 'chef_zero/endpoints/actor_default_key_endpoint' +require 'chef_zero/endpoints/actor_keys_endpoint' require 'chef_zero/endpoints/cookbooks_endpoint' require 'chef_zero/endpoints/cookbook_endpoint' require 'chef_zero/endpoints/cookbook_version_endpoint' +require 'chef_zero/endpoints/cookbook_artifacts_endpoint' +require 'chef_zero/endpoints/cookbook_artifact_endpoint' +require 'chef_zero/endpoints/cookbook_artifact_identifier_endpoint' require 'chef_zero/endpoints/containers_endpoint' require 'chef_zero/endpoints/container_endpoint' +require 'chef_zero/endpoints/controls_endpoint' +require 'chef_zero/endpoints/dummy_endpoint' require 'chef_zero/endpoints/data_bags_endpoint' require 'chef_zero/endpoints/data_bag_endpoint' require 'chef_zero/endpoints/data_bag_item_endpoint' @@ -61,6 +72,8 @@ require 'chef_zero/endpoints/environment_recipes_endpoint' require 'chef_zero/endpoints/environment_role_endpoint' require 'chef_zero/endpoints/license_endpoint' require 'chef_zero/endpoints/node_endpoint' +require 'chef_zero/endpoints/nodes_endpoint' +require 'chef_zero/endpoints/node_identifiers_endpoint' require 'chef_zero/endpoints/organizations_endpoint' require 'chef_zero/endpoints/organization_endpoint' require 'chef_zero/endpoints/organization_association_requests_endpoint' @@ -69,8 +82,14 @@ require 'chef_zero/endpoints/organization_authenticate_user_endpoint' require 'chef_zero/endpoints/organization_users_endpoint' require 'chef_zero/endpoints/organization_user_endpoint' require 'chef_zero/endpoints/organization_validator_key_endpoint' -require 'chef_zero/endpoints/principal_endpoint' require 'chef_zero/endpoints/policies_endpoint' +require 'chef_zero/endpoints/policy_endpoint' +require 'chef_zero/endpoints/policy_revisions_endpoint' +require 'chef_zero/endpoints/policy_revision_endpoint' +require 'chef_zero/endpoints/policy_groups_endpoint' +require 'chef_zero/endpoints/policy_group_endpoint' +require 'chef_zero/endpoints/policy_group_policy_endpoint' +require 'chef_zero/endpoints/principal_endpoint' require 'chef_zero/endpoints/role_endpoint' require 'chef_zero/endpoints/role_environments_endpoint' require 'chef_zero/endpoints/sandboxes_endpoint' @@ -85,14 +104,16 @@ require 'chef_zero/endpoints/user_organizations_endpoint' require 'chef_zero/endpoints/file_store_file_endpoint' require 'chef_zero/endpoints/not_found_endpoint' require 'chef_zero/endpoints/version_endpoint' +require 'chef_zero/endpoints/server_api_version_endpoint' module ChefZero + class Server DEFAULT_OPTIONS = { - :host => '127.0.0.1', + :host => ['127.0.0.1'], :port => 8889, - :log_level => :info, + :log_level => :warn, :generate_real_keys => true, :single_org => 'chef', :ssl => false @@ -101,6 +122,7 @@ module ChefZero GLOBAL_ENDPOINTS = [ '/license', '/version', + '/server_api_version' ] def initialize(options = {}) @@ -120,6 +142,7 @@ module ChefZero def port if @port @port + # If options[:port] is not an Array or an Enumerable, it is just an Integer. elsif !options[:port].respond_to?(:each) options[:port] else @@ -142,10 +165,11 @@ module ChefZero # def url sch = @options[:ssl] ? 'https' : 'http' - @url ||= if @options[:host].include?(':') - URI("#{sch}://[#{@options[:host]}]:#{port}").to_s + hosts = Array(@options[:host]) + @url ||= if hosts.first.include?(':') + URI("#{sch}://[#{hosts.first}]:#{port}").to_s else - URI("#{sch}://#{@options[:host]}:#{port}").to_s + URI("#{sch}://#{hosts.first}:#{port}").to_s end end @@ -243,12 +267,26 @@ module ChefZero # @return [Thread] # the thread the background process is running in # + def listen(hosts, port) + hosts.each do |host| + @server.listen(host, port) + end + true + rescue Errno::EADDRINUSE + ChefZero::Log.warn("Port #{port} not available") + @server.listeners.each { |l| l.close } + @server.listeners.clear + false + end + def start_background(wait = 5) @server = WEBrick::HTTPServer.new( :DoNotListen => true, :AccessLog => [], :Logger => WEBrick::Log.new(StringIO.new, 7), + :RequestTimeout => 300, :SSLEnable => options[:ssl], + :SSLOptions => ssl_opts, :SSLCertName => [ [ 'CN', WEBrick::Utils::getservername ] ], :StartCallback => proc { @running = true @@ -258,22 +296,17 @@ module ChefZero @server.mount('/', Rack::Handler::WEBrick, app) # Pick a port - if options[:port].respond_to?(:each) - options[:port].each do |port| - begin - @server.listen(options[:host], port) - @port = port - break - rescue Errno::EADDRINUSE - ChefZero::Log.info("Port #{port} in use: #{$!}") - end + # If options[:port] can be an Enumerator, an Array, or an Integer, + # we need something that can respond to .each (Enum and Array can already). + Array(options[:port]).each do |port| + if listen(Array(options[:host]), port) + @port = port + break end - if !@port - raise Errno::EADDRINUSE, "No port in :port range #{options[:port]} is available" - end - else - @server.listen(options[:host], options[:port]) - @port = options[:port] + end + if !@port + raise Errno::EADDRINUSE, + "No port in :port range #{options[:port]} is available" end # Start the server in the background @@ -440,20 +473,43 @@ module ChefZero end end - if contents['cookbooks'] - contents['cookbooks'].each_pair do |name_version, cookbook| - if name_version =~ /(.+)-(\d+\.\d+\.\d+)$/ - cookbook_data = ChefData::CookbookData.to_hash(cookbook, $1, $2) - else - cookbook_data = ChefData::CookbookData.to_hash(cookbook, name_version) + if contents['policies'] + contents['policies'].each_pair do |policy_name, policy_struct| + # data_store.create_dir(['organizations', org_name, 'policies', policy_name], "revisions", :recursive) + dejsonize_children(policy_struct).each do |revision, policy_data| + data_store.set(['organizations', org_name, 'policies', policy_name, + "revisions", revision], policy_data, :create, :create_dir) + end + end + end + + if contents['policy_groups'] + contents['policy_groups'].each_pair do |group_name, group| + group['policies'].each do |policy_name, policy_revision| + data_store.set(['organizations', org_name, 'policy_groups', group_name, 'policies', policy_name], FFI_Yajl::Encoder.encode(policy_revision['revision_id'], :pretty => true), :create, :create_dir) end - raise "No version specified" if !cookbook_data[:version] - data_store.create_dir(['organizations', org_name, 'cookbooks'], cookbook_data[:cookbook_name], :recursive) - data_store.set(['organizations', org_name, 'cookbooks', cookbook_data[:cookbook_name], cookbook_data[:version]], FFI_Yajl::Encoder.encode(cookbook_data, :pretty => true), :create) - cookbook_data.values.each do |files| - next unless files.is_a? Array - files.each do |file| - data_store.set(['organizations', org_name, 'file_store', 'checksums', file[:checksum]], get_file(cookbook, file[:path]), :create) + end + end + + %w(cookbooks cookbook_artifacts).each do |cookbook_type| + if contents[cookbook_type] + contents[cookbook_type].each_pair do |name_version, cookbook| + if cookbook_type == 'cookbook_artifacts' + name, dash, identifier = name_version.rpartition('-') + cookbook_data = ChefData::CookbookData.to_hash(cookbook, name, identifier) + elsif name_version =~ /(.+)-(\d+\.\d+\.\d+)$/ + cookbook_data = ChefData::CookbookData.to_hash(cookbook, $1, $2) + else + cookbook_data = ChefData::CookbookData.to_hash(cookbook, name_version) + end + raise "No version specified" if !cookbook_data[:version] + data_store.create_dir(['organizations', org_name, cookbook_type], cookbook_data[:cookbook_name], :recursive) + data_store.set(['organizations', org_name, cookbook_type, cookbook_data[:cookbook_name], cookbook_data[:version]], FFI_Yajl::Encoder.encode(cookbook_data, :pretty => true), :create) + cookbook_data.values.each do |files| + next unless files.is_a? Array + files.each do |file| + data_store.set(['organizations', org_name, 'file_store', 'checksums', file[:checksum]], get_file(cookbook, file[:path]), :create) + end end end end @@ -478,13 +534,13 @@ module ChefZero private - def open_source_endpoints + def endpoints result = if options[:osc_compat] # OSC-only [ [ "/organizations/*/users", ActorsEndpoint.new(self) ], [ "/organizations/*/users/*", ActorEndpoint.new(self) ], - [ "/organizations/*/authenticate_user", OrganizationAuthenticateUserEndpoint.new(self) ], + [ "/organizations/*/authenticate_user", OrganizationAuthenticateUserEndpoint.new(self) ] ] else # EC-only @@ -498,11 +554,13 @@ module ChefZero [ "/users/*/association_requests", UserAssociationRequestsEndpoint.new(self) ], [ "/users/*/association_requests/count", UserAssociationRequestsCountEndpoint.new(self) ], [ "/users/*/association_requests/*", UserAssociationRequestEndpoint.new(self) ], + [ "/users/*/keys", ActorKeysEndpoint.new(self) ], + [ "/users/*/keys/default", ActorDefaultKeyEndpoint.new(self) ], + [ "/users/*/keys/*", ActorKeyEndpoint.new(self) ], [ "/users/*/organizations", UserOrganizationsEndpoint.new(self) ], [ "/authenticate_user", AuthenticateUserEndpoint.new(self) ], [ "/system_recovery", SystemRecoveryEndpoint.new(self) ], [ "/license", LicenseEndpoint.new(self) ], - [ "/organizations", OrganizationsEndpoint.new(self) ], [ "/organizations/*", OrganizationEndpoint.new(self) ], [ "/organizations/*/_validator_key", OrganizationValidatorKeyEndpoint.new(self) ], @@ -522,11 +580,22 @@ module ChefZero end result + [ # Both + [ "/dummy", DummyEndpoint.new(self) ], [ "/organizations/*/clients", ActorsEndpoint.new(self) ], [ "/organizations/*/clients/*", ActorEndpoint.new(self) ], + [ "/organizations/*/clients/*/keys", ActorKeysEndpoint.new(self) ], + [ "/organizations/*/clients/*/keys/default", ActorDefaultKeyEndpoint.new(self) ], + [ "/organizations/*/clients/*/keys/*", ActorKeyEndpoint.new(self) ], + [ "/organizations/*/users/*/keys", OrganizationUserKeysEndpoint.new(self) ], + [ "/organizations/*/users/*/keys/default", OrganizationUserDefaultKeyEndpoint.new(self) ], + [ "/organizations/*/users/*/keys/*", OrganizationUserKeyEndpoint.new(self) ], + [ "/organizations/*/controls", ControlsEndpoint.new(self) ], [ "/organizations/*/cookbooks", CookbooksEndpoint.new(self) ], [ "/organizations/*/cookbooks/*", CookbookEndpoint.new(self) ], [ "/organizations/*/cookbooks/*/*", CookbookVersionEndpoint.new(self) ], + [ "/organizations/*/cookbook_artifacts", CookbookArtifactsEndpoint.new(self) ], + [ "/organizations/*/cookbook_artifacts/*", CookbookArtifactEndpoint.new(self) ], + [ "/organizations/*/cookbook_artifacts/*/*", CookbookArtifactIdentifierEndpoint.new(self) ], [ "/organizations/*/data", DataBagsEndpoint.new(self) ], [ "/organizations/*/data/*", DataBagEndpoint.new(self) ], [ "/organizations/*/data/*/*", DataBagItemEndpoint.new(self) ], @@ -538,9 +607,16 @@ module ChefZero [ "/organizations/*/environments/*/nodes", EnvironmentNodesEndpoint.new(self) ], [ "/organizations/*/environments/*/recipes", EnvironmentRecipesEndpoint.new(self) ], [ "/organizations/*/environments/*/roles/*", EnvironmentRoleEndpoint.new(self) ], - [ "/organizations/*/nodes", RestListEndpoint.new(self) ], + [ "/organizations/*/nodes", NodesEndpoint.new(self) ], [ "/organizations/*/nodes/*", NodeEndpoint.new(self) ], - [ "/organizations/*/policies/*/*", PoliciesEndpoint.new(self) ], + [ "/organizations/*/nodes/*/_identifiers", NodeIdentifiersEndpoint.new(self) ], + [ "/organizations/*/policies", PoliciesEndpoint.new(self) ], + [ "/organizations/*/policies/*", PolicyEndpoint.new(self) ], + [ "/organizations/*/policies/*/revisions", PolicyRevisionsEndpoint.new(self) ], + [ "/organizations/*/policies/*/revisions/*", PolicyRevisionEndpoint.new(self) ], + [ "/organizations/*/policy_groups", PolicyGroupsEndpoint.new(self) ], + [ "/organizations/*/policy_groups/*", PolicyGroupEndpoint.new(self) ], + [ "/organizations/*/policy_groups/*/policies/*", PolicyGroupPolicyEndpoint.new(self) ], [ "/organizations/*/principals/*", PrincipalEndpoint.new(self) ], [ "/organizations/*/roles", RestListEndpoint.new(self) ], [ "/organizations/*/roles/*", RoleEndpoint.new(self) ], @@ -551,6 +627,7 @@ module ChefZero [ "/organizations/*/search", SearchesEndpoint.new(self) ], [ "/organizations/*/search/*", SearchEndpoint.new(self) ], [ "/version", VersionEndpoint.new(self) ], + [ "/server_api_version", ServerAPIVersionEndpoint.new(self) ], # Internal [ "/organizations/*/file_store/**", FileStoreFileEndpoint.new(self) ] @@ -565,7 +642,7 @@ module ChefZero def app return @app if @app - router = RestRouter.new(open_source_endpoints) + router = RestRouter.new(endpoints) router.not_found = NotFoundEndpoint.new if options[:single_org] @@ -633,5 +710,16 @@ module ChefZero end value end + + ## Disable unsecure ssl + ## Ref: https://www.ruby-lang.org/en/news/2014/10/27/changing-default-settings-of-ext-openssl/ + def ssl_opts + ssl_opts = OpenSSL::SSL::OP_ALL + ssl_opts &= ~OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS if defined?(OpenSSL::SSL::OP_DONT_INSERT_EMPTY_FRAGMENTS) + ssl_opts |= OpenSSL::SSL::OP_NO_COMPRESSION if defined?(OpenSSL::SSL::OP_NO_COMPRESSION) + ssl_opts |= OpenSSL::SSL::OP_NO_SSLv2 if defined?(OpenSSL::SSL::OP_NO_SSLv2) + ssl_opts |= OpenSSL::SSL::OP_NO_SSLv3 if defined?(OpenSSL::SSL::OP_NO_SSLv3) + ssl_opts + end end end diff --git a/lib/chef_zero/version.rb b/lib/chef_zero/version.rb index 3e07823..d39f82c 100644 --- a/lib/chef_zero/version.rb +++ b/lib/chef_zero/version.rb @@ -1,3 +1,3 @@ module ChefZero - VERSION = '4.2.2' + VERSION = '4.7.1' end diff --git a/spec/run_oc_pedant.rb b/spec/run_oc_pedant.rb index 79d92e7..d874fb8 100644 --- a/spec/run_oc_pedant.rb +++ b/spec/run_oc_pedant.rb @@ -5,48 +5,193 @@ require 'bundler/setup' require 'chef_zero/server' require 'rspec/core' -tmpdir = nil +# This file runs oc-chef-pedant specs and is invoked by `rake pedant` +# and other Rake tasks. Run `rake -T` to list tasks. +# +# Options for oc-chef-pedant and rspec can be specified via +# ENV['PEDANT_OPTS'] and ENV['RSPEC_OPTS'], respectively. +# +# The log level can be specified via ENV['LOG_LEVEL']. +# +# Example: +# +# $ PEDANT_OPTS="--focus-users --skip-keys" \ +# > RSPEC_OPTS="--fail-fast --profile 5" \ +# > LOG_LEVEL=debug \ +# > rake pedant +# -begin - if ENV['FILE_STORE'] - require 'tmpdir' - require 'chef_zero/data_store/raw_file_store' - tmpdir = Dir.mktmpdir - data_store = ChefZero::DataStore::RawFileStore.new(tmpdir, true) - data_store = ChefZero::DataStore::DefaultFacade.new(data_store, false, false) - server = ChefZero::Server.new(:port => 8889, :single_org => false, :data_store => data_store) - server.start_background +DEFAULT_SERVER_OPTIONS = { + port: 8889, + single_org: false, +}.freeze - else - server = ChefZero::Server.new(:port => 8889, :single_org => false)#, :log_level => :debug) - server.start_background +DEFAULT_LOG_LEVEL = :warn + +def log_level + return ENV['LOG_LEVEL'].downcase.to_sym if ENV['LOG_LEVEL'] + return :debug if ENV['DEBUG'] + DEFAULT_LOG_LEVEL +end + +def start_chef_server(opts={}) + opts = DEFAULT_SERVER_OPTIONS.merge(opts) + opts[:log_level] = log_level + + ChefZero::Server.new(opts).tap {|server| server.start_background } +end + +def start_cheffs_server(chef_repo_path) + require 'chef/version' + require 'chef/config' + require 'chef/chef_fs/config' + require 'chef/chef_fs/chef_fs_data_store' + require 'chef_zero/server' + + Dir.mkdir(chef_repo_path) if !File.exists?(chef_repo_path) + + # 11.6 and below had a bug where it couldn't create the repo children automatically + if Chef::VERSION.to_f < 11.8 + %w(clients cookbooks data_bags environments nodes roles users).each do |child| + Dir.mkdir("#{chef_repo_path}/#{child}") if !File.exists?("#{chef_repo_path}/#{child}") + end end + # Start the new server + Chef::Config.repo_mode = 'hosted_everything' + Chef::Config.chef_repo_path = chef_repo_path + Chef::Config.versioned_cookbooks = true + chef_fs_config = Chef::ChefFS::Config.new + + data_store = Chef::ChefFS::ChefFSDataStore.new(chef_fs_config.local_fs, chef_fs_config.chef_config) + data_store = ChefZero::DataStore::V1ToV2Adapter.new(data_store, 'pedant-testorg') + data_store = ChefZero::DataStore::DefaultFacade.new(data_store, 'pedant-testorg', false) + data_store.create(%w(organizations pedant-testorg users), 'pivotal', '{}') + data_store.set(%w(organizations pedant-testorg groups admins), '{ "users": [ "pivotal" ] }') + data_store.set(%w(organizations pedant-testorg groups users), '{ "users": [ "pivotal" ] }') + + start_chef_server(data_store: data_store) +end + +def pedant_args_from_env + args_from_env('PEDANT_OPTS') +end + +def rspec_args_from_env + args_from_env('RSPEC_OPTS') +end + +def args_from_env(key) + return [] unless ENV[key] + ENV[key].split +end + +begin + tmpdir = nil + server = + if ENV['FILE_STORE'] + require 'tmpdir' + require 'chef_zero/data_store/raw_file_store' + tmpdir = Dir.mktmpdir + data_store = ChefZero::DataStore::RawFileStore.new(tmpdir, true) + data_store = ChefZero::DataStore::DefaultFacade.new(data_store, false, false) + + start_chef_server(data_store: data_store) + + elsif ENV['CHEF_FS'] + require 'tmpdir' + tmpdir = Dir.mktmpdir + start_cheffs_server(tmpdir) + + else + start_chef_server + end + require 'rspec/core' require 'pedant' - require 'pedant/opensource/platform' - require 'pedant/multitenant' require 'pedant/organization' - #Pedant::Config.rerun = true + # Pedant::Config.rerun = true Pedant.config.suite = 'api' + Pedant.config[:config_file] = 'spec/support/oc_pedant.rb' - Pedant.setup([ - '--skip-knife', - '--skip-keys', - '--skip-controls', - '--skip-acl', + + # Because ChefFS can only ever have one user (pivotal), we can't do most of the + # tests that involve multiple + chef_fs_skips = if ENV['CHEF_FS'] + [ '--skip-association', + '--skip-users', + '--skip-organizations', + '--skip-multiuser', + '--skip-user-keys', + + # chef-zero has some non-removable quirks, such as the fact that files + # with 255-character names cannot be stored in local mode. This is + # reserved only for quirks that are *irrevocable* and by design; and + # should barely be used at all. + '--skip-chef-zero-quirks', + ] + else + [] + end + # The latest released Chef doesn't do ACLs, Cookbook Artifacts or Policies yet + chef_fs_skips << '--skip-acl' + chef_fs_skips << '--skip-cookbook-artifacts' + chef_fs_skips << '--skip-policies' + + # Multi-keys don't work prior to 12.8 + unless Gem::Requirement.new(">= 12.8.0").satisfied_by?(Gem::Version.new(Chef::VERSION)) + chef_fs_skips << '--skip-keys' + end + + # These things aren't supported by Chef Zero in any mode of operation: + default_skips = [ + # "the goal is that only authorization, authentication and validation tests + # are turned off" - @jkeiser + # + # ...but we're not there yet + + # Chef Zero does not intend to support validation the way erchef does. '--skip-validation', + + # Chef Zero does not intend to support authentication the way erchef does. '--skip-authentication', + + # Chef Zero does not intend to support authorization the way erchef does. '--skip-authorization', + + # Omnibus tests depend on erchef features that are specific to erchef and + # bundled in the omnibus package. Currently the only test in this category + # is for the search reindexing script. '--skip-omnibus', + + # USAGs (user-specific association groups) are Authz groups that contain + # only one user and represent that user's association with an org. Though + # there are good reasons for them, they don't work well in practice and + # only the manage console really uses them. Since Chef Zero + Manage is a + # quite unusual configuration, we're ignoring them. '--skip-usags', - '--skip-internal_orgs', - '--skip-rename_org' - ]) - result = RSpec::Core::Runner.run(Pedant.config.rspec_args) + # Chef 12 features not yet 100% supported by Chef Zero + + # The universe endpoint is unlikely to ever make sense for Chef Zero + '--skip-universe', + ] + + # The knife tests are very slow and don't give us a lot of extra coverage, + # so we run them in a different entry in the travis test matrix. + pedant_args = + if ENV["PEDANT_KNIFE_TESTS"] + default_skips + %w{ --focus-knife } + else + default_skips + chef_fs_skips + %w{ --skip-knife } + end + + Pedant.setup(pedant_args + pedant_args_from_env) + + rspec_args = Pedant.config.rspec_args + rspec_args_from_env + result = RSpec::Core::Runner.run(rspec_args) server.stop if server.running? ensure diff --git a/spec/run_pedant.rb b/spec/run_pedant.rb deleted file mode 100644 index 30ffa2d..0000000 --- a/spec/run_pedant.rb +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env ruby -require 'bundler' -require 'bundler/setup' - -require 'chef_zero/server' -require 'rspec/core' - -tmpdir = nil - -def start_local_server(chef_repo_path) - Dir.mkdir(chef_repo_path) if !File.exists?(chef_repo_path) - - # 11.6 and below had a bug where it couldn't create the repo children automatically - if Chef::VERSION.to_f < 11.8 - %w(clients cookbooks data_bags environments nodes roles users).each do |child| - Dir.mkdir("#{chef_repo_path}/#{child}") if !File.exists?("#{chef_repo_path}/#{child}") - end - end - - # Start the new server - Chef::Config.repo_mode = 'everything' - Chef::Config.chef_repo_path = chef_repo_path - Chef::Config.versioned_cookbooks = true - chef_fs = Chef::ChefFS::Config.new.local_fs - data_store = Chef::ChefFS::ChefFSDataStore.new(chef_fs) - server = ChefZero::Server.new(:port => 8889, :data_store => data_store) - ENV['SINGLE_ORG'] = 'true' - server.start_background - server -end - -begin - if ENV['CHEF_FS'] - require 'chef/chef_fs/chef_fs_data_store' - require 'chef/chef_fs/config' - require 'tmpdir' - require 'fileutils' - require 'chef/version' - require 'chef_zero/data_store/v1_to_v2_adapter' - - # Create chef repository - tmpdir = Dir.mktmpdir - chef_repo_path = "#{tmpdir}/repo" - - # Capture setup data into master_chef_repo_path - server = start_local_server(chef_repo_path) - - elsif ENV['SINGLE_ORG'] - server = ChefZero::Server.new(:port => 8889, :single_org => 'chef') - server.start_background - - elsif ENV['FILE_STORE'] - require 'tmpdir' - require 'chef_zero/data_store/raw_file_store' - tmpdir = Dir.mktmpdir - data_store = ChefZero::DataStore::RawFileStore.new(tmpdir, true) - data_store = ChefZero::DataStore::DefaultFacade.new(data_store, true, false) - server = ChefZero::Server.new(:port => 8889, :single_org => 'chef', :data_store => data_store) - server.start_background - - else - server = ChefZero::Server.new(:port => 8889, :single_org => false, :osc_compat => true) - server.data_store.create_dir([ 'organizations' ], 'pedant') - server.start_background - end - - unless ENV['SKIP_PEDANT'] - require 'pedant' - require 'pedant/opensource' - - #Pedant::Config.rerun = true - - Pedant.config.suite = 'api' - Pedant.config[:config_file] = 'spec/support/pedant.rb' - - Pedant.setup([ - '--skip-knife', - '--skip-validation', - '--skip-authentication', - '--skip-authorization', - '--skip-keys', - '--skip-omnibus', - '--skip-cookbook_artifacts' - ]) - - result = RSpec::Core::Runner.run(Pedant.config.rspec_args) - else - require 'net/http' - response = Net::HTTP.new('127.0.0.1', 8889).get("/environments", { 'Accept' => 'application/json'}).body - if response =~ /_default/ - result = 0 - else - puts "GET /environments returned #{response}. Expected _default!" - result = 1 - end - end - - server.stop if server.running? -ensure - FileUtils.remove_entry_secure(tmpdir) if tmpdir -end - -exit(result) diff --git a/spec/server_spec.rb b/spec/server_spec.rb index 558ef35..123a13e 100644 --- a/spec/server_spec.rb +++ b/spec/server_spec.rb @@ -1,4 +1,5 @@ require 'chef_zero/server' +require 'net/http' require 'uri' describe ChefZero::Server do @@ -26,6 +27,10 @@ describe ChefZero::Server do expect { ChefZero::Server.new(:port => 8889.upto(8889)).start_background }.to raise_error Errno::EADDRINUSE end + it 'has a very patient request timeout' do + expect(@server.server.config[:RequestTimeout]).to eq 300 + end + context 'accept headers' do def get_nodes(accepts) uri = URI(@server.url) diff --git a/spec/support/oc_pedant.rb b/spec/support/oc_pedant.rb index 60e3629..6e39878 100644 --- a/spec/support/oc_pedant.rb +++ b/spec/support/oc_pedant.rb @@ -45,6 +45,16 @@ maximum_search_time 0 # # to be enabled for Pedant tests to work correctly explicit_port_url true +server_api_version 0 + +internal_server chef_server + +# see dummy_endpoint.rb for details. +search_server chef_server +search_commit_url "/dummy" +search_url_fmt "/dummy?fq=+X_CHEF_type_CHEF_X:%{type}&q=%{query}&wt=json" + + # We're starting to break tests up into groups based on different # criteria. The proper API tests (the results of which are viewable # to OPC customers) should be the only ones run by Pedant embedded in @@ -59,9 +69,12 @@ explicit_port_url true # value. include_internal false -# This is the bit that is different from pedant.rb -org({:name => "pedant-testorg", - :create_me => true}) +key = 'spec/support/stickywicket.pem' + +org(name: "pedant-testorg", + create_me: !ENV['CHEF_FS'], + validator_key: key) + internal_account_url chef_server delete_org true @@ -72,13 +85,15 @@ delete_org true # are using pre-existing users, you must supply a ':key_file' key, # which should be the fully-qualified path /on the machine Pedant is # running on/ to a private key for that user. -key = 'spec/support/stickywicket.pem' superuser_name 'pivotal' superuser_key key webui_key key -# Set the platform_class -platform_class Pedant::MultiTenantPlatform +def cheffs_or_else_user(value) + ENV['CHEF_FS'] ? "pivotal" : value +end + +keyfile_maybe = ENV['CHEF_FS'] ? { key_file: key } : { key_file: nil } requestors({ :clients => { @@ -105,24 +120,26 @@ requestors({ :users => { # An administrator in the testing organization :admin => { - :name => "pedant_admin_user", - :create_me => true, + :name => cheffs_or_else_user("pedant_admin_user"), + :create_me => !ENV['CHEF_FS'], + :associate => !ENV['CHEF_FS'], :create_knife => true - }, + }.merge(keyfile_maybe), :non_admin => { - :name => "pedant_user", - :create_me => true, + :name => cheffs_or_else_user("pedant_user"), + :create_me => !ENV['CHEF_FS'], + :associate => !ENV['CHEF_FS'], :create_knife => true - }, + }.merge(keyfile_maybe), # A user that is not a member of the testing organization :bad => { - :name => "pedant-nobody", - :create_me => true, + :name => cheffs_or_else_user("pedant-nobody"), + :create_me => !ENV['CHEF_FS'], :create_knife => true, :associate => false - }, + }.merge(keyfile_maybe), } }) @@ -132,3 +149,4 @@ verify_error_messages false ruby_users_endpoint? false ruby_acls_endpoint? false ruby_org_assoc? false +chef_12? true diff --git a/spec/support/pedant.rb b/spec/support/pedant.rb deleted file mode 100644 index 29a0ec1..0000000 --- a/spec/support/pedant.rb +++ /dev/null @@ -1,129 +0,0 @@ -# Copyright: Copyright (c) 2012 Opscode, Inc. -# License: Apache License, Version 2.0 -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# This annotated Pedant configuration file details the various -# configuration settings available to you. It is separate from the -# actual Pedant::Config class because not all settings have sane -# defaults, and not all settings are appropriate in all settings. - -################################################################################ -# You MUST specify the address of the server the API requests will be -# sent to. Only specify protocol, hostname, and port. -if ENV['SINGLE_ORG'] || ENV['FILE_STORE'] - chef_server 'http://127.0.0.1:8889' -else - chef_server 'http://127.0.0.1:8889/organizations/pedant' -end - -# If you are doing development testing, you can specify the address of -# the Solr server. The presence of this parameter will enable tests -# to force commits to Solr, greatly decreasing the amout of time -# needed for testing the search endpoint. This is only an -# optimization for development! If you are testing a "live" Chef -# Server, or otherwise do not have access to the Solr server from your -# testing location, you should not specify a value for this parameter. -# The tests will still run, albeit slower, as they will now need to -# poll for a period to ensure they are querying committed results. -#search_server "http://localhost:8983" - -# Related to the 'search_server' parameter, this specifies the maximum -# amout of time (in seconds) that search endpoint requests should be -# retried before giving up. If not explicitly set, it will default to -# 65 seconds; only set it if you know that your Solr commit interval -# differs significantly from this. -maximum_search_time 0 - -# OSC sends erchef a host header with a port, so this option needs -# # to be enabled for Pedant tests to work correctly -explicit_port_url true - -# We're starting to break tests up into groups based on different -# criteria. The proper API tests (the results of which are viewable -# to OPC customers) should be the only ones run by Pedant embedded in -# OPC installs. There are other specs that help us keep track of API -# cruft that we want to come back and fix later; these shouldn't be -# viewable to customers, but we should be able to run them in -# development and CI environments. If this parameter is missing or -# explicitly `false` only the customer-friendly tests will be run. -# -# This is mainly here for documentation purposes, since the -# command-line `opscode-pedant` utility ultimately determines this -# value. -include_internal false - -# Test users. The five users specified below are required; their -# names (:user, :non_org_user, etc.) are indicative of their role -# within the tests. All users must have a ':name' key. If they have -# a ':create_me' key, Pedant will create these users for you. If you -# are using pre-existing users, you must supply a ':key_file' key, -# which should be the fully-qualified path /on the machine Pedant is -# running on/ to a private key for that user. -key = 'spec/support/stickywicket.pem' -superuser_name 'admin' -superuser_key key -webui_key key - -# The Policies endpoint is feature-flagged during development. Zero supports -# the policies endpoint, so turn it on: -policies? true - -# Set the platform_class -platform_class Pedant::OpenSourcePlatform - -requestors({ - :clients => { - # The the admin user, for the purposes of getting things rolling - :admin => { - :name => "pedant_admin_client", - :create_me => true, - :create_knife => true, - :admin => true - }, - :non_admin => { - :name => 'pedant_client', - :create_me => true, - :create_knife => true - }, - :bad => { - :name => 'bad_client', - :bogus => true - } - }, - :users => { - :admin => { - :name => "admin", - :key_file => key, - :create_me => false, - :create_knife => false, - :admin => true - }, - :non_admin => { - :name => "pedant_non_admin_user", - :create_me => true, - :create_knife => true, - :admin => false - }, - # A user for Knife tests. A knife.rb and key files will be set up - # for this user - :knife_user => { - :name => "knifey", - :create_me => true, - :create_knife => true - } - } -}) - -self[:tags] = [:validation, :authentication, :authorization] -verify_error_messages false |