pax_global_header00006660000000000000000000000064136233506300014513gustar00rootroot0000000000000052 comment=e0001e2ebcd9f289799adfbade4a795c14e35f42 cheffish-15.0.1/000077500000000000000000000000001362335063000133565ustar00rootroot00000000000000cheffish-15.0.1/.expeditor/000077500000000000000000000000001362335063000154375ustar00rootroot00000000000000cheffish-15.0.1/.expeditor/config.yml000066400000000000000000000026721362335063000174360ustar00rootroot00000000000000# Documentation available at https://expeditor.chef.io/docs/getting-started/ --- # Slack channel in Chef Software slack to send notifications about build failures, etc slack: notify_channel: chef-infra-notify # This publish is triggered by the `built_in:publish_rubygems` artifact_action. rubygems: - cheffish github: # This deletes the GitHub PR branch after successfully merged into the release branch delete_branch_on_merge: true # The tag format to use (e.g. v1.0.0) version_tag_format: "v{{version}}" # allow bumping the minor release via label minor_bump_labels: - "Expeditor: Bump Version Minor" # allow bumping the major release via label major_bump_labels: - "Expeditor: Bump Version Major" changelog: rollup_header: Changes not yet released to rubygems.org # These actions are taken, in order they are specified, anytime a Pull Request is merged. merge_actions: - built_in:bump_version: ignore_labels: - "Expeditor: Skip Version Bump" - "Expeditor: Skip All" - bash:.expeditor/update_version.sh: only_if: built_in:bump_version - built_in:update_changelog: ignore_labels: - "Expeditor: Skip Changelog" - "Expeditor: Skip All" - built_in:build_gem: only_if: built_in:bump_version promote: actions: - built_in:rollover_changelog - built_in:publish_rubygems pipelines: - verify: description: Pull Request validation tests public: true cheffish-15.0.1/.expeditor/run_linux_tests.sh000077500000000000000000000023451362335063000212470ustar00rootroot00000000000000#!/bin/bash # # This script runs a passed in command, but first setups up the bundler caching on the repo set -ue export USER="root" echo "--- dependencies" export LANG=C.UTF-8 LANGUAGE=C.UTF-8 S3_URL="s3://public-cd-buildkite-cache/${BUILDKITE_PIPELINE_SLUG}/${BUILDKITE_LABEL}" pull_s3_file() { aws s3 cp "${S3_URL}/$1" "$1" || echo "Could not pull $1 from S3" } push_s3_file() { if [ -f "$1" ]; then aws s3 cp "$1" "${S3_URL}/$1" || echo "Could not push $1 to S3 for caching." fi } apt-get update -y apt-get install awscli -y echo "--- bundle install" pull_s3_file "bundle.tar.gz" pull_s3_file "bundle.sha256" if [ -f bundle.tar.gz ]; then tar -xzf bundle.tar.gz fi if [ -n "${RESET_BUNDLE_CACHE:-}" ]; then rm bundle.sha256 fi bundle config --local path vendor/bundle bundle install --jobs=7 --retry=3 echo "--- bundle cache" if test -f bundle.sha256 && shasum --check bundle.sha256 --status; then echo "Bundled gems have not changed. Skipping upload to s3" else echo "Bundled gems have changed. Uploading to s3" shasum -a 256 Gemfile.lock > bundle.sha256 tar -czf bundle.tar.gz vendor/ push_s3_file bundle.tar.gz push_s3_file bundle.sha256 fi echo "+++ bundle exec task" bundle exec $@ cheffish-15.0.1/.expeditor/update_version.sh000077500000000000000000000007021362335063000210240ustar00rootroot00000000000000#!/bin/sh # # After a PR merge, Chef Expeditor will bump the PATCH version in the VERSION file. # It then executes this file to update any other files/components with that new version. # set -evx sed -i -r "s/^(\s*)VERSION = \".+\"/\1VERSION = \"$(cat VERSION)\"/" lib/cheffish/version.rb # Once Expeditor finshes executing this script, it will commit the changes and push # the commit as a new tag corresponding to the value in the VERSION file. cheffish-15.0.1/.expeditor/verify.pipeline.yml000066400000000000000000000011221362335063000212660ustar00rootroot00000000000000--- expeditor: defaults: buildkite: retry: automatic: limit: 1 timeout_in_minutes: 30 steps: - label: lint-chefstyle command: - .expeditor/run_linux_tests.sh "rake style" expeditor: executor: docker: image: ruby:2.6-buster - label: run-specs-ruby-2.6 command: - .expeditor/run_linux_tests.sh rspec expeditor: executor: docker: image: ruby:2.6-buster - label: run-specs-ruby-2.7 command: - .expeditor/run_linux_tests.sh rspec expeditor: executor: docker: image: ruby:2.7-buster cheffish-15.0.1/.github/000077500000000000000000000000001362335063000147165ustar00rootroot00000000000000cheffish-15.0.1/.github/CODEOWNERS000066400000000000000000000002721362335063000163120ustar00rootroot00000000000000# Order is important. The last matching pattern has the most precedence. * @chef/chef-infra-reviewers .expeditor/** @chef/jex-team *.md @chef/docs-team cheffish-15.0.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001362335063000171015ustar00rootroot00000000000000cheffish-15.0.1/.github/ISSUE_TEMPLATE/BUG_TEMPLATE.md000066400000000000000000000011651362335063000213360ustar00rootroot00000000000000--- name: � Bug Report about: If something isn't working as expected �. labels: "Status: Untriaged, Type: Bug" --- # Version: [Version of the project installed] # Environment: [Details about the environment such as the Operating System, cookbook details, etc...] # Scenario: [What you are trying to achieve and you can't?] # Steps to Reproduce: [If you are filing an issue what are the things we need to do in order to repro your problem?] # Expected Result: [What are you expecting to happen as the consequence of above reproduction steps?] # Actual Result: [What actually happens after the reproduction steps?] cheffish-15.0.1/.github/ISSUE_TEMPLATE/DESIGN_PROPOSAL.md000066400000000000000000000023721362335063000217170ustar00rootroot00000000000000--- name: Design Proposal about: I have a significant change I would like to propose and discuss before starting labels: "Status: Untriaged, Type: Design Proposal" --- ### When a Change Needs a Design Proposal A design proposal should be opened any time a change meets one of the following qualifications: - Significantly changes the user experience of a project in a way that impacts users. - Significantly changes the underlying architecture of the project in a way that impacts other developers. - Changes the development or testing process of the project such as a change of CI systems or test frameworks. ### Why We Use This Process - Allows all interested parties (including any community member) to discuss large impact changes to a project. - Serves as a durable paper trail for discussions regarding project architecture. - Forces design discussions to occur before PRs are created. - Reduces PR refactoring and rejected PRs. --- ## Motivation ## Specification ## Downstream Impact cheffish-15.0.1/.github/ISSUE_TEMPLATE/ENHANCEMENT_REQUEST_TEMPLATE.md000066400000000000000000000014041362335063000236520ustar00rootroot00000000000000--- name: 🚀 Enhancement Request about: I have a suggestion (and may want to implement it 🙂)! labels: "Status: Untriaged" --- ### Describe the Enhancement ### Describe the Need ### Current Alternative ### Can We Help You Implement This? cheffish-15.0.1/.github/ISSUE_TEMPLATE/SUPPORT_QUESTION.md000066400000000000000000000006621362335063000221320ustar00rootroot00000000000000--- name: 🤗 Support Question about: If you have a question 💬, please check out our Slack! --- We use GitHub issues to track bugs and feature requests. If you need help please post to our Mailing List or join the Chef Community Slack. * Chef Community Slack at * Chef Mailing List Support issues opened here will be closed and redirected to Slack or Discourse. cheffish-15.0.1/.gitignore000066400000000000000000000000741362335063000153470ustar00rootroot00000000000000Gemfile.lock pkg binstubs/ chef-client-version.gemfile.lock cheffish-15.0.1/.rspec000066400000000000000000000000311362335063000144650ustar00rootroot00000000000000-f documentation --color cheffish-15.0.1/.rubocop.yml000066400000000000000000000000411362335063000156230ustar00rootroot00000000000000AllCops: TargetRubyVersion: 2.6cheffish-15.0.1/CHANGELOG.md000066400000000000000000000521241362335063000151730ustar00rootroot00000000000000# Cheffish Change Log ## [v15.0.1](https://github.com/chef/cheffish/tree/v15.0.1) (2020-02-20) #### Merged Pull Requests - Update testing and fix Chefstyle warnings [#154](https://github.com/chef/cheffish/pull/154) ([tas50](https://github.com/tas50)) ### Changes not yet released to rubygems.org #### Merged Pull Requests - Update testing and fix Chefstyle warnings [#154](https://github.com/chef/cheffish/pull/154) ([tas50](https://github.com/tas50)) ## [v15.0.0](https://github.com/chef/cheffish/tree/v15.0.0) (2020-02-20) #### Merged Pull Requests - dropping ruby versions and bumping deps [#155](https://github.com/chef/cheffish/pull/155) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v14.0.13](https://github.com/chef/cheffish/tree/v14.0.13) (2019-12-20) #### Merged Pull Requests - Resolve chefstyle warnings and test on Ruby 2.6 [#141](https://github.com/chef/cheffish/pull/141) ([tas50](https://github.com/tas50)) - Add empty? to Cheffish::MergedConfig [#134](https://github.com/chef/cheffish/pull/134) ([josh-barker](https://github.com/josh-barker)) - Update travis testing to Ruby 2.6.1 [#145](https://github.com/chef/cheffish/pull/145) ([tas50](https://github.com/tas50)) - Set the author of the gem to Chef Software [#143](https://github.com/chef/cheffish/pull/143) ([tas50](https://github.com/tas50)) - Remove the Boolean magic type [#144](https://github.com/chef/cheffish/pull/144) ([tas50](https://github.com/tas50)) - Add build kite verify config [#147](https://github.com/chef/cheffish/pull/147) ([tas50](https://github.com/tas50)) - Add github templates [#148](https://github.com/chef/cheffish/pull/148) ([tas50](https://github.com/tas50)) - Update ownership for the new OSS Guidelines [#150](https://github.com/chef/cheffish/pull/150) ([tas50](https://github.com/tas50)) - Substitute require for require_relative [#153](https://github.com/chef/cheffish/pull/153) ([tas50](https://github.com/tas50)) ## [v14.0.4](https://github.com/chef/cheffish/tree/v14.0.4) (2018-11-14) #### Merged Pull Requests - Test on the latest Ruby releases in Travis [#138](https://github.com/chef/cheffish/pull/138) ([tas50](https://github.com/tas50)) - Skip shipping the readme in the gem + add a license [#139](https://github.com/chef/cheffish/pull/139) ([tas50](https://github.com/tas50)) - private_key: Fix cipher comparison to ignore case [#137](https://github.com/chef/cheffish/pull/137) ([tas50](https://github.com/tas50)) ## [v13.1.0](https://github.com/chef/cheffish/tree/v13.1.0) (2017-10-23) [Full Changelog](https://github.com/chef/cheffish/compare/v13.0.0...v13.1.0) **Merged pull requests:** - fix nested merge configs [\#130](https://github.com/chef/cheffish/pull/130) ([lamont-granquist](https://github.com/lamont-granquist)) - deprecate method\_missing in cheffish merged\_config [\#129](https://github.com/chef/cheffish/pull/129) ([lamont-granquist](https://github.com/lamont-granquist)) - test on recent rubies, and chefstyle [\#128](https://github.com/chef/cheffish/pull/128) ([thommay](https://github.com/thommay)) - remove Chef::Mash [\#127](https://github.com/chef/cheffish/pull/127) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v13.0.0](https://github.com/chef/cheffish/tree/v13.0.0) (2017-04-03) [Full Changelog](https://github.com/chef/cheffish/compare/v5.0.1...v13.0.0) **Merged pull requests:** - Release Cheffish 13 [\#124](https://github.com/chef/cheffish/pull/124) ([thommay](https://github.com/thommay)) ## [v5.0.1](https://github.com/chef/cheffish/tree/v5.0.1) (2017-03-07) [Full Changelog](https://github.com/chef/cheffish/compare/v5.0.0...v5.0.1) **Merged pull requests:** - Remove a couple of deprecated dependencies [\#120](https://github.com/chef/cheffish/pull/120) ([thommay](https://github.com/thommay)) ## [v5.0.0](https://github.com/chef/cheffish/tree/v5.0.0) (2017-02-08) [Full Changelog](https://github.com/chef/cheffish/compare/v4.1.1...v5.0.0) **Merged pull requests:** - MergedConfig: Use Chef::Mash for storage, add tests [\#119](https://github.com/chef/cheffish/pull/119) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v4.1.1](https://github.com/chef/cheffish/tree/v4.1.1) (2017-02-08) [Full Changelog](https://github.com/chef/cheffish/compare/v4.1.0...v4.1.1) ## [v4.1.0](https://github.com/chef/cheffish/tree/v4.1.0) (2017-01-18) [Full Changelog](https://github.com/chef/cheffish/compare/v4.0.0...v4.1.0) **Merged pull requests:** - Ready 4.1.0 release [\#118](https://github.com/chef/cheffish/pull/118) ([thommay](https://github.com/thommay)) - fix travis matrix and chefstyle [\#117](https://github.com/chef/cheffish/pull/117) ([lamont-granquist](https://github.com/lamont-granquist)) - Avoid redefining the Chef::Resource\#name method [\#116](https://github.com/chef/cheffish/pull/116) ([adamleff](https://github.com/adamleff)) - MergedConfig: Use Chef::Mash as storage [\#115](https://github.com/chef/cheffish/pull/115) ([elthariel](https://github.com/elthariel)) - Override to\_h to call to\_hash method. [\#79](https://github.com/chef/cheffish/pull/79) ([mikenairn](https://github.com/mikenairn)) ## [v4.0.0](https://github.com/chef/cheffish/tree/v4.0.0) (2016-08-25) [Full Changelog](https://github.com/chef/cheffish/compare/v3.0.1...v4.0.0) **Merged pull requests:** - Bump version to 4.0.0 [\#113](https://github.com/chef/cheffish/pull/113) ([jkeiser](https://github.com/jkeiser)) - Restrict to Ruby 2.1 and bring in chef-zero 5.0 [\#112](https://github.com/chef/cheffish/pull/112) ([jkeiser](https://github.com/jkeiser)) ## [v3.0.1](https://github.com/chef/cheffish/tree/v3.0.1) (2016-08-24) [Full Changelog](https://github.com/chef/cheffish/compare/v3.0.0...v3.0.1) **Merged pull requests:** - Bump version to 3.0.1 [\#111](https://github.com/chef/cheffish/pull/111) ([jkeiser](https://github.com/jkeiser)) - Fix JSON rendering for arrays [\#110](https://github.com/chef/cheffish/pull/110) ([jkeiser](https://github.com/jkeiser)) - fix travis matrix [\#108](https://github.com/chef/cheffish/pull/108) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v3.0.0](https://github.com/chef/cheffish/tree/v3.0.0) (2016-08-11) [Full Changelog](https://github.com/chef/cheffish/compare/v2.0.5...v3.0.0) **Merged pull requests:** - Drop support for Chef \< 12.6; remove compat\_resource dependency [\#100](https://github.com/chef/cheffish/pull/100) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v2.0.5](https://github.com/chef/cheffish/tree/v2.0.5) (2016-08-03) [Full Changelog](https://github.com/chef/cheffish/compare/v2.0.4...v2.0.5) **Merged pull requests:** - Cleaning up some provisioning warnings [\#107](https://github.com/chef/cheffish/pull/107) ([tyler-ball](https://github.com/tyler-ball)) ## [v2.0.4](https://github.com/chef/cheffish/tree/v2.0.4) (2016-04-14) [Full Changelog](https://github.com/chef/cheffish/compare/v2.0.3...v2.0.4) **Merged pull requests:** - Use server\_api: 0 for cheffish spec requests \(since the software does… [\#104](https://github.com/chef/cheffish/pull/104) ([jkeiser](https://github.com/jkeiser)) ## [v2.0.3](https://github.com/chef/cheffish/tree/v2.0.3) (2016-03-31) [Full Changelog](https://github.com/chef/cheffish/compare/v2.0.2...v2.0.3) **Merged pull requests:** - travis fixes, no github-changelog-generator [\#102](https://github.com/chef/cheffish/pull/102) ([lamont-granquist](https://github.com/lamont-granquist)) - fix object model insanity [\#101](https://github.com/chef/cheffish/pull/101) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v2.0.2](https://github.com/chef/cheffish/tree/v2.0.2) (2016-02-23) [Full Changelog](https://github.com/chef/cheffish/compare/v2.0.1...v2.0.2) **Merged pull requests:** - Fix the data\_bag property extraction [\#99](https://github.com/chef/cheffish/pull/99) ([afiune](https://github.com/afiune)) ## [v2.0.1](https://github.com/chef/cheffish/tree/v2.0.1) (2016-01-29) [Full Changelog](https://github.com/chef/cheffish/compare/v2.0.0...v2.0.1) **Merged pull requests:** - Make NodeProperties work with chef-provisioning [\#97](https://github.com/chef/cheffish/pull/97) ([jkeiser](https://github.com/jkeiser)) ## [v2.0.0](https://github.com/chef/cheffish/tree/v2.0.0) (2016-01-28) [Full Changelog](https://github.com/chef/cheffish/compare/v1.6.0...v2.0.0) **Merged pull requests:** - Prep for release 2.0.0 [\#96](https://github.com/chef/cheffish/pull/96) ([chefsalim](https://github.com/chefsalim)) - Move provider code into resources [\#93](https://github.com/chef/cheffish/pull/93) ([jkeiser](https://github.com/jkeiser)) - Support arbitrarily named private keys [\#87](https://github.com/chef/cheffish/pull/87) ([hfinucane](https://github.com/hfinucane)) - Convert to basic 12.5 resources [\#82](https://github.com/chef/cheffish/pull/82) ([jkeiser](https://github.com/jkeiser)) ## [v1.6.0](https://github.com/chef/cheffish/tree/v1.6.0) (2015-10-15) [Full Changelog](https://github.com/chef/cheffish/compare/v1.5.0...v1.6.0) **Merged pull requests:** - proxying to\_h to to\_hash [\#88](https://github.com/chef/cheffish/pull/88) ([tyler-ball](https://github.com/tyler-ball)) - Add gemspec files to allow bundler to run from the gem [\#86](https://github.com/chef/cheffish/pull/86) ([ksubrama](https://github.com/ksubrama)) - Pull in a non-12.4.0 version of Chef in travis [\#81](https://github.com/chef/cheffish/pull/81) ([jkeiser](https://github.com/jkeiser)) - Ship the Gemfile so people can run our tests [\#80](https://github.com/chef/cheffish/pull/80) ([jkeiser](https://github.com/jkeiser)) - Fixing the issue of updataing acls unnecessarily [\#76](https://github.com/chef/cheffish/pull/76) ([ckaushik](https://github.com/ckaushik)) ## [v1.5.0](https://github.com/chef/cheffish/tree/v1.5.0) (2015-09-16) [Full Changelog](https://github.com/chef/cheffish/compare/v1.4.2...v1.5.0) **Merged pull requests:** - Make cheffish not depend on the chef gem. [\#74](https://github.com/chef/cheffish/pull/74) ([ksubrama](https://github.com/ksubrama)) ## [v1.4.2](https://github.com/chef/cheffish/tree/v1.4.2) (2015-09-04) [Full Changelog](https://github.com/chef/cheffish/compare/v1.4.1...v1.4.2) **Merged pull requests:** - Raise an error if we can't find \*either\* let variable \*or\* resource m… [\#73](https://github.com/chef/cheffish/pull/73) ([jkeiser](https://github.com/jkeiser)) ## [v1.4.1](https://github.com/chef/cheffish/tree/v1.4.1) (2015-09-04) [Full Changelog](https://github.com/chef/cheffish/compare/v1.4.0...v1.4.1) **Merged pull requests:** - Trim the matrix combos in Travis; don't notify Slack on every success [\#72](https://github.com/chef/cheffish/pull/72) ([jkeiser](https://github.com/jkeiser)) - De-float versions and rely on latest released [\#71](https://github.com/chef/cheffish/pull/71) ([jkeiser](https://github.com/jkeiser)) ## [v1.4.0](https://github.com/chef/cheffish/tree/v1.4.0) (2015-09-02) [Full Changelog](https://github.com/chef/cheffish/compare/v1.3.1...v1.4.0) **Merged pull requests:** - Modified Resources and Providers creation [\#70](https://github.com/chef/cheffish/pull/70) ([afiune](https://github.com/afiune)) - A handful of fixes to make the Cheffish matrix pass on all the relevant Chef versions. [\#68](https://github.com/chef/cheffish/pull/68) ([randomcamel](https://github.com/randomcamel)) - cdoherty's travis matrix with 12.3.0 fixes [\#64](https://github.com/chef/cheffish/pull/64) ([jkeiser](https://github.com/jkeiser)) - Add provides statements to avoid chef-client warnings [\#60](https://github.com/chef/cheffish/pull/60) ([stevendanna](https://github.com/stevendanna)) - Fix sense typos in test names. [\#59](https://github.com/chef/cheffish/pull/59) ([randomcamel](https://github.com/randomcamel)) - Use correct user-association endpoint for Chef 12 [\#50](https://github.com/chef/cheffish/pull/50) ([stevendanna](https://github.com/stevendanna)) ## [v1.3.1](https://github.com/chef/cheffish/tree/v1.3.1) (2015-08-05) [Full Changelog](https://github.com/chef/cheffish/compare/v1.3.0...v1.3.1) **Merged pull requests:** - Pin chef server api version to 0, fixes \#56 [\#57](https://github.com/chef/cheffish/pull/57) ([sersut](https://github.com/sersut)) ## [v1.3.0](https://github.com/chef/cheffish/tree/v1.3.0) (2015-07-29) [Full Changelog](https://github.com/chef/cheffish/compare/v1.2.1...v1.3.0) ## [v1.2.1](https://github.com/chef/cheffish/tree/v1.2.1) (2015-07-17) [Full Changelog](https://github.com/chef/cheffish/compare/v1.2...v1.2.1) **Merged pull requests:** - avoid nilerror [\#55](https://github.com/chef/cheffish/pull/55) ([lamont-granquist](https://github.com/lamont-granquist)) - Rewrite README.md [\#51](https://github.com/chef/cheffish/pull/51) ([randomcamel](https://github.com/randomcamel)) ## [v1.2](https://github.com/chef/cheffish/tree/v1.2) (2015-05-02) [Full Changelog](https://github.com/chef/cheffish/compare/v1.1.2...v1.2) **Merged pull requests:** - Add .logged\_warnings/errors/info to ChefRun; add emit\_no\_warnings\_or\_err... [\#47](https://github.com/chef/cheffish/pull/47) ([jkeiser](https://github.com/jkeiser)) - Use expect\_recipe universally in Cheffish, use generic rspec matchers instead of update\_acl [\#44](https://github.com/chef/cheffish/pull/44) ([jkeiser](https://github.com/jkeiser)) ## [v1.1.2](https://github.com/chef/cheffish/tree/v1.1.2) (2015-04-08) [Full Changelog](https://github.com/chef/cheffish/compare/v1.1.1...v1.1.2) **Merged pull requests:** - Fixing bug @patrick-wright discovered in debug log [\#45](https://github.com/chef/cheffish/pull/45) ([tyler-ball](https://github.com/tyler-ball)) ## [v1.1.1](https://github.com/chef/cheffish/tree/v1.1.1) (2015-04-07) [Full Changelog](https://github.com/chef/cheffish/compare/v1.1.0...v1.1.1) ## [v1.1.0](https://github.com/chef/cheffish/tree/v1.1.0) (2015-04-07) [Full Changelog](https://github.com/chef/cheffish/compare/v1.0.0...v1.1.0) **Merged pull requests:** - Add encapsulated Chef runs that capture and stream output [\#43](https://github.com/chef/cheffish/pull/43) ([jkeiser](https://github.com/jkeiser)) ## [v1.0.0](https://github.com/chef/cheffish/tree/v1.0.0) (2015-04-02) [Full Changelog](https://github.com/chef/cheffish/compare/v1.0.0.rc.1...v1.0.0) ## [v1.0.0.rc.1](https://github.com/chef/cheffish/tree/v1.0.0.rc.1) (2015-04-01) [Full Changelog](https://github.com/chef/cheffish/compare/v0.10...v1.0.0.rc.1) ## [v0.10](https://github.com/chef/cheffish/tree/v0.10) (2015-03-17) [Full Changelog](https://github.com/chef/cheffish/compare/v0.9.2...v0.10) **Merged pull requests:** - Allow node attributes to be used as input to new\_json / augment\_new\_json \(fixes chef/chef-provisioning\#21\) [\#42](https://github.com/chef/cheffish/pull/42) ([jkeiser](https://github.com/jkeiser)) - Extracting the spec helper to its own class in lib so we can leverage it in chef-provisioning for testing there [\#41](https://github.com/chef/cheffish/pull/41) ([tyler-ball](https://github.com/tyler-ball)) - Update metadata.rb [\#39](https://github.com/chef/cheffish/pull/39) ([oker1](https://github.com/oker1)) ## [v0.9.2](https://github.com/chef/cheffish/tree/v0.9.2) (2015-01-27) [Full Changelog](https://github.com/chef/cheffish/compare/v0.9.1...v0.9.2) **Merged pull requests:** - Use appropriate function call for chef 12 [\#33](https://github.com/chef/cheffish/pull/33) ([elliott-davis](https://github.com/elliott-davis)) ## [v0.9.1](https://github.com/chef/cheffish/tree/v0.9.1) (2015-01-16) [Full Changelog](https://github.com/chef/cheffish/compare/v0.9...v0.9.1) **Merged pull requests:** - Fix remove\_role calling self.role instead of referencing block variable. [\#31](https://github.com/chef/cheffish/pull/31) ([causton81](https://github.com/causton81)) - Add user full name to chef\_user [\#27](https://github.com/chef/cheffish/pull/27) ([charlesjohnson](https://github.com/charlesjohnson)) - Fix ChefMirror referencing chef\_server :client\_key [\#23](https://github.com/chef/cheffish/pull/23) ([johnbellone](https://github.com/johnbellone)) ## [v0.9](https://github.com/chef/cheffish/tree/v0.9) (2014-11-05) [Full Changelog](https://github.com/chef/cheffish/compare/v0.8.4...v0.9) **Merged pull requests:** - Work with chef12 [\#29](https://github.com/chef/cheffish/pull/29) ([jkeiser](https://github.com/jkeiser)) ## [v0.8.4](https://github.com/chef/cheffish/tree/v0.8.4) (2014-11-04) [Full Changelog](https://github.com/chef/cheffish/compare/v0.8.3...v0.8.4) ## [v0.8.3](https://github.com/chef/cheffish/tree/v0.8.3) (2014-09-26) [Full Changelog](https://github.com/chef/cheffish/compare/v0.8.2...v0.8.3) **Merged pull requests:** - Honor settings in recipes [\#25](https://github.com/chef/cheffish/pull/25) ([johnewart](https://github.com/johnewart)) ## [v0.8.2](https://github.com/chef/cheffish/tree/v0.8.2) (2014-09-08) [Full Changelog](https://github.com/chef/cheffish/compare/v0.8.1...v0.8.2) ## [v0.8.1](https://github.com/chef/cheffish/tree/v0.8.1) (2014-09-08) [Full Changelog](https://github.com/chef/cheffish/compare/v0.8...v0.8.1) **Merged pull requests:** - perform a deep merge of the current resource and new resource in new\_json [\#20](https://github.com/chef/cheffish/pull/20) ([mwrock](https://github.com/mwrock)) ## [v0.8](https://github.com/chef/cheffish/tree/v0.8) (2014-09-05) [Full Changelog](https://github.com/chef/cheffish/compare/v0.7.1...v0.8) **Merged pull requests:** - Enterprise resources [\#17](https://github.com/chef/cheffish/pull/17) ([jkeiser](https://github.com/jkeiser)) - chef\_acl resource [\#16](https://github.com/chef/cheffish/pull/16) ([jkeiser](https://github.com/jkeiser)) - Fix problem when setting public key from String. [\#15](https://github.com/chef/cheffish/pull/15) ([johnbellone](https://github.com/johnbellone)) - improve debug experience for MergedConfig [\#8](https://github.com/chef/cheffish/pull/8) ([mwrock](https://github.com/mwrock)) ## [v0.7.1](https://github.com/chef/cheffish/tree/v0.7.1) (2014-08-19) [Full Changelog](https://github.com/chef/cheffish/compare/v0.7...v0.7.1) ## [v0.7](https://github.com/chef/cheffish/tree/v0.7) (2014-07-15) [Full Changelog](https://github.com/chef/cheffish/compare/blah...v0.7) ## [blah](https://github.com/chef/cheffish/tree/blah) (2014-06-20) [Full Changelog](https://github.com/chef/cheffish/compare/v0.6.2...blah) **Merged pull requests:** - fix get\_private\_keys for edge cases [\#6](https://github.com/chef/cheffish/pull/6) ([lamont-granquist](https://github.com/lamont-granquist)) ## [v0.6.2](https://github.com/chef/cheffish/tree/v0.6.2) (2014-06-18) [Full Changelog](https://github.com/chef/cheffish/compare/v0.6.1...v0.6.2) ## [v0.6.1](https://github.com/chef/cheffish/tree/v0.6.1) (2014-06-18) [Full Changelog](https://github.com/chef/cheffish/compare/v0.6...v0.6.1) ## [v0.6](https://github.com/chef/cheffish/tree/v0.6) (2014-06-18) [Full Changelog](https://github.com/chef/cheffish/compare/v0.5...v0.6) ## [v0.5](https://github.com/chef/cheffish/tree/v0.5) (2014-06-04) [Full Changelog](https://github.com/chef/cheffish/compare/v0.5.beta.4...v0.5) ## [v0.5.beta.4](https://github.com/chef/cheffish/tree/v0.5.beta.4) (2014-05-31) [Full Changelog](https://github.com/chef/cheffish/compare/v0.5.beta.3...v0.5.beta.4) ## [v0.5.beta.3](https://github.com/chef/cheffish/tree/v0.5.beta.3) (2014-05-28) [Full Changelog](https://github.com/chef/cheffish/compare/v0.5.beta.2...v0.5.beta.3) ## [v0.5.beta.2](https://github.com/chef/cheffish/tree/v0.5.beta.2) (2014-05-28) [Full Changelog](https://github.com/chef/cheffish/compare/v0.5.beta...v0.5.beta.2) ## [v0.5.beta](https://github.com/chef/cheffish/tree/v0.5.beta) (2014-05-23) [Full Changelog](https://github.com/chef/cheffish/compare/v0.4.1...v0.5.beta) ## [v0.4.1](https://github.com/chef/cheffish/tree/v0.4.1) (2014-05-07) [Full Changelog](https://github.com/chef/cheffish/compare/v0.4...v0.4.1) ## [v0.4](https://github.com/chef/cheffish/tree/v0.4) (2014-05-01) [Full Changelog](https://github.com/chef/cheffish/compare/v0.3...v0.4) **Merged pull requests:** - Skip 1.8.7 Travis builds since cheffish doesn’t support them. [\#5](https://github.com/chef/cheffish/pull/5) ([andrewdotn](https://github.com/andrewdotn)) - Support PKCS\#8 SHA1 fingerprints used by AWS for generated keys. [\#4](https://github.com/chef/cheffish/pull/4) ([andrewdotn](https://github.com/andrewdotn)) ## [v0.3](https://github.com/chef/cheffish/tree/v0.3) (2014-04-24) [Full Changelog](https://github.com/chef/cheffish/compare/v0.2.2...v0.3) **Merged pull requests:** - Dt/local server stop [\#3](https://github.com/chef/cheffish/pull/3) ([doubt72](https://github.com/doubt72)) ## [v0.2.2](https://github.com/chef/cheffish/tree/v0.2.2) (2014-04-13) [Full Changelog](https://github.com/chef/cheffish/compare/v0.2.1...v0.2.2) ## [v0.2.1](https://github.com/chef/cheffish/tree/v0.2.1) (2014-04-11) [Full Changelog](https://github.com/chef/cheffish/compare/v0.2...v0.2.1) ## [v0.2](https://github.com/chef/cheffish/tree/v0.2) (2014-03-04) [Full Changelog](https://github.com/chef/cheffish/compare/v0.1...v0.2) ## [v0.1](https://github.com/chef/cheffish/tree/v0.1) (2013-12-12) \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)*cheffish-15.0.1/CODE_OF_CONDUCT.md000066400000000000000000000001331362335063000161520ustar00rootroot00000000000000Please refer to the Chef Community Code of Conduct at https://www.chef.io/code-of-conduct/ cheffish-15.0.1/Gemfile000066400000000000000000000011201362335063000146430ustar00rootroot00000000000000source "https://rubygems.org" gemspec group :development do gem "chefstyle", git: "https://github.com/chef/chefstyle.git", branch: "master" gem "rake" gem "rspec", "~> 3.0" end # Allow Travis to run tests with different dependency versions if ENV["GEMFILE_MOD"] puts ENV["GEMFILE_MOD"] instance_eval(ENV["GEMFILE_MOD"]) else group :development do gem "chef", "~> 15" gem "ohai", "~> 15" end end group :docs do gem "yard" gem "redcarpet" gem "github-markup" end group :debug do gem "pry" gem "pry-byebug" gem "pry-stack_explorer" gem "rb-readline" end cheffish-15.0.1/LICENSE000066400000000000000000000251421362335063000143670ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. cheffish-15.0.1/README.md000066400000000000000000000127101362335063000146360ustar00rootroot00000000000000# Cheffish [![Build status](https://badge.buildkite.com/3faec2ae5810e1b2f530e478b63859554663fc023b3ad6c3d4.svg?branch=master)](https://buildkite.com/chef-oss/chef-cheffish-master-verify) [![Gem Version](https://badge.fury.io/rb/cheffish.svg)](http://badge.fury.io/rb/cheffish) **Umbrella Project**: [Chef Infra](https://github.com/chef/chef-oss-practices/blob/master/projects/chef-infra.md) **Project State**: [Active](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md#active) **Issues [Response Time Maximum](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md)**: 14 days **Pull Request [Response Time Maximum](https://github.com/chef/chef-oss-practices/blob/master/repo-management/repo-states.md)**: 14 days This library provides a variety of convergent resources for interacting with the Chef Server; along the way, it happens to provide some very useful and sophisticated ways of running Chef resources as recipes in RSpec examples. **This document may have errors, but it should have enough pointers to get you oriented.** There are essentially 3 collections here: ## Resource/Provider Pairs for Manipulating Chef Servers You'd use these in recipes/cookbooks. They are documented on the [main Chef docs site](https://docs.chef.io). - [chef_acl](https://docs.chef.io/resource_chef_acl.html) - [chef_client](https://docs.chef.io/resource_chef_client.html) - [chef_container](https://docs.chef.io/resource_chef_container.html) - [chef_data_bag](https://docs.chef.io/resource_chef_data_bag.html) - [chef_data_bag_item](https://docs.chef.io/resource_chef_data_bag_item.html) - [chef_environment](https://docs.chef.io/resource_chef_environment.html) - [chef_group](https://docs.chef.io/resource_chef_group.html) - [chef_mirror](https://docs.chef.io/resource_chef_mirror.html) - [chef_node](https://docs.chef.io/resource_chef_node.html) - [chef_organization](https://docs.chef.io/resource_chef_organization.html) - [chef_resolved_cookbooks](https://docs.chef.io/resource_chef_resolved_cookbooks.html) - [chef_role](https://docs.chef.io/resource_chef_role.html) - [chef_user](https://docs.chef.io/resource_chef_user.html) - [private_key](https://docs.chef.io/resource_private_key.html) - DEPRECATED - [public_key](https://docs.chef.io/resource_public_key.html) - DEPRECATED ## Base/Helper Classes To support the resource/provider pairs. ## RSpec Support Most of these RSpec...things were developed for testing the resource/provider pairs above; *however*, you can also `require cheffish/rspec/chef_run_support` for any RSpec `expect`s you'd like, as we do for `chef-provisioning` and its drivers (especially `chef-provisioning-aws`). The awesomeness here is that instead of instantiating a `run_context` and a `node` and a `resource` as Ruby objects, you can test your resources in an actual recipe: ```ruby when_the_chef_12_server "exists", organization: 'some-org', server_scope: :context, port: 8900..9000 do file "/tmp/something_important.json" do content "A resource in its native environment." end end ``` An enclosing context that spins up `chef-zero` (local mode) Chef servers as dictated by `server_scope`. `Chef::Config` will be set up with the appropriate server URLs (see the `with_*` operators below). `server_scope`: - `:context` - `:example` *[default?]* - ? `port`: - port number (8900 is the default) - port range (server will continue trying up this range until it finds a free port) ```ruby expect_recipe { # unquoted recipe DSL here. }.to be_truthy # or write your own matchers. ``` Converges the recipe using `expect()` (parentheses), which tests for a value and **cannot** be used with `raise_error`. ```ruby expect_converge { # unquoted recipe DSL here. }.to raise_error(ArgumentException) ``` Converges the recipe using `expect{ }` (curly brackets), which wraps the block in a `begin..rescue..end` to detect when the block raises an exception; hence, this is **only** for `raise_error`. The blocks for the following appear to be mostly optional: what they actually do is set the `Chef::Config` variable in the name to the given value, and if you provide a block, the change is scoped to that block. Probably this would be clearer if it were aliased to (and preferring) `using` rather than `with`. - with_chef_server(server_url, options = {}, &block) - with_chef_local_server(options, &block) - with_chef_environment(name, &block) - with_chef_data_bag_item_encryption(encryption_options, &block) - with_chef_data_bag(name) - Takes a block, though this is not noted in the method signature. get_private_key(name) ### RSpec matchers These are used with `expect_recipe` or `expect_converge`: ```ruby expect_recipe { file "/tmp/a_file.json" do content "Very important content." end }.to be_idempotent.and emit_no_warnings_or_errors ``` `be_idempotent` - Runs the provided recipe *again* (`expect_(recipe|converge)` ran it the first time) and asks the Chef run if it updated anything (using `updated?`, which appears to be defined on `Chef::Resource` instead of `Chef::Client`, so there's some clarification to be done there); the matcher is satisfied if the answer is "no." `emit_no_warnings_or_errors` - Greps the Chef client run's log output for WARN/ERROR lines; matcher is satisfied if there aren't any. `have_updated` - Sifts the recipe's event stream(!) to determine if any resources were updated; matcher is satisfied is the answer is "yes." - This is *not* the opposite of `be_idempotent`. `partially_match` - TBD cheffish-15.0.1/Rakefile000066400000000000000000000017341362335063000150300ustar00rootroot00000000000000require "bundler/gem_tasks" begin require "rspec/core/rake_task" RSpec::Core::RakeTask.new do |t| t.pattern = "spec/**/*_spec.rb" end rescue LoadError desc "rspec is not installed, this task is disabled" task :spec do abort "rspec is not installed. bundle install first to make sure all dependencies are installed." end end begin require "chefstyle" require "rubocop/rake_task" desc "Run Chefstyle tests" RuboCop::RakeTask.new(:style) do |task| task.options += ["--display-cop-names", "--no-color"] end rescue LoadError puts "chefstyle gem is not installed. bundle install first to make sure all dependencies are installed." end begin require "yard" YARD::Rake::YardocTask.new(:docs) rescue LoadError puts "yard is not available. bundle install first to make sure all dependencies are installed." end task :console do require "irb" require "irb/completion" require "cheffish" ARGV.clear IRB.start end task default: %i{style spec} cheffish-15.0.1/VERSION000066400000000000000000000000061362335063000144220ustar00rootroot0000000000000015.0.1cheffish-15.0.1/bin/000077500000000000000000000000001362335063000141265ustar00rootroot00000000000000cheffish-15.0.1/bin/resource000077500000000000000000000012531362335063000157040ustar00rootroot00000000000000#!/usr/bin/env ruby require "cheffish/chef_run" def post(resource_type, name, properties) chef_run = Cheffish::ChefRun.new begin r = chef_run.client.build_resource(resource_type, name) do properties.each { |attr, value| public_send(attr, value) } end chef_run.client.add_resource(r) chef_run.converge puts "CODE: #{chef_run.updated? ? 201 : 200}" puts "STDOUT: #{chef_run.stdout}\nSTDERR: #{chef_run.stderr}\nLOGS: #{chef_run.logs}" rescue puts "CODE: 400" puts "ERROR: #{$!}\nBACKTRACE: #{$!.backtrace}\nSTDOUT: #{chef_run.stdout}\nSTDERR: #{chef_run.stderr}\nLOGS: #{chef_run.logs}" end end post(ARGV.shift, ARGV.shift, Hash[*ARGV]) cheffish-15.0.1/cheffish.gemspec000066400000000000000000000013741362335063000165070ustar00rootroot00000000000000$:.unshift(File.dirname(__FILE__) + "/lib") require "cheffish/version" Gem::Specification.new do |s| s.name = "cheffish" s.version = Cheffish::VERSION s.platform = Gem::Platform::RUBY s.license = "Apache-2.0" s.summary = "A set of Chef resources for configuring Chef." s.description = s.summary s.author = "Chef Software Inc." s.email = "oss@chef.io" s.homepage = "https://github.com/chef/cheffish" s.required_ruby_version = ">= 2.6.0" s.add_dependency "chef-zero", ">= 14.0" s.add_dependency "net-ssh" s.bindir = "bin" s.executables = %w{ } s.require_path = "lib" s.files = %w{Gemfile Rakefile LICENSE} + Dir.glob("*.gemspec") + Dir.glob("{lib,spec}/**/*", File::FNM_DOTMATCH).reject { |f| File.directory?(f) } end cheffish-15.0.1/lib/000077500000000000000000000000001362335063000141245ustar00rootroot00000000000000cheffish-15.0.1/lib/chef/000077500000000000000000000000001362335063000150315ustar00rootroot00000000000000cheffish-15.0.1/lib/chef/resource/000077500000000000000000000000001362335063000166605ustar00rootroot00000000000000cheffish-15.0.1/lib/chef/resource/chef_acl.rb000066400000000000000000000462231362335063000207400ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" require "chef/chef_fs/data_handler/acl_data_handler" require "chef/chef_fs/parallelizer" require "uri" class Chef class Resource class ChefAcl < Cheffish::BaseResource resource_name :chef_acl # Path of the thing being secured, e.g. nodes, nodes/*, nodes/mynode, # */*, **, roles/base, data/secrets, cookbooks/apache2, /users/*, # /organizations/foo/nodes/x property :path, String, name_property: true # Whether to change things recursively. true means it will descend all children # and make the same modifications to them. :on_change will only descend if # the parent has changed. :on_change is the default. property :recursive, [ true, false, :on_change ], default: :on_change # rights :read, :users => 'jkeiser', :groups => [ 'admins', 'users' ] # rights [ :create, :read ], :users => [ 'jkeiser', 'adam' ] # rights :all, :users => 'jkeiser' def rights(*values) if values.size == 0 @rights else args = values.pop args[:permissions] ||= [] values.each do |value| args[:permissions] |= Array(value) end @rights ||= [] @rights << args end end # remove_rights :read, :users => 'jkeiser', :groups => [ 'admins', 'users' ] # remove_rights [ :create, :read ], :users => [ 'jkeiser', 'adam' ] # remove_rights :all, :users => [ 'jkeiser', 'adam' ] def remove_rights(*values) if values.size == 0 @remove_rights else args = values.pop args[:permissions] ||= [] values.each do |value| args[:permissions] |= Array(value) end @remove_rights ||= [] @remove_rights << args end end action :create do if new_resource.remove_rights && new_resource.complete Chef::Log.warn("'remove_rights' is redundant when 'complete' is specified: all rights not specified in a 'rights' declaration will be removed.") end # Verify that we're not destroying all hope of ACL recovery here if new_resource.complete && (!new_resource.rights || !new_resource.rights.any? { |r| r[:permissions].include?(:all) || r[:permissions].include?(:grant) }) # NOTE: if superusers exist, this should turn into a warning. raise "'complete' specified on chef_acl resource, but no GRANT permissions were granted. I'm sorry Dave, I can't let you remove all access to an object with no hope of recovery." end # Find all matching paths so we can update them (resolve * and **) paths = match_paths(new_resource.path) if paths.size == 0 && !new_resource.path.split("/").any? { |p| p == "*" } raise "Path #{new_resource.path} cannot have an ACL set on it!" end # Go through the matches and update the ACLs for them paths.each do |path| create_acl(path) end end action_class.class_eval do # Update the ACL if necessary. def create_acl(path) changed = false # There may not be an ACL path for some valid paths (/ and /organizations, # for example). We want to recurse into these, but we don't want to try to # update nonexistent ACLs for them. acl = acl_path(path) if acl # It's possible to make a custom container current_json = current_acl(acl) if current_json # Compare the desired and current json for the ACL, and update if different. modify = {} desired_acl(acl).each do |permission, desired_json| differences = json_differences(sort_values(current_json[permission]), sort_values(desired_json)) if differences.size > 0 # Verify we aren't trying to destroy grant permissions if permission == "grant" && desired_json["actors"] == [] && desired_json["groups"] == [] # NOTE: if superusers exist, this should turn into a warning. raise "chef_acl attempted to remove all actors from GRANT! I'm sorry Dave, I can't let you remove access to an object with no hope of recovery." end modify[differences] ||= {} modify[differences][permission] = desired_json end end if modify.size > 0 changed = true description = [ "update acl #{path} at #{rest_url(path)}" ] + modify.flat_map do |diffs, permissions| diffs.map { |diff| " #{permissions.keys.join(", ")}:#{diff}" } end converge_by description do modify.values.each do |permissions| permissions.each do |permission, desired_json| rest.put(rest_url("#{acl}/#{permission}"), { permission => desired_json }) end end end end end end # If we have been asked to recurse, do so. # If recurse is on_change, then we will recurse if there is no ACL, or if # the ACL has changed. if new_resource.recursive == true || (new_resource.recursive == :on_change && (!acl || changed)) children, _error = list(path, "*") Chef::ChefFS::Parallelizer.parallel_do(children) do |child| next if child.split("/")[-1] == "containers" create_acl(child) end # containers mess up our descent, so we do them last Chef::ChefFS::Parallelizer.parallel_do(children) do |child| next if child.split("/")[-1] != "containers" create_acl(child) end end end # Get the current ACL for the given path def current_acl(acl_path) @current_acls ||= {} unless @current_acls.key?(acl_path) @current_acls[acl_path] = begin rest.get(rest_url(acl_path)) rescue Net::HTTPServerException => e unless e.response.code == "404" && new_resource.path.split("/").any? { |p| p == "*" } raise end end end @current_acls[acl_path] end # Get the desired acl for the given acl path def desired_acl(acl_path) result = new_resource.raw_json ? new_resource.raw_json.dup : {} # Calculate the JSON based on rights add_rights(acl_path, result) if new_resource.complete result = Chef::ChefFS::DataHandler::AclDataHandler.new.normalize(result, nil) else # If resource is incomplete, use current json to fill any holes current_acl(acl_path).each do |permission, perm_hash| if !result[permission] result[permission] = perm_hash.dup else result[permission] = result[permission].dup perm_hash.each do |type, actors| if !result[permission][type] result[permission][type] = actors else result[permission][type] = result[permission][type].dup result[permission][type] |= actors end end end end remove_rights(result) end result end def sort_values(json) json.each do |key, value| json[key] = value.sort if value.is_a?(Array) end json end def add_rights(acl_path, json) if new_resource.rights new_resource.rights.each do |rights| if rights[:permissions].delete(:all) rights[:permissions] |= current_acl(acl_path).keys end Array(rights[:permissions]).each do |permission| ace = json[permission.to_s] ||= {} # WTF, no distinction between users and clients? The Chef API doesn't # let us distinguish, so we have no choice :/ This means that: # 1. If you specify :users => 'foo', and client 'foo' exists, it will # pick that (whether user 'foo' exists or not) # 2. If you specify :clients => 'foo', and user 'foo' exists but # client 'foo' does not, it will pick user 'foo' and put it in the # ACL # 3. If an existing item has user 'foo' on it and you specify :clients # => 'foo' instead, idempotence will not notice that anything needs # to be updated and nothing will happen. if rights[:users] ace["actors"] ||= [] ace["actors"] |= Array(rights[:users]) end if rights[:clients] ace["actors"] ||= [] ace["actors"] |= Array(rights[:clients]) end if rights[:groups] ace["groups"] ||= [] ace["groups"] |= Array(rights[:groups]) end end end end end def remove_rights(json) if new_resource.remove_rights new_resource.remove_rights.each do |rights| rights[:permissions].each do |permission| if permission == :all json.each_key do |key| ace = json[key] = json[key.dup] ace["actors"] = ace["actors"] - Array(rights[:users]) if rights[:users] && ace["actors"] ace["actors"] = ace["actors"] - Array(rights[:clients]) if rights[:clients] && ace["actors"] ace["groups"] = ace["groups"] - Array(rights[:groups]) if rights[:groups] && ace["groups"] end else ace = json[permission.to_s] = json[permission.to_s].dup if ace ace["actors"] = ace["actors"] - Array(rights[:users]) if rights[:users] && ace["actors"] ace["actors"] = ace["actors"] - Array(rights[:clients]) if rights[:clients] && ace["actors"] ace["groups"] = ace["groups"] - Array(rights[:groups]) if rights[:groups] && ace["groups"] end end end end end end def load_current_resource; end # # Matches chef_acl paths like nodes, nodes/*. # # == Examples # match_paths('nodes'): [ 'nodes' ] # match_paths('nodes/*'): [ 'nodes/x', 'nodes/y', 'nodes/z' ] # match_paths('*'): [ 'clients', 'environments', 'nodes', 'roles', ... ] # match_paths('/'): [ '/' ] # match_paths(''): [ '' ] # match_paths('/*'): [ '/organizations', '/users' ] # match_paths('/organizations/*/*'): [ '/organizations/foo/clients', '/organizations/foo/environments', ..., '/organizations/bar/clients', '/organizations/bar/environments', ... ] # def match_paths(path) # Turn multiple slashes into one # nodes//x -> nodes/x path = path.gsub(%r{[/]+}, "/") # If it's absolute, start the matching with /. If it's relative, start with '' (relative root). if path[0] == "/" matches = [ "/" ] else matches = [ "" ] end # Split the path, and get rid of the empty path at the beginning and end # (/a/b/c/ -> [ 'a', 'b', 'c' ]) parts = path.split("/").select { |x| x != "" }.to_a # Descend until we find the matches: # path = 'a/b/c' # parts = [ 'a', 'b', 'c' ] # Starting matches = [ '' ] parts.each_with_index do |part, index| # For each match, list / and set matches to that. # # Example: /*/foo # 1. To start, # matches = [ '/' ], part = '*'. # list('/', '*') = [ '/organizations, '/users' ] # 2. matches = [ '/organizations', '/users' ], part = 'foo' # list('/organizations', 'foo') = [ '/organizations/foo' ] # list('/users', 'foo') = [ '/users/foo' ] # # Result: /*/foo = [ '/organizations/foo', '/users/foo' ] # matches = Chef::ChefFS::Parallelizer.parallelize(matches) do |pth| found, error = list(pth, part) if error if parts[0..index - 1].all? { |p| p != "*" } raise error end [] else found end end.flatten(1).to_a end matches end # # Takes a normal path and finds the Chef path to get / set its ACL. # # nodes/x -> nodes/x/_acl # nodes -> containers/nodes/_acl # '' -> organizations/_acl (the org acl) # /organizations/foo -> /organizations/foo/organizations/_acl # /users/foo -> /users/foo/_acl # /organizations/foo/nodes/x -> /organizations/foo/nodes/x/_acl # def acl_path(path) parts = path.split("/").select { |x| x != "" }.to_a prefix = (path[0] == "/") ? "/" : "" case parts.size when 0 # /, empty (relative root) # The root of the server has no publicly visible ACLs. Only nodes/*, etc. if prefix == "" ::File.join("organizations", "_acl") end when 1 # nodes, roles, etc. # The top level organizations and users containers have no publicly # visible ACLs. Only nodes/*, etc. if prefix == "" ::File.join("containers", path, "_acl") end when 2 # /organizations/NAME, /users/NAME, nodes/NAME, roles/NAME, etc. if prefix == "/" && parts[0] == "organizations" ::File.join(path, "organizations", "_acl") else ::File.join(path, "_acl") end when 3 # /organizations/NAME/nodes, cookbooks/NAME/VERSION, etc. if prefix == "/" ::File.join("/", parts[0], parts[1], "containers", parts[2], "_acl") else ::File.join(parts[0], parts[1], "_acl") end when 4 # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION/BLAH # /organizations/NAME/nodes/NAME, cookbooks/NAME/VERSION, etc. if prefix == "/" ::File.join(path, "_acl") else ::File.join(parts[0], parts[1], "_acl") end else # /organizations/NAME/cookbooks/NAME/VERSION/..., cookbooks/NAME/VERSION/A/B/... if prefix == "/" ::File.join("/", parts[0], parts[1], parts[2], parts[3], "_acl") else ::File.join(parts[0], parts[1], "_acl") end end end # # Lists the securable children under a path (the ones that either have ACLs # or have children with ACLs). # # list('nodes', 'x') -> [ 'nodes/x' ] # list('nodes', '*') -> [ 'nodes/x', 'nodes/y', 'nodes/z' ] # list('', '*') -> [ 'clients', 'environments', 'nodes', 'roles', ... ] # list('/', '*') -> [ '/organizations'] # list('cookbooks', 'x') -> [ 'cookbooks/x' ] # list('cookbooks/x', '*') -> [ ] # Individual cookbook versions do not have their own ACLs # list('/organizations/foo/nodes', '*') -> [ '/organizations/foo/nodes/x', '/organizations/foo/nodes/y' ] # # The list of children of an organization is == the list of containers. If new # containers are added, the list of children will grow. This allows the system # to extend to new types of objects and allow cheffish to work with them. # def list(path, child) # TODO make ChefFS understand top level organizations and stop doing this altogether. parts = path.split("/").select { |x| x != "" }.to_a absolute = (path[0] == "/") if absolute && parts[0] == "organizations" return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 3 else return [ [], "ACLs cannot be set on children of #{path}" ] if parts.size > 1 end error = nil if child == "*" case parts.size when 0 # /*, * if absolute results = [ "/organizations", "/users" ] else results, error = rest_list("containers") end when 1 # /organizations/*, /users/*, roles/*, nodes/*, etc. results, error = rest_list(path) unless error results = results.map { |result| ::File.join(path, result) } end when 2 # /organizations/NAME/* results, error = rest_list(::File.join(path, "containers")) unless error results = results.map { |result| ::File.join(path, result) } end when 3 # /organizations/NAME/TYPE/* results, error = rest_list(path) unless error results = results.map { |result| ::File.join(path, result) } end end else if child == "data_bags" && (parts.size == 0 || (parts.size == 2 && parts[0] == "organizations")) child = "data" end if absolute # /, /users/, /organizations/, /organizations/foo/, /organizations/foo/nodes/ ... results = [ ::File.join("/", parts[0..2], child) ] elsif parts.size == 0 # (nodes, roles, etc.) results = [ child ] else # nodes/, roles/, etc. results = [ ::File.join(parts[0], child) ] end end [ results, error ] end def rest_url(path) path[0] == "/" ? URI.join(rest.url, path) : path end def rest_list(path) # All our rest lists are hashes where the keys are the names [ rest.get(rest_url(path)).keys, nil ] rescue Net::HTTPServerException => e if e.response.code == "405" || e.response.code == "404" parts = path.split("/").select { |p| p != "" }.to_a # We KNOW we expect these to exist. Other containers may or may not. unless (parts.size == 1 || (parts.size == 3 && parts[0] == "organizations")) && %w{clients containers cookbooks data environments groups nodes roles}.include?(parts[-1]) return [ [], "Cannot get list of #{path}: HTTP response code #{e.response.code}" ] end end raise end end end end end cheffish-15.0.1/lib/chef/resource/chef_client.rb000066400000000000000000000033511362335063000214520ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/chef_actor_base" class Chef class Resource class ChefClient < Cheffish::ChefActorBase resource_name :chef_client # Client attributes property :chef_client_name, Cheffish::NAME_REGEX, name_property: true property :admin, [TrueClass, FalseClass] property :validator, [TrueClass, FalseClass] # Input key property :source_key # String or OpenSSL::PKey::* property :source_key_path, String property :source_key_pass_phrase # Output public key (if so desired) property :output_key_path, String property :output_key_format, Symbol, default: :openssh, equal_to: %i{pem der openssh} # Proc that runs just before the resource executes. Called with (resource) def before(&block) block ? @before = block : @before end # Proc that runs after the resource completes. Called with (resource, json, private_key, public_key) def after(&block) block ? @after = block : @after end action :create do create_actor end action :delete do delete_actor end action_class.class_eval do def actor_type "client" end def actor_path "clients" end # # Helpers # def resource_class Chef::Resource::ChefClient end def data_handler Chef::ChefFS::DataHandler::ClientDataHandler.new end def keys { "name" => :chef_client_name, "admin" => :admin, "validator" => :validator, "public_key" => :source_key, } end end end end end cheffish-15.0.1/lib/chef/resource/chef_container.rb000066400000000000000000000027061362335063000221610ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" require "chef/chef_fs/data_handler/container_data_handler" class Chef class Resource class ChefContainer < Cheffish::BaseResource resource_name :chef_container property :chef_container_name, Cheffish::NAME_REGEX, name_property: true action :create do unless @current_exists converge_by "create container #{new_resource.chef_container_name} at #{rest.url}" do rest.post("containers", normalize_for_post(new_json)) end end end action :delete do if @current_exists converge_by "delete container #{new_resource.chef_container_name} at #{rest.url}" do rest.delete("containers/#{new_resource.chef_container_name}") end end end action_class.class_eval do def load_current_resource @current_exists = rest.get("containers/#{new_resource.chef_container_name}") rescue Net::HTTPServerException => e if e.response.code == "404" @current_exists = false else raise end end def new_json {} end def data_handler Chef::ChefFS::DataHandler::ContainerDataHandler.new end def keys { "containername" => :chef_container_name, "containerpath" => :chef_container_name } end end end end end cheffish-15.0.1/lib/chef/resource/chef_data_bag.rb000066400000000000000000000026611362335063000217210ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" class Chef class Resource class ChefDataBag < Cheffish::BaseResource resource_name :chef_data_bag property :data_bag_name, Cheffish::NAME_REGEX, name_property: true action :create do unless current_resource_exists? converge_by "create data bag #{new_resource.data_bag_name} at #{rest.url}" do rest.post("data", { "name" => new_resource.data_bag_name }) end end end action :delete do if current_resource_exists? converge_by "delete data bag #{new_resource.data_bag_name} at #{rest.url}" do rest.delete("data/#{new_resource.data_bag_name}") end end end action_class.class_eval do def load_current_resource @current_resource = json_to_resource(rest.get("data/#{new_resource.data_bag_name}")) rescue Net::HTTPServerException => e if e.response.code == "404" @current_resource = not_found_resource else raise end end # # Helpers # # Gives us new_json, current_json, not_found_json, etc. def resource_class Chef::Resource::ChefDataBag end def json_to_resource(json) Chef::Resource::ChefDataBag.new(json["name"], run_context) end end end end end cheffish-15.0.1/lib/chef/resource/chef_data_bag_item.rb000066400000000000000000000320131362335063000227310ustar00rootroot00000000000000require_relative "../../cheffish" require "chef/config" require_relative "../../cheffish/base_resource" require "chef/chef_fs/data_handler/data_bag_item_data_handler" require "chef/encrypted_data_bag_item" class Chef class Resource class ChefDataBagItem < Cheffish::BaseResource resource_name :chef_data_bag_item def initialize(*args) super if !property_is_set?(:data_bag) && run_context.cheffish.current_data_bag data_bag run_context.cheffish.current_data_bag end encryption = run_context.cheffish.current_data_bag_item_encryption if encryption encrypt true if encryption[:encrypt_all] secret encryption[:secret] if encryption[:secret] secret_path encryption[:secret_path] || run_context.config[:encrypted_data_bag_secret] if encryption[:secret_path] || run_context.config[:encrypted_data_bag_secret] encryption_cipher encryption[:encryption_cipher] if encryption[:encryption_cipher] encryption_version encryption[:encryption_version] || run_context.config[:data_bag_encrypt_version] if encryption[:encryption_version] || run_context.config[:data_bag_encrypt_version] old_secret encryption[:old_secret] if encryption[:old_secret] old_secret_path encryption[:old_secret_path] if encryption[:old_secret_path] end end # If data_bag and id are not specified, take them from name. # name can either be id, or data_bag/id property :id, String, default: lazy { name.split("/", 2)[-1] } property :data_bag, String, default: lazy { split = name.split("/", 2) split.size >= 2 ? split[0] : nil } property :raw_data, Hash # If secret or secret_path are set, encrypt is assumed true. encrypt exists mainly for with_secret and with_secret_path property :encrypt, [TrueClass, FalseClass], default: lazy { if secret.nil? && secret_path.nil? false else true end } property :secret, String property :secret_path, String property :encryption_version, Integer # Old secret (or secrets) to read the old data bag when we are changing keys and re-encrypting data property :old_secret, [String, Array] property :old_secret_path, [String, Array] # value 'ip_address', '127.0.0.1' # value [ 'pushy', 'port' ], '9000' # value 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # value 'ip_address', :delete attr_reader :raw_data_modifiers def value(raw_data_path, value = NOT_PASSED, &block) @raw_data_modifiers ||= [] if value != NOT_PASSED @raw_data_modifiers << [ raw_data_path, value ] elsif block @raw_data_modifiers << [ raw_data_path, block ] else raise "value requires either a value or a block" end end action :create do differences = calculate_differences if current_resource_exists? if differences.size > 0 description = [ "update data bag item #{new_resource.id} at #{rest.url}" ] + differences converge_by description do rest.put("data/#{new_resource.data_bag}/#{new_resource.id}", normalize_for_put(new_json)) end end else description = [ "create data bag item #{new_resource.id} at #{rest.url}" ] + differences converge_by description do rest.post("data/#{new_resource.data_bag}", normalize_for_post(new_json)) end end end action :delete do if current_resource_exists? converge_by "delete data bag item #{new_resource.id} at #{rest.url}" do rest.delete("data/#{new_resource.data_bag}/#{new_resource.id}") end end end action_class.class_eval do def load_current_resource begin json = rest.get("data/#{new_resource.data_bag}/#{new_resource.id}") resource = Chef::Resource::ChefDataBagItem.new(new_resource.name, run_context) resource.raw_data json @current_resource = resource rescue Net::HTTPServerException => e if e.response.code == "404" @current_resource = not_found_resource else raise end end # Determine if data bag is encrypted and if so, what its version is _first_real_key, first_real_value = (current_resource.raw_data || {}).select { |key, value| key != "id" && !value.nil? }.first if first_real_value if first_real_value.is_a?(Hash) && first_real_value["version"].is_a?(Integer) && first_real_value["version"] > 0 && first_real_value.key?("encrypted_data") current_resource.encrypt true current_resource.encryption_version first_real_value["version"] decrypt_error = nil # Check if the desired secret is the one (which it generally should be) if new_resource.secret || new_resource.secret_path begin Chef::EncryptedDataBagItem::Decryptor.for(first_real_value, new_secret).for_decrypted_item current_resource.secret new_secret rescue Chef::EncryptedDataBagItem::DecryptionFailure decrypt_error = $! end end # If the current secret doesn't work, look through the specified old secrets unless current_resource.secret old_secrets = [] if new_resource.old_secret old_secrets += Array(new_resource.old_secret) end if new_resource.old_secret_path old_secrets += Array(new_resource.old_secret_path).map do |secret_path| Chef::EncryptedDataBagItem.load_secret(new_resource.old_secret_file) end end old_secrets.each do |secret| Chef::EncryptedDataBagItem::Decryptor.for(first_real_value, secret).for_decrypted_item current_resource.secret secret rescue Chef::EncryptedDataBagItem::DecryptionFailure decrypt_error = $! end # If we couldn't figure out the secret, emit a warning (this isn't a fatal flaw unless we # need to reuse one of the values from the data bag) unless current_resource.secret if decrypt_error Chef::Log.warn "Existing data bag is encrypted, but could not decrypt: #{decrypt_error.message}." else Chef::Log.warn "Existing data bag is encrypted, but no secret was specified." end end end end else # There are no encryptable values, so pretend encryption is the same as desired current_resource.encrypt(new_resource.encrypt) unless new_resource.encrypt.nil? current_resource.encryption_version(new_resource.encryption_version) if new_resource.encryption_version if new_resource.secret || new_resource.secret_path current_resource.secret new_secret end end end def new_json @new_json ||= begin if new_encrypt # Encrypt new stuff result = encrypt(new_decrypted, new_secret, new_resource.encryption_version) else result = new_decrypted end result end end def new_encrypt new_resource.encrypt.nil? ? current_resource.encrypt : new_resource.encrypt end def new_secret @new_secret ||= begin if new_resource.secret new_resource.secret elsif new_resource.secret_path Chef::EncryptedDataBagItem.load_secret(new_resource.secret_path) elsif new_resource.encrypt.nil? current_resource.secret else raise "Data bag item #{new_resource.name} has encryption on but no secret or secret_path is specified" end end end def decrypt(json, secret) Chef::EncryptedDataBagItem.new(json, secret).to_hash end def encrypt(json, secret, version) old_version = run_context.config[:data_bag_encrypt_version] run_context.config[:data_bag_encrypt_version] = version begin Chef::EncryptedDataBagItem.encrypt_data_bag_item(json, secret) ensure run_context.config[:data_bag_encrypt_version] = old_version end end # Get the desired (new) json pre-encryption, for comparison purposes def new_decrypted @new_decrypted ||= begin if new_resource.complete result = new_resource.raw_data || {} else result = current_decrypted.merge(new_resource.raw_data || {}) end result["id"] = new_resource.id _result = apply_modifiers(new_resource.raw_data_modifiers, result) end end # Get the current json decrypted, for comparison purposes def current_decrypted @current_decrypted ||= begin if current_resource.secret decrypt(current_resource.raw_data || { "id" => new_resource.id }, current_resource.secret) elsif current_resource.encrypt raise "Could not decrypt current data bag item #{current_resource.name}" else current_resource.raw_data || { "id" => new_resource.id } end end end # Figure out the differences between new and current def calculate_differences if new_encrypt if current_resource.encrypt # Both are encrypted, check if the encryption type is the same description = "" if new_secret != current_resource.secret description << " with new secret" end if new_resource.encryption_version != current_resource.encryption_version description << " from v#{current_resource.encryption_version} to v#{new_resource.encryption_version} encryption" end if description != "" # Encryption is different, we're reencrypting differences = [ "re-encrypt#{description}"] else # Encryption is the same, we're just updating differences = [] end else # New stuff should be encrypted, old is not. Encrypting. differences = [ "encrypt with v#{new_resource.encryption_version} encryption" ] end # Get differences in the actual json if current_resource.secret json_differences(current_decrypted, new_decrypted, false, "", differences) elsif current_resource.encrypt # Encryption is different and we can't read the old values. Only allow the change # if we're overwriting the data bag item unless new_resource.complete raise "Cannot encrypt #{new_resource.name} due to failure to decrypt existing resource. Set 'complete true' to overwrite or add the old secret as old_secret / old_secret_path." end _differences = [ "overwrite data bag item (cannot decrypt old data bag item)"] differences = (new_resource.raw_data.keys & current_resource.raw_data.keys).map { |key| "overwrite #{key}" } differences += (new_resource.raw_data.keys - current_resource.raw_data.keys).map { |key| "add #{key}" } differences += (current_resource.raw_data.keys - new_resource.raw_data.keys).map { |key| "remove #{key}" } else json_differences(current_decrypted, new_decrypted, false, "", differences) end else if current_resource.encrypt # New stuff should not be encrypted, old is. Decrypting. differences = [ "decrypt data bag item to plaintext" ] else differences = [] end json_differences(current_decrypted, new_decrypted, true, "", differences) end differences end # # Helpers # def resource_class Chef::Resource::ChefDataBagItem end def data_handler Chef::ChefFS::DataHandler::DataBagItemDataHandler.new end def keys { "id" => :id, "data_bag" => :data_bag, "raw_data" => :raw_data, } end def not_found_resource resource = super resource.data_bag new_resource.data_bag resource end def fake_entry FakeEntry.new("#{new_resource.id}.json", FakeEntry.new(new_resource.data_bag)) end end end end end cheffish-15.0.1/lib/chef/resource/chef_environment.rb000066400000000000000000000103721362335063000225410ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" require "chef/environment" require "chef/chef_fs/data_handler/environment_data_handler" class Chef class Resource class ChefEnvironment < Cheffish::BaseResource resource_name :chef_environment property :environment_name, Cheffish::NAME_REGEX, name_property: true property :description, String property :cookbook_versions, Hash, callbacks: { "should have valid cookbook versions" => lambda { |value| Chef::Environment.validate_cookbook_versions(value) }, } property :default_attributes, Hash property :override_attributes, Hash # default 'ip_address', '127.0.0.1' # default [ 'pushy', 'port' ], '9000' # default 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # default 'ip_address', :delete attr_reader :default_attribute_modifiers def default(attribute_path, value = NOT_PASSED, &block) @default_attribute_modifiers ||= [] if value != NOT_PASSED @default_attribute_modifiers << [ attribute_path, value ] elsif block @default_attribute_modifiers << [ attribute_path, block ] else raise "default requires either a value or a block" end end # override 'ip_address', '127.0.0.1' # override [ 'pushy', 'port' ], '9000' # override 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # override 'ip_address', :delete attr_reader :override_attribute_modifiers def override(attribute_path, value = NOT_PASSED, &block) @override_attribute_modifiers ||= [] if value != NOT_PASSED @override_attribute_modifiers << [ attribute_path, value ] elsif block @override_attribute_modifiers << [ attribute_path, block ] else raise "override requires either a value or a block" end end alias :attributes :default_attributes alias :attribute :default action :create do differences = json_differences(current_json, new_json) if current_resource_exists? if differences.size > 0 description = [ "update environment #{new_resource.environment_name} at #{rest.url}" ] + differences converge_by description do rest.put("environments/#{new_resource.environment_name}", normalize_for_put(new_json)) end end else description = [ "create environment #{new_resource.environment_name} at #{rest.url}" ] + differences converge_by description do rest.post("environments", normalize_for_post(new_json)) end end end action :delete do if current_resource_exists? converge_by "delete environment #{new_resource.environment_name} at #{rest.url}" do rest.delete("environments/#{new_resource.environment_name}") end end end action_class.class_eval do def load_current_resource @current_resource = json_to_resource(rest.get("environments/#{new_resource.environment_name}")) rescue Net::HTTPServerException => e if e.response.code == "404" @current_resource = not_found_resource else raise end end def augment_new_json(json) # Apply modifiers json["default_attributes"] = apply_modifiers(new_resource.default_attribute_modifiers, json["default_attributes"]) json["override_attributes"] = apply_modifiers(new_resource.override_attribute_modifiers, json["override_attributes"]) json end # # Helpers # def resource_class Chef::Resource::ChefEnvironment end def data_handler Chef::ChefFS::DataHandler::EnvironmentDataHandler.new end def keys { "name" => :environment_name, "description" => :description, "cookbook_versions" => :cookbook_versions, "default_attributes" => :default_attributes, "override_attributes" => :override_attributes, } end end end end end cheffish-15.0.1/lib/chef/resource/chef_group.rb000066400000000000000000000050631362335063000213320ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" require "chef/run_list/run_list_item" require "chef/chef_fs/data_handler/group_data_handler" class Chef class Resource class ChefGroup < Cheffish::BaseResource resource_name :chef_group property :group_name, Cheffish::NAME_REGEX, name_property: true property :users, ArrayType property :clients, ArrayType property :groups, ArrayType property :remove_users, ArrayType property :remove_clients, ArrayType property :remove_groups, ArrayType action :create do differences = json_differences(current_json, new_json) if current_resource_exists? if differences.size > 0 description = [ "update group #{new_resource.group_name} at #{rest.url}" ] + differences converge_by description do rest.put("groups/#{new_resource.group_name}", normalize_for_put(new_json)) end end else description = [ "create group #{new_resource.group_name} at #{rest.url}" ] + differences converge_by description do rest.post("groups", normalize_for_post(new_json)) end end end action :delete do if current_resource_exists? converge_by "delete group #{new_resource.group_name} at #{rest.url}" do rest.delete("groups/#{new_resource.group_name}") end end end action_class.class_eval do def load_current_resource @current_resource = json_to_resource(rest.get("groups/#{new_resource.group_name}")) rescue Net::HTTPServerException => e if e.response.code == "404" @current_resource = not_found_resource else raise end end def augment_new_json(json) # Apply modifiers json["users"] |= new_resource.users json["clients"] |= new_resource.clients json["groups"] |= new_resource.groups json["users"] -= new_resource.remove_users json["clients"] -= new_resource.remove_clients json["groups"] -= new_resource.remove_groups json end # # Helpers # def resource_class Chef::Resource::ChefGroup end def data_handler Chef::ChefFS::DataHandler::GroupDataHandler.new end def keys { "name" => :group_name, "groupname" => :group_name, } end end end end end cheffish-15.0.1/lib/chef/resource/chef_mirror.rb000066400000000000000000000201211362335063000215000ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" require "chef/chef_fs/file_pattern" require "chef/chef_fs/file_system" require "chef/chef_fs/parallelizer" require "chef/chef_fs/file_system/chef_server/chef_server_root_dir" require "chef/chef_fs/file_system/repository/chef_repository_file_system_root_dir" class Chef class Resource class ChefMirror < Cheffish::BaseResource resource_name :chef_mirror # Path of the data to mirror, e.g. nodes, nodes/*, nodes/mynode, # */*, **, roles/base, data/secrets, cookbooks/apache2, etc. property :path, String, name_property: true # Local path. Can be a string (top level of repository) or hash # (:chef_repo_path, :node_path, etc.) # If neither chef_repo_path nor versioned_cookbooks are set, they default to their # Chef::Config values. If chef_repo_path is set but versioned_cookbooks is not, # versioned_cookbooks defaults to true. property :chef_repo_path, [ String, Hash ] # Whether the repo path should contain cookbooks with versioned names, # i.e. cookbooks/mysql-1.0.0, cookbooks/mysql-1.2.0, etc. # Defaults to true if chef_repo_path is specified, or to Chef::Config.versioned_cookbooks otherwise. property :versioned_cookbooks, [TrueClass, FalseClass] # Whether to purge deleted things: if we do not have cookbooks/x locally and we # *do* have cookbooks/x remotely, then :upload with purge will delete it. # Defaults to false. property :purge, [TrueClass, FalseClass] # Whether to freeze cookbooks on upload property :freeze_on_upload, [TrueClass, FalseClass] # `freeze` is an already-existing instance method on Object, so we can't use it or we'll throw # a deprecation warning. `freeze` has been renamed to `freeze_on_upload` and this method # is here to log a deprecation warning. def freeze(arg = nil) Chef::Log.warn("Property `freeze` on the `chef_mirror` resource has changed to `freeze_on_upload`." \ "Please use `freeze_on_upload` instead. This will raise an exception in a future version of the cheffish gem.") set_or_return( :freeze_on_upload, arg, kind_of: [TrueClass, FalseClass] ) end # If this is true, only new files will be copied. File contents will not be # diffed, so changed files will never be uploaded. property :no_diff, [TrueClass, FalseClass] # Number of parallel threads to list/upload/download with. Defaults to 10. property :concurrency, Integer, default: 10, desired_state: false action :upload do with_modified_config do copy_to(local_fs, remote_fs) end end action :download do with_modified_config do copy_to(remote_fs, local_fs) end end action_class.class_eval do def with_modified_config # pre-Chef-12 ChefFS reads versioned_cookbooks out of Chef::Config instead of # taking it as an input, so we need to modify it for the duration of copy_to @old_versioned_cookbooks = Chef::Config.versioned_cookbooks # If versioned_cookbooks is explicitly set, set it. if !new_resource.versioned_cookbooks.nil? Chef::Config.versioned_cookbooks = new_resource.versioned_cookbooks # If new_resource.chef_repo_path is set, versioned_cookbooks defaults to true. # Otherwise, it stays at its current Chef::Config value. elsif new_resource.chef_repo_path Chef::Config.versioned_cookbooks = true end begin yield ensure Chef::Config.versioned_cookbooks = @old_versioned_cookbooks end end def copy_to(src_root, dest_root) if new_resource.concurrency <= 0 raise "chef_mirror.concurrency must be above 0! Was set to #{new_resource.concurrency}" end # Honor concurrency Chef::ChefFS::Parallelizer.threads = new_resource.concurrency - 1 # We don't let the user pass absolute paths; we want to reserve those for # multi-org support (/organizations/foo). if new_resource.path[0] == "/" raise "Absolute paths in chef_mirror not yet supported." end # Copy! path = Chef::ChefFS::FilePattern.new("/#{new_resource.path}") ui = CopyListener.new(self) error = Chef::ChefFS::FileSystem.copy_to(path, src_root, dest_root, nil, options, ui, proc { |p| p.path }) if error raise "Errors while copying:#{ui.errors.map { |e| "#{e}\n" }.join("")}" end end def local_fs # If chef_repo_path is set to a string, put it in the form it usually is in # chef config (:chef_repo_path, :node_path, etc.) path_config = new_resource.chef_repo_path if path_config.is_a?(Hash) chef_repo_path = path_config.delete(:chef_repo_path) elsif path_config chef_repo_path = path_config path_config = {} else chef_repo_path = Chef::Config.chef_repo_path path_config = Chef::Config end chef_repo_path = Array(chef_repo_path).flatten # Go through the expected object paths and figure out the local paths for each. case repo_mode when "hosted_everything" object_names = %w{acls clients cookbooks containers data_bags environments groups nodes roles} else object_names = %w{clients cookbooks data_bags environments nodes roles users} end object_paths = {} object_names.each do |object_name| variable_name = "#{object_name[0..-2]}_path" # cookbooks -> cookbook_path if path_config[variable_name.to_sym] paths = Array(path_config[variable_name.to_sym]).flatten else paths = chef_repo_path.map { |path| ::File.join(path, object_name) } end object_paths[object_name] = paths.map { |path| ::File.expand_path(path) } end # Set up the root dir Chef::ChefFS::FileSystem::Repository::ChefRepositoryFileSystemRootDir.new(object_paths) end def remote_fs config = { chef_server_url: new_resource.chef_server[:chef_server_url], node_name: new_resource.chef_server[:options][:client_name], client_key: new_resource.chef_server[:options][:signing_key_filename], repo_mode: repo_mode, versioned_cookbooks: Chef::Config.versioned_cookbooks, } Chef::ChefFS::FileSystem::ChefServer::ChefServerRootDir.new("remote", config) end def repo_mode new_resource.chef_server[:chef_server_url] =~ %r{/organizations/} ? "hosted_everything" : "everything" end def options result = { purge: new_resource.purge, freeze: new_resource.freeze_on_upload, diff: new_resource.no_diff, dry_run: whyrun_mode?, } result[:diff] = !result[:diff] result[:repo_mode] = repo_mode result[:concurrency] = new_resource.concurrency if new_resource.concurrency result end def load_current_resource; end class CopyListener def initialize(mirror) @mirror = mirror @errors = [] end attr_reader :mirror attr_reader :errors # TODO output is not *always* indicative of a change. We may want to give # ChefFS the ability to tell us that info. For now though, assuming any output # means change is pretty damn close to the truth. def output(str) mirror.converge_by str do end end def warn(str) mirror.converge_by "WARNING: #{str}" do end end def error(str) mirror.converge_by "ERROR: #{str}" do end @errors << str end end end end end end cheffish-15.0.1/lib/chef/resource/chef_node.rb000066400000000000000000000052371362335063000211260ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" require "chef/chef_fs/data_handler/node_data_handler" require_relative "../../cheffish/node_properties" class Chef class Resource class ChefNode < Cheffish::BaseResource resource_name :chef_node include Cheffish::NodeProperties action :create do differences = json_differences(current_json, new_json) if current_resource_exists? if differences.size > 0 description = [ "update node #{new_resource.name} at #{rest.url}" ] + differences converge_by description do rest.put("nodes/#{new_resource.name}", normalize_for_put(new_json)) end end else description = [ "create node #{new_resource.name} at #{rest.url}" ] + differences converge_by description do rest.post("nodes", normalize_for_post(new_json)) end end end action :delete do if current_resource_exists? converge_by "delete node #{new_resource.name} at #{rest.url}" do rest.delete("nodes/#{new_resource.name}") end end end action_class.class_eval do def load_current_resource @current_resource = json_to_resource(rest.get("nodes/#{new_resource.name}")) rescue Net::HTTPServerException => e if e.response.code == "404" @current_resource = not_found_resource else raise end end def augment_new_json(json) # Preserve tags even if "attributes" was overwritten directly json["normal"]["tags"] = current_json["normal"]["tags"] unless json["normal"]["tags"] # Apply modifiers json["run_list"] = apply_run_list_modifiers(new_resource.run_list_modifiers, new_resource.run_list_removers, json["run_list"]) json["normal"] = apply_modifiers(new_resource.attribute_modifiers, json["normal"]) # Preserve default/override/automatic even when "complete true" json["default"] = current_json["default"] json["override"] = current_json["override"] json["automatic"] = current_json["automatic"] json end # # Helpers # def resource_class Chef::Resource::ChefNode end def data_handler Chef::ChefFS::DataHandler::NodeDataHandler.new end def keys { "name" => :name, "chef_environment" => :chef_environment, "run_list" => :run_list, "normal" => :attributes, } end end end end end cheffish-15.0.1/lib/chef/resource/chef_organization.rb000066400000000000000000000145221362335063000227020ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" require "chef/run_list/run_list_item" require "chef/chef_fs/data_handler/data_handler_base" class Chef class Resource class ChefOrganization < Cheffish::BaseResource resource_name :chef_organization property :organization_name, Cheffish::NAME_REGEX, name_property: true property :full_name, String # A list of users who must at least be invited to the org (but may already be # members). Invites will be sent to users who are not already invited/in the org. property :invites, ArrayType # A list of users who must be members of the org. This will use the # new Chef 12 POST /organizations/ORG/users endpoint to add them # directly to the org. If you do not have permission to perform # this operation, and the users are not a part of the org, the # resource update will fail. property :members, ArrayType # A list of users who must not be members of the org. These users will be removed # from the org and invites will be revoked (if any). property :remove_members, ArrayType action :create do differences = json_differences(current_json, new_json) if current_resource_exists? if differences.size > 0 description = [ "update organization #{new_resource.organization_name} at #{rest.url}" ] + differences converge_by description do rest.put("#{rest.root_url}/organizations/#{new_resource.organization_name}", normalize_for_put(new_json)) end end else description = [ "create organization #{new_resource.organization_name} at #{rest.url}" ] + differences converge_by description do rest.post("#{rest.root_url}/organizations", normalize_for_post(new_json)) end end # Revoke invites and memberships when asked invites_to_remove.each do |user| if outstanding_invites.key?(user) converge_by "revoke #{user}'s invitation to organization #{new_resource.organization_name}" do rest.delete("#{rest.root_url}/organizations/#{new_resource.organization_name}/association_requests/#{outstanding_invites[user]}") end end end members_to_remove.each do |user| if existing_members.include?(user) converge_by "remove #{user} from organization #{new_resource.organization_name}" do rest.delete("#{rest.root_url}/organizations/#{new_resource.organization_name}/users/#{user}") end end end # Invite and add members when asked new_resource.invites.each do |user| if !existing_members.include?(user) && !outstanding_invites.key?(user) converge_by "invite #{user} to organization #{new_resource.organization_name}" do rest.post("#{rest.root_url}/organizations/#{new_resource.organization_name}/association_requests", { "user" => user }) end end end new_resource.members.each do |user| unless existing_members.include?(user) converge_by "Add #{user} to organization #{new_resource.organization_name}" do rest.post("#{rest.root_url}/organizations/#{new_resource.organization_name}/users/", { "username" => user }) end end end end action_class.class_eval do def existing_members @existing_members ||= rest.get("#{rest.root_url}/organizations/#{new_resource.organization_name}/users").map { |u| u["user"]["username"] } end def outstanding_invites @outstanding_invites ||= begin invites = {} rest.get("#{rest.root_url}/organizations/#{new_resource.organization_name}/association_requests").each do |r| invites[r["username"]] = r["id"] end invites end end def invites_to_remove if new_resource.complete if new_resource.property_is_set?(:invites) || new_resource.property_is_set?(:members) result = outstanding_invites.keys result -= new_resource.invites if new_resource.property_is_set?(:invites) result -= new_resource.members if new_resource.property_is_set?(:members) result else [] end else new_resource.remove_members end end def members_to_remove if new_resource.complete if new_resource.property_is_set?(:members) existing_members - (new_resource.invites | new_resource.members) else [] end else new_resource.remove_members end end end action :delete do if current_resource_exists? converge_by "delete organization #{new_resource.organization_name} at #{rest.url}" do rest.delete("#{rest.root_url}/organizations/#{new_resource.organization_name}") end end end action_class.class_eval do def load_current_resource @current_resource = json_to_resource(rest.get("#{rest.root_url}/organizations/#{new_resource.organization_name}")) rescue Net::HTTPServerException => e if e.response.code == "404" @current_resource = not_found_resource else raise end end # # Helpers # def resource_class Chef::Resource::ChefOrganization end def data_handler OrganizationDataHandler.new end def keys { "name" => :organization_name, "full_name" => :full_name, } end class OrganizationDataHandler < Chef::ChefFS::DataHandler::DataHandlerBase def normalize(organization, entry) # Normalize the order of the keys for easier reading normalize_hash(organization, { "name" => remove_dot_json(entry.name), "full_name" => remove_dot_json(entry.name), "org_type" => "Business", "clientname" => "#{remove_dot_json(entry.name)}-validator", "billing_plan" => "platform-free", }) end end end end end end cheffish-15.0.1/lib/chef/resource/chef_resolved_cookbooks.rb000066400000000000000000000036201362335063000240670ustar00rootroot00000000000000require_relative "../../cheffish/base_resource" require "chef_zero" class Chef class Resource class ChefResolvedCookbooks < Cheffish::BaseResource resource_name :chef_resolved_cookbooks def initialize(*args) super require "berkshelf" berksfile Berkshelf::Berksfile.new("/tmp/Berksfile") @cookbooks_from = [] end extend Forwardable def_delegators :@berksfile, :cookbook, :extension, :group, :metadata, :source def cookbooks_from(path = nil) if path @cookbooks_from << path else @cookbooks_from end end property :berksfile action :resolve do new_resource.cookbooks_from.each do |path| ::Dir.entries(path).each do |name| if ::File.directory?(::File.join(path, name)) && name != "." && name != ".." new_resource.berksfile.cookbook name, path: ::File.join(path, name) end end end new_resource.berksfile.install # Ridley really really wants a key :/ if new_resource.chef_server[:options][:signing_key_filename] new_resource.berksfile.upload( server_url: new_resource.chef_server[:chef_server_url], client_name: new_resource.chef_server[:options][:client_name], client_key: new_resource.chef_server[:options][:signing_key_filename] ) else file = Tempfile.new("privatekey") begin file.write(ChefZero::PRIVATE_KEY) file.close new_resource.berksfile.upload( server_url: new_resource.chef_server[:chef_server_url], client_name: new_resource.chef_server[:options][:client_name] || "me", client_key: file.path ) ensure file.close file.unlink end end end end end end cheffish-15.0.1/lib/chef/resource/chef_role.rb000066400000000000000000000132641362335063000211410ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/base_resource" require "chef/run_list/run_list_item" require "chef/chef_fs/data_handler/role_data_handler" class Chef class Resource class ChefRole < Cheffish::BaseResource resource_name :chef_role property :role_name, Cheffish::NAME_REGEX, name_property: true property :description, String property :run_list, Array # We should let them specify it as a series of parameters too property :env_run_lists, Hash property :default_attributes, Hash property :override_attributes, Hash # default_attribute 'ip_address', '127.0.0.1' # default_attribute [ 'pushy', 'port' ], '9000' # default_attribute 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # default_attribute 'ip_address', :delete attr_reader :default_attribute_modifiers def default_attribute(attribute_path, value = NOT_PASSED, &block) @default_attribute_modifiers ||= [] if value != NOT_PASSED @default_attribute_modifiers << [ attribute_path, value ] elsif block @default_attribute_modifiers << [ attribute_path, block ] else raise "default_attribute requires either a value or a block" end end # override_attribute 'ip_address', '127.0.0.1' # override_attribute [ 'pushy', 'port' ], '9000' # override_attribute 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # override_attribute 'ip_address', :delete attr_reader :override_attribute_modifiers def override_attribute(attribute_path, value = NOT_PASSED, &block) @override_attribute_modifiers ||= [] if value != NOT_PASSED @override_attribute_modifiers << [ attribute_path, value ] elsif block @override_attribute_modifiers << [ attribute_path, block ] else raise "override_attribute requires either a value or a block" end end # Order matters--if two things here are in the wrong order, they will be flipped in the run list # recipe 'apache', 'mysql' # recipe 'recipe@version' # recipe 'recipe' # role '' attr_reader :run_list_modifiers attr_reader :run_list_removers def recipe(*recipes) if recipes.size == 0 raise ArgumentError, "At least one recipe must be specified" end @run_list_modifiers ||= [] @run_list_modifiers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") } end def role(*roles) if roles.size == 0 raise ArgumentError, "At least one role must be specified" end @run_list_modifiers ||= [] @run_list_modifiers += roles.map { |role| Chef::RunList::RunListItem.new("role[#{role}]") } end def remove_recipe(*recipes) if recipes.size == 0 raise ArgumentError, "At least one recipe must be specified" end @run_list_removers ||= [] @run_list_removers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") } end def remove_role(*roles) if roles.size == 0 raise ArgumentError, "At least one role must be specified" end @run_list_removers ||= [] @run_list_removers += roles.map { |recipe| Chef::RunList::RunListItem.new("role[#{role}]") } end action :create do differences = json_differences(current_json, new_json) if current_resource_exists? if differences.size > 0 description = [ "update role #{new_resource.role_name} at #{rest.url}" ] + differences converge_by description do rest.put("roles/#{new_resource.role_name}", normalize_for_put(new_json)) end end else description = [ "create role #{new_resource.role_name} at #{rest.url}" ] + differences converge_by description do rest.post("roles", normalize_for_post(new_json)) end end end action :delete do if current_resource_exists? converge_by "delete role #{new_resource.role_name} at #{rest.url}" do rest.delete("roles/#{new_resource.role_name}") end end end action_class.class_eval do def load_current_resource @current_resource = json_to_resource(rest.get("roles/#{new_resource.role_name}")) rescue Net::HTTPServerException => e if e.response.code == "404" @current_resource = not_found_resource else raise end end def augment_new_json(json) # Apply modifiers json["run_list"] = apply_run_list_modifiers(new_resource.run_list_modifiers, new_resource.run_list_removers, json["run_list"]) json["default_attributes"] = apply_modifiers(new_resource.default_attribute_modifiers, json["default_attributes"]) json["override_attributes"] = apply_modifiers(new_resource.override_attribute_modifiers, json["override_attributes"]) json end # # Helpers # def resource_class Chef::Resource::ChefRole end def data_handler Chef::ChefFS::DataHandler::RoleDataHandler.new end def keys { "name" => :role_name, "description" => :description, "run_list" => :run_list, "env_run_lists" => :env_run_lists, "default_attributes" => :default_attributes, "override_attributes" => :override_attributes, } end end end end end cheffish-15.0.1/lib/chef/resource/chef_user.rb000066400000000000000000000046621362335063000211600ustar00rootroot00000000000000require_relative "../../cheffish" require_relative "../../cheffish/chef_actor_base" class Chef class Resource class ChefUser < Cheffish::ChefActorBase resource_name :chef_user # Client attributes property :user_name, Cheffish::NAME_REGEX, name_property: true property :display_name, String property :admin, [TrueClass, FalseClass] property :email, String property :external_authentication_uid property :recovery_authentication_enabled, [TrueClass, FalseClass] property :password, String # Hmm. There is no way to idempotentize this. # property :salt # TODO server doesn't support sending or receiving these, but it's the only way to backup / restore a user # property :hashed_password # property :hash_type # Input key property :source_key # String or OpenSSL::PKey::* property :source_key_path, String property :source_key_pass_phrase # Output public key (if so desired) property :output_key_path, String property :output_key_format, %i{pem der openssh}, default: :openssh # Proc that runs just before the resource executes. Called with (resource) def before(&block) block ? @before = block : @before end # Proc that runs after the resource completes. Called with (resource, json, private_key, public_key) def after(&block) block ? @after = block : @after end action :create do create_actor end action :delete do delete_actor end action_class.class_eval do # # Helpers # # Gives us new_json, current_json, not_found_json, etc. def actor_type "user" end def actor_path "#{rest.root_url}/users" end def resource_class Chef::Resource::ChefUser end def data_handler Chef::ChefFS::DataHandler::UserDataHandler.new end def keys { "name" => :user_name, "username" => :user_name, "display_name" => :display_name, "admin" => :admin, "email" => :email, "password" => :password, "external_authentication_uid" => :external_authentication_uid, "recovery_authentication_enabled" => :recovery_authentication_enabled, "public_key" => :source_key, } end end end end end cheffish-15.0.1/lib/chef/resource/private_key.rb000066400000000000000000000244131362335063000215330ustar00rootroot00000000000000require "openssl/cipher" require_relative "../../cheffish/base_resource" require "openssl" require_relative "../../cheffish/key_formatter" class Chef class Resource class PrivateKey < Cheffish::BaseResource resource_name :private_key allowed_actions :create, :delete, :regenerate, :nothing default_action :create # Path to private key. Set to :none to create the key in memory and not on disk. property :path, [ String, :none ], name_property: true property :format, %i{pem der}, default: :pem property :type, %i{rsa dsa}, default: :rsa # TODO support :ec # These specify an optional public_key you can spit out if you want. property :public_key_path, String property :public_key_format, %i{openssh pem der}, default: :openssh # Specify this if you want to copy another private key but give it a different format / password property :source_key property :source_key_path, String property :source_key_pass_phrase # RSA and DSA property :size, Integer, default: 2048 # RSA-only property :exponent, Integer # For RSA # PEM-only property :pass_phrase, String property :cipher, String, equal_to: OpenSSL::Cipher.ciphers.map(&:downcase), default: "des-ede3-cbc", coerce: proc { |x| x.downcase } # Set this to regenerate the key if it does not have the desired characteristics (like size, type, etc.) property :regenerate_if_different, [TrueClass, FalseClass] # Proc that runs after the resource completes. Called with (resource, private_key) def after(&block) block ? @after = block : @after end # We are not interested in Chef's cloning behavior here. def load_prior_resource(*args) Chef::Log.debug("Overloading #{resource_name}.load_prior_resource with NOOP") end action :create do create_key(false, :create) end action :regenerate do create_key(true, :regenerate) end action :delete do if current_resource.path converge_by "delete private key #{new_path}" do ::File.unlink(new_path) end end end action_class.class_eval do def create_key(regenerate, action) if @should_create_directory Cheffish.inline_resource(self, action) do directory run_context.config[:private_key_write_path] end end final_private_key = nil if new_source_key # # Create private key from source # desired_output = encode_private_key(new_source_key) if current_resource.path == :none || desired_output != IO.read(new_path) converge_by "reformat key at #{new_resource.source_key_path} to #{new_resource.format} private key #{new_path} (#{new_resource.pass_phrase ? ", #{new_resource.cipher} password" : ""})" do IO.write(new_path, desired_output) end end final_private_key = new_source_key else # # Generate a new key # if current_resource.action == [ :delete ] || regenerate || (new_resource.regenerate_if_different && (!current_private_key || current_resource.size != new_resource.size || current_resource.type != new_resource.type)) case new_resource.type when :rsa if new_resource.exponent final_private_key = OpenSSL::PKey::RSA.generate(new_resource.size, new_resource.exponent) else final_private_key = OpenSSL::PKey::RSA.generate(new_resource.size) end when :dsa final_private_key = OpenSSL::PKey::DSA.generate(new_resource.size) end generated_key = true elsif !current_private_key raise "Could not read private key from #{current_resource.path}: missing pass phrase?" else final_private_key = current_private_key generated_key = false end if generated_key generated_description = " (#{new_resource.size} bits#{new_resource.pass_phrase ? ", #{new_resource.cipher} password" : ""})" if new_path != :none action = current_resource.path == :none ? "create" : "overwrite" converge_by "#{action} #{new_resource.type} private key #{new_path}#{generated_description}" do write_private_key(final_private_key) end else converge_by "generate private key#{generated_description}" do end end else # Warn if existing key has different characteristics than expected if current_resource.size != new_resource.size Chef::Log.warn("Mismatched key size! #{current_resource.path} is #{current_resource.size} bytes, desired is #{new_resource.size} bytes. Use action :regenerate to force key regeneration.") elsif current_resource.type != new_resource.type Chef::Log.warn("Mismatched key type! #{current_resource.path} is #{current_resource.type}, desired is #{new_resource.type} bytes. Use action :regenerate to force key regeneration.") end if current_resource.format != new_resource.format converge_by "change format of #{new_resource.type} private key #{new_path} from #{current_resource.format} to #{new_resource.format}" do write_private_key(current_private_key) end elsif (@current_file_mode & 0077) != 0 new_mode = @current_file_mode & 07700 converge_by "change mode of private key #{new_path} to #{new_mode.to_s(8)}" do ::File.chmod(new_mode, new_path) end end end end if new_resource.public_key_path public_key_path = new_resource.public_key_path public_key_format = new_resource.public_key_format Cheffish.inline_resource(self, action) do public_key public_key_path do source_key final_private_key format public_key_format end end end if new_resource.after new_resource.after.call(new_resource, final_private_key) end end def encode_private_key(key) key_format = {} key_format[:format] = new_resource.format if new_resource.format key_format[:pass_phrase] = new_resource.pass_phrase if new_resource.pass_phrase key_format[:cipher] = new_resource.cipher if new_resource.cipher Cheffish::KeyFormatter.encode(key, key_format) end def write_private_key(key) ::File.open(new_path, "w") do |file| file.chmod(0600) file.write(encode_private_key(key)) end end def new_source_key @new_source_key ||= begin if new_resource.source_key.is_a?(String) source_key, _source_key_format = Cheffish::KeyFormatter.decode(new_resource.source_key, new_resource.source_key_pass_phrase) source_key elsif new_resource.source_key new_resource.source_key elsif new_resource.source_key_path source_key, _source_key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.source_key_path), new_resource.source_key_pass_phrase, new_resource.source_key_path) source_key else nil end end end attr_reader :current_private_key def new_path new_key_with_path[1] end def new_key_with_path path = new_resource.path if path.is_a?(Symbol) return [ nil, path ] elsif Pathname.new(path).relative? private_key, private_key_path = Cheffish.get_private_key_with_path(path, run_context.config) if private_key return [ private_key, (private_key_path || :none) ] elsif run_context.config[:private_key_write_path] @should_create_directory = true path = ::File.join(run_context.config[:private_key_write_path], path) return [ nil, path ] else raise "Could not find key #{path} and Chef::Config.private_key_write_path is not set." end elsif ::File.exist?(path) return [ IO.read(path), path ] else return [ nil, path ] end end def load_current_resource resource = Chef::Resource::PrivateKey.new(new_resource.name, run_context) new_key, new_path = new_key_with_path if new_path != :none && ::File.exist?(new_path) resource.path new_path @current_file_mode = ::File.stat(new_path).mode else resource.path :none end if new_key begin key, key_format = Cheffish::KeyFormatter.decode(new_key, new_resource.pass_phrase, new_path) if key @current_private_key = key resource.format key_format[:format] resource.type(key_format[:type]) if key_format[:type] resource.size(key_format[:size]) if key_format[:size] resource.exponent(key_format[:exponent]) if key_format[:exponent] resource.pass_phrase(key_format[:pass_phrase]) if key_format[:pass_phrase] resource.cipher(key_format[:cipher]) if key_format[:cipher] end rescue # If there's an error reading, we assume format and type are wrong and don't futz with them Chef::Log.warn("Error reading #{new_path}: #{$!}") end else resource.action :delete end @current_resource = resource end end end end end cheffish-15.0.1/lib/chef/resource/public_key.rb000066400000000000000000000070271362335063000213410ustar00rootroot00000000000000require "openssl/cipher" require_relative "../../cheffish/base_resource" require "openssl" require_relative "../../cheffish/key_formatter" class Chef class Resource class PublicKey < Cheffish::BaseResource resource_name :public_key allowed_actions :create, :delete, :nothing default_action :create property :path, String, name_property: true property :format, %i{pem der openssh}, default: :openssh property :source_key property :source_key_path, String property :source_key_pass_phrase # We are not interested in Chef's cloning behavior here. def load_prior_resource(*args) Chef::Log.debug("Overloading #{resource_name}.load_prior_resource with NOOP") end action :create do unless new_source_key raise "No source key specified" end desired_output = encode_public_key(new_source_key) if Array(current_resource.action) == [ :delete ] || desired_output != IO.read(new_resource.path) converge_by "write #{new_resource.format} public key #{new_resource.path} from #{new_source_key_publicity} key #{new_resource.source_key_path}" do IO.write(new_resource.path, desired_output) # TODO permissions on file? end end end action :delete do if Array(current_resource.action) == [ :create ] converge_by "delete public key #{new_resource.path}" do ::File.unlink(new_resource.path) end end end action_class.class_eval do def encode_public_key(key) key_format = {} key_format[:format] = new_resource.format if new_resource.format Cheffish::KeyFormatter.encode(key, key_format) end attr_reader :current_public_key attr_reader :new_source_key_publicity def new_source_key @new_source_key ||= begin if new_resource.source_key.is_a?(String) source_key, _source_key_format = Cheffish::KeyFormatter.decode(new_resource.source_key, new_resource.source_key_pass_phrase) elsif new_resource.source_key source_key = new_resource.source_key elsif new_resource.source_key_path source_key, _source_key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.source_key_path), new_resource.source_key_pass_phrase, new_resource.source_key_path) else return nil end if source_key.private? @new_source_key_publicity = "private" source_key.public_key else @new_source_key_publicity = "public" source_key end end end def load_current_resource if ::File.exist?(new_resource.path) resource = Chef::Resource::PublicKey.new(new_resource.path, run_context) begin key, key_format = Cheffish::KeyFormatter.decode(IO.read(new_resource.path), nil, new_resource.path) if key @current_public_key = key resource.format key_format[:format] end rescue # If there is an error reading we assume format and such is broken end @current_resource = resource else not_found_resource = Chef::Resource::PublicKey.new(new_resource.path, run_context) not_found_resource.action :delete @current_resource = not_found_resource end end end end end end cheffish-15.0.1/lib/cheffish.rb000066400000000000000000000113361362335063000162340ustar00rootroot00000000000000module Cheffish NAME_REGEX = /^[.\-[:alnum:]_]+$/.freeze def self.inline_resource(provider, provider_action, *resources, &block) BasicChefClient.inline_resource(provider, provider_action, *resources, &block) end def self.default_chef_server(config = profiled_config) { chef_server_url: config[:chef_server_url], options: { client_name: config[:node_name], signing_key_filename: config[:client_key], }, } end def self.chef_server_api(chef_server = default_chef_server) # Pin the server api version to 0 until https://github.com/chef/cheffish/issues/56 # gets the correct compatibility fix. chef_server[:options] ||= {} chef_server[:options][:api_version] = "0" Cheffish::ServerAPI.new(chef_server[:chef_server_url], chef_server[:options]) end def self.profiled_config(config = Chef::Config) if config.profile && config.profiles && config.profiles[config.profile] MergedConfig.new(config.profiles[config.profile], config) else config end end def self.load_chef_config(chef_config = Chef::Config) if ::Gem::Version.new(::Chef::VERSION) >= ::Gem::Version.new("12.0.0") chef_config.config_file = ::Chef::Knife.chef_config_dir else chef_config.config_file = ::Chef::Knife.locate_config_file end config_fetcher = Chef::ConfigFetcher.new(chef_config.config_file, chef_config.config_file_jail) if chef_config.config_file.nil? Chef::Log.warn("No config file found or specified on command line, using command line options.") elsif config_fetcher.config_missing? Chef::Log.warn("Did not find config file: #{chef_config.config_file}, using command line options.") else config_content = config_fetcher.read_config config_file_path = chef_config.config_file begin chef_config.from_string(config_content, config_file_path) rescue Exception => error Chef::Log.fatal("Configuration error #{error.class}: #{error.message}") filtered_trace = error.backtrace.grep(/#{Regexp.escape(config_file_path)}/) filtered_trace.each { |line| Chef::Log.fatal(" " + line ) } Chef::Application.fatal!("Aborting due to error in '#{config_file_path}'", 2) end end Cheffish.profiled_config(chef_config) end def self.honor_local_mode(local_mode_default = true, &block) if !Chef::Config.key?(:local_mode) && !local_mode_default.nil? Chef::Config.local_mode = local_mode_default end if Chef::Config.local_mode && !Chef::Config.key?(:cookbook_path) && !Chef::Config.key?(:chef_repo_path) Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd) end begin require "chef/local_mode" Chef::LocalMode.with_server_connectivity(&block) rescue LoadError Chef::Application.setup_server_connectivity if block_given? begin yield ensure Chef::Application.destroy_server_connectivity end end end end def self.get_private_key(name, config = profiled_config) key, _key_path = get_private_key_with_path(name, config) key end def self.get_private_key_with_path(name, config = profiled_config) if config[:private_keys] && config[:private_keys][name] named_key = config[:private_keys][name] if named_key.is_a?(String) Chef::Log.info("Got key #{name} from Chef::Config.private_keys.#{name}, which points at #{named_key}. Reading key from there ...") return [ IO.read(named_key), named_key] else Chef::Log.info("Got key #{name} raw from Chef::Config.private_keys.#{name}.") return [ named_key.to_pem, nil ] end elsif config[:private_key_paths] config[:private_key_paths].each do |private_key_path| next unless File.exist?(private_key_path) Dir.entries(private_key_path).sort.each do |key| ext = File.extname(key) if key == name || ext == "" || ext == ".pem" key_name = key[0..-(ext.length + 1)] if key_name == name || key == name Chef::Log.info("Reading key #{name} from file #{private_key_path}/#{key}") return [ IO.read("#{private_key_path}/#{key}"), "#{private_key_path}/#{key}" ] end end end end end nil end def self.node_attributes(klass) klass.include Cheffish::NodeProperties end end # Include all recipe objects so require 'cheffish' brings in the whole recipe DSL require "chef/run_list/run_list_item" require_relative "cheffish/basic_chef_client" require_relative "cheffish/server_api" require "chef/knife" require "chef/config_fetcher" require "chef/log" require "chef/application" require_relative "cheffish/recipe_dsl" require_relative "cheffish/node_properties" cheffish-15.0.1/lib/cheffish/000077500000000000000000000000001362335063000157035ustar00rootroot00000000000000cheffish-15.0.1/lib/cheffish/array_property.rb000066400000000000000000000015721362335063000213170ustar00rootroot00000000000000require "chef/property" module Cheffish # A typical array property. Defaults to [], accepts multiple args to setter, accumulates values. class ArrayProperty < Chef::Property def initialize(**options) options[:is] ||= Array options[:default] ||= [] options[:coerce] ||= proc { |v| v.is_a?(Array) ? v : [ v ] } super end # Support my_property 'a', 'b', 'c'; my_property 'a'; and my_property ['a', 'b'] def emit_dsl declared_in.class_eval(<<-EOM, __FILE__, __LINE__ + 1) def #{name}(*values) property = self.class.properties[#{name.inspect}] if values.empty? property.get(self) elsif property.is_set?(self) property.set(self, property.get(self) + values.flatten) else property.set(self, values.flatten) end end EOM end end end cheffish-15.0.1/lib/cheffish/base_properties.rb000066400000000000000000000006631362335063000214230ustar00rootroot00000000000000require "chef/mixin/properties" require_relative "array_property" require_relative "../cheffish" module Cheffish module BaseProperties include Chef::Mixin::Properties def initialize(*args) super chef_server run_context.cheffish.current_chef_server end ArrayType = ArrayProperty.new property :chef_server, Hash property :raw_json, Hash property :complete, [TrueClass, FalseClass] end end cheffish-15.0.1/lib/cheffish/base_resource.rb000066400000000000000000000177251362335063000210650ustar00rootroot00000000000000require "chef/resource" require_relative "base_properties" module Cheffish class BaseResource < Chef::Resource include Cheffish::BaseProperties declare_action_class.class_eval do def rest @rest ||= Cheffish.chef_server_api(new_resource.chef_server) end def current_resource_exists? Array(current_resource.action) != [ :delete ] end def not_found_resource resource = resource_class.new(new_resource.name, run_context) resource.action :delete resource end def normalize_for_put(json) data_handler.normalize_for_put(json, fake_entry) end def normalize_for_post(json) data_handler.normalize_for_post(json, fake_entry) end def new_json @new_json ||= begin if new_resource.complete result = normalize(resource_to_json(new_resource)) else # If the resource is incomplete, we use the current json to fill any holes result = current_json.merge(resource_to_json(new_resource)) end augment_new_json(result) end end # Meant to be overridden def augment_new_json(json) json end def current_json @current_json ||= begin result = normalize(resource_to_json(current_resource)) result = augment_current_json(result) result end end # Meant to be overridden def augment_current_json(json) json end def resource_to_json(resource) json = resource.raw_json || {} keys.each do |json_key, resource_key| value = resource.send(resource_key) # This takes care of Chef ImmutableMash and ImmutableArray value = value.to_hash if value.is_a?(Hash) value = value.to_a if value.is_a?(Array) json[json_key] = value if value end json end def json_to_resource(json) resource = resource_class.new(new_resource.name, run_context) keys.each do |json_key, resource_key| resource.send(resource_key, json.delete(json_key)) end # Set the leftover to raw_json resource.raw_json json resource end def normalize(json) data_handler.normalize(json, fake_entry) end def json_differences(old_json, new_json, print_values = true, name = "", result = nil) result ||= [] json_differences_internal(old_json, new_json, print_values, name, result) result end def json_differences_internal(old_json, new_json, print_values, name, result) if old_json.is_a?(Hash) && new_json.is_a?(Hash) removed_keys = old_json.keys.inject({}) { |hash, key| hash[key] = true; hash } new_json.each_pair do |new_key, new_value| if old_json.key?(new_key) removed_keys.delete(new_key) if new_value != old_json[new_key] json_differences_internal(old_json[new_key], new_value, print_values, name == "" ? new_key : "#{name}.#{new_key}", result) end else if print_values result << " add #{name == "" ? new_key : "#{name}.#{new_key}"} = #{new_value.inspect}" else result << " add #{name == "" ? new_key : "#{name}.#{new_key}"}" end end end removed_keys.keys.each do |removed_key| result << " remove #{name == "" ? removed_key : "#{name}.#{removed_key}"}" end else old_json = old_json.to_s if old_json.is_a?(Symbol) new_json = new_json.to_s if new_json.is_a?(Symbol) if old_json != new_json if print_values result << " update #{name} from #{old_json.inspect} to #{new_json.inspect}" else result << " update #{name}" end end end end def apply_modifiers(modifiers, json) return json if !modifiers || modifiers.size == 0 # If the attributes have nothing, set them to {} so we have something to add to if json json = Marshal.load(Marshal.dump(json)) # Deep copy else json = {} end modifiers.each do |path, value| path = [path] unless path.is_a?(Array) path = path.map(&:to_s) parent = 0.upto(path.size - 2).inject(json) do |hash, index| if hash.nil? nil elsif !hash.is_a?(Hash) raise "Attempt to set #{path} to #{value} when #{path[0..index - 1]} is not a hash" else hash[path[index]] end end if !parent.nil? && !parent.is_a?(Hash) raise "Attempt to set #{path} to #{value} when #{path[0..-2]} is not a hash" end existing_value = parent ? parent[path[-1]] : nil if value.is_a?(Proc) value = value.call(existing_value) end if value == :delete parent.delete(path[-1]) if parent else # Create parent if necessary, overwriting values parent = path[0..-2].inject(json) do |hash, path_part| hash[path_part] = {} unless hash[path_part] hash[path_part] end if path.size > 0 parent[path[-1]] = value else json = value end end end json end def apply_run_list_modifiers(add_to_run_list, delete_from_run_list, run_list) return run_list if (!add_to_run_list || add_to_run_list.size == 0) && (!delete_from_run_list || !delete_from_run_list.size) delete_from_run_list ||= [] add_to_run_list ||= [] run_list = Chef::RunList.new(*run_list) result = [] add_to_run_list_index = 0 run_list_index = 0 while run_list_index < run_list.run_list_items.size # See if the desired run list has this item found_desired = add_to_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) } if found_desired # If so, copy all items up to that desired run list (to preserve order). # If a run list item is out of order (run_list = X, B, Y, A, Z and desired = A, B) # then this will give us X, A, B. When A is found later, nothing will be copied # because found_desired will be less than add_to_run_list_index. The result will # be X, A, B, Y, Z. if found_desired >= add_to_run_list_index result += add_to_run_list[add_to_run_list_index..found_desired].map(&:to_s) add_to_run_list_index = found_desired + 1 end else # If not, just copy it in unless delete_from_run_list.index { |item| same_run_list_item(item, run_list[run_list_index]) } result << run_list[run_list_index].to_s end end run_list_index += 1 end # Copy any remaining desired items at the end result += add_to_run_list[add_to_run_list_index..-1].map(&:to_s) result end def same_run_list_item(a, b) a_name = a.name b_name = b.name # Handle "a::default" being the same as "a" if a.type == :recipe && a_name =~ /(.+)::default$/ a_name = $1 elsif b.type == :recipe && b_name =~ /(.+)::default$/ b_name = $1 end a_name == b_name && a.type == b.type # We want to replace things with same name and different version end private # Needed to be able to use DataHandler classes def fake_entry FakeEntry.new("#{new_resource.send(keys.values.first)}.json") end class FakeEntry def initialize(name, parent = nil) @name = name @parent = parent @org = nil end attr_reader :name attr_reader :parent attr_reader :org end end end end cheffish-15.0.1/lib/cheffish/basic_chef_client.rb000066400000000000000000000117471362335063000216460ustar00rootroot00000000000000require_relative "version" require "chef/dsl/recipe" require "chef/event_dispatch/base" require "chef/event_dispatch/dispatcher" require "chef/node" require "chef/run_context" require "chef/runner" require "forwardable" require "chef/providers" require "chef/resources" module Cheffish class BasicChefClient include Chef::DSL::Recipe def initialize(node = nil, events = nil, **chef_config) unless node node = Chef::Node.new node.name "basic_chef_client" node.automatic[:platform] = "basic_chef_client" node.automatic[:platform_version] = Cheffish::VERSION end # Decide on the config we want for this chef client @chef_config = chef_config with_chef_config do @cookbook_name = "basic_chef_client" @event_catcher = BasicChefClientEvents.new dispatcher = Chef::EventDispatch::Dispatcher.new(@event_catcher) case events when Array events.each { |e| dispatcher.register(e) } if events when !nil # rubocop: disable Lint/LiteralAsCondition dispatcher.register(events) end @run_context = Chef::RunContext.new(node, {}, dispatcher) @updated = [] @cookbook_name = "basic_chef_client" end end extend Forwardable # Stuff recipes need attr_reader :chef_config attr_reader :run_context attr_accessor :cookbook_name attr_accessor :recipe_name def add_resource(resource) with_chef_config do resource.run_context = run_context run_context.resource_collection.insert(resource) end end def load_block(&block) with_chef_config do @recipe_name = "block" instance_eval(&block) end end def converge with_chef_config do Chef::Runner.new(run_context).converge end end def updates @event_catcher.updates end def updated? @event_catcher.updates.size > 0 end # Builds a resource sans context, which can be later used in a new client's # add_resource() method. def self.build_resource(type, name, created_at = nil, &resource_attrs_block) created_at ||= caller[0] result = BasicChefClient.new.tap do |client| client.with_chef_config do client.build_resource(type, name, created_at, &resource_attrs_block) end end result end def self.inline_resource(provider, provider_action, *resources, &block) events = ProviderEventForwarder.new(provider, provider_action) client = BasicChefClient.new(provider.node, events) client.with_chef_config do resources.each do |resource| client.add_resource(resource) end end client.load_block(&block) if block client.converge client.updated? end def self.converge_block(node = nil, events = nil, &block) client = BasicChefClient.new(node, events) client.load_block(&block) client.converge client.updated? end def with_chef_config(&block) old_chef_config = Chef::Config.save if chef_config[:log_location] old_loggers = Chef::Log.loggers Chef::Log.init(chef_config[:log_location]) end if chef_config[:log_level] old_level = Chef::Log.level Chef::Log.level(chef_config[:log_level]) end # if chef_config[:stdout] # old_stdout = $stdout # $stdout = chef_config[:stdout] # end # if chef_config[:stderr] # old_stderr = $stderr # $stderr = chef_config[:stderr] # end begin deep_merge_config(chef_config, Chef::Config) yield ensure # $stdout = old_stdout if chef_config[:stdout] # $stderr = old_stderr if chef_config[:stderr] if old_loggers Chef::Log.logger = old_loggers.shift old_loggers.each { |l| Chef::Log.loggers.push(l) } elsif chef_config[:log_level] Chef::Log.level = old_level end Chef::Config.restore(old_chef_config) end end def deep_merge_config(src, dest) src.each do |name, value| if value.is_a?(Hash) && dest[name].is_a?(Hash) deep_merge_config(value, dest[name]) else dest[name] = value end end end class BasicChefClientEvents < Chef::EventDispatch::Base def initialize @updates = [] end attr_reader :updates # Called after a resource has been completely converged. def resource_updated(resource, action) updates << [ resource, action ] end end class ProviderEventForwarder < Chef::EventDispatch::Base def initialize(provider, provider_action) @provider = provider @provider_action = provider_action end attr_reader :provider attr_reader :provider_action def resource_update_applied(resource, action, update) provider.run_context.events.resource_update_applied(provider.new_resource, provider_action, update) end end end end cheffish-15.0.1/lib/cheffish/chef_actor_base.rb000066400000000000000000000115471362335063000213270ustar00rootroot00000000000000require_relative "key_formatter" require_relative "base_resource" module Cheffish class ChefActorBase < Cheffish::BaseResource action_class.class_eval do def create_actor if new_resource.before new_resource.before.call(new_resource) end # Create or update the client/user current_public_key = new_json["public_key"] differences = json_differences(current_json, new_json) if current_resource_exists? # Update the actor if it's different if differences.size > 0 description = [ "update #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences converge_by description do result = rest.put("#{actor_path}/#{new_resource.name}", normalize_for_put(new_json)) current_public_key, _current_public_key_format = Cheffish::KeyFormatter.decode(result["public_key"]) if result["public_key"] end end else # Create the actor if it's missing unless new_public_key raise "You must specify a public key to create a #{actor_type}! Use the private_key resource to create a key, and pass it in with source_key_path." end description = [ "create #{actor_type} #{new_resource.name} at #{actor_path}" ] + differences converge_by description do result = rest.post((actor_path).to_s, normalize_for_post(new_json)) current_public_key, _current_public_key_format = Cheffish::KeyFormatter.decode(result["public_key"]) if result["public_key"] end end # Write out the public key if new_resource.output_key_path # TODO use inline_resource key_content = Cheffish::KeyFormatter.encode(current_public_key, { format: new_resource.output_key_format }) if !current_resource.output_key_path action = "create" elsif key_content != IO.read(current_resource.output_key_path) action = "overwrite" else action = nil end if action converge_by "#{action} public key #{new_resource.output_key_path}" do IO.write(new_resource.output_key_path, key_content) end end # TODO permissions? end if new_resource.after new_resource.after.call(self, new_json, server_private_key, server_public_key) end end def delete_actor if current_resource_exists? converge_by "delete #{actor_type} #{new_resource.name} at #{actor_path}" do rest.delete("#{actor_path}/#{new_resource.name}") Chef::Log.info("#{new_resource} deleted #{actor_type} #{new_resource.name} at #{rest.url}") end end if current_resource.output_key_path converge_by "delete public key #{current_resource.output_key_path}" do ::File.unlink(current_resource.output_key_path) end end end def new_public_key @new_public_key ||= begin if new_resource.source_key if new_resource.source_key.is_a?(String) key, _key_format = Cheffish::KeyFormatter.decode(new_resource.source_key) if key.private? key.public_key else key end elsif new_resource.source_key.private? new_resource.source_key.public_key else new_resource.source_key end elsif new_resource.source_key_path source_key_path = new_resource.source_key_path if Pathname.new(source_key_path).relative? source_key_str, source_key_path = Cheffish.get_private_key_with_path(source_key_path, run_context.config) else source_key_str = IO.read(source_key_path) end source_key, _source_key_format = Cheffish::KeyFormatter.decode(source_key_str, new_resource.source_key_pass_phrase, source_key_path) if source_key.private? source_key.public_key else source_key end else nil end end end def augment_new_json(json) if new_public_key json["public_key"] = new_public_key.to_pem end json end def load_current_resource begin json = rest.get("#{actor_path}/#{new_resource.name}") @current_resource = json_to_resource(json) rescue Net::HTTPServerException => e if e.response.code == "404" @current_resource = not_found_resource else raise end end if new_resource.output_key_path && ::File.exist?(new_resource.output_key_path) current_resource.output_key_path = new_resource.output_key_path end end end end end cheffish-15.0.1/lib/cheffish/chef_run.rb000066400000000000000000000103641362335063000200250ustar00rootroot00000000000000require_relative "basic_chef_client" module Cheffish class ChefRun # # @param chef_config A hash with symbol keys that looks suspiciously similar to `Chef::Config`. # Some possible options: # - stdout: - where to stream stdout to # - stderr: - where to stream stderr to # - log_level: :debug|:info|:warn|:error|:fatal # - log_location: - where to stream logs to # - verbose_logging: true|false - true if you want verbose logging in :debug # def initialize(chef_config = {}) @chef_config = chef_config || {} end attr_reader :chef_config class StringIOTee < StringIO def initialize(*streams) super() @streams = streams.flatten.select { |s| !s.nil? } end attr_reader :streams def write(*args, &block) super streams.each { |s| s.write(*args, &block) } end end def client @client ||= begin chef_config = self.chef_config.dup chef_config[:log_level] ||= :debug unless chef_config.key?(:log_level) chef_config[:verbose_logging] = false unless chef_config.key?(:verbose_logging) chef_config[:stdout] = StringIOTee.new(chef_config[:stdout]) chef_config[:stderr] = StringIOTee.new(chef_config[:stderr]) chef_config[:log_location] = StringIOTee.new(chef_config[:log_location]) @client = ::Cheffish::BasicChefClient.new(nil, [ event_sink, Chef::Formatters.new(:doc, chef_config[:stdout], chef_config[:stderr]) ], chef_config) end end def event_sink @event_sink ||= EventSink.new end # # output # def stdout @client ? client.chef_config[:stdout].string : nil end def stderr @client ? client.chef_config[:stderr].string : nil end def logs @client ? client.chef_config[:log_location].string : nil end def logged_warnings logs.lines.select { |l| l =~ /^\[[^\]]*\] WARN:/ }.join("\n") end def logged_errors logs.lines.select { |l| l =~ /^\[[^\]]*\] ERROR:/ }.join("\n") end def logged_info logs.lines.select { |l| l =~ /^\[[^\]]*\] INFO:/ }.join("\n") end def resources client.run_context.resource_collection end def compile_recipe(&recipe) client.load_block(&recipe) end def converge client.converge @converged = true rescue RuntimeError => e @raised_exception = e raise end def reset @client = nil @converged = nil @stdout = nil @stderr = nil @logs = nil @raised_exception = nil end def converged? !!@converged end def converge_failed? @raised_exception.nil? ? false : true end def updated? client.updated? end def up_to_date? !client.updated? end def output_for_failure_message message = "" if stdout && !stdout.empty? message << "--- ---\n" message << "--- Chef Client Output ---\n" message << "--- ---\n" message << stdout message << "\n" unless stdout.end_with?("\n") end if stderr && !stderr.empty? message << "--- ---\n" message << "--- Chef Client Error Output ---\n" message << "--- ---\n" message << stderr message << "\n" unless stderr.end_with?("\n") end if logs && !logs.empty? message << "--- ---\n" message << "--- Chef Client Logs ---\n" message << "--- ---\n" message << logs end message end class EventSink def initialize @events = [] end attr_reader :events def method_missing(method, *args) @events << [ method, *args ] end def respond_to_missing?(method_name, include_private = false) # Chef::EventDispatch::Dispatcher calls #respond_to? to see (basically) if we'll accept an event; # obviously, per above #method_missing, we'll accept whatever we're given. if there's a problem, it # will surface higher up the stack. true end end end end cheffish-15.0.1/lib/cheffish/chef_run_data.rb000066400000000000000000000006101362335063000210070ustar00rootroot00000000000000require "chef/config" require_relative "with_pattern" module Cheffish class ChefRunData def initialize(config) @local_servers = [] @current_chef_server = Cheffish.default_chef_server(config) end extend Cheffish::WithPattern with :data_bag with :environment with :data_bag_item_encryption with :chef_server attr_reader :local_servers end end cheffish-15.0.1/lib/cheffish/chef_run_listener.rb000066400000000000000000000007151362335063000217310ustar00rootroot00000000000000require "chef/event_dispatch/base" module Cheffish class ChefRunListener < Chef::EventDispatch::Base def initialize(node) @node = node end attr_reader :node def run_complete(node) disconnect end def run_failed(exception) disconnect end private def disconnect # Stop the servers if node.run_context node.run_context.cheffish.local_servers.each(&:stop) end end end end cheffish-15.0.1/lib/cheffish/key_formatter.rb000066400000000000000000000057641362335063000211170ustar00rootroot00000000000000require "openssl" require "net/ssh" require "etc" require "socket" require "digest/md5" require "base64" module Cheffish class KeyFormatter # Returns nil or key, format def self.decode(str, pass_phrase = nil, filename = "") key_format = {} key_format[:format] = format_of(str) case key_format[:format] when :openssh key = decode_openssh_key(str, filename) else begin key = OpenSSL::PKey.read(str) { pass_phrase } rescue return nil end end key_format[:type] = type_of(key) if type_of(key) key_format[:size] = size_of(key) if size_of(key) key_format[:pass_phrase] = pass_phrase if pass_phrase # TODO cipher, exponent [key, key_format] end def self.encode(key, key_format) format = key_format[:format] || :pem case format when :openssh encode_openssh_key(key) when :pem if key_format[:pass_phrase] cipher = key_format[:cipher] || "DES-EDE3-CBC" key.to_pem(OpenSSL::Cipher.new(cipher), key_format[:pass_phrase]) else key.to_pem end when :der key.to_der when :fingerprint, :pkcs1md5fingerprint hexes = Digest::MD5.hexdigest(key.to_der) # Put : between every pair of hexes hexes.scan(/../).join(":") when :rfc4716md5fingerprint _type, base64_data, _etc = encode_openssh_key(key).split data = Base64.decode64(base64_data) hexes = Digest::MD5.hexdigest(data) hexes.scan(/../).join(":") when :pkcs8sha1fingerprint if RUBY_VERSION.to_f >= 2.0 raise "PKCS8 SHA1 not supported in Ruby #{RUBY_VERSION}" end require "openssl_pkcs8" pkcs8_pem = key.to_pem_pkcs8 pkcs8_base64 = pkcs8_pem.split("\n").reject { |l| l =~ /^-----/ } pkcs8_data = Base64.decode64(pkcs8_base64.join) hexes = Digest::SHA1.hexdigest(pkcs8_data) hexes.scan(/../).join(":") else raise "Unrecognized key format #{format}" end end def self.encode_openssh_key(key) # TODO there really isn't a method somewhere in net/ssh or openssl that does this?? type = key.ssh_type data = [ key.to_blob ].pack("m0") "#{type} #{data} #{Etc.getlogin}@#{Socket.gethostname}" end def self.decode_openssh_key(str, filename = "") Net::SSH::KeyFactory.load_data_public_key(str, filename) end def self.format_of(key_contents) if key_contents.start_with?("-----BEGIN ") :pem elsif key_contents.start_with?("ssh-rsa ", "ssh-dss ") :openssh else :der end end def self.type_of(key) case key.class when OpenSSL::PKey::RSA :rsa when OpenSSL::PKey::DSA :dsa else nil end end def self.size_of(key) case key.class when OpenSSL::PKey::RSA key.n.num_bytes * 8 else nil end end end end cheffish-15.0.1/lib/cheffish/merged_config.rb000066400000000000000000000041151362335063000210210ustar00rootroot00000000000000require "chef/mash" module Cheffish class MergedConfig def initialize(*configs) @configs = configs.map { |config| Mash.from_hash config.to_hash } @merge_arrays = Mash.new end include Enumerable attr_reader :configs def merge_arrays(*symbols) if symbols.size > 0 symbols.each do |symbol| @merge_arrays[symbol] = true end else @merge_arrays end end def [](name) if @merge_arrays[name] configs.select { |c| !c[name].nil? }.collect_concat { |c| c[name] } else result_configs = [] configs.each do |config| value = config[name] unless value.nil? if value.respond_to?(:keys) result_configs << value elsif result_configs.size > 0 return result_configs[0] else return value end end end if result_configs.size > 1 MergedConfig.new(*result_configs) elsif result_configs.size == 1 result_configs[0] else nil end end end def method_missing(name, *args) $stderr.puts "WARN: deprecated use of method_missing on a Cheffish::MergedConfig object at #{caller[0]}" if args.count > 0 raise NoMethodError, "Unexpected method #{name} for MergedConfig with arguments #{args}" else self[name] end end def key?(name) configs.any? { |config| config.key?(name) } end alias_method :has_key?, :key? def keys configs.flat_map(&:keys).uniq end def values keys.map { |key| self[key] } end def empty? configs.empty? end def each_pair(&block) each(&block) end def each keys.each do |key| if block_given? yield key, self[key] end end end def to_hash result = {} each_pair do |key, value| result[key] = value end result end def to_h to_hash end def to_s to_hash.to_s end end end cheffish-15.0.1/lib/cheffish/node_properties.rb000066400000000000000000000064751362335063000214450ustar00rootroot00000000000000require_relative "base_properties" module Cheffish module NodeProperties include Cheffish::BaseProperties # Grab environment from with_environment def initialize(*args) super chef_environment run_context.cheffish.current_environment end property :node_properties_name, Cheffish::NAME_REGEX, name_property: true property :chef_environment, Cheffish::NAME_REGEX property :run_list, Array # We should let them specify it as a series of parameters too property :attributes, Hash # attribute 'ip_address', '127.0.0.1' # attribute [ 'pushy', 'port' ], '9000' # attribute 'ip_addresses' do |existing_value| # (existing_value || []) + [ '127.0.0.1' ] # end # attribute 'ip_address', :delete attr_accessor :attribute_modifiers def attribute(attribute_path, value = Chef::NOT_PASSED, &block) @attribute_modifiers ||= [] if value != Chef::NOT_PASSED @attribute_modifiers << [ attribute_path, value ] elsif block @attribute_modifiers << [ attribute_path, block ] else raise "attribute requires either a value or a block" end end # Patchy tags # tag 'webserver', 'apache', 'myenvironment' def tag(*tags) attribute "tags" do |existing_tags| existing_tags ||= [] tags.each do |tag| unless existing_tags.include?(tag.to_s) existing_tags << tag.to_s end end existing_tags end end def remove_tag(*tags) attribute "tags" do |existing_tags| if existing_tags tags.each do |tag| existing_tags.delete(tag.to_s) end end existing_tags end end # NON-patchy tags # tags :a, :b, :c # removes all other tags def tags(*tags) if tags.size == 0 attribute("tags") else tags = tags[0] if tags.size == 1 && tags[0].is_a?(Array) attribute("tags", tags.map(&:to_s)) end end # Order matters--if two things here are in the wrong order, they will be flipped in the run list # recipe 'apache', 'mysql' # recipe 'recipe@version' # recipe 'recipe' # role '' attr_accessor :run_list_modifiers attr_accessor :run_list_removers def recipe(*recipes) if recipes.size == 0 raise ArgumentError, "At least one recipe must be specified" end @run_list_modifiers ||= [] @run_list_modifiers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") } end def role(*roles) if roles.size == 0 raise ArgumentError, "At least one role must be specified" end @run_list_modifiers ||= [] @run_list_modifiers += roles.map { |role| Chef::RunList::RunListItem.new("role[#{role}]") } end def remove_recipe(*recipes) if recipes.size == 0 raise ArgumentError, "At least one recipe must be specified" end @run_list_removers ||= [] @run_list_removers += recipes.map { |recipe| Chef::RunList::RunListItem.new("recipe[#{recipe}]") } end def remove_role(*roles) if roles.size == 0 raise ArgumentError, "At least one role must be specified" end @run_list_removers ||= [] @run_list_removers += roles.map { |role| Chef::RunList::RunListItem.new("role[#{role}]") } end end end cheffish-15.0.1/lib/cheffish/recipe_dsl.rb000066400000000000000000000111401362335063000203360ustar00rootroot00000000000000require_relative "../cheffish" require "chef/version" require "chef_zero/server" require "chef/chef_fs/chef_fs_data_store" require "chef/chef_fs/config" require_relative "chef_run_data" require_relative "chef_run_listener" require "chef/client" require "chef/config" require "chef_zero/version" require_relative "merged_config" require_relative "../chef/resource/chef_acl" require_relative "../chef/resource/chef_client" require_relative "../chef/resource/chef_container" require_relative "../chef/resource/chef_data_bag" require_relative "../chef/resource/chef_data_bag_item" require_relative "../chef/resource/chef_environment" require_relative "../chef/resource/chef_group" require_relative "../chef/resource/chef_mirror" require_relative "../chef/resource/chef_node" require_relative "../chef/resource/chef_organization" require_relative "../chef/resource/chef_role" require_relative "../chef/resource/chef_user" require_relative "../chef/resource/private_key" require_relative "../chef/resource/public_key" require "chef/util/path_helper" class Chef module DSL module Recipe def with_chef_data_bag(name) run_context.cheffish.with_data_bag(name, &block) end def with_chef_environment(name, &block) run_context.cheffish.with_environment(name, &block) end def with_chef_data_bag_item_encryption(encryption_options, &block) run_context.cheffish.with_data_bag_item_encryption(encryption_options, &block) end def with_chef_server(server_url, options = {}, &block) run_context.cheffish.with_chef_server({ chef_server_url: server_url, options: options }, &block) end def with_chef_local_server(options, &block) options[:host] ||= "127.0.0.1" options[:log_level] ||= Chef::Log.level options[:port] ||= ChefZero::VERSION.to_f >= 2.2 ? 8901.upto(9900) : 8901 # Create the data store chef-zero will use options[:data_store] ||= begin unless options[:chef_repo_path] raise "chef_repo_path must be specified to with_chef_local_server" end # Ensure all paths are given %w{acl client cookbook container data_bag environment group node role}.each do |type| # Set the options as symbol keys and then copy to string keys string_key = "#{type}_path" symbol_key = "#{type}_path".to_sym options[symbol_key] ||= begin if options[:chef_repo_path].is_a?(String) Chef::Util::PathHelper.join(options[:chef_repo_path], "#{type}s") else options[:chef_repo_path].map { |path| Chef::Util::PathHelper.join(path, "#{type}s") } end end # Copy over to string keys for things that use string keys (ChefFS)... # TODO: Fix ChefFS to take symbols or use something that is insensitive to the difference options[string_key] = options[symbol_key] end chef_fs = Chef::ChefFS::Config.new(options).local_fs chef_fs.write_pretty_json = true Chef::ChefFS::ChefFSDataStore.new(chef_fs) end # Start the chef-zero server Chef::Log.info("Starting chef-zero on port #{options[:port]} with repository at #{options[:data_store].chef_fs.fs_description}") chef_zero_server = ChefZero::Server.new(options) chef_zero_server.start_background run_context.cheffish.local_servers << chef_zero_server with_chef_server(chef_zero_server.url, &block) end def get_private_key(name) Cheffish.get_private_key(name, run_context.config) end end end class Config default(:profile) { ENV["CHEF_PROFILE"] || "default" } configurable(:private_keys) default(:private_key_paths) { [ Chef::Util::PathHelper.join(config_dir, "keys"), Chef::Util::PathHelper.join(user_home, ".ssh") ] } default(:private_key_write_path) { private_key_paths.first } end class RunContext def cheffish node.run_state[:cheffish] ||= begin run_data = Cheffish::ChefRunData.new(config) events.register(Cheffish::ChefRunListener.new(node)) run_data end end def config node.run_state[:chef_config] ||= Cheffish.profiled_config(Chef::Config) end end Chef::Client.when_run_starts do |run_status| # Pulling on cheffish_run_data makes it initialize right now run_status.node.run_state[:chef_config] = config = Cheffish.profiled_config(Chef::Config) run_status.node.run_state[:cheffish] = Cheffish::ChefRunData.new(config) run_status.events.register(Cheffish::ChefRunListener.new(run_status.node)) end end cheffish-15.0.1/lib/cheffish/rspec.rb000066400000000000000000000002421362335063000173420ustar00rootroot00000000000000require_relative "rspec/chef_run_support" require_relative "rspec/repository_support" require_relative "rspec/matchers" module Cheffish module RSpec end end cheffish-15.0.1/lib/cheffish/rspec/000077500000000000000000000000001362335063000170175ustar00rootroot00000000000000cheffish-15.0.1/lib/cheffish/rspec/chef_run_support.rb000066400000000000000000000042011362335063000227260ustar00rootroot00000000000000require "chef_zero/rspec" require "chef/server_api" require_relative "repository_support" require "uri" require_relative "../chef_run" require_relative "recipe_run_wrapper" require_relative "matchers" module Cheffish module RSpec module ChefRunSupport include ChefZero::RSpec include RepositorySupport def self.extended(klass) klass.class_eval do include ChefRunSupportInstanceMethods end end def when_the_chef_12_server(*args, **options, &block) if Gem::Version.new(ChefZero::VERSION) >= Gem::Version.new("3.1") when_the_chef_server(*args, osc_compat: false, single_org: false, **options, &block) end end def with_converge(&recipe) before :each do r = recipe(&recipe) r.converge end end module ChefRunSupportInstanceMethods def rest ::Chef::ServerAPI.new(Chef::Config.chef_server_url, api_version: "0") end def get(path, *args) if path[0] == "/" path = URI.join(rest.url, path) end rest.get(path, *args) end def chef_config {} end def expect_recipe(str = nil, file = nil, line = nil, &recipe) r = recipe(str, file, line, &recipe) r.converge expect(r) end def expect_converge(str = nil, file = nil, line = nil, &recipe) expect { converge(str, file, line, &recipe) } end def recipe(str = nil, file = nil, line = nil, &recipe) unless recipe if file && line recipe = proc { eval(str, nil, file, line) } # rubocop:disable Security/Eval else recipe = proc { eval(str) } # rubocop:disable Security/Eval end end RecipeRunWrapper.new(chef_config, &recipe) end def converge(str = nil, file = nil, line = nil, &recipe) r = recipe(str, file, line, &recipe) r.converge r end def chef_client @chef_client ||= ChefRun.new(chef_config) end end end end end cheffish-15.0.1/lib/cheffish/rspec/matchers.rb000066400000000000000000000002661362335063000211560ustar00rootroot00000000000000require_relative "matchers/have_updated" require_relative "matchers/be_idempotent" require_relative "matchers/partially_match" require_relative "matchers/emit_no_warnings_or_errors" cheffish-15.0.1/lib/cheffish/rspec/matchers/000077500000000000000000000000001362335063000206255ustar00rootroot00000000000000cheffish-15.0.1/lib/cheffish/rspec/matchers/be_idempotent.rb000066400000000000000000000005451362335063000237740ustar00rootroot00000000000000require "rspec/matchers" RSpec::Matchers.define :be_idempotent do match do |recipe| @recipe = recipe recipe.reset recipe.converge recipe.up_to_date? end failure_message do "#{@recipe} is not idempotent! Converging it a second time caused updates.\n#{@recipe.output_for_failure_message}" end supports_block_expectations end cheffish-15.0.1/lib/cheffish/rspec/matchers/emit_no_warnings_or_errors.rb000066400000000000000000000005341362335063000266120ustar00rootroot00000000000000require "rspec/matchers" RSpec::Matchers.define :emit_no_warnings_or_errors do match do |recipe| @recipe = recipe @warn_err = recipe.logs.lines.select { |l| l =~ /warn|err/i }.join("\n") @warn_err.empty? end failure_message do "#{@recipe} emitted warnings and errors!\n#{@warn_err}" end supports_block_expectations end cheffish-15.0.1/lib/cheffish/rspec/matchers/have_updated.rb000066400000000000000000000026601362335063000236070ustar00rootroot00000000000000require "rspec/matchers" RSpec::Matchers.define :have_updated do |resource_name, *expected_actions| match do |recipe| @recipe = recipe actual = @recipe.event_sink.events actual_actions = actual.select { |event, resource, action| event == :resource_updated && resource.to_s == resource_name } .map { |event, resource, action| action } expect(actual_actions).to eq(expected_actions) end failure_message do actual = @recipe.event_sink.events updates = actual.select { |event, resource, action| event == :resource_updated }.to_a result = "expected that the chef_run would #{expected_actions.join(",")} #{resource_name}." if updates.size > 0 result << " Actual updates were #{updates.map { |event, resource, action| "#{resource} => #{action.inspect}" }.join(", ")}" else result << " Nothing was updated." end result end failure_message_when_negated do actual = @recipe.event_sink.events updates = actual.select { |event, resource, action| event == :resource_updated }.to_a result = "expected that the chef_run would not #{expected_actions.join(",")} #{resource_name}." if updates.size > 0 result << " Actual updates were #{updates.map { |event, resource, action| "#{resource} => #{action.inspect}" }.join(", ")}" else result << " Nothing was updated." end result end end RSpec::Matchers.define_negated_matcher :not_have_updated, :have_updated cheffish-15.0.1/lib/cheffish/rspec/matchers/partially_match.rb000066400000000000000000000032551362335063000243340ustar00rootroot00000000000000module Cheffish module RSpec module Matchers class PartiallyMatch include ::RSpec::Matchers::Composable def initialize(example, expected) @example = example @expected = expected end def matches?(actual) @actual = actual partially_matches_values(@expected, actual) end def failure_message "expected #{@actual} to match #{@expected}" end def failure_message_when_negated "expected #{@actual} not to match #{@expected}" end protected def partially_matches_values(expected, actual) if Hash === actual return partially_matches_hashes(expected, actual) if Hash === expected || Array === expected elsif Array === expected && Enumerable === actual && !(Struct === actual) return partially_matches_arrays(expected, actual) end return true if actual == expected begin expected === actual rescue ArgumentError # Some objects, like 0-arg lambdas on 1.9+, raise # ArgumentError for `expected === actual`. false end end def partially_matches_hashes(expected, actual) expected.all? { |key, value| partially_matches_values(value, actual[key]) } end def partially_matches_arrays(expected, actual) expected.all? { |e| actual.any? { |a| partially_matches_values(e, a) } } end end end end end module RSpec module Matchers def partially_match(expected) Cheffish::RSpec::Matchers::PartiallyMatch.new(self, expected) end end end cheffish-15.0.1/lib/cheffish/rspec/recipe_run_wrapper.rb000066400000000000000000000060711362335063000232430ustar00rootroot00000000000000require_relative "../chef_run" require "forwardable" module Cheffish module RSpec class RecipeRunWrapper < ChefRun def initialize(chef_config, example: nil, &recipe) super(chef_config) @recipe = recipe @example = example || recipe.binding.eval("self") end attr_reader :recipe attr_reader :example def client unless @client super example = self.example # # Support for both resources and rspec example's let variables: # # In 12.4, the elimination of a bunch of metaprogramming in 12.4 # changed how Chef DSL is defined in code: resource methods are now # explicitly defined in `Chef::DSL::Recipe`. In 12.3, no actual # methods were defined and `respond_to?(:file)` would return false. # If we reach `method_missing` here, it means that we either have a # 12.3-ish resource or we want to call a `let` variable. # @client.instance_eval { @rspec_example = example } def @client.method_missing(name, *args, &block) # rubocop:disable Lint/NestedMethodDefinition # If there is a let variable, call it. This is because in 12.4, # the parent class is going to call respond_to?(name) to find out # if someone was doing weird things, and then call send(). This # would result in an infinite loop, coming right. Back. Here. # A fix to chef is incoming, but we still need this if we want to # work with Chef 12.4. if Gem::Version.new(Chef::VERSION) >= Gem::Version.new("12.4") if @rspec_example.respond_to?(name) return @rspec_example.public_send(name, *args, &block) end end # In 12.3 or below, method_missing was the only way to call # resources. If we are in 12.4, we still need to call the crazy # method_missing metaprogramming because backcompat. begin super rescue NameError if @rspec_example.respond_to?(name) @rspec_example.public_send(name, *args, &block) else raise end end end # This is called by respond_to?, and is required to make sure the # resource knows that we will in fact call the given method. def @client.respond_to_missing?(name, include_private = false) # rubocop:disable Lint/NestedMethodDefinition @rspec_example.respond_to?(name, include_private) || super end # Respond true to is_a?(Chef::Provider) so that Chef::Recipe::DSL.build_resource # will hook resources up to the example let variables as well (via # enclosing_provider). # Please don't hurt me def @client.is_a?(klass) # rubocop:disable Lint/NestedMethodDefinition klass == Chef::Provider || super(klass) end @client.load_block(&recipe) end @client end end end end cheffish-15.0.1/lib/cheffish/rspec/repository_support.rb000066400000000000000000000061001362335063000233540ustar00rootroot00000000000000module Cheffish module RSpec module RepositorySupport def when_the_repository(desc, *tags, &block) context("when the chef repo #{desc}", *tags) do include_context "with a chef repo" extend WhenTheRepositoryClassMethods module_eval(&block) end end ::RSpec.shared_context "with a chef repo" do before :each do raise "Can only create one directory per test" if @repository_dir @repository_dir = Dir.mktmpdir("chef_repo") Chef::Config.chef_repo_path = @repository_dir %w{client cookbook data_bag environment node role user}.each do |object_name| Chef::Config.delete("#{object_name}_path".to_sym) end end after :each do if @repository_dir begin %w{client cookbook data_bag environment node role user}.each do |object_name| Chef::Config.delete("#{object_name}_path".to_sym) end Chef::Config.delete(:chef_repo_path) FileUtils.remove_entry_secure(@repository_dir) ensure @repository_dir = nil end end Dir.chdir(@old_cwd) if @old_cwd end def directory(relative_path, &block) old_parent_path = @parent_path @parent_path = path_to(relative_path) FileUtils.mkdir_p(@parent_path) instance_eval(&block) if block @parent_path = old_parent_path end def file(relative_path, contents) filename = path_to(relative_path) dir = File.dirname(filename) FileUtils.mkdir_p(dir) unless dir == "." File.open(filename, "w") do |file| raw = case contents when Hash, Array JSON.pretty_generate(contents) else contents end file.write(raw) end end def symlink(relative_path, relative_dest) filename = path_to(relative_path) dir = File.dirname(filename) FileUtils.mkdir_p(dir) unless dir == "." dest_filename = path_to(relative_dest) File.symlink(dest_filename, filename) end def path_to(relative_path) File.expand_path(relative_path, (@parent_path || @repository_dir)) end def cwd(relative_path) @old_cwd = Dir.pwd Dir.chdir(path_to(relative_path)) end module WhenTheRepositoryClassMethods def directory(*args, &block) before :each do directory(*args, &block) end end def file(*args, &block) before :each do file(*args, &block) end end def symlink(*args, &block) before :each do symlink(*args, &block) end end def path_to(*args, &block) before :each do file(*args, &block) end end end end end end end cheffish-15.0.1/lib/cheffish/server_api.rb000066400000000000000000000031241362335063000203670ustar00rootroot00000000000000# # Author:: John Keiser () # 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 "chef/version" require "chef/http" require "chef/http/authenticator" require "chef/http/cookie_manager" require "chef/http/decompressor" require "chef/http/json_input" require "chef/http/json_output" if Gem::Version.new(Chef::VERSION) >= Gem::Version.new("11.12") require "chef/http/remote_request_id" end module Cheffish # Exactly like Chef::ServerAPI, but requires you to pass in what keys you want (no defaults) class ServerAPI < Chef::HTTP def initialize(url, options = {}) super(url, options) root_url = URI.parse(url) root_url.path = "" @root_url = root_url.to_s end attr_reader :root_url use Chef::HTTP::JSONInput use Chef::HTTP::JSONOutput use Chef::HTTP::CookieManager use Chef::HTTP::Decompressor use Chef::HTTP::Authenticator if Gem::Version.new(Chef::VERSION) >= Gem::Version.new("11.12") use Chef::HTTP::RemoteRequestID end end end cheffish-15.0.1/lib/cheffish/version.rb000066400000000000000000000000601362335063000177110ustar00rootroot00000000000000module Cheffish VERSION = "15.0.1".freeze end cheffish-15.0.1/lib/cheffish/with_pattern.rb000066400000000000000000000006671362335063000207510ustar00rootroot00000000000000module Cheffish module WithPattern def with(symbol) class_eval <= 2.0) do expect(key_to_format(sample_private_key, :pkcs8sha1fingerprint)).to eq( "88:7e:3a:bd:26:9f:b5:c5:d8:ae:52:f9:df:0b:64:a4:5c:17:0a:87" ) end it "computes the PKCS#1 MD5 public key fingerprint correctly" do expect(key_to_format(sample_public_key, :pkcs1md5fingerprint)).to eq( "1f:e8:da:c1:16:c3:72:7d:90:e2:b7:64:c4:b4:55:20" ) end it "computes the RFC4716 MD5 public key fingerprint correctly" do expect(key_to_format(sample_public_key, :rfc4716md5fingerprint)).to eq( "b0:13:4f:da:cf:8c:dc:a7:4a:1f:d2:3a:51:92:cf:6b" ) end it "defaults to the PKCS#1 MD5 public key fingerprint" do expect(key_to_format(sample_public_key, :fingerprint)).to eq( key_to_format(sample_public_key, :pkcs1md5fingerprint) ) end end end cheffish-15.0.1/spec/functional/merged_config_spec.rb000066400000000000000000000037161362335063000226100ustar00rootroot00000000000000require "cheffish/merged_config" describe "merged_config" do let(:config) do Cheffish::MergedConfig.new({ test: "val" }) end let(:collision) do c1 = { :test1 => "c1.1", "test2" => "c1.2" } c2 = { "test1" => "c2.1", "test3" => "c2.3" } Cheffish::MergedConfig.new(c1, c2) end let(:config_mismatch) do c1 = { test: { test: "val" } } c2 = { test: [2, 3, 4] } Cheffish::MergedConfig.new(c1, c2) end let(:config_hashes) do c1 = { test: { test: "val" } } c2 = { test: { test2: "val2" } } Cheffish::MergedConfig.new(c1, c2) end let(:nested_config) do c1 = { test: { test: "val" } } c2 = { test: { test2: "val2" } } mc = Cheffish::MergedConfig.new(c2) Cheffish::MergedConfig.new(c1, mc) end let(:empty_config) do Cheffish::MergedConfig.new end it "returns value in config" do expect(config.test).to eq("val") end it "raises a NoMethodError if calling an unknown method with arguments" do expect { config.merge({ some: "hash" }) }.to raise_error(NoMethodError) end it "has an informative string representation" do expect((config).to_s).to eq("{\"test\"=>\"val\"}") end it "has indifferent str/sym access" do expect(config["test"]).to eq("val") end it "respects precedence between the different configs" do expect(collision["test1"]).to eq("c1.1") expect(collision[:test1]).to eq("c1.1") end it "merges the configs" do expect(collision[:test2]).to eq("c1.2") expect(collision[:test3]).to eq("c2.3") end it "handle merged value type mismatch" do expect(config_mismatch[:test]).to eq("test" => "val") end it "merges values when they're hashes" do expect(config_hashes[:test].keys).to eq(%w{test test2}) end it "supports nested merged configs" do expect(nested_config[:test].keys).to eq(%w{test test2}) end it "supports empty?" do expect(empty_config.empty?).to eq true expect(nested_config.empty?).to eq false end end cheffish-15.0.1/spec/functional/server_api_spec.rb000066400000000000000000000004401362335063000221460ustar00rootroot00000000000000require "cheffish" describe "api version" do let(:server_api) do Cheffish.chef_server_api({ chef_server_url: "my.chef.server" }) end it "is pinned to 0" do expect(Cheffish::ServerAPI).to receive(:new).with("my.chef.server", { api_version: "0" }) server_api end end cheffish-15.0.1/spec/integration/000077500000000000000000000000001362335063000166335ustar00rootroot00000000000000cheffish-15.0.1/spec/integration/chef_acl_spec.rb000066400000000000000000001032001362335063000217120ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" require "chef_zero/version" require "uri" if Gem::Version.new(ChefZero::VERSION) >= Gem::Version.new("3.1") describe Chef::Resource::ChefAcl do extend Cheffish::RSpec::ChefRunSupport # let(:chef_config) { super().merge(log_level: :debug, stdout: STDOUT, stderr: STDERR, log_location: STDOUT) } context "Rights attributes" do when_the_chef_server "has a node named x", osc_compat: false do node "x", {} it 'Converging chef_acl "nodes/x" changes nothing' do expect_recipe do chef_acl "nodes/x" end.to be_up_to_date expect(get("nodes/x/_acl")).to partially_match({}) end it 'Converging chef_acl "nodes/x" with "complete true" and no rights raises an error' do expect_converge do chef_acl "nodes/x" do complete true end end.to raise_error(RuntimeError) end it "Removing all :grant rights from a node raises an error" do expect_converge do chef_acl "nodes/x" do remove_rights :grant, users: %w{pivotal}, groups: %w{admins users clients} end end.to raise_error(RuntimeError) end context 'and a user "blarghle"' do user "blarghle", {} it 'Converging chef_acl "nodes/x" with user "blarghle" adds the user' do expect_recipe do chef_acl "nodes/x" do rights :read, users: %w{blarghle} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match("read" => { "actors" => %w{blarghle} }) end it 'Converging chef_acl "nodes/x" with "complete true" removes all ACLs except those specified' do expect_recipe do chef_acl "nodes/x" do rights :grant, users: %w{blarghle} complete true end end.to be_updated expect(get("nodes/x/_acl")).to eq( "create" => { "actors" => [], "groups" => [] }, "read" => { "actors" => [], "groups" => [] }, "update" => { "actors" => [], "groups" => [] }, "delete" => { "actors" => [], "groups" => [] }, "grant" => { "actors" => ["blarghle"], "groups" => [] } ) end end it 'Converging chef_acl "nodes/x" with "complete true" removes all ACLs except those specified in :all' do expect_recipe do chef_acl "nodes/x" do rights :all, users: %w{blarghle} complete true end end.to be_updated expect(get("nodes/x/_acl")).to eq( "create" => { "actors" => ["blarghle"], "groups" => [] }, "read" => { "actors" => ["blarghle"], "groups" => [] }, "update" => { "actors" => ["blarghle"], "groups" => [] }, "delete" => { "actors" => ["blarghle"], "groups" => [] }, "grant" => { "actors" => ["blarghle"], "groups" => [] } ) end context 'and a client "blarghle"' do user "blarghle", {} it 'Converging chef_acl "nodes/x" with client "blarghle" adds the client' do expect_recipe do chef_acl "nodes/x" do rights :read, clients: %w{blarghle} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match("read" => { "actors" => %w{blarghle} }) end end context 'and a group "blarghle"' do group "blarghle", {} it 'Converging chef_acl "nodes/x" with group "blarghle" adds the group' do expect_recipe do chef_acl "nodes/x" do rights :read, groups: %w{blarghle} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match("read" => { "groups" => %w{blarghle} }) end end context "and multiple users and groups" do user "u1", {} user "u2", {} user "u3", {} client "c1", {} client "c2", {} client "c3", {} group "g1", {} group "g2", {} group "g3", {} it "Converging chef_acls should ignore order of the values in the acls" do expect_recipe do chef_acl "nodes/x" do rights :create, users: %w{u1 u2 u3}, clients: %w{c1 c2 c3}, groups: %w{g1 g2 g3} end end.to be_updated expect_recipe do chef_acl "nodes/x" do rights :create, users: %w{u2 u3 u1}, clients: %w{c3 c2 c1}, groups: %w{g1 g2 g3} end end.to be_up_to_date end it 'Converging chef_acl "nodes/x" with multiple groups, users and clients in an acl makes the appropriate changes' do expect_recipe do chef_acl "nodes/x" do rights :create, users: %w{u1 u2 u3}, clients: %w{c1 c2 c3}, groups: %w{g1 g2 g3} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match( "create" => { "groups" => %w{g1 g2 g3}, "actors" => %w{u1 u2 u3 c1 c2 c3} } ) end it 'Converging chef_acl "nodes/x" with multiple groups, users and clients across multiple "rights" groups makes the appropriate changes' do expect_recipe do chef_acl "nodes/x" do rights :create, users: %w{u1}, clients: %w{c1}, groups: %w{g1} rights :create, users: %w{u2 u3}, clients: %w{c2 c3}, groups: %w{g2} rights :read, users: %w{u1} rights :read, groups: %w{g1} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match( "create" => { "groups" => %w{g1 g2}, "actors" => %w{u1 u2 u3 c1 c2 c3} }, "read" => { "groups" => %w{g1}, "actors" => %w{u1} } ) end it 'Converging chef_acl "nodes/x" with rights [ :read, :create, :update, :delete, :grant ] modifies all rights' do expect_recipe do chef_acl "nodes/x" do rights %i{create read update delete grant}, users: %w{u1 u2}, clients: %w{c1}, groups: %w{g1} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match( "create" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} }, "read" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} }, "update" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} }, "delete" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} }, "grant" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} } ) end it 'Converging chef_acl "nodes/x" with rights :all modifies all rights' do expect_recipe do chef_acl "nodes/x" do rights :all, users: %w{u1 u2}, clients: %w{c1}, groups: %w{g1} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match( "create" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} }, "read" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} }, "update" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} }, "delete" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} }, "grant" => { "groups" => %w{g1}, "actors" => %w{u1 u2 c1} } ) end end it 'Converging chef_acl "nodes/y" throws a 404' do expect_converge do chef_acl "nodes/y" end.to raise_error(Net::HTTPServerException) end end when_the_chef_server "has a node named x with user blarghle in its acl", osc_compat: false do user "blarghle", {} node "x", {} do acl "read" => { "actors" => %w{blarghle} } end it 'Converging chef_acl "nodes/x" with that user changes nothing' do expect_recipe do chef_acl "nodes/x" do rights :read, users: %w{blarghle} end end.to be_up_to_date expect(get("nodes/x/_acl")).to partially_match({}) end end when_the_chef_server "has a node named x with users foo and bar in all its acls", osc_compat: false do user "foo", {} user "bar", {} node "x", {} do acl "create" => { "actors" => %w{foo bar} }, "read" => { "actors" => %w{foo bar} }, "update" => { "actors" => %w{foo bar} }, "delete" => { "actors" => %w{foo bar} }, "grant" => { "actors" => %w{foo bar} } end it 'Converging chef_acl "nodes/x" with remove_rights :all removes foo from everything' do expect_recipe do chef_acl "nodes/x" do remove_rights :all, users: %w{foo} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match( "create" => { "actors" => exclude("foo") }, "read" => { "actors" => exclude("foo") }, "update" => { "actors" => exclude("foo") }, "delete" => { "actors" => exclude("foo") }, "grant" => { "actors" => exclude("foo") } ) end end ::RSpec::Matchers.define_negated_matcher :exclude, :include context "recursive" do when_the_chef_server "has a nodes container with user blarghle in its acl", osc_compat: false do user "blarghle", {} acl_for "containers/nodes", "read" => { "actors" => %w{blarghle} } node "x", {} do acl "read" => { "actors" => [] } end it 'Converging chef_acl "nodes" makes no changes' do expect do expect_recipe do chef_acl "nodes" do rights :read, users: %w{blarghle} end end.to be_up_to_date end.to((not_change { get("containers/nodes/_acl") }).and(not_change { get("nodes/x/_acl") })) end RSpec::Matchers.define_negated_matcher :not_change, :change it 'Converging chef_acl "nodes" with recursive :on_change makes no changes' do expect do expect_recipe do chef_acl "nodes" do rights :read, users: %w{blarghle} recursive :on_change end end.to be_up_to_date end.to((not_change { get("containers/nodes/_acl") }).and(not_change { get("nodes/x/_acl") })) end it 'Converging chef_acl "nodes" with recursive true changes nodes/x\'s acls' do expect_recipe do chef_acl "nodes" do rights :read, users: %w{blarghle} recursive true end end.to be_updated expect(get("nodes/x/_acl")).to partially_match("read" => { "actors" => %w{blarghle} }) end it 'Converging chef_acl "" with recursive false does not change nodes/x\'s acls' do expect_recipe do chef_acl "" do rights :read, users: %w{blarghle} recursive false end end.to be_updated expect(get("containers/nodes/_acl")).to partially_match({}) expect(get("nodes/x/_acl")).to partially_match({}) end it 'Converging chef_acl "" with recursive :on_change does not change nodes/x\'s acls' do expect_recipe do chef_acl "" do rights :read, users: %w{blarghle} recursive :on_change end end.to be_updated expect(get("containers/nodes/_acl")).to partially_match({}) expect(get("nodes/x/_acl")).to partially_match({}) end it 'Converging chef_acl "" with recursive true changes nodes/x\'s acls' do expect_recipe do chef_acl "" do rights :read, users: %w{blarghle} recursive true end end.to be_updated expect(get("/organizations/_acl")).to partially_match("read" => { "actors" => %w{blarghle} }) expect(get("nodes/x/_acl")).to partially_match("read" => { "actors" => %w{blarghle} }) end end end end context "ACLs on each type of thing" do when_the_chef_server "has an organization named foo", osc_compat: false, single_org: false do organization "foo" do user "u", {} client "x", {} container "x", {} cookbook "x", "1.0.0", {} data_bag "x", { "y" => {} } environment "x", {} group "x", {} node "x", {} role "x", {} sandbox "x", {} user "x", {} end organization "bar" do user "u", {} node "x", {} end context "and the chef server URL points at /organizations/foo" do before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end context "relative paths" do it "chef_acl 'nodes/x' changes the acls" do expect_recipe do chef_acl "nodes/x" do rights :read, users: %w{u} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it "chef_acl '*/*' changes the acls" do expect_recipe do chef_acl "*/*" do rights :read, users: %w{u} end end.to be_updated %w{clients containers cookbooks data environments groups nodes roles}.each do |type| expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match( "read" => { "actors" => %w{u} } ) end end end context "absolute paths" do %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| it "chef_acl '/organizations/foo/#{type}/x' changes the acl" do expect_recipe do chef_acl "/organizations/foo/#{type}/x" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| it "chef_acl '/organizations/foo/#{type}/x' changes the acl" do expect_recipe do chef_acl "/organizations/foo/#{type}/x" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end %w{clients containers cookbooks data environments groups nodes roles}.each do |type| it "chef_acl '/*/*/#{type}/*' changes the acl" do expect_recipe do chef_acl "/*/*/#{type}/*" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end it "chef_acl '/*/*/*/x' changes the acls" do expect_recipe do chef_acl "/*/*/*/x" do rights :read, users: %w{u} end end.to be_updated %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match( "read" => { "actors" => %w{u} } ) end end it "chef_acl '/*/*/*/*' changes the acls" do expect_recipe do chef_acl "/*/*/*/*" do rights :read, users: %w{u} end end.to be_updated %w{clients containers cookbooks data environments groups nodes roles}.each do |type| expect(get("/organizations/foo/#{type}/x/_acl")).to partially_match( "read" => { "actors" => %w{u} } ) end end it 'chef_acl "/organizations/foo/data_bags/x" changes the acl' do expect_recipe do chef_acl "/organizations/foo/data_bags/x" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/data/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "/*/*/data_bags/*" changes the acl' do expect_recipe do chef_acl "/*/*/data_bags/*" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/data/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it "chef_acl '/organizations/foo/cookbooks/x/1.0.0' raises an error" do expect_converge do chef_acl "/organizations/foo/cookbooks/x/1.0.0" do rights :read, users: %w{u} end end.to raise_error(%r{ACLs cannot be set on children of /organizations/foo/cookbooks/x}) end it "chef_acl '/organizations/foo/cookbooks/*/*' raises an error" do pending expect_converge do chef_acl "/organizations/foo/cookbooks/*/*" do rights :read, users: %w{u} end end.to raise_error(%r{ACLs cannot be set on children of /organizations/foo/cookbooks/*}) end it 'chef_acl "/organizations/foo/data/x/y" raises an error' do expect_converge do chef_acl "/organizations/foo/data/x/y" do rights :read, users: %w{u} end end.to raise_error(%r{ACLs cannot be set on children of /organizations/foo/data/x}) end it 'chef_acl "/organizations/foo/data/*/*" raises an error' do pending expect_converge do chef_acl "/organizations/foo/data/*/*" do rights :read, users: %w{u} end end.to raise_error(%r{ACLs cannot be set on children of /organizations/foo/data/*}) end it 'chef_acl "/organizations/foo" changes the acl' do expect_recipe do chef_acl "/organizations/foo" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/organizations/_acl")).to partially_match("read" => { "actors" => %w{u} }) expect(get("/organizations/foo/nodes/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "/organizations/*" changes the acl' do expect_recipe do chef_acl "/organizations/*" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/organizations/_acl")).to partially_match("read" => { "actors" => %w{u} }) expect(get("/organizations/foo/nodes/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "/users/x" changes the acl' do expect_recipe do chef_acl "/users/x" do rights :read, users: %w{u} end end.to be_updated expect(get("/users/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "/users/*" changes the acl' do expect_recipe do chef_acl "/users/*" do rights :read, users: %w{u} end end.to be_updated expect(get("/users/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "/*/x" changes the acl' do expect_recipe do chef_acl "/*/x" do rights :read, users: %w{u} end end.to be_updated expect(get("/users/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "/*/*" changes the acl' do expect_recipe do chef_acl "/*/*" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/organizations/_acl")).to partially_match("read" => { "actors" => %w{u} }) expect(get("/users/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end end context "and the chef server URL points at /organizations/bar" do before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url.to_s, "/organizations/bar").to_s end it "chef_acl '/organizations/foo/nodes/*' changes the acl" do expect_recipe do chef_acl "/organizations/foo/nodes/*" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/nodes/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end context "and the chef server URL points at /" do before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url.to_s, "/").to_s end it "chef_acl '/organizations/foo/nodes/*' changes the acl" do expect_recipe do chef_acl "/organizations/foo/nodes/*" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/nodes/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end end when_the_chef_server 'has a user "u" in single org mode', osc_compat: false do user "u", {} client "x", {} container "x", {} cookbook "x", "1.0.0", {} data_bag "x", { "y" => {} } environment "x", {} group "x", {} node "x", {} role "x", {} sandbox "x", {} user "x", {} %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| it "chef_acl #{type}/x' changes the acl" do expect_recipe do chef_acl "#{type}/x" do rights :read, users: %w{u} end end.to be_updated expect(get("#{type}/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end %w{clients containers cookbooks data environments groups nodes roles}.each do |type| it "chef_acl '#{type}/*' changes the acl" do expect_recipe do chef_acl "#{type}/*" do rights :read, users: %w{u} end end.to be_updated expect(get("#{type}/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end it "chef_acl '*/x' changes the acls" do expect_recipe do chef_acl "*/x" do rights :read, users: %w{u} end end.to be_updated %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| expect(get("#{type}/x/_acl")).to partially_match( "read" => { "actors" => %w{u} } ) end end it "chef_acl '*/*' changes the acls" do expect_recipe do chef_acl "*/*" do rights :read, users: %w{u} end end.to be_updated %w{clients containers cookbooks data environments groups nodes roles}.each do |type| expect(get("#{type}/x/_acl")).to partially_match( "read" => { "actors" => %w{u} } ) end end it "chef_acl 'groups/*' changes the acl" do expect_recipe do chef_acl "groups/*" do rights :read, users: %w{u} end end.to be_updated %w{admins billing-admins clients users x}.each do |n| expect(get("groups/#{n}/_acl")).to partially_match( "read" => { "actors" => %w{u} } ) end end it 'chef_acl "data_bags/x" changes the acl' do expect_recipe do chef_acl "data_bags/x" do rights :read, users: %w{u} end end.to be_updated expect(get("data/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "data_bags/*" changes the acl' do expect_recipe do chef_acl "data_bags/*" do rights :read, users: %w{u} end end.to be_updated expect(get("data/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "" changes the organization acl' do expect_recipe do chef_acl "" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/_acl")).to partially_match("read" => { "actors" => %w{u} }) expect(get("nodes/x/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end end context "ACLs on each container type" do when_the_chef_server "has an organization named foo", osc_compat: false, single_org: false do organization "foo" do user "u", {} client "x", {} container "x", {} cookbook "x", "1.0.0", {} data_bag "x", { "y" => {} } environment "x", {} group "x", {} node "x", {} role "x", {} sandbox "x", {} user "x", {} end %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| it "chef_acl '/organizations/foo/#{type}' changes the acl" do expect_recipe do chef_acl "/organizations/foo/#{type}" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/containers/#{type}/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end %w{clients containers cookbooks data environments groups nodes roles}.each do |type| it "chef_acl '/*/*/#{type}' changes the acl" do expect_recipe do chef_acl "/*/*/#{type}" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/containers/#{type}/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end it "chef_acl '/*/*/*' changes the acls" do expect_recipe do chef_acl "/*/*/*" do rights :read, users: %w{u} end end.to be_updated %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| expect(get("/organizations/foo/containers/#{type}/_acl")).to partially_match( "read" => { "actors" => %w{u} } ) end end it 'chef_acl "/organizations/foo/data_bags" changes the acl' do expect_recipe do chef_acl "/organizations/foo/data_bags" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/containers/data/_acl")).to partially_match("read" => { "actors" => %w{u} }) end it 'chef_acl "/*/*/data_bags" changes the acl' do expect_recipe do chef_acl "/*/*/data_bags" do rights :read, users: %w{u} end end.to be_updated expect(get("/organizations/foo/containers/data/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end when_the_chef_server 'has a user "u" in single org mode', osc_compat: false do user "u", {} client "x", {} container "x", {} cookbook "x", "1.0.0", {} data_bag "x", { "y" => {} } environment "x", {} group "x", {} node "x", {} role "x", {} sandbox "x", {} user "x", {} %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| it "chef_acl #{type}' changes the acl" do expect_recipe do chef_acl type.to_s do rights :read, users: %w{u} end end.to be_updated expect(get("containers/#{type}/_acl")).to partially_match("read" => { "actors" => %w{u} }) end end it "chef_acl '*' changes the acls" do expect_recipe do chef_acl "*" do rights :read, users: %w{u} end end.to be_updated %w{clients containers cookbooks data environments groups nodes roles sandboxes}.each do |type| expect(get("containers/#{type}/_acl")).to partially_match( "read" => { "actors" => %w{u} } ) end end end end context "remove_rights" do when_the_chef_server 'has a node "x" with "u", "c" and "g" in its acl', osc_compat: false do user "u", {} user "u2", {} client "c", {} client "c2", {} group "g", {} group "g2", {} node "x", {} do acl "create" => { "actors" => %w{u c}, "groups" => [ "g" ] }, "read" => { "actors" => %w{u c}, "groups" => [ "g" ] }, "update" => { "actors" => %w{u c}, "groups" => [ "g" ] } end it 'chef_acl with remove_rights "u" removes the user\'s rights' do expect_recipe do chef_acl "nodes/x" do remove_rights :read, users: %w{u} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match("read" => { "actors" => exclude("u") }) end it 'chef_acl with remove_rights "c" removes the client\'s rights' do expect_recipe do chef_acl "nodes/x" do remove_rights :read, clients: %w{c} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match("read" => { "actors" => exclude("c") }) end it 'chef_acl with remove_rights "g" removes the group\'s rights' do expect_recipe do chef_acl "nodes/x" do remove_rights :read, groups: %w{g} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match( "read" => { "groups" => exclude("g") } ) end it 'chef_acl with remove_rights [ :create, :read ], "u", "c", "g" removes all three' do expect_recipe do chef_acl "nodes/x" do remove_rights %i{create read}, users: %w{u}, clients: %w{c}, groups: %w{g} end end.to be_updated expect(get("nodes/x/_acl")).to partially_match( "create" => { "actors" => exclude("u").and(exclude("c")), "groups" => exclude("g") }, "read" => { "actors" => exclude("u").and(exclude("c")), "groups" => exclude("g") } ) end it 'chef_acl with remove_rights "u2", "c2", "g2" has no effect' do expect do expect_recipe do chef_acl "nodes/x" do remove_rights :read, users: %w{u2}, clients: %w{c2}, groups: %w{g2} end end.to be_up_to_date end.not_to(change { get("nodes/x/_acl") }) end end end when_the_chef_server "has a node named data_bags", osc_compat: false do user "blarghle", {} node "data_bags", {} it 'Converging chef_acl "nodes/data_bags" with user "blarghle" adds the user' do expect_recipe do chef_acl "nodes/data_bags" do rights :read, users: %w{blarghle} end end.to be_updated expect(get("nodes/data_bags/_acl")).to partially_match("read" => { "actors" => %w{blarghle} }) end end when_the_chef_server "has a node named data_bags in multi-org mode", osc_compat: false, single_org: false do user "blarghle", {} organization "foo" do node "data_bags", {} end it 'Converging chef_acl "/organizations/foo/nodes/data_bags" with user "blarghle" adds the user' do expect_recipe do chef_acl "/organizations/foo/nodes/data_bags" do rights :read, users: %w{blarghle} end end.to be_updated expect(get("/organizations/foo/nodes/data_bags/_acl")).to partially_match("read" => { "actors" => %w{blarghle} }) end end when_the_chef_server "has a user named data_bags in multi-org mode", osc_compat: false, single_org: false do user "data_bags", {} user "blarghle", {} it 'Converging chef_acl "/users/data_bags" with user "blarghle" adds the user' do expect_recipe do chef_acl "/users/data_bags" do rights :read, users: %w{blarghle} end end.to be_updated expect(get("/users/data_bags/_acl")).to partially_match("read" => { "actors" => %w{blarghle} }) end end end end cheffish-15.0.1/spec/integration/chef_client_spec.rb000066400000000000000000000067071362335063000224470ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" require "support/key_support" require "chef/resource/chef_client" repo_path = Dir.mktmpdir("chef_repo") describe Chef::Resource::ChefClient do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server "is in multi-org mode" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end context "and is empty" do context "and we have a private key with a path" do with_converge do private_key "#{repo_path}/blah.pem" end context 'and we run a recipe that creates client "blah"' do it "the client gets created" do expect_recipe do chef_client "blah" do source_key_path "#{repo_path}/blah.pem" end end.to have_updated "chef_client[blah]", :create client = get("clients/blah") expect(client["name"]).to eq("blah") key, _format = Cheffish::KeyFormatter.decode(client["public_key"]) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end context 'and we run a recipe that creates client "blah" with output_key_path' do with_converge do chef_client "blah" do source_key_path "#{repo_path}/blah.pem" output_key_path "#{repo_path}/blah.pub" end end it "the output public key gets created" do expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for("#{repo_path}/blah.pem") end end end context "and a private_key 'blah' resource" do before :each do Chef::Config.private_key_paths = [ repo_path ] end with_converge do private_key "blah" end context "and a chef_client 'foobar' resource with source_key_path 'blah'" do it "the client is accessible via the given private key" do expect_recipe do chef_client "foobar" do source_key_path "blah" end end.to have_updated "chef_client[foobar]", :create client = get("clients/foobar") key, _format = Cheffish::KeyFormatter.decode(client["public_key"]) expect(key).to be_public_key_for("#{repo_path}/blah.pem") private_key = Cheffish::KeyFormatter.decode(Cheffish.get_private_key("blah")) expect(key).to be_public_key_for(private_key) end end end end end when_the_chef_server "is in OSC mode" do context "and is empty" do context "and we have a private key with a path" do with_converge do private_key "#{repo_path}/blah.pem" end context 'and we run a recipe that creates client "blah"' do it "the client gets created" do expect_recipe do chef_client "blah" do source_key_path "#{repo_path}/blah.pem" end end.to have_updated "chef_client[blah]", :create client = get("clients/blah") expect(client["name"]).to eq("blah") key, _format = Cheffish::KeyFormatter.decode(client["public_key"]) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end end end end end cheffish-15.0.1/spec/integration/chef_container_spec.rb000066400000000000000000000015631362335063000231460ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe Chef::Resource::ChefContainer do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server "is in multi-org mode" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end it 'Converging chef_container "x" creates the container' do expect_recipe do chef_container "x" end.to have_updated("chef_container[x]", :create) expect { get("containers/x") }.not_to raise_error end context "and already has a container named x" do container "x", {} it 'Converging chef_container "x" changes nothing' do expect_recipe do chef_container "x" end.not_to have_updated("chef_container[x]", :create) end end end end cheffish-15.0.1/spec/integration/chef_data_bag_item_spec.rb000066400000000000000000000014361362335063000237230ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe Chef::Resource::ChefDataBagItem do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server "foo" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end context 'when data bag "bag" exists' do with_converge { chef_data_bag "bag" } it 'runs a recipe that creates a chef_data_bag_item "bag/item"' do expect_recipe do chef_data_bag_item "bag/item" end.to have_updated "chef_data_bag_item[bag/item]", :create # expect(get('data_bags/bag')['name']).to eq('bag') # expect(get('data_bags/bag/item')['id']).to eq('item') end end end end cheffish-15.0.1/spec/integration/chef_group_spec.rb000066400000000000000000000212321362335063000223130ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe Chef::Resource::ChefGroup do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server "is in multi-org mode" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end context "and is empty" do group "g", {} user "u", {} client "c", {} it 'Converging chef_group "x" creates the group with no members' do expect_recipe do chef_group "x" end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => [], "groups" => [], "users" => [], "clients" => [], }) end it 'chef_group "x" action :delete does nothing' do expect_recipe do chef_group "x" do action :delete end end.to not_have_updated("chef_group[x]", :delete).and not_have_updated("chef_group[x]", :create) expect { get("groups/x") }.to raise_error(Net::HTTPServerException) end it 'Converging chef_group "x" creates the group with the given members' do expect_recipe do chef_group "x" do groups "g" users "u" clients "c" end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c u}, "groups" => %w{g}, "users" => %w{u}, "clients" => %w{c}, }) end end context "and has a group named x" do group "g", {} group "g2", {} group "g3", {} group "g4", {} user "u", {} user "u2", {} user "u3", {} user "u4", {} client "c", {} client "c2", {} client "c3", {} client "c4", {} group "x", { "users" => %w{u u2}, "clients" => %w{c c2}, "groups" => %w{g g2}, } it 'Converging chef_group "x" changes nothing' do expect_recipe do chef_group "x" end.not_to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c c2 u u2}, "groups" => %w{g g2}, "users" => %w{u u2}, "clients" => %w{c c2}, }) end it 'chef_group "x" action :delete deletes the group' do expect_recipe do chef_group "x" do action :delete end end.to have_updated("chef_group[x]", :delete) expect { get("groups/x") }.to raise_error(Net::HTTPServerException) end it 'Converging chef_group "x" with existing users changes nothing' do expect_recipe do chef_group "x" do users "u" clients "c" groups "g" end end.not_to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c c2 u u2}, "groups" => %w{g g2}, "users" => %w{u u2}, "clients" => %w{c c2}, }) end it 'Converging chef_group "x" adds new users' do expect_recipe do chef_group "x" do users "u3" clients "c3" groups "g3" end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c c2 c3 u u2 u3}, "groups" => %w{g g2 g3}, "users" => %w{u u2 u3}, "clients" => %w{c c2 c3}, }) end it 'Converging chef_group "x" with multiple users adds new users' do expect_recipe do chef_group "x" do users "u3", "u4" clients "c3", "c4" groups "g3", "g4" end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c c2 c3 c4 u u2 u3 u4}, "groups" => %w{g g2 g3 g4}, "users" => %w{u u2 u3 u4}, "clients" => %w{c c2 c3 c4}, }) end it 'Converging chef_group "x" with multiple users in an array adds new users' do expect_recipe do chef_group "x" do users %w{u3 u4} clients %w{c3 c4} groups %w{g3 g4} end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c c2 c3 c4 u u2 u3 u4}, "groups" => %w{g g2 g3 g4}, "users" => %w{u u2 u3 u4}, "clients" => %w{c c2 c3 c4}, }) end it 'Converging chef_group "x" with multiple users declarations adds new users' do expect_recipe do chef_group "x" do users "u3" users "u4" clients "c3" clients "c4" groups "g3" groups "g4" end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c c2 c3 c4 u u2 u3 u4}, "groups" => %w{g g2 g3 g4}, "users" => %w{u u2 u3 u4}, "clients" => %w{c c2 c3 c4}, }) end it 'Converging chef_group "x" removes desired users' do expect_recipe do chef_group "x" do remove_users "u2" remove_clients "c2" remove_groups "g2" end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c u}, "groups" => %w{g}, "users" => %w{u}, "clients" => %w{c}, }) end it 'Converging chef_group "x" with multiple users removes desired users' do expect_recipe do chef_group "x" do remove_users "u", "u2" remove_clients "c", "c2" remove_groups "g", "g2" end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => [], "groups" => [], "users" => [], "clients" => [], }) end it 'Converging chef_group "x" with multiple users in an array removes desired users' do expect_recipe do chef_group "x" do remove_users %w{u u2} remove_clients %w{c c2} remove_groups %w{g g2} end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => [], "groups" => [], "users" => [], "clients" => [], }) end it 'Converging chef_group "x" with multiple remove_ declarations removes desired users' do expect_recipe do chef_group "x" do remove_users "u" remove_users "u2" remove_clients "c" remove_clients "c2" remove_groups "g" remove_groups "g2" end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => [], "groups" => [], "users" => [], "clients" => [], }) end it 'Converging chef_group "x" adds and removes desired users' do expect_recipe do chef_group "x" do users "u3" clients "c3" groups "g3" remove_users "u" remove_clients "c" remove_groups "g" end end.to have_updated("chef_group[x]", :create) expect(get("groups/x")).to eq({ "name" => "x", "groupname" => "x", "orgname" => "foo", "actors" => %w{c2 c3 u2 u3}, "groups" => %w{g2 g3}, "users" => %w{u2 u3}, "clients" => %w{c2 c3}, }) end end end end cheffish-15.0.1/spec/integration/chef_mirror_spec.rb000066400000000000000000000453171362335063000225030ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe Chef::Resource::ChefMirror do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server "is in multi-org mode" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end describe "basic download and upload" do when_the_repository "is full of stuff" do file "nodes/x.json", {} file "roles/x.json", {} directory "cookbooks/x" do file "metadata.rb", 'name "x"; version "2.0.0"' end it "Download grabs defaults" do expect_recipe do chef_mirror "" do action :download end end.to have_updated("chef_mirror[]", :download) expect(File.exist?(path_to("groups/admins.json"))).to be true expect(File.exist?(path_to("environments/_default.json"))).to be true end it "Upload uploads everything" do expect_recipe do chef_mirror "" do action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("nodes/x") }.not_to raise_error expect { get("roles/x") }.not_to raise_error expect { get("cookbooks/x/2.0.0") }.not_to raise_error end it "chef_mirror with concurrency 0 fails with a reasonable message" do expect do converge do chef_mirror "" do concurrency 0 action :download end end end.to raise_error(/chef_mirror.concurrency must be above 0/) end end end context "and the Chef server has a node and role in it" do node "x", {} role "x", {} when_the_repository "is empty" do it "Download grabs the node and role" do expect_recipe do chef_mirror "" do action :download end end.to have_updated("chef_mirror[]", :download) expect(File.exist?(path_to("nodes/x.json"))).to be true expect(File.exist?(path_to("roles/x.json"))).to be true end it "Upload uploads nothing" do expect_recipe do chef_mirror "" do action :upload end end.not_to have_updated("chef_mirror[]", :upload) end end end context "and the Chef server has nodes and roles named x" do node "x", {} role "x", {} when_the_repository "has nodes and roles named y" do file "nodes/y.json", {} file "roles/y.json", {} it "Download grabs the x's" do expect_recipe do chef_mirror "" do action :download end end.to have_updated("chef_mirror[]", :download) expect(File.exist?(path_to("nodes/x.json"))).to be true expect(File.exist?(path_to("roles/x.json"))).to be true expect(File.exist?(path_to("nodes/y.json"))).to be true expect(File.exist?(path_to("roles/y.json"))).to be true end it "Upload uploads the y's" do expect_recipe do chef_mirror "" do action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("nodes/x") }.not_to raise_error expect { get("roles/x") }.not_to raise_error expect { get("nodes/y") }.not_to raise_error expect { get("roles/y") }.not_to raise_error end it "Download with purge grabs the x's and deletes the y's" do expect_recipe do chef_mirror "" do purge true action :download end end.to have_updated("chef_mirror[]", :download) expect(File.exist?(path_to("nodes/x.json"))).to be true expect(File.exist?(path_to("roles/x.json"))).to be true end it "Upload with :purge uploads the y's and deletes the x's" do expect_recipe do chef_mirror "*/*.json" do purge true action :upload end end.to have_updated("chef_mirror[*/*.json]", :upload) expect { get("nodes/y") }.not_to raise_error expect { get("roles/y") }.not_to raise_error end end end describe "chef_repo_path" do when_the_repository "has stuff but no chef_repo_path" do file "repo/nodes/x.json", {} file "repo/roles/x.json", {} file "repo2/nodes/y.json", {} file "repo2/roles/y.json", {} before do Chef::Config.delete(:chef_repo_path) Chef::Config.delete(:node_path) Chef::Config.delete(:cookbook_path) Chef::Config.delete(:role_path) end it "Upload with chef_repo_path('repo') uploads everything" do repo_path = path_to("repo") expect_recipe do chef_mirror "" do chef_repo_path repo_path action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("nodes/x") }.not_to raise_error expect { get("roles/x") }.not_to raise_error expect { get("nodes/y") }.to raise_error(/404/) expect { get("roles/y") }.to raise_error(/404/) end it "Upload with chef_repo_path(:chef_repo_path) with multiple paths uploads everything" do repo_path = path_to("repo") repo2_path = path_to("repo2") expect_recipe do chef_mirror "" do chef_repo_path chef_repo_path: [ repo_path, repo2_path ] action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("nodes/x") }.not_to raise_error expect { get("roles/x") }.not_to raise_error expect { get("nodes/y") }.not_to raise_error expect { get("roles/y") }.not_to raise_error end it "Upload with chef_repo_path(:node_path, :role_path) uploads everything" do repo_path = path_to("repo") repo2_path = path_to("repo2") expect_recipe do chef_mirror "" do chef_repo_path chef_repo_path: "/blahblah", node_path: "#{repo_path}/nodes", role_path: "#{repo2_path}/roles" action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("nodes/x") }.not_to raise_error expect { get("roles/x") }.to raise_error(/404/) expect { get("nodes/y") }.to raise_error(/404/) expect { get("roles/y") }.not_to raise_error end it "Upload with chef_repo_path(:chef_repo_path, :role_path) uploads everything" do repo_path = path_to("repo") repo2_path = path_to("repo2") expect_recipe do chef_mirror "" do chef_repo_path chef_repo_path: repo_path, role_path: "#{repo2_path}/roles" action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("nodes/x") }.not_to raise_error expect { get("roles/x") }.to raise_error(/404/) expect { get("nodes/y") }.to raise_error(/404/) expect { get("roles/y") }.not_to raise_error end it "Upload with chef_repo_path(:node_path, :role_path) with multiple paths uploads everything" do repo_path = path_to("repo") repo2_path = path_to("repo2") expect_recipe do chef_mirror "" do chef_repo_path chef_repo_path: %w{foo bar}, node_path: [ "#{repo_path}/nodes", "#{repo2_path}/nodes" ], role_path: [ "#{repo_path}/roles", "#{repo2_path}/roles" ] action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("nodes/x") }.not_to raise_error expect { get("roles/x") }.not_to raise_error expect { get("nodes/y") }.not_to raise_error expect { get("roles/y") }.not_to raise_error end end end describe "cookbook upload, chef_repo_path and versioned_cookbooks" do when_the_repository "has cookbooks in non-versioned format" do file "cookbooks/x-1.0.0/metadata.rb", 'name "x-1.0.0"; version "2.0.0"' file "cookbooks/y-1.0.0/metadata.rb", 'name "y-3.0.0"; version "4.0.0"' it "chef_mirror :upload uploads everything" do expect_recipe do chef_mirror "" do action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("cookbooks/x-1.0.0/2.0.0") }.not_to raise_error expect { get("cookbooks/y-3.0.0/4.0.0") }.not_to raise_error end context "and Chef::Config.versioned_cookbooks is false" do before do Chef::Config.versioned_cookbooks false end it "chef_mirror :upload uploads everything" do expect_recipe do chef_mirror "" do action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("cookbooks/x-1.0.0/2.0.0") }.not_to raise_error expect { get("cookbooks/y-3.0.0/4.0.0") }.not_to raise_error end end context "and Chef::Config.chef_repo_path is not set but versioned_cookbooks is false" do before do Chef::Config.delete(:chef_repo_path) Chef::Config.versioned_cookbooks false end it "chef_mirror :upload with chef_repo_path and versioned_cookbooks false uploads cookbooks with name including version" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir versioned_cookbooks false action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("cookbooks/x-1.0.0/2.0.0") }.not_to raise_error expect { get("cookbooks/y-3.0.0/4.0.0") }.not_to raise_error end end end when_the_repository "has cookbooks in versioned_cookbook format" do file "cookbooks/x-1.0.0/metadata.rb", 'name "x"; version "1.0.0"' file "cookbooks/x-2.0.0/metadata.rb", 'name "x"; version "2.0.0"' context "and Chef::Config.versioned_cookbooks is true" do before do Chef::Config.versioned_cookbooks true end it "chef_mirror :upload uploads everything" do expect_recipe do chef_mirror "" do action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("cookbooks/x/1.0.0") }.not_to raise_error expect { get("cookbooks/x/2.0.0") }.not_to raise_error end end context "and Chef::Config.chef_repo_path set somewhere else" do before do Chef::Config.chef_repo_path = "/x/y/z" end it "chef_mirror :upload with chef_repo_path uploads cookbooks" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("cookbooks/x/1.0.0") }.not_to raise_error expect { get("cookbooks/x/2.0.0") }.not_to raise_error end end context "and Chef::Config.chef_repo_path is not set but versioned_cookbooks is false" do before do Chef::Config.delete(:chef_repo_path) Chef::Config.versioned_cookbooks false end it "chef_mirror :upload with chef_repo_path uploads cookbooks with name split from version" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("cookbooks/x/1.0.0") }.not_to raise_error expect { get("cookbooks/x/2.0.0") }.not_to raise_error end it "chef_mirror :upload with chef_repo_path and versioned_cookbooks uploads cookbooks with name split from version" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir versioned_cookbooks true action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("cookbooks/x/1.0.0") }.not_to raise_error expect { get("cookbooks/x/2.0.0") }.not_to raise_error end end context "and Chef::Config.chef_repo_path is not set but versioned_cookbooks is true" do before do Chef::Config.delete(:chef_repo_path) Chef::Config.versioned_cookbooks true end it "chef_mirror :upload with chef_repo_path uploads cookbooks with name split from version" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir action :upload end end.to have_updated("chef_mirror[]", :upload) expect { get("cookbooks/x/1.0.0") }.not_to raise_error expect { get("cookbooks/x/2.0.0") }.not_to raise_error end end end end describe "cookbook download, chef_repo_path, and versioned_cookbooks" do context "when the Chef server has a cookbook with multiple versions" do cookbook "x", "1.0.0", "metadata.rb" => 'name "x"; version "1.0.0"' cookbook "x", "2.0.0", "metadata.rb" => 'name "x"; version "2.0.0"' when_the_repository "is empty" do it "chef_mirror :download downloads the latest version of the cookbook" do expect_recipe do chef_mirror "" do action :download end end.to have_updated("chef_mirror[]", :download) expect(File.read(path_to("cookbooks/x/metadata.rb"))).to eq('name "x"; version "2.0.0"') end it "chef_mirror :download with versioned_cookbooks = true downloads all versions of the cookbook" do expect_recipe do chef_mirror "" do versioned_cookbooks true action :download end end.to have_updated("chef_mirror[]", :download) expect(File.read(path_to("cookbooks/x-1.0.0/metadata.rb"))).to eq('name "x"; version "1.0.0"') expect(File.read(path_to("cookbooks/x-2.0.0/metadata.rb"))).to eq('name "x"; version "2.0.0"') end context "and Chef::Config.chef_repo_path is set elsewhere" do before do Chef::Config.chef_repo_path = "/x/y/z" end it "chef_mirror :download with chef_repo_path downloads all versions of the cookbook" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir action :download end end.to have_updated("chef_mirror[]", :download) expect(File.read(path_to("cookbooks/x-1.0.0/metadata.rb"))).to eq('name "x"; version "1.0.0"') expect(File.read(path_to("cookbooks/x-2.0.0/metadata.rb"))).to eq('name "x"; version "2.0.0"') end it "chef_mirror :download with chef_repo_path and versioned_cookbooks = false downloads the latest version of the cookbook" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir versioned_cookbooks false action :download end end.to have_updated("chef_mirror[]", :download) expect(File.read(path_to("cookbooks/x/metadata.rb"))).to eq('name "x"; version "2.0.0"') end end context "and Chef::Config.versioned_cookbooks is true" do before do Chef::Config.versioned_cookbooks = true end it "chef_mirror :download downloads all versions of the cookbook" do expect_recipe do chef_mirror "" do action :download end end.to have_updated("chef_mirror[]", :download) expect(File.read(path_to("cookbooks/x-1.0.0/metadata.rb"))).to eq('name "x"; version "1.0.0"') expect(File.read(path_to("cookbooks/x-2.0.0/metadata.rb"))).to eq('name "x"; version "2.0.0"') end it "chef_mirror :download with versioned_cookbooks = false downloads the latest version of the cookbook" do expect_recipe do chef_mirror "" do versioned_cookbooks false action :download end end.to have_updated("chef_mirror[]", :download) expect(File.read(path_to("cookbooks/x/metadata.rb"))).to eq('name "x"; version "2.0.0"') end context "and Chef::Config.chef_repo_path is set elsewhere" do before do Chef::Config.chef_repo_path = "/x/y/z" end it "chef_mirror :download with chef_repo_path downloads all versions of the cookbook" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir action :download end end.to have_updated("chef_mirror[]", :download) expect(File.read(path_to("cookbooks/x-1.0.0/metadata.rb"))).to eq('name "x"; version "1.0.0"') expect(File.read(path_to("cookbooks/x-2.0.0/metadata.rb"))).to eq('name "x"; version "2.0.0"') end it "chef_mirror :download with chef_repo_path and versioned_cookbooks = false downloads the latest version of the cookbook" do repository_dir = @repository_dir expect_recipe do chef_mirror "" do chef_repo_path repository_dir versioned_cookbooks false action :download end end.to have_updated("chef_mirror[]", :download) expect(File.read(path_to("cookbooks/x/metadata.rb"))).to eq('name "x"; version "2.0.0"') end end end end end end end end cheffish-15.0.1/spec/integration/chef_node_spec.rb000066400000000000000000000625641362335063000221210ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe Chef::Resource::ChefNode do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server "is in multi-org mode" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end context "and is empty" do context 'and we run a recipe that creates node "blah"' do it "the node gets created" do expect_recipe do chef_node "blah" end.to have_updated "chef_node[blah]", :create expect(get("nodes/blah")["name"]).to eq("blah") end end # TODO why-run mode context "and another chef server is running on port 8899" do before :each do @server = ChefZero::Server.new(port: 8899) @server.start_background end after :each do @server.stop end context 'and a recipe is run that creates node "blah" on the second chef server using with_chef_server' do it "the node is created on the second chef server but not the first" do expect_recipe do with_chef_server "http://127.0.0.1:8899" chef_node "blah" end.to have_updated "chef_node[blah]", :create expect { get("nodes/blah") }.to raise_error(Net::HTTPServerException) expect(get("http://127.0.0.1:8899/nodes/blah")["name"]).to eq("blah") end end context 'and a recipe is run that creates node "blah" on the second chef server using chef_server' do it "the node is created on the second chef server but not the first" do expect_recipe do chef_node "blah" do chef_server({ chef_server_url: "http://127.0.0.1:8899" }) end end.to have_updated "chef_node[blah]", :create expect { get("nodes/blah") }.to raise_error(Net::HTTPServerException) expect(get("http://127.0.0.1:8899/nodes/blah")["name"]).to eq("blah") end end end end context 'and has a node named "blah"' do node "blah", {} it 'chef_node "blah" does not get created or updated' do expect_recipe do chef_node "blah" end.not_to have_updated "chef_node[blah]", :create end end context 'and has a node named "blah" with tags' do node "blah", { "normal" => { "tags" => %w{a b} }, } context 'with chef_node "blah" that sets attributes' do with_converge do chef_node "blah" do attributes({}) end end it "the tags in attributes are used" do expect(get("nodes/blah")["normal"]["tags"]).to eq(%w{a b}) end end context 'with chef_node "blah" that sets attributes with tags in them' do with_converge do chef_node "blah" do attributes "tags" => %w{c d} end end it "the tags in attributes are used" do expect(get("nodes/blah")["normal"]["tags"]).to eq(%w{c d}) end end end describe "#complete" do context 'when the Chef server has a node named "blah" with everything in it' do node "blah", { "chef_environment" => "blah", "run_list" => [ "recipe[bjork]" ], "normal" => { "foo" => "bar", "tags" => %w{a b} }, "default" => { "foo2" => "bar2" }, "automatic" => { "foo3" => "bar3" }, "override" => { "foo4" => "bar4" }, } it "chef_node with no attributes modifies nothing" do expect_recipe do chef_node "blah" end.to be_up_to_date expect(get("nodes/blah")).to include( "name" => "blah", "chef_environment" => "blah", "run_list" => [ "recipe[bjork]" ], "normal" => { "foo" => "bar", "tags" => %w{a b} }, "default" => { "foo2" => "bar2" }, "automatic" => { "foo3" => "bar3" }, "override" => { "foo4" => "bar4" } ) end it "chef_node with complete true removes everything except default, automatic and override" do expect_recipe do chef_node "blah" do complete true end end.to be_updated expect(get("nodes/blah")).to include( "name" => "blah", "chef_environment" => "_default", "run_list" => [ ], "normal" => { "tags" => %w{a b} }, "default" => { "foo2" => "bar2" }, "automatic" => { "foo3" => "bar3" }, "override" => { "foo4" => "bar4" } ) end it "chef_node with complete true sets the given attributes" do expect_recipe do chef_node "blah" do chef_environment "x" run_list [ "recipe[y]" ] attributes "a" => "b" tags "c", "d" complete true end end.to be_updated expect(get("nodes/blah")).to include( "name" => "blah", "chef_environment" => "x", "run_list" => [ "recipe[y]" ], "normal" => { "a" => "b", "tags" => %w{c d} }, "default" => { "foo2" => "bar2" }, "automatic" => { "foo3" => "bar3" }, "override" => { "foo4" => "bar4" } ) end it "chef_node with complete true and partial attributes sets the given attributes" do expect_recipe do chef_node "blah" do chef_environment "x" recipe "y" attribute "a", "b" tags "c", "d" complete true end end.to be_updated expect(get("nodes/blah")).to include( "name" => "blah", "chef_environment" => "x", "run_list" => [ "recipe[y]" ], "normal" => { "a" => "b", "tags" => %w{c d} }, "default" => { "foo2" => "bar2" }, "automatic" => { "foo3" => "bar3" }, "override" => { "foo4" => "bar4" } ) end end end describe "#attributes" do context "with a node with normal attributes a => b and c => { d => e }" do node "blah", { "normal" => { "a" => "b", "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y", }, "chef_environment" => "desert", } it "chef_node with attributes {} removes all normal attributes but leaves tags, automatic and environment alone" do expect_recipe do chef_node "blah" do attributes({}) end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "tags" => %w{a b} }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attributes { c => d } replaces normal but not tags/automatic/environment" do expect_recipe do chef_node "blah" do attributes "c" => "d" end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "c" => "d", "tags" => %w{a b} }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attributes { c => f => g, y => z } replaces normal but not tags/automatic/environment" do expect_recipe do chef_node "blah" do attributes "c" => { "f" => "g" }, "y" => "z" end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "c" => { "f" => "g" }, "y" => "z", "tags" => %w{a b} }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it 'chef_node with attributes { tags => [ "x" ] } replaces normal and tags but not automatic/environment' do expect_recipe do chef_node "blah" do attributes "tags" => [ "x" ] end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "tags" => [ "x" ] }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it 'chef_node with tags "x" and attributes { "tags" => [ "y" ] } sets tags to "x"' do expect_recipe do chef_node "blah" do tags "x" attributes "tags" => [ "y" ] end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "tags" => [ "x" ], }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end end end describe "#attribute" do context "with a node with normal attributes a => b and c => { d => e }" do node "blah", { "normal" => { "a" => "b", "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y", }, "chef_environment" => "desert", } context "basic scenarios" do it "chef_node with no attributes, leaves it alone" do expect_recipe do chef_node "blah" end.not_to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute d, e adds the attribute" do expect_recipe do chef_node "blah" do attribute "d", "e" end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "d" => "e", "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it 'chef_node with attribute tags, [ "x" ] replaces tags' do expect_recipe do chef_node "blah" do attribute "tags", [ "x" ] end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "tags" => [ "x" ], }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute c, x replaces the attribute" do expect_recipe do chef_node "blah" do attribute "c", "x" end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => "x", "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute c, { d => x } replaces the attribute" do expect_recipe do chef_node "blah" do attribute "c", { "d" => "x" } end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "x" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [ c, d ], x replaces the attribute" do expect_recipe do chef_node "blah" do attribute %w{c d}, "x" end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "x" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [ a, b ], x raises an error" do expect do converge do chef_node "blah" do attribute %w{a b}, "x" end end end.to raise_error(/Attempt to set \["a", "b"\] to x when \["a"\] is not a hash/) end it "chef_node with attribute [ a, b, c ], x raises an error" do expect do converge do chef_node "blah" do attribute %w{a b c}, "x" end end end.to raise_error(/Attempt to set \["a", "b", "c"\] to x when \["a"\] is not a hash/) end it "chef_node with attribute [ x, y ], z adds a new attribute" do expect_recipe do chef_node "blah" do attribute %w{x y}, "z" end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "x" => { "y" => "z" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [], {} clears all attributes" do expect_recipe do chef_node "blah" do attribute([], {}) end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => {}, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end end context "delete" do it "chef_node with attribute a, :delete deletes the attribute" do expect_recipe do chef_node "blah" do attribute "a", :delete end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute c, :delete deletes the attribute" do expect_recipe do chef_node "blah" do attribute "c", :delete end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [ c, d ], :delete deletes the attribute" do expect_recipe do chef_node "blah" do attribute %w{c d}, :delete end end.to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => {}, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute xyz, :delete does nothing" do expect_recipe do chef_node "blah" do attribute "xyz", :delete end end.not_to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [ c, x ], :delete does nothing" do expect_recipe do chef_node "blah" do attribute %w{c x}, :delete end end.not_to have_updated("chef_node[blah]", :create) expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end end context "types" do it "chef_node with attribute a, true sets a to true" do expect_recipe do chef_node "blah" do attribute "a", true end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => true, "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute a, 1 sets a to 1" do expect_recipe do chef_node "blah" do attribute "a", 1 end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => 1, "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it 'chef_node with attribute a, "1" sets a to "1"' do expect_recipe do chef_node "blah" do attribute "a", "1" end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "1", "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it 'chef_node with attribute a, "" sets a to ""' do expect_recipe do chef_node "blah" do attribute "a", "" end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "", "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute a, nil sets a to nil" do expect_recipe do chef_node "blah" do attribute "a", nil end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => nil, "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end end context "multiple attribute definitions" do it "chef_node with attribute a, x and c, y replaces both attributes" do expect_recipe do chef_node "blah" do attribute "a", "x" attribute "c", "y" end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "x", "c" => "y", "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute m, x and n, y adds both attributes" do expect_recipe do chef_node "blah" do attribute "m", "x" attribute "n", "y" end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "m" => "x", "n" => "y", "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [x, y], z and [x, yy], zz adds both attributes" do expect_recipe do chef_node "blah" do attribute %w{x y}, "z" attribute %w{x yy}, "zz" end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "x" => { "y" => "z", "yy" => "zz", }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end describe "precedence" do it "chef_node with attribute a, 1 and a, 2 sets a to 2" do expect_recipe do chef_node "blah" do attribute "a", 1 attribute "a", 2 end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => 2, "c" => { "d" => "e" }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [ x, y ], 1 and [ x, y ], 2 sets [ x, y ], 2" do expect_recipe do chef_node "blah" do attribute %w{x y}, 1 attribute %w{x y}, 2 end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e" }, "x" => { "y" => 2 }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [ c, e ], { a => 1 }, [ c, e ], { b => 2 } sets b only" do expect_recipe do chef_node "blah" do attribute %w{c e}, { "a" => 1 } attribute %w{c e}, { "b" => 2 } end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e", "e" => { "b" => 2 } }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [ c, e ], { a => 1 }, [ c, e, b ], 2 sets both" do expect_recipe do chef_node "blah" do attribute %w{c e}, { "a" => 1 } attribute %w{c e b}, 2 end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e", "e" => { "a" => 1, "b" => 2 } }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end it "chef_node with attribute [ c, e, b ], 2, [ c, e ], { a => 1 } sets a only" do expect_recipe do chef_node "blah" do attribute %w{c e b}, 2 attribute %w{c e}, { "a" => 1 } end end.to be_updated expect(get("nodes/blah")).to include( "normal" => { "a" => "b", "c" => { "d" => "e", "e" => { "a" => 1 } }, "tags" => %w{a b}, }, "automatic" => { "x" => "y" }, "chef_environment" => "desert" ) end end end end end end when_the_chef_server "is in OSC mode" do context "and is empty" do context 'and we run a recipe that creates node "blah"' do it "the node gets created" do expect_recipe do chef_node "blah" end.to have_updated "chef_node[blah]", :create expect(get("nodes/blah")["name"]).to eq("blah") end end end end end cheffish-15.0.1/spec/integration/chef_organization_spec.rb000066400000000000000000000224231362335063000236660ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe Chef::Resource::ChefOrganization do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server "is in multi-org mode" do context "and chef_server_url is pointed at the top level" do user "u", {} user "u2", {} it 'chef_organization "x" creates the organization' do expect_recipe do chef_organization "x" end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("x") end end context "and chef_server_url is pointed at /organizations/foo" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end context "and is empty" do user "u", {} user "u2", {} it 'chef_organization "x" creates the organization' do expect_recipe do chef_organization "x" end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("x") end it 'chef_organization "x" with full_name creates the organization' do expect_recipe do chef_organization "x" do full_name "Hi" end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("Hi") end it 'chef_organization "x" and inviting users creates the invites' do expect_recipe do chef_organization "x" do invites "u", "u2" end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{u u2}) end it 'chef_organization "x" adds members' do expect_recipe do chef_organization "x" do members "u", "u2" end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{u u2}) end end context "and already has an organization named x" do user "u", {} user "u2", {} user "u3", {} user "member", {} user "member2", {} user "invited", {} user "invited2", {} organization "x", { "full_name" => "Lo" } do org_member "member", "member2" org_invite "invited", "invited2" end it 'chef_organization "x" changes nothing' do expect_recipe do chef_organization "x" end.not_to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("Lo") end it 'chef_organization "x" with "complete true" reverts the full_name' do expect_recipe do chef_organization "x" do complete true end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("x") end it 'chef_organization "x" with new full_name updates the organization' do expect_recipe do chef_organization "x" do full_name "Hi" end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("Hi") end context "invites and membership tests" do it 'chef_organization "x" and inviting users creates the invites' do expect_recipe do chef_organization "x" do invites "u", "u2" end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{invited invited2 u u2}) end it 'chef_organization "x" adds members' do expect_recipe do chef_organization "x" do members "u", "u2" end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{member member2 u u2}) end it 'chef_organization "x" does nothing when inviting already-invited users and members' do expect_recipe do chef_organization "x" do invites "invited", "member" end end.not_to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{invited invited2}) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{member member2}) end it 'chef_organization "x" does nothing when adding members who are already members' do expect_recipe do chef_organization "x" do members "member" end end.not_to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{invited invited2}) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{member member2}) end it 'chef_organization "x" upgrades invites to members when asked' do expect_recipe do chef_organization "x" do members "invited" end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{invited member member2}) expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{invited2}) end it 'chef_organization "x" removes members and invites when asked' do expect_recipe do chef_organization "x" do remove_members "invited", "member" end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{invited2}) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{member2}) end it 'chef_organization "x" does nothing when asked to remove non-members' do expect_recipe do chef_organization "x" do remove_members "u", "u2" end end.not_to have_updated("chef_organization[x]", :create) expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{invited invited2}) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{member member2}) end it 'chef_organization "x" with "complete true" reverts the full_name but does not remove invites or members' do expect_recipe do chef_organization "x" do complete true end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("x") expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{invited invited2}) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{member member2}) end it 'chef_organization "x" with members [] and "complete true" removes invites and members' do expect_recipe do chef_organization "x" do members [] complete true end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("x") expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq([]) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq([]) end it 'chef_organization "x" with invites [] and "complete true" removes invites but not members' do expect_recipe do chef_organization "x" do invites [] complete true end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("x") expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq([]) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{member member2}) end it 'chef_organization "x" with invites, members and "complete true" removes all non-specified invites and members' do expect_recipe do chef_organization "x" do invites "invited", "u" members "member", "u2" complete true end end.to have_updated("chef_organization[x]", :create) expect(get("/organizations/x")["full_name"]).to eq("x") expect(get("/organizations/x/association_requests").map { |u| u["username"] }).to eq(%w{invited u}) expect(get("/organizations/x/users").map { |u| u["user"]["username"] }).to eq(%w{member u2}) end end end end end end cheffish-15.0.1/spec/integration/chef_role_spec.rb000066400000000000000000000047501362335063000221260ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe Chef::Resource::ChefRole do extend Cheffish::RSpec::ChefRunSupport when_the_chef_12_server "is in multi-org mode" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end context "and is empty" do context 'and we run a recipe that creates role "blah"' do it "the role gets created" do expect_recipe do chef_role "blah" end.to have_updated "chef_role[blah]", :create expect(get("roles/blah")["name"]).to eq("blah") end end # TODO why-run mode context "and another chef server is running on port 8899" do before :each do @server = ChefZero::Server.new(port: 8899) @server.start_background end after :each do @server.stop end context 'and a recipe is run that creates role "blah" on the second chef server using with_chef_server' do it "the role is created on the second chef server but not the first" do expect_recipe do with_chef_server "http://127.0.0.1:8899" chef_role "blah" end.to have_updated "chef_role[blah]", :create expect { get("roles/blah") }.to raise_error(Net::HTTPServerException) expect(get("http://127.0.0.1:8899/roles/blah")["name"]).to eq("blah") end end context 'and a recipe is run that creates role "blah" on the second chef server using chef_server' do it "the role is created on the second chef server but not the first" do expect_recipe do chef_role "blah" do chef_server({ chef_server_url: "http://127.0.0.1:8899" }) end end.to have_updated "chef_role[blah]", :create expect { get("roles/blah") }.to raise_error(Net::HTTPServerException) expect(get("http://127.0.0.1:8899/roles/blah")["name"]).to eq("blah") end end end end end when_the_chef_server "is in OSC mode" do context "and is empty" do context 'and we run a recipe that creates role "blah"' do it "the role gets created" do expect_recipe do chef_role "blah" end.to have_updated "chef_role[blah]", :create expect(get("roles/blah")["name"]).to eq("blah") end end end end end cheffish-15.0.1/spec/integration/chef_user_spec.rb000066400000000000000000000053241362335063000221410ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" require "support/key_support" repo_path = Dir.mktmpdir("chef_repo") describe Chef::Resource::ChefUser do extend Cheffish::RSpec::ChefRunSupport with_converge do private_key "#{repo_path}/blah.pem" end when_the_chef_server "is empty" do context 'and we run a recipe that creates user "blah"' do it "the user gets created" do expect_recipe do chef_user "blah" do source_key_path "#{repo_path}/blah.pem" end end.to have_updated "chef_user[blah]", :create user = get("/users/blah") expect(user["name"]).to eq("blah") key, _format = Cheffish::KeyFormatter.decode(user["public_key"]) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end context 'and we run a recipe that creates user "blah" with output_key_path' do with_converge do chef_user "blah" do source_key_path "#{repo_path}/blah.pem" output_key_path "#{repo_path}/blah.pub" end end it "the output public key gets created" do expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for("#{repo_path}/blah.pem") end end end when_the_chef_12_server "is in multi-org mode" do context "and chef_server_url is pointed at the top level" do context 'and we run a recipe that creates user "blah"' do it "the user gets created" do expect_recipe do chef_user "blah" do source_key_path "#{repo_path}/blah.pem" end end.to have_updated "chef_user[blah]", :create user = get("/users/blah") expect(user["name"]).to eq("blah") key, _format = Cheffish::KeyFormatter.decode(user["public_key"]) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end end context "and chef_server_url is pointed at /organizations/foo" do organization "foo" before :each do Chef::Config.chef_server_url = URI.join(Chef::Config.chef_server_url, "/organizations/foo").to_s end context 'and we run a recipe that creates user "blah"' do it "the user gets created" do expect_recipe do chef_user "blah" do source_key_path "#{repo_path}/blah.pem" end end.to have_updated "chef_user[blah]", :create user = get("/users/blah") expect(user["name"]).to eq("blah") key, _format = Cheffish::KeyFormatter.decode(user["public_key"]) expect(key).to be_public_key_for("#{repo_path}/blah.pem") end end end end end cheffish-15.0.1/spec/integration/private_key_spec.rb000066400000000000000000000361211362335063000225170ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" require "support/key_support" repo_path = Dir.mktmpdir("chef_repo") describe Chef::Resource::PrivateKey do extend Cheffish::RSpec::ChefRunSupport before :each do FileUtils.remove_entry_secure(repo_path) Dir.mkdir(repo_path) end context "with a recipe with a private_key" do it "the private_key is created in pem format" do expect_recipe do private_key "#{repo_path}/blah" end.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with("-----BEGIN") expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) end end context 'with a private_key "blah" resource' do before :each do Dir.mkdir("#{repo_path}/other_keys") Chef::Config.private_key_paths = [ repo_path, "#{repo_path}/other_keys" ] end it "the private key is created in the private_key_write_path" do expect_recipe do private_key "blah" end.to have_updated "private_key[blah]", :create expect(Chef::Config.private_key_write_path).to eq(repo_path) expect(File.exist?("#{repo_path}/blah")).to be true expect(File.exist?("#{repo_path}/other_keys/blah")).to be false expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) expect(OpenSSL::PKey.read(Cheffish.get_private_key("blah"))).to be_kind_of(OpenSSL::PKey::RSA) end context "and the private key already exists somewhere not in the write path" do before :each do recipe { private_key "#{repo_path}/other_keys/blah" }.converge end it "the private expect(key).to not update" do expect_recipe do private_key "blah" end.not_to have_updated "private_key[blah]", :create expect(File.exist?("#{repo_path}/blah")).to be false expect(File.exist?("#{repo_path}/other_keys/blah")).to be true end end end context "with a private key" do before :each do Cheffish::BasicChefClient.converge_block do private_key "#{repo_path}/blah" end end context "and a private_key that copies it in der format" do it "the private_key is copied in der format and is identical" do expect_recipe do private_key "#{repo_path}/blah.der" do source_key_path "#{repo_path}/blah" format :der end end.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with("-----BEGIN") expect(key_str).not_to start_with("ssh-") expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah") end end it "a private_key that copies it from in-memory as a string succeeds" do expect_recipe do private_key "#{repo_path}/blah.der" do source_key IO.read("#{repo_path}/blah") format :der end end.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with("-----BEGIN") expect(key_str).not_to start_with("ssh-") expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah") end it "a private_key that copies it from in-memory as a key succeeds" do key = OpenSSL::PKey.read(IO.read("#{repo_path}/blah")) expect_recipe do private_key "#{repo_path}/blah.der" do source_key key format :der end end.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with("-----BEGIN") expect(key_str).not_to start_with("ssh-") expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah") end context "and a public_key recipe" do it "the public_key is created" do expect_recipe do public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" end end.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end context "and a public key" do before :each do Cheffish::BasicChefClient.converge_block do public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" end end end context "and public_key resource based off the public key file" do it "the second public_key is created" do expect_recipe do public_key "#{repo_path}/blah.pub2" do source_key_path "#{repo_path}/blah.pub" end end.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end context "and another public_key based off the first public_key in-memory in a string" do it "the second public_key is created" do expect_recipe do public_key "#{repo_path}/blah.pub2" do source_key IO.read("#{repo_path}/blah.pub") end end.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end it "and another public_key based off the first public_key in-memory in a key, the second public_key is created" do key, _format = Cheffish::KeyFormatter.decode(IO.read("#{repo_path}/blah.pub")) expect_recipe do public_key "#{repo_path}/blah.pub2" do source_key key end end.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end context "and another public_key in :pem format based off the first public_key" do it "the second public_key is created" do expect_recipe do public_key "#{repo_path}/blah.pub2" do source_key_path "#{repo_path}/blah.pub" format :pem end end.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end context "and another public_key in :der format based off the first public_key" do it "the second public_key is created" do expect_recipe do public_key "#{repo_path}/blah.pub2" do source_key_path "#{repo_path}/blah.pub" format :pem end end.to have_updated "public_key[#{repo_path}/blah.pub2]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end end context "and a public_key resource in pem format" do it "the public_key is created" do expect_recipe do public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" format :pem end end.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("-----BEGIN") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end context "and a public_key resource in der format" do it "the public_key is created in openssh format" do expect_recipe do public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" format :der end end.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).not_to start_with("-----BEGIN") expect(IO.read("#{repo_path}/blah.pub")).not_to start_with("ssh-rsa") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end end context "with a recipe with a private_key in der format" do it "the private_key is created" do expect_recipe do private_key "#{repo_path}/blah" do format :der end end.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).not_to start_with("-----BEGIN") expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) end end context "with a private key in der format" do before :each do Cheffish::BasicChefClient.converge_block do private_key "#{repo_path}/blah" do format :der end end end context "and a public_key" do it "the public_key is created in openssh format" do expect_recipe do public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" end end.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah" end end end context "with a recipe with a private_key with a pass_phrase" do it "the private_key is created" do expect_recipe do private_key "#{repo_path}/blah" do pass_phrase "hello" end end.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with("-----BEGIN") expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"), "hello")).to be_kind_of(OpenSSL::PKey::RSA) end end context "with a private key with a pass phrase" do before :each do Cheffish::BasicChefClient.converge_block do private_key "#{repo_path}/blah" do pass_phrase "hello" end end end context "and a private_key that copies it in der format" do it "the private_key is copied in der format and is identical" do expect_recipe do private_key "#{repo_path}/blah.der" do source_key_path "#{repo_path}/blah" source_key_pass_phrase "hello" format :der end end.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with("-----BEGIN") expect(key_str).not_to start_with("ssh-") expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah", "hello") end end context "and a private_key resource pointing at it without a pass_phrase" do it "the run fails with an exception" do expect do converge do private_key "#{repo_path}/blah" end end.to raise_error(/missing pass phrase?/) end end context "and a private_key resource with no pass phrase and regenerate_if_different" do it "the private_key is regenerated" do expect_recipe do private_key "#{repo_path}/blah" do regenerate_if_different true end end.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with("-----BEGIN") expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) end end it "a private_key resource that copies it from in-memory as a string succeeds" do expect_recipe do private_key "#{repo_path}/blah.der" do source_key IO.read("#{repo_path}/blah") source_key_pass_phrase "hello" format :der end end.to have_updated "private_key[#{repo_path}/blah.der]", :create key_str = IO.read("#{repo_path}/blah.der") expect(key_str).not_to start_with("-----BEGIN") expect(key_str).not_to start_with("ssh-") expect("#{repo_path}/blah.der").to match_private_key("#{repo_path}/blah", "hello") end context "and a public_key" do it "the public_key is created in openssh format" do expect_recipe do public_key "#{repo_path}/blah.pub" do source_key_path "#{repo_path}/blah" source_key_pass_phrase "hello" end end.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah", "hello" end end context "and a public_key derived from the private key in an in-memory string" do it "the public_key is created in openssh format" do expect_recipe do public_key "#{repo_path}/blah.pub" do source_key IO.read("#{repo_path}/blah") source_key_pass_phrase "hello" end end.to have_updated "public_key[#{repo_path}/blah.pub]", :create expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for "#{repo_path}/blah", "hello" end end end context "with a recipe with a private_key and public_key_path" do it "the private_key and public_key are created" do expect_recipe do private_key "#{repo_path}/blah" do public_key_path "#{repo_path}/blah.pub" end end.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with("-----BEGIN") expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) expect(IO.read("#{repo_path}/blah.pub")).to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub").to be_public_key_for("#{repo_path}/blah") end end context "with a recipe with a private_key and public_key_path and public_key_format" do it "the private_key and public_key are created" do expect_recipe do private_key "#{repo_path}/blah" do public_key_path "#{repo_path}/blah.pub.der" public_key_format :der end end.to have_updated "private_key[#{repo_path}/blah]", :create expect(IO.read("#{repo_path}/blah")).to start_with("-----BEGIN") expect(OpenSSL::PKey.read(IO.read("#{repo_path}/blah"))).to be_kind_of(OpenSSL::PKey::RSA) expect(IO.read("#{repo_path}/blah.pub.der")).not_to start_with("ssh-rsa ") expect("#{repo_path}/blah.pub.der").to be_public_key_for("#{repo_path}/blah") end end context "with a recipe with a private_key with path :none" do it "the private_key is created" do got_private_key = nil expect_recipe do private_key "in_memory" do path :none after { |resource, private_key| got_private_key = private_key } end end.to have_updated "private_key[in_memory]", :create expect(got_private_key).to be_kind_of(OpenSSL::PKey::RSA) end end end cheffish-15.0.1/spec/integration/recipe_dsl_spec.rb000066400000000000000000000012461362335063000223060ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" require "tmpdir" describe "Cheffish Recipe DSL" do extend Cheffish::RSpec::ChefRunSupport context "when we include with_chef_local_server" do before :each do @tmp_repo = Dir.mktmpdir("chef_repo") end after :each do FileUtils.remove_entry_secure @tmp_repo end it "chef_nodes get put into said server" do tmp_repo = @tmp_repo expect_recipe do with_chef_local_server chef_repo_path: tmp_repo chef_node "blah" end.to have_updated "chef_node[blah]", :create expect(File).to exist("#{@tmp_repo}/nodes/blah.json") end end end cheffish-15.0.1/spec/integration/rspec/000077500000000000000000000000001362335063000177475ustar00rootroot00000000000000cheffish-15.0.1/spec/integration/rspec/converge_spec.rb000066400000000000000000000117541362335063000231260ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe "Cheffish::RSpec::ChefRunSupport" do extend Cheffish::RSpec::ChefRunSupport let(:temp_file) { Tempfile.new("test") } context "#recipe" do it "recipe { file ... } updates the file" do result = recipe do file temp_file.path do content "test" end end expect(result.updated?).to be_falsey expect(IO.read(temp_file.path)).to eq "" end it "recipe 'file ...' does not update the file" do result = recipe <<-EOM file temp_file.path do content 'test' end EOM expect(result.updated?).to be_falsey expect(IO.read(temp_file.path)).to eq "" end it "recipe 'file ...' with file and line number does not update the file" do result = recipe(<<-EOM, __FILE__, __LINE__ + 1) file temp_file.path do content 'test' end EOM expect(result.updated?).to be_falsey expect(IO.read(temp_file.path)).to eq "" end end context "#converge" do it "converge { file ... } updates the file" do result = converge do file temp_file.path do content "test" end end expect(result.updated?).to be_truthy expect(IO.read(temp_file.path)).to eq "test" end it "converge 'file ...' updates the file" do result = converge <<-EOM file temp_file.path do content 'test' end EOM expect(result.updated?).to be_truthy expect(IO.read(temp_file.path)).to eq "test" end it "converge 'file ...' with file and line number updates the file" do result = converge(<<-EOM, __FILE__, __LINE__ + 1) file temp_file.path do content 'test' end EOM expect(result.updated?).to be_truthy expect(IO.read(temp_file.path)).to eq "test" end end context "#expect_recipe" do it "expect_recipe { file ... }.to be_updated updates the file, and be_idempotent does not fail" do expect_recipe do file temp_file.path do content "test" end end.to be_updated.and be_idempotent expect(IO.read(temp_file.path)).to eq "test" end it "expect_recipe 'file ...'.to be_updated updates the file, and be_idempotent does not fail" do expect_recipe(<<-EOM).to be_updated.and be_idempotent file temp_file.path do content 'test' end EOM expect(IO.read(temp_file.path)).to eq "test" end it "expect_recipe('file ...', file, line).to be_updated updates the file, and be_idempotent does not fail" do expect_recipe(<<-EOM, __FILE__, __LINE__ + 1).to be_updated.and be_idempotent file temp_file.path do content 'test' end EOM expect(IO.read(temp_file.path)).to eq "test" end it "expect_recipe { file ... }.to be_up_to_date fails" do expect do expect_recipe do file temp_file.path do content "test" end end.to be_up_to_date end.to raise_error RSpec::Expectations::ExpectationNotMetError end it "expect_recipe { }.to be_updated fails" do expect do expect_recipe {}.to be_updated end.to raise_error RSpec::Expectations::ExpectationNotMetError end it "expect_recipe { }.to be_up_to_date succeeds" do expect_recipe {}.to be_up_to_date end it "expect_recipe { }.to be_idempotent succeeds" do expect_recipe {}.to be_idempotent end end context "#expect_converge" do it "expect_converge { file ... }.not_to raise_error updates the file" do expect_converge do file temp_file.path do content "test" end end.not_to raise_error expect(IO.read(temp_file.path)).to eq "test" end it "expect_converge('file ...').not_to raise_error updates the file" do expect_converge(<<-EOM).not_to raise_error file temp_file.path do content 'test' end EOM expect(IO.read(temp_file.path)).to eq "test" end it "expect_converge('file ...', file, line).not_to raise_error updates the file" do expect_converge(<<-EOM, __FILE__, __LINE__ + 1).not_to raise_error file temp_file.path do content 'test' end EOM expect(IO.read(temp_file.path)).to eq "test" end it "expect_converge { raise 'oh no' }.to raise_error passes" do expect_converge do raise "oh no" end.to raise_error("oh no") end end context "when there is a let variable" do let(:let_variable) { "test" } it "converge { let_variable } accesses it" do # Capture the variable outside x = nil converge { x = let_variable } expect(x).to eq "test" end it "converge with a file resource referencing let_variable accesses let_variable" do converge do file temp_file.path do content let_variable end end expect(IO.read(temp_file.path)).to eq "test" end end end cheffish-15.0.1/spec/support/000077500000000000000000000000001362335063000160245ustar00rootroot00000000000000cheffish-15.0.1/spec/support/key_support.rb000066400000000000000000000022761362335063000207440ustar00rootroot00000000000000RSpec::Matchers.define :be_public_key_for do |private_key, pass_phrase| match do |public_key| if public_key.is_a?(String) public_key, _public_key_format = Cheffish::KeyFormatter.decode(IO.read(File.expand_path(public_key)), pass_phrase, public_key) end if private_key.is_a?(String) private_key, _private_key_format = Cheffish::KeyFormatter.decode(IO.read(File.expand_path(private_key)), pass_phrase, private_key) end encrypted = public_key.public_encrypt("hi there") expect(private_key.private_decrypt(encrypted)).to eq("hi there") end end RSpec::Matchers.define :match_private_key do |expected, pass_phrase| match do |actual| if expected.is_a?(String) expected, _format = Cheffish::KeyFormatter.decode(IO.read(File.expand_path(expected)), pass_phrase, expected) end if actual.is_a?(String) actual, _format = Cheffish::KeyFormatter.decode(IO.read(File.expand_path(actual)), pass_phrase, actual) end encrypted = actual.public_encrypt("hi there") expect(expected.private_decrypt(encrypted)).to eq("hi there") encrypted = expected.public_encrypt("hi there") expect(actual.private_decrypt(encrypted)).to eq("hi there") end end cheffish-15.0.1/spec/support/spec_support.rb000066400000000000000000000003601362335063000210760ustar00rootroot00000000000000require "cheffish/rspec" require "cheffish" RSpec.configure do |config| config.filter_run focus: true config.run_all_when_everything_filtered = true config.before :each do Chef::Config.reset end end require "chef/providers" cheffish-15.0.1/spec/unit/000077500000000000000000000000001362335063000152675ustar00rootroot00000000000000cheffish-15.0.1/spec/unit/get_private_key_spec.rb000066400000000000000000000103641362335063000220130ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" describe Cheffish do let(:directory_that_exists) do Dir.mktmpdir("cheffish-rspec") end let(:directory_that_does_not_exist) do dir = Dir.mktmpdir("cheffish-rspec") FileUtils.remove_entry dir dir end let(:private_key_contents) { "contents of private key" } let(:private_key_pem_contents) { "contents of private key pem" } let(:private_key_garbage_contents) { "da vinci virus" } def setup_key key_file = File.expand_path("ned_stark", directory_that_exists) File.open(key_file, "w+") do |f| f.write private_key_contents end key_file end def setup_arbitrarily_named_key key_file = File.expand_path("ned_stark.xxx", directory_that_exists) File.open(key_file, "w+") do |f| f.write private_key_contents end key_file end def setup_pem_key key_file = File.expand_path("ned_stark.pem", directory_that_exists) File.open(key_file, "w+") do |f| f.write private_key_pem_contents end key_file end def setup_garbage_key key_file = File.expand_path("ned_stark.pem.bak", directory_that_exists) File.open(key_file, "w+") do |f| f.write private_key_garbage_contents end key_file end shared_examples_for "returning the contents of the key file if it finds one" do it "returns nil if it cannot find the private key file" do expect(Cheffish.get_private_key("ned_stark", config)).to be_nil end it "returns the contents of the key if it doesn't have an extension" do setup_key expect(Cheffish.get_private_key("ned_stark", config)).to eq(private_key_contents) end it "returns the contents of the key if it has an extension" do setup_pem_key expect(Cheffish.get_private_key("ned_stark", config)).to eq(private_key_pem_contents) end it "returns the contents of arbitrarily named keys" do setup_arbitrarily_named_key expect(Cheffish.get_private_key("ned_stark.xxx", config)).to eq(private_key_contents) end # we arbitrarily prefer "ned_stark" over "ned_stark.pem" for deterministic behavior it "returns the contents of the key that does not have an extension if both exist" do setup_key setup_pem_key expect(Cheffish.get_private_key("ned_stark", config)).to eq(private_key_contents) end end describe "#get_private_key" do context "when private_key_paths has a directory which is empty" do let(:config) do { private_key_paths: [ directory_that_exists ] } end it_behaves_like "returning the contents of the key file if it finds one" context "when it also has a garbage file" do before { setup_garbage_key } it "does not return the da vinci virus if we find only the garbage file" do setup_garbage_key expect(Cheffish.get_private_key("ned_stark", config)).to be_nil end it_behaves_like "returning the contents of the key file if it finds one" end end context "when private_key_paths leads with a directory that does not exist and then an empty directory" do let(:config) do { private_key_paths: [ directory_that_does_not_exist, directory_that_exists ] } end it_behaves_like "returning the contents of the key file if it finds one" end context "when private_keys is empty" do let(:config) do { private_keys: {} } end it "returns nil" do expect(Cheffish.get_private_key("ned_stark", config)).to be_nil end end context "when private_keys contains the path to a key" do let(:name) { "ned_stark" } let(:config) do { private_keys: { name => setup_key } } end it "returns the contents of the key file" do setup_key expect(Cheffish.get_private_key(name, config)).to eq(private_key_contents) end end context "when private_keys contains the path to a key" do let(:name) { "ned_stark" } let(:key) { double("key", to_pem: private_key_contents) } let(:config) do { private_keys: { name => key } } end it "returns the contents of the key file" do expect(Cheffish.get_private_key(name, config)).to eq(private_key_contents) end end end end cheffish-15.0.1/spec/unit/recipe_run_wrapper_spec.rb000066400000000000000000000017751362335063000225330ustar00rootroot00000000000000require "support/spec_support" require "cheffish/rspec/chef_run_support" # require 'cheffish/rspec/recipe_run_wrapper' module MyModule def respond_to_missing?(name, *args) if name == :allowable_method true else false end end end describe Cheffish::RSpec::RecipeRunWrapper do extend Cheffish::RSpec::ChefRunSupport let(:run_wrapper) do Cheffish::RSpec::RecipeRunWrapper.new(chef_config) do log "test recipe in specs" end end context "defines #respond_to_missing? on the client" do it "calls the new super.respond_to_missing" do run_wrapper.client.extend MyModule expect(run_wrapper.client.respond_to?(:allowable_method)).to be_truthy expect(run_wrapper.client.respond_to?(:not_an_allowable_method)).to be_falsey end end context "does not define #respond_to_missing? on the client" do it "calls the original super.respond_to_missing" do expect(run_wrapper.client.respond_to?(:nonexistent_method)).to be_falsey end end end