diff options
74 files changed, 2812 insertions, 881 deletions
@@ -8,6 +8,7 @@ InstalledFiles _yardoc coverage doc/ +binstubs/ lib/bundler/man pkg rdoc @@ -17,3 +18,4 @@ test/version_tmp tmp rspec.failures gemfiles/*.lock +.ruby-version @@ -0,0 +1,2 @@ +--color +-fd diff --git a/.travis.yml b/.travis.yml index 985730f..4994db3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,41 +1,39 @@ -rvm: 1.9.3 - +rvm: 2.1 gemfile: Gemfile -script: bundle exec rake pedant +# 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 update + - bundle exec rake pedant matrix: include: - - rvm: 1.8.7 - - rvm: 1.8.7 - env: SKIP_PEDANT=true - gemfile: gemfiles/no-pedant.gemfile - - rvm: jruby-19mode - - rvm: jruby-19mode - env: SKIP_PEDANT=true - gemfile: gemfiles/no-pedant.gemfile - - rvm: 2.0.0 - - rvm: 2.1.1 - - rvm: 2.1.1 + - rvm: 2.1 + env: PEDANT_KNIFE_TESTS=true PEDANT_ALLOW_RVM=1 + - rvm: 2.1 env: SINGLE_ORG=true - - rvm: 2.1.1 + - rvm: 2.1 env: CHEF_FS=true - - rvm: 2.1.1 + - rvm: 2.1 + env: + - CHEF_FS=true + - "GEMFILE_MOD=\"gem 'chef', github: 'chef/chef'\"" + - rvm: 2.1 env: FILE_STORE=true - - rvm: 2.1.1 + - rvm: 2.1 script: bundle exec rake chef_spec env: TEST=chef_spec - - rvm: 2.1.1 + - rvm: 2.1 script: bundle exec rake spec env: TEST=rake_spec - allow_failures: - - rvm: 1.8.7 - - rvm: jruby-19mode - - rvm: 2.1.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 72165d4..8627141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,220 +1,572 @@ -Chef Zero CHANGELOG -=================== +# Change Log -# 3.2 (9/26/2014) +## [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) -- removed 'json' gem dependency, replaced it with 'ffi-yajl' +**Implemented enhancements:** -# 3.1.3 (9/3/2014) +- Downgrade info log message to debug [\#221](https://github.com/chef/chef-zero/pull/221) ([stanhu](https://github.com/stanhu)) -- fixes for running Chef local mode in multi-org mode +## [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) -# 3.1.2 (8/29/2014) +**Implemented enhancements:** -- add default to rspec for cookbooks -- add /organizations/NAME/organization/_acl as an alias for /organizations/NAME/organizations/_acl +- Add external\_authentication\_uid to actors endpoint for querying [\#217](https://github.com/chef/chef-zero/pull/217) ([kmacgugan](https://github.com/kmacgugan)) -# 3.1.1 (8/28/2014) +**Merged pull requests:** -- fix minor bug with unknown container acls +- Depend on rack \< 2 to restore Ruby 2.1 compat [\#219](https://github.com/chef/chef-zero/pull/219) ([tas50](https://github.com/tas50)) -# 3.1 (8/28/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) -- 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 +**Fixed bugs:** -# 3.0 (7/22/2014) +- Log responses only at debug log level [\#216](https://github.com/chef/chef-zero/pull/216) ([stevendanna](https://github.com/stevendanna)) -- Enterprise Chef support (organizations, ACLs, groups, much more) -- SSL support (@sawanoboly) +## [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) -# 2.2 (6/18/2014) +**Fixed bugs:** -- 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))` +- 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)) -# 2.1.5 (6/2/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) -- fix issue with :single_org => <value> not being honored +**Implemented enhancements:** -# 2.1.4 (5/27/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 issue with global Thread.exit_on_exception being set +**Fixed bugs:** -# 2.1.3 (5/27/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)) -- 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 +## [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) -# 2.1.2 (5/27/2014) +**Merged pull requests:** -- fix build_uri (and thus cookbook downloads) +- 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.1.1 (5/26/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) -- flip defaults off in V1ToV2Adapater, allowing most chef tests to pass against 2.1.1 +**Merged pull requests:** -# 2.1 (5/26/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)) -- **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 +## [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.0.2 (1/20/2014) +**Merged pull requests:** -- 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 +- 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.0.1 (1/3/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) -- 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 +**Closed issues:** -# 2.0.0 (12/17/2013) +- Please bump hashie version if possible [\#97](https://github.com/chef/chef-zero/issues/97) -- Remove Puma (and `--socket` option) -- Use a cleaner threading approach -- Implement a better `running?` check +**Merged pull requests:** -# 1.7.3 +- 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)) -- (Backport) Read JSON, not a file path in `from_json` +## [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) -# 1.6.3 +## [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) -- (Backport) Read JSON, not a file path in `from_json` +**Fixed bugs:** -# 1.5.5 +- 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 issue with - in term (name:a-b) +## [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) -# 1.5.4 +**Implemented enhancements:** -- Fix issue where run_lists in format cookbook::recipe@version do not depsolve +- 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)) -# 1.5.3 +**Fixed bugs:** -- Add Server: chef-zero header to response +- 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.5.2 +**Merged pull requests:** -- Fix a couple of search query issues (make parentheses and NOT term:value work) +- Autogenerated changelog [\#163](https://github.com/chef/chef-zero/pull/163) ([jkeiser](https://github.com/jkeiser)) -# 1.5.1 +## [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) -- Add Unix domain socket support (e.g. chef-zero --socket /tmp/chef-zero.sock) (stevendanna) +**Merged pull requests:** -# 1.5 +- Make server\_scope: :context work again [\#143](https://github.com/chef/chef-zero/pull/143) ([jkeiser](https://github.com/jkeiser)) -- Add -d option for daemon mode (sethvargo) -- Fix bug with cookbook metadata.rb files that rely on __FILE__ +## [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.4 +**Merged pull requests:** -- Run with downgraded Puma 1.6 in order to work on Windows (2.x doesn't yet) +- 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.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) -- Fix bug with search when JSON contains the same key in different places +**Merged pull requests:** -# 1.2.1 +- Don't pollute global Chef server options [\#125](https://github.com/chef/chef-zero/pull/125) ([jkeiser](https://github.com/jkeiser)) -- Fix search when JSON contains integers +## [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.2 +**Merged pull requests:** -- Allow rspec users to specify cookbook NAME, VERSION, { :frozen => true } -- Documentation fix +- 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.1.3 +## [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) -- 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 +- 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)) -- Allow rspec users to specify the same data twice (overwrites) +## [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.1.1 +**Closed issues:** -- Fix broken rspec functionality (jkeiser, reset) +- 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.1 +**Merged pull requests:** -- Create plugin system to allow other storage besides memory +- Policyfile get/set API [\#111](https://github.com/chef/chef-zero/pull/111) ([danielsdeleo](https://github.com/danielsdeleo)) -# 1.0.1 +## [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) -- Fix depsolver crash with frozen version strings (sethvargo) +**Closed issues:** -# 1.0 +- missing `else`? [\#102](https://github.com/chef/chef-zero/issues/102) -- Increased testing of server +**Merged pull requests:** -# 0.9.13 +- 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)) -- Remove extra require of 'thin' so rspec users don't get broke +## [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) -# 0.9.12 +## [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) -- Switch from thin to puma (sethvargo) +**Closed issues:** -# 0.9.11 +- 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) -- Support full cookbook metadata.rb syntax, including "depends" +**Merged pull requests:** -# 0.9.10 +- Removing 'json' gem dependency, replacing with 'ffi-yajl' [\#93](https://github.com/chef/chef-zero/pull/93) ([tyler-ball](https://github.com/tyler-ball)) -- Add -d flag to print debug output (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) -# 0.9.9 +**Merged pull requests:** -- Remove chef as a dependency so we can run on jruby (reset) -- Server assumes json is acceptable if Accept header is not sent (stevendanna) +- 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.8 +## [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) -- Support runlists with a::b in them in depsolver +## [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.7 +## [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) -- Return file URLs and other important things in depsolver response +## [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.6 +**Implemented enhancements:** -- Make 404 a JSON response +- 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.5 +**Closed issues:** -- Fix crash in 405 error response generator -- Add ability to verify request/response pairs from rspec api +- SSL support [\#86](https://github.com/chef/chef-zero/issues/86) -# 0.9.4 +## [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) -- Ruby 1.8.7 support +**Closed issues:** -# 0.9.3 +- Vagrant on windows fails to load Chef-Zero due to Chef dependencies [\#55](https://github.com/chef/chef-zero/issues/55) -- 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 +**Merged pull requests:** -# 0.9.2 +- 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)) -- Speed increase for rspec (only start server once) -- Support CTRL+C when running rspec chef-zero tests +## [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.1 +**Merged pull requests:** -- Switch from webrick -> thin -- Bugfixes +- Allow server to try multiple ports [\#67](https://github.com/chef/chef-zero/pull/67) ([jkeiser](https://github.com/jkeiser)) -# 0.9 +## [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) -- Initial code-complete release with working server +**Merged pull requests:** + +- Honor :single\_org =\> 'orgname' parameter everywhere [\#66](https://github.com/chef/chef-zero/pull/66) ([jkeiser](https://github.com/jkeiser)) + +## [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) + +## [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) + +## [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) + +## [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) + +## [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) + +**Closed issues:** + +- 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) + +**Merged pull requests:** + +- 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,6 +1,20 @@ source 'https://rubygems.org' gemspec -gem 'rest-client', :github => 'opscode/rest-client' -gem 'chef-pedant', :github => 'opscode/chef-pedant', :ref => '81f3b4ecbc09d04950f2819b38a6a8f906ada2a7' -gem 'chef', :github => 'opscode/chef', :ref => 'a2dee150e68a611249577a8adcb2c3e329d6762d' +# gem 'rest-client', :github => 'chef/rest-client' + +gem 'oc-chef-pedant', :github => 'chef/chef-server' + +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/opscode/chef-zero.png?label=ready&title=Ready)](https://waffle.io/opscode/chef-zero)[![Stories in Progress](https://badge.waffle.io/opscode/chef-zero.png?label=in+progress&title=In+Progress)](https://waffle.io/opscode/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,18 +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" task :spec do system('rspec spec/*_spec.rb') end -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 task :oc_pedant do - require File.expand_path('spec/run_oc_pedant') + run_oc_pedant end task :chef_spec do @@ -26,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 c6bc909..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' - 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 ab45d76..da3802d 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/data_store/memory_store_v2.rb b/lib/chef_zero/data_store/memory_store_v2.rb index 9f8b80e..d330972 100644 --- a/lib/chef_zero/data_store/memory_store_v2.rb +++ b/lib/chef_zero/data_store/memory_store_v2.rb @@ -47,7 +47,7 @@ module ChefZero def create(path, name, data, *options) if !data.is_a?(String) - raise "set only works with strings" + raise "set only works with strings (given data: #{data.inspect})" end parent = _get(path, options.include?(:create_dir)) diff --git a/lib/chef_zero/data_store/v1_to_v2_adapter.rb b/lib/chef_zero/data_store/v1_to_v2_adapter.rb index 576f27d..d9ea6e1 100644 --- a/lib/chef_zero/data_store/v1_to_v2_adapter.rb +++ b/lib/chef_zero/data_store/v1_to_v2_adapter.rb @@ -112,9 +112,13 @@ module ChefZero begin yield rescue DataAlreadyExistsError => e - raise DataAlreadyExistsError.new([ 'organizations', single_org ] + e.path, e) + err = DataAlreadyExistsError.new([ 'organizations', single_org ] + e.path, e) + err.set_backtrace(e.backtrace) + raise err rescue DataNotFoundError => e - raise DataNotFoundError.new([ 'organizations', single_org ] + e.path, e) + err = DataNotFoundError.new([ 'organizations', single_org ] + e.path, e) + err.set_backtrace(e.backtrace) + raise e end end 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 new file mode 100644 index 0000000..37493da --- /dev/null +++ b/lib/chef_zero/endpoints/policies_endpoint.rb @@ -0,0 +1,26 @@ +require 'chef_zero/chef_data/data_normalizer' + +module ChefZero + module Endpoints + # /organizations/ORG/policies + class PoliciesEndpoint < RestBase + # GET /organizations/ORG/policies + def get(request) + 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"]) + + response_data[policy_name] = { + uri: policy_uri, + revisions: hashify_list(revisions) + } + end + + return json_response(200, response_data) + 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/endpoints/version_endpoint.rb b/lib/chef_zero/endpoints/version_endpoint.rb new file mode 100644 index 0000000..d38c33e --- /dev/null +++ b/lib/chef_zero/endpoints/version_endpoint.rb @@ -0,0 +1,12 @@ +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # /version + class VersionEndpoint < RestBase + def get(request) + text_response(200, "chef-zero #{ChefZero::VERSION}\n") + end + end + end +end diff --git a/lib/chef_zero/rest_base.rb b/lib/chef_zero/rest_base.rb index 3fa017a..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,16 +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 already_json_response(response_code, json_text) - [response_code, {"Content-Type" => "application/json"}, json_text] + def text_response(response_code, text) + [response_code, {"Content-Type" => "text/plain"}, text] + end + + # 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 @@ -192,10 +268,11 @@ module ChefZero 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" end - "#{base_uri}/#{rest_path[2..-1].join('/')}" - 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) @@ -205,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 36a7d0a..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 @@ -24,6 +27,14 @@ module ChefZero @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 @@ -36,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 3c4b502..8867f37 100644 --- a/lib/chef_zero/rspec.rb +++ b/lib/chef_zero/rspec.rb @@ -4,71 +4,86 @@ require 'chef_zero/rest_request' module ChefZero module RSpec - def self.server - @server - end - def self.server=(value) - @server = value - end - def self.client_key - @client_key - end - def self.client_key=(value) - @client_key = value - end - def self.request_log - @request_log ||= [] - end - def self.clear_request_log - @request_log = [] + module RSpecClassMethods + attr_accessor :server + attr_accessor :client_key + attr_reader :request_log + + def clear_request_log + @request_log = [] + end + + def set_server_options(chef_server_options) + if server && chef_server_options != server.options + server.stop + self.server = nil + end + + unless server + # TODO: can this be logged easily? + # pp :zero_opts => chef_server_options + + # Set up configuration so that clients will point to the server + self.server = ChefZero::Server.new(chef_server_options) + self.client_key = Tempfile.new(['chef_zero_client_key', '.pem']) + client_key.write(ChefZero::PRIVATE_KEY) + client_key.close + # Start the server + server.start_background + server.on_response do |request, response| + request_log << [ request, response ] + end + else + server.clear_data + end + clear_request_log + end end + extend RSpecClassMethods def when_the_chef_server(description, *tags, &block) - if tags.last.is_a?(Hash) - opts = tags.last - else - opts = {} - end context "When the Chef server #{description}", *tags do - before :each do + extend WhenTheChefServerClassMethods + include WhenTheChefServerInstanceMethods - default_opts = {:port => 8900, :signals => false, :log_requests => true} - server_opts = if self.respond_to?(:chef_zero_opts) - default_opts.merge(chef_zero_opts) - else - default_opts + # Take the passed-in options + + 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 - server_opts = server_opts.merge(opts) + } + + # Merge in chef_server_options from let(:chef_server_options) + def 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 - if ChefZero::RSpec.server && server_opts != ChefZero::RSpec.server.options - ChefZero::RSpec.server.stop - ChefZero::RSpec.server = nil + before chef_server_options[:server_scope] do + 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}") - unless ChefZero::RSpec.server - # TODO: can this be logged easily? - # pp :zero_opts => server_opts - - # Set up configuration so that clients will point to the server - ChefZero::RSpec.server = ChefZero::Server.new(server_opts) - ChefZero::RSpec.client_key = Tempfile.new(['chef_zero_client_key', '.pem']) - ChefZero::RSpec.client_key.write(ChefZero::PRIVATE_KEY) - ChefZero::RSpec.client_key.close - # Start the server - ChefZero::RSpec.server.start_background - ChefZero::RSpec.server.on_response do |request, response| - ChefZero::RSpec.request_log << [ request, response ] - end - else - ChefZero::RSpec.server.clear_data + ChefZero::RSpec.set_server_options(chef_server_options) + + if chef_server_options[:organization] + organization chef_server_options[:organization] end - ChefZero::RSpec.clear_request_log 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 - Chef::Config.chef_server_url = ChefZero::RSpec.server.url + if chef_server_options[:organization] + Chef::Config.chef_server_url = "#{ChefZero::RSpec.server.url}/organizations/#{chef_server_options[:organization]}" + else + Chef::Config.chef_server_url = ChefZero::RSpec.server.url + end Chef::Config.node_name = 'admin' Chef::Config.client_key = ChefZero::RSpec.client_key.path Chef::Config.http_retry_count = 0 @@ -76,224 +91,261 @@ module ChefZero end if defined?(Chef::Config) - after :each do + 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 end end - def self.organization(name, org = '{}', &block) - before(:each) { organization(name, org, &block) } - end + instance_eval(&block) + end + end - def organization(name, org = '{}', &block) - ChefZero::RSpec.server.data_store.set([ 'organizations', name, 'org' ], dejsonize(org), :create_dir, :create) - prev_org_name = @current_org - @current_org = name - prev_object_path = @current_object_path - @current_object_path = "organizations/#{name}" - if block_given? - begin - instance_eval(&block) - ensure - @current_org = prev_org_name - @current_object_path = prev_object_path - end - end - end + module WhenTheChefServerClassMethods + def organization(name, org = '{}', &block) + before(chef_server_options[:server_scope]) { organization(name, org, &block) } + end - def self.acl_for(path, data) - before(:each) { acl_for(path, data) } - end + def acl_for(path, data) + before(chef_server_options[:server_scope]) { acl_for(path, data) } + end - def acl_for(path, data) - ChefZero::RSpec.server.load_data({ 'acls' => { path => data } }, current_org) - end + def client(name, data, &block) + before(chef_server_options[:server_scope]) { client(name, data, &block) } + end - def acl(data) - acl_for(@current_object_path, data) - end + def container(name, data, &block) + before(chef_server_options[:server_scope]) { container(name, data, &block) } + end - def self.client(name, data, &block) - before(:each) { client(name, data, &block) } + def cookbook(name, version, data = {}, options = {}, &block) + before(chef_server_options[:server_scope]) do + cookbook(name, version, data, &block) end + end - def client(name, data, &block) - with_object_path("clients/#{name}") do - ChefZero::RSpec.server.load_data({ 'clients' => { name => data } }, current_org) - instance_eval(&block) if block_given? - end - end + def cookbook_artifact(name, identifier, data = {}, &block) + before(chef_server_options[:server_scope]) { cookbook_artifact(name, identifier, data, &block) } + end - def self.container(name, data, &block) - before(:each) { container(name, data, &block) } - end + def data_bag(name, data, &block) + before(chef_server_options[:server_scope]) { data_bag(name, data, &block) } + end - def container(name, data, &block) - with_object_path("containers/#{name}") do - ChefZero::RSpec.server.load_data({ 'containers' => { name => data } }, current_org) - instance_eval(&block) if block_given? - end - end + def environment(name, data, &block) + before(chef_server_options[:server_scope]) { environment(name, data, &block) } + end - def self.cookbook(name, version, data = {}, options = {}, &block) - before(:each) do - cookbook(name, version, data, &block) - end - end + def group(name, data, &block) + before(chef_server_options[:server_scope]) { group(name, data, &block) } + end - def cookbook(name, version, data = {}, options = {}, &block) - with_object_path("cookbooks/#{name}") do - if data.has_key?('metadata.rb') - if data['metadata.rb'].nil? - data.delete('metadata.rb') - end - else - data['metadata.rb'] = "name #{name.inspect}; version #{version.inspect}" - end - ChefZero::RSpec.server.load_data({ 'cookbooks' => { "#{name}-#{version}" => data.merge(options) }}, current_org) - instance_eval(&block) if block_given? - end - end + def node(name, data, &block) + before(chef_server_options[:server_scope]) { node(name, data, &block) } + end - def self.data_bag(name, data, &block) - before(:each) { data_bag(name, data, &block) } - end + def org_invite(*usernames) + before(chef_server_options[:server_scope]) { org_invite(*usernames) } + end - def data_bag(name, data, &block) - with_object_path("data/#{name}") do - ChefZero::RSpec.server.load_data({ 'data' => { name => data }}, current_org) - instance_eval(&block) if block_given? - end - end + def org_member(*usernames) + before(chef_server_options[:server_scope]) { org_member(*usernames) } + end - def self.environment(name, data, &block) - before(:each) { environment(name, data, &block) } - end + def policy(name, data, &block) + before(chef_server_options[:server_scope]) { policy(name, data, &block) } + end - def environment(name, data, &block) - with_object_path("environments/#{name}") do - ChefZero::RSpec.server.load_data({ 'environments' => { name => data } }, current_org) - instance_eval(&block) if block_given? - end - end + def policy_group(name, data, &block) + before(chef_server_options[:server_scope]) { policy_group(name, data, &block) } + end - def self.group(name, data, &block) - before(:each) { group(name, data, &block) } - end + def role(name, data, &block) + before(chef_server_options[:server_scope]) { role(name, data, &block) } + end - def group(name, data, &block) - with_object_path("groups/#{name}") do - ChefZero::RSpec.server.load_data({ 'groups' => { name => data } }, current_org) - instance_eval(&block) if block_given? - end - end + def sandbox(name, data, &block) + before(chef_server_options[:server_scope]) { sandbox(name, data, &block) } + end - def self.node(name, data, &block) - before(:each) { node(name, data, &block) } - end + def user(name, data, &block) + before(chef_server_options[:server_scope]) { user(name, data, &block) } + end + end - def node(name, data, &block) - with_object_path("nodes/#{name}") do - ChefZero::RSpec.server.load_data({ 'nodes' => { name => data } }, current_org) - instance_eval(&block) if block_given? + module WhenTheChefServerInstanceMethods + def organization(name, org = '{}', &block) + ChefZero::RSpec.server.data_store.set([ 'organizations', name, 'org' ], dejsonize(org), :create_dir, :create) + prev_org_name = @current_org + @current_org = name + prev_object_path = @current_object_path + @current_object_path = "organizations/#{name}" + if block_given? + begin + instance_eval(&block) + ensure + @current_org = prev_org_name + @current_object_path = prev_object_path end end + end - def self.org_invite(*usernames) - before(:each) { org_invite(*usernames) } + def acl_for(path, data) + ChefZero::RSpec.server.load_data({ 'acls' => { path => data } }, current_org) + end + + def acl(data) + acl_for(@current_object_path, data) + end + + def client(name, data, &block) + with_object_path("clients/#{name}") do + ChefZero::RSpec.server.load_data({ 'clients' => { name => data } }, current_org) + instance_eval(&block) if block_given? end + end - def org_invite(*usernames) - ChefZero::RSpec.server.load_data({ 'invites' => usernames }, current_org) + def container(name, data, &block) + with_object_path("containers/#{name}") do + ChefZero::RSpec.server.load_data({ 'containers' => { name => data } }, current_org) + instance_eval(&block) if block_given? end + end - def self.org_member(*usernames) - before(:each) { org_member(*usernames) } + 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') + end + else + data['metadata.rb'] = "name #{name.inspect}; version #{version.inspect}" + end + ChefZero::RSpec.server.load_data({ 'cookbooks' => { "#{name}-#{version}" => data.merge(options) }}, current_org) + instance_eval(&block) if block_given? end + end - def org_member(*usernames) - ChefZero::RSpec.server.load_data({ 'members' => usernames }, current_org) + 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 self.role(name, data, &block) - before(:each) { role(name, data, &block) } + def data_bag(name, data, &block) + with_object_path("data/#{name}") do + ChefZero::RSpec.server.load_data({ 'data' => { 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) - instance_eval(&block) if block_given? - end + def environment(name, data, &block) + with_object_path("environments/#{name}") do + ChefZero::RSpec.server.load_data({ 'environments' => { name => data } }, current_org) + instance_eval(&block) if block_given? end + end - def self.sandbox(name, data, &block) - before(:each) { sandbox(name, data, &block) } + def group(name, data, &block) + with_object_path("groups/#{name}") do + ChefZero::RSpec.server.load_data({ 'groups' => { name => data } }, current_org) + instance_eval(&block) if block_given? end + end - def sandbox(name, data, &block) - with_object_path("sandboxes/#{name}") do - ChefZero::RSpec.server.load_data({ 'sandboxes' => { name => data } }, current_org) - instance_eval(&block) if block_given? - end + def node(name, data, &block) + with_object_path("nodes/#{name}") do + ChefZero::RSpec.server.load_data({ 'nodes' => { name => data } }, current_org) + instance_eval(&block) if block_given? end + end + + def org_invite(*usernames) + ChefZero::RSpec.server.load_data({ 'invites' => usernames }, current_org) + end + + def org_member(*usernames) + ChefZero::RSpec.server.load_data({ 'members' => usernames }, current_org) + end - def self.user(name, data, &block) - before(:each) { user(name, data, &block) } + 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 user(name, data, &block) - if ChefZero::RSpec.server.options[:osc_compat] - with_object_path("users/#{name}") do - ChefZero::RSpec.server.load_data({ 'users' => { name => data }}, current_org) - instance_eval(&block) if block_given? - end - else - old_object_path = @current_object_path - @current_object_path = "users/#{name}" - begin - ChefZero::RSpec.server.load_data({ 'users' => { name => data }}, current_org) - instance_eval(&block) if block_given? - ensure - @current_object_path = old_object_path - 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 dejsonize(data) - if data.is_a?(String) - data - else - FFI_Yajl::Encoder.encode(data, :pretty => true) - end + def role(name, data, &block) + with_object_path("roles/#{name}") do + ChefZero::RSpec.server.load_data({ 'roles' => { name => data } }, current_org) + instance_eval(&block) if block_given? end + end - def current_org - @current_org || ChefZero::RSpec.server.options[:single_org] || nil + def sandbox(name, data, &block) + with_object_path("sandboxes/#{name}") do + ChefZero::RSpec.server.load_data({ 'sandboxes' => { name => data } }, current_org) + instance_eval(&block) if block_given? end + end - def with_object_path(object_path) + def user(name, data, &block) + if ChefZero::RSpec.server.options[:osc_compat] + with_object_path("users/#{name}") do + ChefZero::RSpec.server.load_data({ 'users' => { name => data }}, current_org) + instance_eval(&block) if block_given? + end + else old_object_path = @current_object_path - @current_object_path = object_path + @current_object_path = "users/#{name}" begin - yield if block_given? + ChefZero::RSpec.server.load_data({ 'users' => { name => data }}, current_org) + instance_eval(&block) if block_given? + ensure + @current_object_path = old_object_path end - @current_object_path = old_object_path end + end -# after :each do -# if @@ChefZero::RSpec.server -# @@ChefZero::RSpec.server.stop -# @@ChefZero::RSpec.server = nil -# end -# if @@ChefZero::RSpec.client_key -# @@ChefZero::RSpec.client_key.unlink -# @@ChefZero::RSpec.client_key = nil -# end -# end + def dejsonize(data) + if data.is_a?(String) + data + else + FFI_Yajl::Encoder.encode(data, :pretty => true) + end + end - instance_eval(&block) + def current_org + @current_org || ChefZero::RSpec.server.options[:single_org] || nil + end + + def with_object_path(object_path) + old_object_path = @current_object_path + @current_object_path = object_path + begin + yield if block_given? + end + @current_object_path = old_object_path end end end diff --git a/lib/chef_zero/server.rb b/lib/chef_zero/server.rb index fb2b127..9c92652 100644 --- a/lib/chef_zero/server.rb +++ b/lib/chef_zero/server.rb @@ -27,6 +27,7 @@ require 'webrick' require 'webrick/https' require 'chef_zero' +require 'chef_zero/socketless_server_map' require 'chef_zero/chef_data/cookbook_data' require 'chef_zero/chef_data/acl_path' require 'chef_zero/rest_router' @@ -39,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' @@ -60,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' @@ -68,6 +82,13 @@ 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/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' @@ -82,18 +103,28 @@ require 'chef_zero/endpoints/user_association_request_endpoint' 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 }.freeze + GLOBAL_ENDPOINTS = [ + '/license', + '/version', + '/server_api_version' + ] + def initialize(options = {}) @options = DEFAULT_OPTIONS.merge(options) if @options[:single_org] && !@options.has_key?(:osc_compat) @@ -101,6 +132,7 @@ module ChefZero end @options.freeze ChefZero::Log.level = @options[:log_level].to_sym + @app = nil end # @return [Hash] @@ -110,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 @@ -132,13 +165,20 @@ 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 + def local_mode_url + raise "Port not yet set, cannot generate URL" unless port.kind_of?(Integer) + "chefzero://localhost:#{port}" + end + + # # The data store for this server (default is in-memory). # @@ -217,7 +257,6 @@ module ChefZero thread.join end - # # Start a Chef Zero server in a forked process. This method returns the PID # to the forked process. @@ -228,12 +267,25 @@ 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), :SSLEnable => options[:ssl], + :SSLOptions => ssl_opts, :SSLCertName => [ [ 'CN', WEBrick::Utils::getservername ] ], :StartCallback => proc { @running = true @@ -243,22 +295,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 @@ -277,9 +324,19 @@ module ChefZero sleep(0.01) end + SocketlessServerMap.instance.register_port(@port, self) + @thread end + def start_socketless + @port = SocketlessServerMap.instance.register_no_listen_server(self) + end + + def handle_socketless_request(request_env) + app.call(request_env) + end + # # Boolean method to determine if the server is currently ready to accept # requests. This method will attempt to make an HTTP request against the @@ -308,6 +365,7 @@ module ChefZero if @thread ChefZero::Log.error("Chef Zero did not stop within #{wait} seconds! Killing...") @thread.kill + SocketlessServerMap.deregister(port) end ensure @server = nil @@ -358,7 +416,6 @@ module ChefZero # } # } def load_data(contents, org_name = nil) - passed_org = !!org_name org_name ||= options[:single_org] if org_name.nil? && contents.keys != [ 'users' ] raise "Must pass an org name to load_data or run in single_org mode" @@ -415,20 +472,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 - 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 + + 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 + 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 @@ -453,13 +533,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 @@ -473,11 +553,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) ], @@ -495,14 +577,24 @@ module ChefZero [ "/organizations/*/*/*/_acl/*", AclEndpoint.new(self) ] ] end - result + - [ + 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) ], @@ -514,8 +606,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/*/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) ], @@ -525,14 +625,23 @@ module ChefZero [ "/organizations/*/sandboxes/*", SandboxEndpoint.new(self) ], [ "/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) ] ] end + def global_endpoint?(ep) + GLOBAL_ENDPOINTS.any? do |g_ep| + ep.start_with?(g_ep) + end + end + def app - router = RestRouter.new(open_source_endpoints) + return @app if @app + router = RestRouter.new(endpoints) router.not_found = NotFoundEndpoint.new if options[:single_org] @@ -540,9 +649,11 @@ module ChefZero else rest_base_prefix = [] end - return proc do |env| + @app = proc do |env| begin - request = RestRequest.new(env, rest_base_prefix) + prefix = global_endpoint?(env['PATH_INFO']) ? [] : rest_base_prefix + + request = RestRequest.new(env, prefix) if @on_request_proc @on_request_proc.call(request) end @@ -576,6 +687,7 @@ module ChefZero end end end + @app end def dejsonize_children(hash) @@ -597,5 +709,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/socketless_server_map.rb b/lib/chef_zero/socketless_server_map.rb new file mode 100644 index 0000000..99351a1 --- /dev/null +++ b/lib/chef_zero/socketless_server_map.rb @@ -0,0 +1,92 @@ +# +# Author:: Daniel DeLeo (<dan@chef.io>) +# Copyright:: Copyright (c) 2012 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'thread' +require 'singleton' + +module ChefZero + + class ServerNotFound < StandardError + end + + class NoSocketlessPortAvailable < StandardError + end + + class SocketlessServerMap + + def self.request(port, request_env) + instance.request(port, request_env) + end + + def self.server_on_port(port) + instance.server_on_port(port) + end + + MUTEX = Mutex.new + + include Singleton + + def initialize() + reset! + end + + def reset! + @servers_by_port = {} + end + + def register_port(port, server) + MUTEX.synchronize do + @servers_by_port[port] = server + end + end + + def register_no_listen_server(server) + MUTEX.synchronize do + 1.upto(1000) do |port| + unless @servers_by_port.key?(port) + @servers_by_port[port] = server + return port + end + end + raise NoSocketlessPortAvailable, "No socketless ports left to register" + end + end + + def has_server_on_port?(port) + @servers_by_port.key?(port) + end + + def server_on_port(port) + @servers_by_port[port] + end + + def deregister(port) + MUTEX.synchronize do + @servers_by_port.delete(port) + end + end + + def request(port, request_env) + server = @servers_by_port[port] + raise ServerNotFound, "No socketless chef-zero server on given port #{port.inspect}" unless server + server.handle_socketless_request(request_env) + end + + end +end + diff --git a/lib/chef_zero/version.rb b/lib/chef_zero/version.rb index 785272e..d39f82c 100644 --- a/lib/chef_zero/version.rb +++ b/lib/chef_zero/version.rb @@ -1,3 +1,3 @@ module ChefZero - VERSION = '3.2.1' + VERSION = '4.7.1' end diff --git a/spec/run_oc_pedant.rb b/spec/run_oc_pedant.rb index 562bf6a..d874fb8 100644 --- a/spec/run_oc_pedant.rb +++ b/spec/run_oc_pedant.rb @@ -5,45 +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', + + # 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 3657da8..0000000 --- a/spec/run_pedant.rb +++ /dev/null @@ -1,100 +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-omnibus' - ]) - - 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 d5da281..d46084f 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 @@ -33,6 +34,12 @@ describe ChefZero::Server do httpcall.get('/nodes', 'Accept' => accepts) end + def get_version + uri = URI(@server.url) + httpcall = Net::HTTP.new(uri.host, uri.port) + httpcall.get('/version', 'Accept' => 'text/plain, application/json') + end + it 'accepts requests with no accept header' do request = Net::HTTP::Get.new('/nodes') request.delete('Accept') @@ -77,6 +84,9 @@ describe ChefZero::Server do expect(get_nodes('a/b;a=b;c=d, application/json;a=b, application/xml;a=b').code).to eq '200' end + it 'accepts /version' do + expect(get_version.body.start_with?('chef-zero')).to be true + end end end end diff --git a/spec/socketless_server_map_spec.rb b/spec/socketless_server_map_spec.rb new file mode 100644 index 0000000..0699b54 --- /dev/null +++ b/spec/socketless_server_map_spec.rb @@ -0,0 +1,76 @@ +require 'chef_zero/socketless_server_map' + + +describe "Socketless Mode" do + + let(:server_map) { ChefZero::SocketlessServerMap.instance.tap { |i| i.reset! } } + + let(:server) { instance_double("ChefZero::Server") } + + let(:second_server) { instance_double("ChefZero::Server") } + + it "registers a socketful server" do + server_map.register_port(8889, server) + expect(server_map).to have_server_on_port(8889) + end + + it "retrieves a server by port" do + server_map.register_port(8889, server) + expect(ChefZero::SocketlessServerMap.server_on_port(8889)).to eq(server) + end + + context "when a no-listen server is registered" do + + let!(:port) { server_map.register_no_listen_server(server) } + + it "assigns the server a low port number" do + expect(port).to eq(1) + end + + context "and another server is registered" do + + let!(:next_port) { server_map.register_no_listen_server(second_server) } + + it "assigns another port when another server is registered" do + expect(next_port).to eq(2) + end + + it "raises NoSocketlessPortAvailable when too many servers are registered" do + expect { 1000.times { server_map.register_no_listen_server(server) } }.to raise_error(ChefZero::NoSocketlessPortAvailable) + end + + it "deregisters a server" do + expect(server_map).to have_server_on_port(1) + server_map.deregister(1) + expect(server_map).to_not have_server_on_port(1) + end + + describe "routing requests to a server" do + + let(:rack_req) do + r = {} + r["REQUEST_METHOD"] = "GET" + r["SCRIPT_NAME"] = "" + r["PATH_INFO"] = "/clients" + r["QUERY_STRING"] = "" + r["rack.input"] = StringIO.new("") + r + end + + let(:rack_response) { [200, {}, ["this is the response body"] ] } + + it "routes a request to the registered port" do + expect(server).to receive(:handle_socketless_request).with(rack_req).and_return(rack_response) + response = server_map.request(1, rack_req) + expect(response).to eq(rack_response) + end + + it "raises ServerNotFound when a request is sent to an unregistered port" do + expect { server_map.request(99, rack_req) }.to raise_error(ChefZero::ServerNotFound) + end + end + end + end + + +end 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 d6100d2..0000000 --- a/spec/support/pedant.rb +++ /dev/null @@ -1,125 +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 - -# 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 |