├── .gitattributes ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── Guardfile ├── LICENSE ├── README.md ├── README.old.md ├── Rakefile ├── Thorfile ├── appveyor.yml ├── lib ├── ridley.rb └── ridley │ ├── chef.rb │ ├── chef │ ├── chefignore.rb │ ├── config.rb │ ├── cookbook.rb │ ├── cookbook │ │ ├── metadata.rb │ │ └── syntax_check.rb │ └── digester.rb │ ├── chef_object.rb │ ├── chef_objects.rb │ ├── chef_objects │ ├── client_object.rb │ ├── cookbook_object.rb │ ├── data_bag_item_obect.rb │ ├── data_bag_object.rb │ ├── environment_object.rb │ ├── node_object.rb │ ├── role_object.rb │ ├── sandbox_object.rb │ └── user_object.rb │ ├── client.rb │ ├── connection.rb │ ├── errors.rb │ ├── helpers.rb │ ├── httpclient_ext.rb │ ├── httpclient_ext │ └── cookie.rb │ ├── logger.rb │ ├── logging.rb │ ├── middleware.rb │ ├── middleware │ ├── chef_auth.rb │ ├── chef_response.rb │ ├── follow_redirects.rb │ └── parse_json.rb │ ├── mixin.rb │ ├── mixin │ ├── checksum.rb │ ├── from_file.rb │ └── params_validate.rb │ ├── resource.rb │ ├── resources.rb │ ├── resources │ ├── client_resource.rb │ ├── cookbook_resource.rb │ ├── data_bag_item_resource.rb │ ├── data_bag_resource.rb │ ├── environment_resource.rb │ ├── node_resource.rb │ ├── role_resource.rb │ ├── sandbox_resource.rb │ ├── search_resource.rb │ └── user_resource.rb │ ├── sandbox_uploader.rb │ └── version.rb ├── ridley.gemspec └── spec ├── acceptance ├── client_resource_spec.rb ├── cookbook_resource_spec.rb ├── data_bag_item_resource_spec.rb ├── data_bag_resource_spec.rb ├── environment_resource_spec.rb ├── node_resource_spec.rb ├── role_resource_spec.rb ├── sandbox_resource_spec.rb ├── search_resource_spec.rb └── user_resource_spec.rb ├── fixtures ├── chefignore ├── encrypted_data_bag_secret ├── example_cookbook │ ├── Guardfile │ ├── README.md │ ├── attributes │ │ └── default.rb │ ├── definitions │ │ └── bad_def.rb │ ├── files │ │ ├── default │ │ │ └── file.h │ │ └── ubuntu │ │ │ └── file.h │ ├── ignores │ │ ├── magic.erb │ │ ├── magic.rb │ │ └── ok.txt │ ├── libraries │ │ └── my_lib.rb │ ├── metadata.rb │ ├── providers │ │ └── defprovider.rb │ ├── recipes │ │ └── default.rb │ ├── resources │ │ └── defresource.rb │ └── templates │ │ └── default │ │ └── temp.txt.erb ├── recipe_one.rb ├── recipe_two.rb └── reset.pem ├── spec_helper.rb ├── support ├── actor_mocking.rb ├── chef_server.rb ├── each_matcher.rb ├── filepath_matchers.rb ├── shared_examples │ └── ridley_resource.rb └── spec_helpers.rb └── unit ├── ridley ├── chef │ ├── chefignore_spec.rb │ ├── cookbook │ │ ├── metadata_spec.rb │ │ └── syntax_check_spec.rb │ ├── cookbook_spec.rb │ └── digester_spec.rb ├── chef_object_spec.rb ├── chef_objects │ ├── client_object_spec.rb │ ├── cookbook_object_spec.rb │ ├── data_bag_item_object_spec.rb │ ├── data_bag_object_spec.rb │ ├── environment_object_spec.rb │ ├── node_object_spec.rb │ ├── role_object_spec.rb │ └── sandbox_object_spec.rb ├── client_spec.rb ├── connection_spec.rb ├── errors_spec.rb ├── logger_spec.rb ├── middleware │ ├── chef_auth_spec.rb │ ├── chef_response_spec.rb │ └── parse_json_spec.rb ├── mixins │ └── from_file_spec.rb ├── resource_spec.rb ├── resources │ ├── client_resource_spec.rb │ ├── cookbook_resource_spec.rb │ ├── data_bag_item_resource_spec.rb │ ├── data_bag_resource_spec.rb │ ├── environment_resource_spec.rb │ ├── node_resource_spec.rb │ ├── role_resource_spec.rb │ ├── sandbox_resource_spec.rb │ ├── search_resource_spec.rb │ └── user_resource_spec.rb └── sandbox_uploader_spec.rb └── ridley_spec.rb /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.h text eol=lf 3 | *.erb text eol=lf 4 | *.txt text eol=lf 5 | *.json text eol=lf 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.sw[op] 19 | .DS_Store 20 | .rspec 21 | .bin/ 22 | vendor/ 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: ruby 3 | addons: 4 | apt: 5 | packages: 6 | - chef 7 | - git 8 | - graphviz 9 | - libarchive12 10 | - libarchive-dev 11 | - libgecode-dev 12 | sources: 13 | - chef-stable-precise 14 | cache: 15 | - apt 16 | - bundler 17 | bundler_args: --without development 18 | dist: precise 19 | branches: 20 | only: 21 | - master 22 | script: "bundle exec thor spec:all" 23 | before_install: 24 | - gem update --system 25 | - gem install bundler 26 | matrix: 27 | include: 28 | - rvm: 2.2.5 29 | - rvm: 2.3.1 30 | - rvm: ruby-head 31 | # Test against master of berkshelf 32 | # - rvm: 2.2.5 33 | # gemfile: berkshelf/Gemfile 34 | # before_install: 35 | # - gem update --system 36 | # - gem install bundler 37 | # - git clone --depth 1 https://github.com/berkshelf/berkshelf 38 | # - cd berkshelf 39 | # - echo "gem 'ridley', :path => '..'" >> Gemfile 40 | # install: bundle install --jobs=3 --retry=3 --path=${BUNDLE_PATH:-vendor/bundle} 41 | # env: 42 | # script: bundle exec thor spec:ci 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Ridley Changelog 2 | 3 | ## 4.6.1 4 | 5 | - Pin to buff-ignore to ensure support for legacy Ruby releases. This will be the last release of Ridley that supports Ruby < 2.1.X. Pin accordingly if you require support for deprecated Ruby releases. 6 | 7 | ## 4.6.0 8 | 9 | ### Enhancements 10 | 11 | - Switch from net_http to httpclient under the hood to add proxy support 12 | 13 | ## 4.5.0 14 | 15 | ### Enhancements 16 | 17 | - Add support for chef server universe endpoint 18 | 19 | ## 4.4.3 20 | 21 | - updating httpclient version dep to ~> 2.7 22 | 23 | ## 4.4.2 24 | 25 | - Bring back 4.4.0 with backcompat fixes! 26 | 27 | ## 4.4.1 28 | 29 | - Revert 4.4.0 while we figure out a regression 30 | 31 | ## 4.4.0 32 | 33 | ### Enhancements 34 | 35 | - Use chef-config gem for Chef configuration, including proxy improvements 36 | 37 | ## 4.3.2 38 | 39 | ### Bug Fixes 40 | 41 | - Supress warning output re: cookbook domain from httpclient 42 | 43 | ## 4.3.1 44 | 45 | - Tighten constraint on varia_model to `~> 0.4.0` 46 | 47 | ## 4.3.0 48 | 49 | ### Enhancements 50 | 51 | - Switch internal HTTP client from `net_http_persistent` to `httpclient` 52 | - Loosen constraint on Hashie to allow for both the 2.x and 3.x line 53 | 54 | ### Bug Fixes 55 | 56 | - Fix missiong constant ValidationFailed when performing a params validation 57 | 58 | ## 4.2.0 59 | 60 | ### Enhancements 61 | 62 | - Support 'root_default' files 63 | 64 | ## 4.1.2 65 | 66 | - Fixed: permission denied errors on Windows while uploading cookbooks 67 | - Fixed: no proc error when evaluating client_key option 68 | - Bump required version of retryable 69 | 70 | ## 4.1.1 71 | 72 | ### Enhancements 73 | 74 | - Add support for metadata source_url field 75 | - Add support for metadata issues_url field 76 | 77 | ## 4.1.0 78 | 79 | - Fix monkey patching issue with options#slice when running under Vagrant 80 | - Bump required version of Celluloid `~> 0.16.0` 81 | - Bump required version of Celluloid-IO `~> 0.16.1` 82 | 83 | ## 4.0.0 84 | 85 | - Update many out of date dependencies 86 | 87 | - buff-config 88 | - buff-extensions 89 | - varia_model 90 | - hashie 91 | 92 | ## 3.1.0 93 | 94 | - Fix issue with evaluating metadata generated by older versions of knife 95 | - Move remaining host-connector code out of Ridley and into Ridley-Connectors 96 | 97 | ## 3.0.0 98 | 99 | - Update Faraday 100 | - Update Celluloid 101 | - Fix noise in chefignore when debug flag is turned on 102 | - Fix issue reading files that contain UTF-8 103 | 104 | ## 2.5.1 105 | 106 | - Fix no method error bug in partial search 107 | 108 | ## 2.5.0 109 | 110 | - Releasing 2.5.0 as a proper new version over 2.4.4. 111 | 112 | ## 2.4.4 113 | 114 | - [#248](https://github.com/RiotGames/ridley/pull/248) Fix some edge cases and styling for deleting attributes from an environment 115 | - [#247](https://github.com/RiotGames/ridley/pull/247) Add support for removing attributes from a node 116 | 117 | ## 2.4.3 118 | 119 | - [#245](https://github.com/RiotGames/ridley/pull/245) Fix for numeric and boolean attribute types 120 | 121 | ## 2.4.2 122 | 123 | - [#244](https://github.com/RiotGames/ridley/pull/244) Fix a bug with deleting deeply nested environment attributes. 124 | 125 | ## 2.4.0 126 | 127 | - Add support for Chef 11 User Objects 128 | 129 | ## 2.1.0 130 | 131 | - [#228](https://github.com/RiotGames/ridley/pull/228) Add a new API for filtering log output. Useful for output you might not want to display because it is sensitive. 132 | 133 | ## 2.0.0 134 | 135 | - [#227](https://github.com/RiotGames/ridley/pull/227) HostCommander and HostConnector code has been moved into its own gem - [found here](https://github.com/RiotGames/ridley-connectors) 136 | - As discussed by @jtimberman in [#225](https://github.com/RiotGames/ridley/issues/225) it makes sense to move this code based on Ridley's main purpose, and gives a decent performance boost to users who don't need this extra functionality. 137 | 138 | ## 1.7.1 139 | 140 | - [#224](https://github.com/RiotGames/ridley/pull/224) Connection#stream will now return true/false on whether it copied the file that was streamed. 141 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | 5 | platforms :jruby do 6 | gem 'jruby-openssl' 7 | end 8 | 9 | group :development do 10 | gem 'yard' 11 | gem 'spork' 12 | gem 'guard', '>= 1.5.0' 13 | gem 'guard-yard' 14 | gem 'guard-rspec' 15 | gem 'guard-spork', platforms: :ruby 16 | gem 'coolline' 17 | gem 'redcarpet', platforms: :ruby 18 | gem 'kramdown', platforms: :jruby 19 | 20 | require 'rbconfig' 21 | 22 | if RbConfig::CONFIG['target_os'] =~ /darwin/i 23 | gem 'growl', require: false 24 | gem 'rb-fsevent', require: false 25 | 26 | if `uname`.strip == 'Darwin' && `sw_vers -productVersion`.strip >= '10.8' 27 | gem 'terminal-notifier-guard', '~> 1.5.3', require: false 28 | end rescue Errno::ENOENT 29 | 30 | elsif RbConfig::CONFIG['target_os'] =~ /linux/i 31 | gem 'libnotify', '~> 0.8.0', require: false 32 | gem 'rb-inotify', require: false 33 | 34 | elsif RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i 35 | gem 'win32console', require: false 36 | gem 'rb-notifu', '>= 0.0.4', require: false 37 | gem 'wdm', require: false 38 | end 39 | end 40 | 41 | group :test do 42 | gem 'thor' 43 | gem 'rake', '>= 0.9.2.2' 44 | gem 'rspec', '~> 3.0' 45 | gem 'fuubar' 46 | gem 'json_spec' 47 | gem 'webmock' 48 | gem 'chef-zero', '~> 1.5.0' 49 | end 50 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | notification :off 2 | 3 | guard 'spork' do 4 | watch('Gemfile') 5 | watch('spec/spec_helper.rb') { :rspec } 6 | watch(%r{^spec/support/.+\.rb$}) { :rspec } 7 | end 8 | 9 | guard 'yard', stdout: '/dev/null', stderr: '/dev/null' do 10 | watch(%r{app/.+\.rb}) 11 | watch(%r{lib/.+\.rb}) 12 | watch(%r{ext/.+\.c}) 13 | end 14 | 15 | guard 'rspec', cli: "--color --drb --format Fuubar", all_on_start: false, all_after_pass: false do 16 | watch(%r{^spec/unit/.+_spec\.rb$}) 17 | watch(%r{^spec/acceptance/.+_spec\.rb$}) 18 | 19 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/unit/#{m[1]}_spec.rb" } 20 | watch('spec/spec_helper.rb') { "spec" } 21 | watch(%r{^spec/support/.+\.rb$}) { "spec" } 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2012-2013 Riot Games 2 | 3 | Jamie Winsor () 4 | Kyle Allan () 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); 7 | you may not use this file except in compliance with the License. 8 | You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, 14 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | See the License for the specific language governing permissions and 16 | limitations under the License. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ridley 2 | [![Gem Version](https://badge.fury.io/rb/ridley.svg)](http://badge.fury.io/rb/ridley) 3 | [![Build Status](https://secure.travis-ci.org/berkshelf/ridley.svg?branch=master)](http://travis-ci.org/berkshelf/ridley) 4 | [![Dependency Status](https://gemnasium.com/berkshelf/ridley.svg?travis)](https://gemnasium.com/berkshelf/ridley) 5 | [![Code Climate](https://codeclimate.com/github/berkshelf/ridley.svg)](https://codeclimate.com/github/berkshelf/ridley) 6 | 7 | A reliable Chef API client with a clean syntax 8 | 9 | Notice 10 | ------ 11 | 12 | This is the HTTP Client API for Berkshelf. It is supported for that purpose, but for all external purposes its use 13 | is deprecated. Chef users should use the `Chef::ServerAPI` class in the `chef` gem. 14 | 15 | The old documentation is still available at [README.old.md](README.old.md) 16 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | -------------------------------------------------------------------------------- /Thorfile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | $:.push File.expand_path("../lib", __FILE__) 3 | 4 | require 'bundler' 5 | require 'bundler/setup' 6 | require 'buff/ruby_engine' 7 | require 'ridley' 8 | 9 | class Default < Thor 10 | extend Buff::RubyEngine 11 | 12 | unless jruby? 13 | require 'thor/rake_compat' 14 | 15 | include Thor::RakeCompat 16 | Bundler::GemHelper.install_tasks 17 | 18 | desc "build", "Build ridley-#{Ridley::VERSION}.gem into the pkg directory" 19 | def build 20 | Rake::Task["build"].invoke 21 | end 22 | 23 | desc "install", "Build and install ridley-#{Ridley::VERSION}.gem into system gems" 24 | def install 25 | Rake::Task["install"].invoke 26 | end 27 | 28 | desc "release", "Create tag v#{Ridley::VERSION} and build and push ridley-#{Ridley::VERSION}.gem to Rubygems" 29 | def release 30 | Rake::Task["release"].invoke 31 | end 32 | end 33 | 34 | class Spec < Thor 35 | namespace :spec 36 | default_task :all 37 | 38 | desc "all", "run all tests" 39 | def all 40 | exec "rspec --color --format=documentation spec" 41 | end 42 | 43 | desc "unit", "run only unit tests" 44 | def unit 45 | exec "rspec --color --format=documentation spec --tag ~type:acceptance" 46 | end 47 | 48 | desc "acceptance", "run only acceptance tests" 49 | def acceptance 50 | exec "rspec --color --format=documentation spec --tag type:acceptance" 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: "master-{build}" 2 | 3 | os: Windows Server 2012 R2 4 | platform: 5 | - x64 6 | 7 | environment: 8 | matrix: 9 | - ruby_version: "23-x64" 10 | - ruby_version: "23" 11 | 12 | clone_depth: 1 13 | skip_tags: true 14 | skip_branch_with_pr: true 15 | branches: 16 | only: 17 | - master 18 | 19 | install: 20 | - SET PATH=C:\Ruby%ruby_version%\bin;%PATH% 21 | - ruby --version 22 | - gem update --system || gem update --system || gem update --system 23 | - gem install bundler --quiet --no-ri --no-rdoc || gem install bundler --quiet --no-ri --no-rdoc || gem install bundler --quiet --no-ri --no-rdoc 24 | - update_rubygems 25 | - gem --version 26 | - bundler --version 27 | - SET BUNDLE_IGNORE_CONFIG=true 28 | - SET CI=true 29 | - SET BUNDLE_WITHOUT=development:guard:maintenance:tools:integration:changelog:docgen:travis:style:omnibus_package:aix:bsd:linux:mac_os_x:solaris 30 | 31 | build_script: 32 | - bundle install || bundle install || bundle install 33 | 34 | test_script: 35 | - SET SPEC_OPTS=--format progress 36 | - bundle exec thor spec:all 37 | -------------------------------------------------------------------------------- /lib/ridley.rb: -------------------------------------------------------------------------------- 1 | require 'addressable/uri' 2 | require 'celluloid' 3 | require 'celluloid/io' 4 | require 'faraday' 5 | require 'forwardable' 6 | require 'hashie' 7 | require 'json' 8 | require 'pathname' 9 | require 'semverse' 10 | require 'httpclient' 11 | require 'ridley/httpclient_ext' 12 | 13 | JSON.create_id = nil 14 | 15 | module Ridley 16 | CHEF_VERSION = '13.6.4'.freeze 17 | 18 | class << self 19 | extend Forwardable 20 | 21 | def_delegator "Ridley::Logging", :logger 22 | alias_method :log, :logger 23 | 24 | def_delegator "Ridley::Logging", :logger= 25 | def_delegator "Ridley::Logging", :set_logger 26 | 27 | # @return [Ridley::Client] 28 | def new(*args) 29 | Client.new(*args) 30 | end 31 | 32 | # Create a new Ridley connection from the Chef config (knife.rb) 33 | # 34 | # @param [#to_s] filepath 35 | # the path to the Chef Config 36 | # 37 | # @param [hash] options 38 | # list of options to pass to the Ridley connection (@see {Ridley::Client#new}) 39 | # 40 | # @return [Ridley::Client] 41 | def from_chef_config(filepath = nil, options = {}) 42 | config = Ridley::Chef::Config.new(filepath).to_hash 43 | 44 | config[:validator_client] = config.delete(:validation_client_name) 45 | config[:validator_path] = config.delete(:validation_key) 46 | config[:client_name] = config.delete(:node_name) 47 | config[:server_url] = config.delete(:chef_server_url) 48 | if config[:ssl_verify_mode] == :verify_none 49 | config[:ssl] = {verify: false} 50 | end 51 | 52 | Client.new(config.merge(options)) 53 | end 54 | 55 | def open(*args, &block) 56 | Client.open(*args, &block) 57 | end 58 | 59 | # @return [Pathname] 60 | def root 61 | @root ||= Pathname.new(File.expand_path('../', File.dirname(__FILE__))) 62 | end 63 | end 64 | 65 | require_relative 'ridley/mixin' 66 | require_relative 'ridley/logging' 67 | require_relative 'ridley/logger' 68 | require_relative 'ridley/chef_object' 69 | require_relative 'ridley/chef_objects' 70 | require_relative 'ridley/client' 71 | require_relative 'ridley/connection' 72 | require_relative 'ridley/chef' 73 | require_relative 'ridley/middleware' 74 | require_relative 'ridley/resource' 75 | require_relative 'ridley/resources' 76 | require_relative 'ridley/sandbox_uploader' 77 | require_relative 'ridley/version' 78 | require_relative 'ridley/errors' 79 | end 80 | 81 | Celluloid.logger = Ridley.logger 82 | -------------------------------------------------------------------------------- /lib/ridley/chef.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | # Classes and modules used for integrating with a Chef Server, the Chef community 3 | # site, and Chef Cookbooks 4 | module Chef 5 | require_relative 'chef/cookbook' 6 | require_relative 'chef/config' 7 | require_relative 'chef/chefignore' 8 | require_relative 'chef/digester' 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/ridley/chef/chefignore.rb: -------------------------------------------------------------------------------- 1 | require 'buff/ignore' 2 | 3 | module Ridley::Chef 4 | class Chefignore < Buff::Ignore::IgnoreFile 5 | include Ridley::Logging 6 | 7 | # The filename of the chefignore 8 | # 9 | # @return [String] 10 | FILENAME = 'chefignore'.freeze 11 | 12 | # Create a new chefignore 13 | # 14 | # @param [#to_s] path 15 | # the path to find a chefignore from (default: `Dir.pwd`) 16 | def initialize(path = Dir.pwd) 17 | ignore = chefignore(path) 18 | 19 | if ignore 20 | log.debug "Using '#{FILENAME}' at '#{ignore}'" 21 | end 22 | 23 | super(ignore, base: path) 24 | end 25 | 26 | private 27 | 28 | # Find the chefignore file in the current directory 29 | # 30 | # @return [String, nil] 31 | # the path to the chefignore file or nil if one was not 32 | # found 33 | def chefignore(path) 34 | Pathname.new(path).ascend do |dir| 35 | next unless dir.directory? 36 | 37 | [ 38 | dir.join(FILENAME), 39 | dir.join('cookbooks', FILENAME), 40 | dir.join('.chef', FILENAME), 41 | ].each do |possible| 42 | return possible.expand_path.to_s if possible.exist? 43 | end 44 | end 45 | 46 | nil 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/ridley/chef/config.rb: -------------------------------------------------------------------------------- 1 | require 'chef-config/config' 2 | require 'chef-config/workstation_config_loader' 3 | require 'socket' 4 | 5 | module Ridley::Chef 6 | class Config 7 | # Create a new Chef Config object. 8 | # 9 | # @param [#to_s] path 10 | # the path to the configuration file 11 | # @param [Hash] options 12 | def initialize(path, options = {}) 13 | ChefConfig::WorkstationConfigLoader.new(path).load 14 | ChefConfig::Config.merge!(options) 15 | ChefConfig::Config.export_proxies # Set proxy settings as environment variables 16 | end 17 | 18 | # Keep defaults that aren't in ChefConfig::Config 19 | def cookbook_copyright(*args, &block) 20 | ChefConfig::Config.cookbook_copyright(*args, &block) || 'YOUR_NAME' 21 | end 22 | def cookbook_email(*args, &block) 23 | ChefConfig::Config.cookbook_email(*args, &block) || 'YOUR_EMAIL' 24 | end 25 | def cookbook_license(*args, &block) 26 | ChefConfig::Config.cookbook_license(*args, &block) || 'reserved' 27 | end 28 | 29 | # The configuration as a hash 30 | def to_hash 31 | ChefConfig::Config.save(true) 32 | end 33 | # Load from a file 34 | def self.from_file(file) 35 | new(file) 36 | end 37 | 38 | # Behave just like ChefConfig::Config in general 39 | def method_missing(name, *args, &block) 40 | ChefConfig::Config.send(name, *args, &block) 41 | end 42 | def respond_to_missing?(name) 43 | ChefConfig::Config.respond_to?(name) 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/ridley/chef/digester.rb: -------------------------------------------------------------------------------- 1 | require 'digest' 2 | 3 | module Ridley::Chef 4 | # Borrowed and modified from: {https://github.com/opscode/chef/blob/11.4.0/lib/chef/digester.rb} 5 | class Digester 6 | class << self 7 | def instance 8 | @instance ||= new 9 | end 10 | 11 | def checksum_for_file(*args) 12 | instance.checksum_for_file(*args) 13 | end 14 | 15 | def md5_checksum_for_file(*args) 16 | instance.generate_md5_checksum_for_file(*args) 17 | end 18 | end 19 | 20 | def validate_checksum(*args) 21 | self.class.validate_checksum(*args) 22 | end 23 | 24 | def checksum_for_file(file) 25 | generate_checksum(file) 26 | end 27 | 28 | def generate_checksum(file) 29 | checksum_file(file, Digest::SHA256.new) 30 | end 31 | 32 | def generate_md5_checksum_for_file(file) 33 | checksum_file(file, Digest::MD5.new) 34 | end 35 | 36 | def generate_md5_checksum(io) 37 | checksum_io(io, Digest::MD5.new) 38 | end 39 | 40 | private 41 | 42 | def checksum_file(file, digest) 43 | File.open(file, 'rb') do |f| 44 | checksum_io(f, digest) 45 | end 46 | end 47 | 48 | def checksum_io(io, digest) 49 | while chunk = io.read(1024 * 8) 50 | digest.update(chunk) 51 | end 52 | digest.hexdigest 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/ridley/chef_object.rb: -------------------------------------------------------------------------------- 1 | require 'varia_model' 2 | 3 | module Ridley 4 | class ChefObject 5 | class << self 6 | # @return [String, nil] 7 | def chef_id 8 | @chef_id 9 | end 10 | 11 | # @param [#to_sym] identifier 12 | # 13 | # @return [String] 14 | def set_chef_id(identifier) 15 | @chef_id = identifier.to_sym 16 | end 17 | 18 | # @return [String] 19 | def chef_type 20 | @chef_type ||= self.class.name.underscore 21 | end 22 | 23 | # @param [#to_s] type 24 | # 25 | # @return [String] 26 | def set_chef_type(type) 27 | @chef_type = type.to_s 28 | attribute(:chef_type, default: type) 29 | end 30 | 31 | # @return [String, nil] 32 | def chef_json_class 33 | @chef_json_class 34 | end 35 | 36 | # @param [String, Symbol] klass 37 | # 38 | # @return [String] 39 | def set_chef_json_class(klass) 40 | @chef_json_class = klass 41 | attribute(:json_class, default: klass) 42 | end 43 | end 44 | 45 | include VariaModel 46 | include Comparable 47 | 48 | # @param [Ridley::Resource] resource 49 | # @param [Hash] new_attrs 50 | def initialize(resource, new_attrs = {}) 51 | @resource = resource 52 | mass_assign(new_attrs) 53 | end 54 | 55 | # Creates a resource on the target remote or updates one if the resource 56 | # already exists. 57 | # 58 | # @raise [Errors::InvalidResource] 59 | # if the resource does not pass validations 60 | # 61 | # @return [Boolean] 62 | def save 63 | raise Errors::InvalidResource.new(self.errors) unless valid? 64 | 65 | mass_assign(resource.create(self)._attributes_) 66 | true 67 | rescue Errors::HTTPConflict 68 | self.update 69 | true 70 | end 71 | 72 | # Updates the instantiated resource on the target remote with any changes made 73 | # to self 74 | # 75 | # @raise [Errors::InvalidResource] 76 | # if the resource does not pass validations 77 | # 78 | # @return [Boolean] 79 | def update 80 | raise Errors::InvalidResource.new(self.errors) unless valid? 81 | 82 | mass_assign(resource.update(self)._attributes_) 83 | true 84 | end 85 | 86 | # Reload the attributes of the instantiated resource 87 | # 88 | # @return [Object] 89 | def reload 90 | new_attributes = resource.find(self)._attributes_ 91 | @_attributes_ = nil 92 | mass_assign(new_attributes) 93 | self 94 | end 95 | 96 | # @return [String] 97 | def chef_id 98 | get_attribute(self.class.chef_id) 99 | end 100 | 101 | def inspect 102 | "#<#{self.class} chef_id:#{self.chef_id}, attributes:#{self._attributes_}>" 103 | end 104 | 105 | # @param [Object] other 106 | # 107 | # @return [Boolean] 108 | def <=>(other) 109 | self.chef_id <=> other.chef_id 110 | end 111 | 112 | def ==(other) 113 | self.chef_id == other.chef_id 114 | end 115 | 116 | # @param [Object] other 117 | # 118 | # @return [Boolean] 119 | def eql?(other) 120 | self.class == other.class && self == other 121 | end 122 | 123 | def hash 124 | self.chef_id.hash 125 | end 126 | 127 | private 128 | 129 | attr_reader :resource 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects.rb: -------------------------------------------------------------------------------- 1 | Dir["#{File.dirname(__FILE__)}/chef_objects/*.rb"].sort.each do |path| 2 | require_relative "chef_objects/#{File.basename(path, '.rb')}" 3 | end 4 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/client_object.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class ClientObject < Ridley::ChefObject 3 | set_chef_id "name" 4 | set_chef_type "client" 5 | set_chef_json_class "Chef::ApiClient" 6 | 7 | attribute :name, 8 | type: String, 9 | required: true 10 | 11 | attribute :admin, 12 | type: Buff::Boolean, 13 | required: true, 14 | default: false 15 | 16 | attribute :validator, 17 | type: Buff::Boolean, 18 | required: true, 19 | default: false 20 | 21 | attribute :certificate, 22 | type: String 23 | 24 | attribute :public_key, 25 | type: String 26 | 27 | attribute :private_key, 28 | type: [ String, Buff::Boolean ], 29 | default: false 30 | 31 | attribute :orgname, 32 | type: String 33 | 34 | # Regenerates the private key of the instantiated client object. The new 35 | # private key will be set to the value of the 'private_key' accessor 36 | # of the instantiated client object. 37 | # 38 | # @return [Boolean] 39 | # true for success and false for failure 40 | def regenerate_key 41 | self.private_key = true 42 | self.save 43 | end 44 | 45 | # Override to_json to reflect to massage the returned attributes based on the type 46 | # of connection. Only OHC/OPC requires the json_class attribute is not present. 47 | def to_json 48 | if resource.connection.hosted? 49 | to_hash.except(:json_class).to_json 50 | else 51 | super 52 | end 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/cookbook_object.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class CookbookObject < Ridley::ChefObject 3 | include Ridley::Logging 4 | 5 | FILE_TYPES = [ 6 | :resources, 7 | :providers, 8 | :recipes, 9 | :definitions, 10 | :libraries, 11 | :attributes, 12 | :files, 13 | :templates, 14 | :root_files 15 | ].freeze 16 | 17 | set_chef_id "cookbook_name" 18 | set_chef_type "cookbook" 19 | set_chef_json_class "Chef::Cookbook" 20 | 21 | attribute :name, 22 | required: true 23 | 24 | attribute :attributes, 25 | type: Array, 26 | default: Array.new 27 | 28 | attribute :cookbook_name, 29 | type: String 30 | 31 | attribute :definitions, 32 | type: Array, 33 | default: Array.new 34 | 35 | attribute :files, 36 | type: Array, 37 | default: Array.new 38 | 39 | attribute :libraries, 40 | type: Array, 41 | default: Array.new 42 | 43 | attribute :metadata, 44 | type: Hashie::Mash 45 | 46 | attribute :providers, 47 | type: Array, 48 | default: Array.new 49 | 50 | attribute :recipes, 51 | type: Array, 52 | default: Array.new 53 | 54 | attribute :resources, 55 | type: Array, 56 | default: Array.new 57 | 58 | attribute :root_files, 59 | type: Array, 60 | default: Array.new 61 | 62 | attribute :templates, 63 | type: Array, 64 | default: Array.new 65 | 66 | attribute :version, 67 | type: String 68 | 69 | attribute :frozen?, 70 | type: Buff::Boolean 71 | 72 | # Download the entire cookbook 73 | # 74 | # @param [String] destination (Dir.mktmpdir) 75 | # the place to download the cookbook too. If no value is provided the cookbook 76 | # will be downloaded to a temporary location 77 | # 78 | # @return [String] 79 | # the path to the directory the cookbook was downloaded to 80 | def download(destination = Dir.mktmpdir) 81 | destination = File.expand_path(destination) 82 | log.debug { "downloading cookbook: '#{name}'" } 83 | 84 | FILE_TYPES.each do |filetype| 85 | next unless manifest.has_key?(filetype) 86 | 87 | manifest[filetype].each do |file| 88 | file_destination = File.join(destination, file[:path].gsub('/', File::SEPARATOR)) 89 | FileUtils.mkdir_p(File.dirname(file_destination)) 90 | download_file(filetype, file[:path], file_destination) 91 | end 92 | end 93 | 94 | destination 95 | end 96 | 97 | # Download a single file from a cookbook 98 | # 99 | # @param [#to_sym] filetype 100 | # the type of file to download. These are broken up into the following types in Chef: 101 | # - attribute 102 | # - definition 103 | # - file 104 | # - library 105 | # - provider 106 | # - recipe 107 | # - resource 108 | # - root_file 109 | # - template 110 | # these types are where the files are stored in your cookbook's structure. For example, a 111 | # recipe would be stored in the recipes directory while a root_file is stored at the root 112 | # of your cookbook 113 | # @param [String] path 114 | # path of the file to download 115 | # @param [String] destination 116 | # where to download the file to 117 | # 118 | # @return [nil] 119 | def download_file(filetype, path, destination) 120 | file_list = case filetype.to_sym 121 | when :attribute, :attributes; attributes 122 | when :definition, :definitions; definitions 123 | when :file, :files; files 124 | when :library, :libraries; libraries 125 | when :provider, :providers; providers 126 | when :recipe, :recipes; recipes 127 | when :resource, :resources; resources 128 | when :root_file, :root_files; root_files 129 | when :template, :templates; templates 130 | else 131 | raise Errors::UnknownCookbookFileType.new(filetype) 132 | end 133 | 134 | file = file_list.find { |f| f[:path] == path } 135 | return nil if file.nil? 136 | 137 | destination = File.expand_path(destination) 138 | log.debug { "downloading '#{filetype}' file: #{file} to: '#{destination}'" } 139 | 140 | resource.connection.stream(file[:url], destination) 141 | end 142 | 143 | # A hash containing keys for all of the different cookbook filetypes with values 144 | # representing each file of that type this cookbook contains 145 | # 146 | # @example 147 | # { 148 | # root_files: [ 149 | # { 150 | # :name => "afile.rb", 151 | # :path => "files/ubuntu-9.10/afile.rb", 152 | # :checksum => "2222", 153 | # :specificity => "ubuntu-9.10" 154 | # }, 155 | # ], 156 | # templates: [ manifest_record1, ... ], 157 | # ... 158 | # } 159 | # 160 | # @return [Hash] 161 | def manifest 162 | {}.tap do |manifest| 163 | FILE_TYPES.each do |filetype| 164 | manifest[filetype] = get_attribute(filetype) 165 | end 166 | end 167 | end 168 | 169 | # Reload the attributes of the instantiated resource 170 | # 171 | # @return [Ridley::CookbookObject] 172 | def reload 173 | mass_assign(resource.find(self, self.version)._attributes_) 174 | self 175 | end 176 | 177 | def to_s 178 | "#{name}: #{manifest}" 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/data_bag_item_obect.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Ridley 4 | class DataBagItemObject < ChefObject 5 | set_chef_id "id" 6 | set_assignment_mode :carefree 7 | 8 | # @return [Ridley::DataBagObject] 9 | attr_reader :data_bag 10 | 11 | attribute :id, 12 | type: String, 13 | required: true 14 | 15 | alias_method :attributes=, :mass_assign 16 | alias_method :attributes, :_attributes_ 17 | 18 | # @param [Ridley::DataBagItemResource] resource 19 | # @param [Ridley::DataBagObject] data_bag 20 | # @param [#to_hash] new_attrs 21 | def initialize(resource, data_bag, new_attrs = {}) 22 | super(resource, new_attrs) 23 | @data_bag = data_bag 24 | end 25 | 26 | # Creates a resource on the target remote or updates one if the resource 27 | # already exists. 28 | # 29 | # @raise [Errors::InvalidResource] 30 | # if the resource does not pass validations 31 | # 32 | # @return [Boolean] 33 | # true if successful and false for failure 34 | def save 35 | raise Errors::InvalidResource.new(self.errors) unless valid? 36 | 37 | mass_assign(resource.create(data_bag, self)._attributes_) 38 | true 39 | rescue Errors::HTTPConflict 40 | self.update 41 | true 42 | end 43 | 44 | # Decrypts this data bag item. 45 | # 46 | # @return [Hash] decrypted attributes 47 | def decrypt 48 | decrypted_hash = Hash[_attributes_.map { |key, value| [key, key == "id" ? value : decrypt_value(value)] }] 49 | mass_assign(decrypted_hash) 50 | end 51 | 52 | # Decrypts an individual value stored inside the data bag item. 53 | # 54 | # @example 55 | # data_bag_item.decrypt_value("Xk0E8lV9r4BhZzcg4wal0X4w9ZexN3azxMjZ9r1MCZc=") 56 | # => {test: {database: {username: "test"}}} 57 | # 58 | # @param [String] an encrypted String value 59 | # 60 | # @return [Hash] a decrypted attribute value 61 | def decrypt_value(value) 62 | case format_version_of(value) 63 | when 0 64 | decrypt_v0_value(value) 65 | when 1 66 | decrypt_v1_value(value) 67 | else 68 | raise NotImplementedError, "Currently decrypting only version 0 & 1 databags are supported" 69 | end 70 | end 71 | 72 | # Reload the attributes of the instantiated resource 73 | # 74 | # @return [Object] 75 | def reload 76 | mass_assign(resource.find(data_bag, self)._attributes_) 77 | self 78 | end 79 | 80 | # Updates the instantiated resource on the target remote with any changes made 81 | # to self 82 | # 83 | # @raise [Errors::InvalidResource] 84 | # if the resource does not pass validations 85 | # 86 | # @return [Boolean] 87 | def update 88 | raise Errors::InvalidResource.new(self.errors) unless valid? 89 | 90 | mass_assign(resource.update(data_bag, self)._attributes_) 91 | true 92 | end 93 | 94 | # @param [#to_hash] hash 95 | # 96 | # @return [Object] 97 | def from_hash(hash) 98 | hash = Hashie::Mash.new(hash.to_hash) 99 | 100 | mass_assign(hash.has_key?(:raw_data) ? hash[:raw_data] : hash) 101 | self 102 | end 103 | 104 | private 105 | 106 | # Shamelessly lifted from https://github.com/opscode/chef/blob/2c0040c95bb942d13ad8c47498df56be43e9a82e/lib/chef/encrypted_data_bag_item.rb#L209-L215 107 | def format_version_of(encrypted_value) 108 | if encrypted_value.respond_to?(:key?) 109 | encrypted_value["version"] 110 | else 111 | 0 112 | end 113 | end 114 | 115 | def decrypt_v0_value(value) 116 | if encrypted_data_bag_secret.nil? 117 | raise Errors::EncryptedDataBagSecretNotSet 118 | end 119 | 120 | decoded_value = Base64.decode64(value) 121 | 122 | cipher = OpenSSL::Cipher::Cipher.new('aes-256-cbc') 123 | cipher.decrypt 124 | cipher.pkcs5_keyivgen(encrypted_data_bag_secret) 125 | decrypted_value = cipher.update(decoded_value) + cipher.final 126 | 127 | YAML.load(decrypted_value) 128 | end 129 | 130 | def decrypt_v1_value(attrs) 131 | if encrypted_data_bag_secret.nil? 132 | raise Errors::EncryptedDataBagSecretNotSet 133 | end 134 | 135 | cipher = OpenSSL::Cipher::Cipher.new(attrs[:cipher]) 136 | cipher.decrypt 137 | cipher.key = Digest::SHA256.digest(encrypted_data_bag_secret) 138 | cipher.iv = Base64.decode64(attrs[:iv]) 139 | decrypted_value = cipher.update(Base64.decode64(attrs[:encrypted_data])) + cipher.final 140 | 141 | YAML.load(decrypted_value)["json_wrapper"] 142 | end 143 | 144 | def encrypted_data_bag_secret 145 | resource.encrypted_data_bag_secret 146 | end 147 | end 148 | end 149 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/data_bag_object.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class DataBagObject < ChefObject 3 | set_chef_id "name" 4 | 5 | attribute :name, 6 | required: true 7 | 8 | def item 9 | DataBagItemProxy.new(self, resource.item_resource) 10 | end 11 | 12 | # @api private 13 | class DataBagItemProxy 14 | attr_reader :data_bag_object 15 | attr_reader :item_resource 16 | 17 | # @param [Ridley::DataBagObject] data_bag_object 18 | # @param [Ridley::DataBagItemResource] item_resource 19 | def initialize(data_bag_object, item_resource) 20 | @data_bag_object = data_bag_object 21 | @item_resource = item_resource 22 | end 23 | 24 | def method_missing(fun, *args, &block) 25 | @item_resource.send(fun, data_bag_object, *args, &block) 26 | end 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/environment_object.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class EnvironmentObject < Ridley::ChefObject 3 | set_chef_id "name" 4 | set_chef_type "environment" 5 | set_chef_json_class "Chef::Environment" 6 | 7 | attribute :name, 8 | required: true 9 | 10 | attribute :description, 11 | default: String.new 12 | 13 | attribute :default_attributes, 14 | default: Hashie::Mash.new 15 | 16 | attribute :override_attributes, 17 | default: Hashie::Mash.new 18 | 19 | attribute :cookbook_versions, 20 | default: Hashie::Mash.new 21 | 22 | # Set an environment level default attribute given the dotted path representation of 23 | # the Chef attribute and value 24 | # 25 | # @example setting and saving an environment level default attribute 26 | # 27 | # obj = environment.find("production") 28 | # obj.set_default_attribute("my_app.billing.enabled", false) 29 | # obj.save 30 | # 31 | # @param [String] key 32 | # @param [Object] value 33 | # 34 | # @return [Hashie::Mash] 35 | def set_default_attribute(key, value) 36 | attr_hash = Hashie::Mash.from_dotted_path(key, value) 37 | self.default_attributes = self.default_attributes.deep_merge(attr_hash) 38 | end 39 | 40 | # Set an environment level override attribute given the dotted path representation of 41 | # the Chef attribute and value 42 | # 43 | # @example setting and saving an environment level override attribute 44 | # 45 | # obj = environment.find("production") 46 | # obj.set_override_attribute("my_app.billing.enabled", false) 47 | # obj.save 48 | # 49 | # @param [String] key 50 | # @param [Object] value 51 | # 52 | # @return [Hashie::Mash] 53 | def set_override_attribute(key, value) 54 | attr_hash = Hashie::Mash.from_dotted_path(key, value) 55 | self.override_attributes = self.override_attributes.deep_merge(attr_hash) 56 | end 57 | 58 | # Removes a environment default attribute given its dotted path 59 | # representation. Returns the default attributes of the environment. 60 | # 61 | # @param [String] key 62 | # the dotted path to an attribute 63 | # 64 | # @return [Hashie::Mash] 65 | def unset_default_attribute(key) 66 | unset_attribute(key, :default) 67 | end 68 | alias :delete_default_attribute :unset_default_attribute 69 | 70 | # Removes a environment override attribute given its dotted path 71 | # representation. Returns the override attributes of the environment. 72 | # 73 | # @param [String] key 74 | # the dotted path to an attribute 75 | # 76 | # @return [Hashie::Mash] 77 | def unset_override_attribute(key) 78 | unset_attribute(key, :override) 79 | end 80 | alias :delete_override_attribute :unset_override_attribute 81 | 82 | private 83 | 84 | # Deletes an attribute at the given precedence using its dotted-path key. 85 | # 86 | # @param [String] key 87 | # the dotted path to an attribute 88 | # @param [Symbol] precedence 89 | # the precedence level to delete the attribute from 90 | # 91 | # @return [Hashie::Mash] 92 | def unset_attribute(key, precedence) 93 | keys = key.split(".") 94 | leaf_key = keys.pop 95 | 96 | attributes_to_change = case precedence 97 | when :default 98 | self.default_attributes 99 | when :override 100 | self.override_attributes 101 | end 102 | 103 | leaf_attributes = keys.inject(attributes_to_change) do |attributes, key| 104 | if attributes[key] && attributes[key].kind_of?(Hashie::Mash) 105 | attributes = attributes[key] 106 | else 107 | return attributes_to_change 108 | end 109 | end 110 | leaf_attributes.delete(leaf_key) 111 | return attributes_to_change 112 | end 113 | end 114 | end 115 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/node_object.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class NodeObject < Ridley::ChefObject 3 | set_chef_id "name" 4 | set_chef_type "node" 5 | set_chef_json_class "Chef::Node" 6 | 7 | attribute :name, 8 | required: true 9 | 10 | attribute :chef_environment, 11 | default: "_default" 12 | 13 | attribute :automatic, 14 | default: Hashie::Mash.new 15 | 16 | attribute :normal, 17 | default: Hashie::Mash.new 18 | 19 | attribute :default, 20 | default: Hashie::Mash.new 21 | 22 | attribute :override, 23 | default: Hashie::Mash.new 24 | 25 | attribute :run_list, 26 | default: Array.new 27 | 28 | alias_method :normal_attributes, :normal 29 | alias_method :automatic_attributes, :automatic 30 | alias_method :default_attributes, :default 31 | alias_method :override_attributes, :override 32 | 33 | # A merged hash containing a deep merge of all of the attributes respecting the node attribute 34 | # precedence level. 35 | # 36 | # @return [hashie::Mash] 37 | def chef_attributes 38 | default.merge(normal.merge(override.merge(automatic))) 39 | end 40 | 41 | # Set a node level normal attribute given the dotted path representation of the Chef 42 | # attribute and value. 43 | # 44 | # @note It is not possible to set any other attribute level on a node and have it persist after 45 | # a Chef Run. This is because all other attribute levels are truncated at the start of a Chef Run. 46 | # 47 | # @example setting and saving a node level normal attribute 48 | # 49 | # obj = node.find("jwinsor-1") 50 | # obj.set_chef_attribute("my_app.billing.enabled", false) 51 | # obj.save 52 | # 53 | # @param [String] key 54 | # dotted path to key to be unset 55 | # @param [Object] value 56 | # 57 | # @return [Hashie::Mash] 58 | def set_chef_attribute(key, value) 59 | attr_hash = Hashie::Mash.from_dotted_path(key, value) 60 | self.normal = self.normal.deep_merge(attr_hash) 61 | end 62 | 63 | # Unset a node level normal attribute given the dotted path representation of the Chef 64 | # attribute and value. 65 | # 66 | # @example unsetting and saving a node level normal attribute 67 | # 68 | # obj = node.find("foonode") 69 | # obj.unset_chef_attribute("my_app.service_one.service_state") 70 | # obj.save 71 | # 72 | # @param [String] key 73 | # dotted path to key to be unset 74 | # 75 | # @return [Hashie::Mash] 76 | def unset_chef_attribute(key) 77 | keys = key.split(".") 78 | leaf_key = keys.pop 79 | attributes = keys.inject(self.normal) do |attributes, key| 80 | if attributes[key] && attributes[key].kind_of?(Hashie::Mash) 81 | attributes = attributes[key] 82 | else 83 | return self.normal 84 | end 85 | end 86 | 87 | attributes.delete(leaf_key) 88 | return self.normal 89 | end 90 | 91 | # Returns the public hostname of the instantiated node. This hostname should be used for 92 | # public communications to the node. 93 | # 94 | # @example 95 | # node.public_hostname => "reset.riotgames.com" 96 | # 97 | # @return [String] 98 | def public_hostname 99 | self.cloud? ? self.automatic[:cloud][:public_hostname] || self.automatic[:fqdn] : self.automatic[:fqdn] 100 | end 101 | 102 | # Returns the public IPv4 address of the instantiated node. This ip address should be 103 | # used for public communications to the node. 104 | # 105 | # @example 106 | # node.public_ipv4 => "10.33.33.1" 107 | # 108 | # @return [String] 109 | def public_ipv4 110 | self.cloud? ? self.automatic[:cloud][:public_ipv4] || self.automatic[:ipaddress] : self.automatic[:ipaddress] 111 | end 112 | alias_method :public_ipaddress, :public_ipv4 113 | 114 | # Returns the cloud provider of the instantiated node. If the node is not identified as 115 | # a cloud node, then nil is returned. 116 | # 117 | # @example 118 | # node_1.cloud_provider => "eucalyptus" 119 | # node_2.cloud_provider => "ec2" 120 | # node_3.cloud_provider => "rackspace" 121 | # node_4.cloud_provider => nil 122 | # 123 | # @return [nil, String] 124 | def cloud_provider 125 | self.cloud? ? self.automatic[:cloud][:provider] : nil 126 | end 127 | 128 | # Returns true if the node is identified as a cloud node. 129 | # 130 | # @return [Boolean] 131 | def cloud? 132 | self.automatic.has_key?(:cloud) 133 | end 134 | 135 | # Returns true if the node is identified as a cloud node using the eucalyptus provider. 136 | # 137 | # @return [Boolean] 138 | def eucalyptus? 139 | self.cloud_provider == "eucalyptus" 140 | end 141 | 142 | # Returns true if the node is identified as a cloud node using the ec2 provider. 143 | # 144 | # @return [Boolean] 145 | def ec2? 146 | self.cloud_provider == "ec2" 147 | end 148 | 149 | # Returns true if the node is identified as a cloud node using the rackspace provider. 150 | # 151 | # @return [Boolean] 152 | def rackspace? 153 | self.cloud_provider == "rackspace" 154 | end 155 | 156 | # Merges the instaniated nodes data with the given data and updates 157 | # the remote with the merged results 158 | # 159 | # @option options [Array] :run_list 160 | # run list items to merge 161 | # @option options [Hash] :attributes 162 | # attributes of normal precedence to merge 163 | # 164 | # @return [Ridley::NodeObject] 165 | def merge_data(options = {}) 166 | new_run_list = Array(options[:run_list]) 167 | new_attributes = options[:attributes] 168 | 169 | unless new_run_list.empty? 170 | self.run_list = self.run_list | new_run_list 171 | end 172 | 173 | unless new_attributes.nil? 174 | self.normal = self.normal.deep_merge(new_attributes) 175 | end 176 | 177 | self 178 | end 179 | end 180 | end 181 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/role_object.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class RoleObject < Ridley::ChefObject 3 | set_chef_id "name" 4 | set_chef_type "role" 5 | set_chef_json_class "Chef::Role" 6 | 7 | attribute :name, 8 | required: true 9 | 10 | attribute :description, 11 | default: String.new 12 | 13 | attribute :default_attributes, 14 | default: Hashie::Mash.new 15 | 16 | attribute :override_attributes, 17 | default: Hashie::Mash.new 18 | 19 | attribute :run_list, 20 | default: Array.new 21 | 22 | attribute :env_run_lists, 23 | default: Hash.new 24 | 25 | # Set a role level override attribute given the dotted path representation of the Chef 26 | # attribute and value 27 | # 28 | # @example setting and saving a node level override attribute 29 | # 30 | # obj = node.role("why_god_why") 31 | # obj.set_override_attribute("my_app.billing.enabled", false) 32 | # obj.save 33 | # 34 | # @param [String] key 35 | # @param [Object] value 36 | # 37 | # @return [Hashie::Mash] 38 | def set_override_attribute(key, value) 39 | attr_hash = Hashie::Mash.from_dotted_path(key, value) 40 | self.override_attributes = self.override_attributes.deep_merge(attr_hash) 41 | end 42 | 43 | # Set a role level default attribute given the dotted path representation of the Chef 44 | # attribute and value 45 | # 46 | # @example setting and saving a node level default attribute 47 | # 48 | # obj = node.role("why_god_why") 49 | # obj.set_default_attribute("my_app.billing.enabled", false) 50 | # obj.save 51 | # 52 | # @param [String] key 53 | # @param [Object] value 54 | # 55 | # @return [Hashie::Mash] 56 | def set_default_attribute(key, value) 57 | attr_hash = Hashie::Mash.from_dotted_path(key, value) 58 | self.default_attributes = self.default_attributes.deep_merge(attr_hash) 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/sandbox_object.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class SandboxObject < ChefObject 3 | set_chef_id "sandbox_id" 4 | 5 | attribute :sandbox_id, 6 | type: String 7 | 8 | attribute :uri, 9 | type: String 10 | 11 | attribute :checksums, 12 | type: Hash 13 | 14 | attribute :is_completed, 15 | type: Buff::Boolean, 16 | default: false 17 | 18 | # Return information about the given checksum 19 | # 20 | # @example 21 | # sandbox.checksum("e5a0f6b48d0712382295ff30bec1f9cc") => { 22 | # needs_upload: true, 23 | # url: "https://s3.amazonaws.com/opscode-platform-production-data/organization" 24 | # } 25 | # 26 | # @param [#to_sym] chk_id 27 | # checksum to retrieve information about 28 | # 29 | # @return [Hash] 30 | # a hash containing the checksum information 31 | def checksum(chk_id) 32 | checksums[chk_id.to_sym] 33 | end 34 | 35 | # Concurrently upload all of this sandboxes files into the checksum containers of the sandbox 36 | # 37 | # @param [Hash] checksums 38 | # a hash of file checksums and file paths 39 | # 40 | # @example 41 | # sandbox.upload( 42 | # "e5a0f6b48d0712382295ff30bec1f9cc" => "/Users/reset/code/rbenv-cookbook/recipes/default.rb", 43 | # "de6532a7fbe717d52020dc9f3ae47dbe" => "/Users/reset/code/rbenv-cookbook/recipes/ohai_plugin.rb" 44 | # ) 45 | def upload(checksums) 46 | resource.upload(self, checksums) 47 | end 48 | 49 | # Notify the Chef Server that uploading to this sandbox has completed 50 | # 51 | # @raise [Ridley::Errors::SandboxCommitError] 52 | def commit 53 | response = resource.commit(self) 54 | set_attribute(:is_completed, response[:is_completed]) 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/ridley/chef_objects/user_object.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class UserObject < Ridley::ChefObject 3 | set_chef_id "name" 4 | set_chef_type "user" 5 | set_chef_json_class "Chef::User" 6 | 7 | attribute :name, 8 | type: String, 9 | required: true 10 | 11 | attribute :admin, 12 | type: Buff::Boolean, 13 | required: true, 14 | default: false 15 | 16 | attribute :certificate, 17 | type: String 18 | 19 | attribute :public_key, 20 | type: String 21 | 22 | attribute :private_key, 23 | type: [ String, Buff::Boolean ], 24 | default: false 25 | 26 | attribute :password, 27 | type: String 28 | 29 | attribute :orgname, 30 | type: String 31 | 32 | # Regenerates the private key of the instantiated user object. The new 33 | # private key will be set to the value of the 'private_key' accessor 34 | # of the instantiated user object. 35 | # 36 | # @return [Boolean] 37 | # true for success and false for failure 38 | def regenerate_key 39 | self.private_key = true 40 | self.save 41 | end 42 | 43 | def authenticate(password) 44 | @resource.authenticate(self.chef_id, password) 45 | end 46 | 47 | # Override to_json to reflect to massage the returned attributes based on the type 48 | # of connection. Only OHC/OPC requires the json_class attribute is not present. 49 | def to_json 50 | if resource.connection.hosted? 51 | to_hash.except(:json_class).to_json 52 | else 53 | super 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /lib/ridley/connection.rb: -------------------------------------------------------------------------------- 1 | require 'open-uri' 2 | require 'retryable' 3 | require 'tempfile' 4 | require 'zlib' 5 | 6 | require 'ridley/helpers' 7 | 8 | module Ridley 9 | class Connection < Faraday::Connection 10 | include Celluloid 11 | task_class TaskThread 12 | 13 | VALID_OPTIONS = [ 14 | :retries, 15 | :retry_interval, 16 | :ssl, 17 | :proxy 18 | ] 19 | 20 | # @return [String] 21 | attr_reader :organization 22 | # @return [String] 23 | attr_reader :client_key 24 | # @return [String] 25 | attr_reader :client_name 26 | # @return [Integer] 27 | # how many retries to attempt on HTTP requests 28 | attr_reader :retries 29 | # @return [Float] 30 | # time to wait between retries 31 | attr_reader :retry_interval 32 | 33 | # @param [String] server_url 34 | # @param [String] client_name 35 | # @param [String] client_key 36 | # 37 | # @option options [Integer] :retries (5) 38 | # retry requests on 5XX failures 39 | # @option options [Float] :retry_interval (0.5) 40 | # how often we should pause between retries 41 | # @option options [Hash] :ssl 42 | # * :verify (Boolean) [true] set to false to disable SSL verification 43 | # @option options [URI, String, Hash] :proxy 44 | # URI, String, or Hash of HTTP proxy options 45 | def initialize(server_url, client_name, client_key, options = {}) 46 | options = options.reverse_merge(retries: 5, retry_interval: 0.5) 47 | @client_name = client_name 48 | @client_key = client_key 49 | @retries = options.delete(:retries) 50 | @retry_interval = options.delete(:retry_interval) 51 | 52 | options[:builder] = Faraday::RackBuilder.new do |b| 53 | b.request :retry, 54 | max: @retries, 55 | interval: @retry_interval, 56 | exceptions: [ 57 | Ridley::Errors::HTTP5XXError, 58 | Errno::ETIMEDOUT, 59 | Faraday::Error::TimeoutError 60 | ] 61 | b.request :chef_auth, client_name, client_key 62 | 63 | b.response :parse_json 64 | b.response :chef_response 65 | 66 | b.adapter :httpclient 67 | end 68 | 69 | uri_hash = Ridley::Helpers.options_slice(Addressable::URI.parse(server_url).to_hash, :scheme, :host, :port) 70 | 71 | unless uri_hash[:port] 72 | uri_hash[:port] = (uri_hash[:scheme] == "https" ? 443 : 80) 73 | end 74 | 75 | if org_match = server_url.match(/.*\/organizations\/(.*)/) 76 | @organization = org_match[1] 77 | end 78 | 79 | unless @organization.nil? 80 | uri_hash[:path] = "/organizations/#{@organization}" 81 | end 82 | 83 | super(Addressable::URI.new(uri_hash), options) 84 | @headers[:user_agent] = "Ridley v#{Ridley::VERSION}" 85 | end 86 | 87 | # @return [Symbol] 88 | def api_type 89 | organization.nil? ? :foss : :hosted 90 | end 91 | 92 | # @return [Boolean] 93 | def hosted? 94 | api_type == :hosted 95 | end 96 | 97 | # @return [Boolean] 98 | def foss? 99 | api_type == :foss 100 | end 101 | 102 | # Override Faraday::Connection#run_request to catch exceptions from {Ridley::Middleware} that 103 | # we expect. Caught exceptions are re-raised with Celluloid#abort so we don't crash the connection. 104 | def run_request(*args) 105 | super 106 | rescue Errors::HTTPError => ex 107 | abort ex 108 | rescue Faraday::Error::ConnectionFailed => ex 109 | abort Errors::ConnectionFailed.new(ex) 110 | rescue Faraday::Error::TimeoutError => ex 111 | abort Errors::TimeoutError.new(ex) 112 | rescue Faraday::Error::ClientError => ex 113 | abort Errors::ClientError.new(ex) 114 | end 115 | 116 | def server_url 117 | self.url_prefix.to_s 118 | end 119 | 120 | # Stream the response body of a remote URL to a file on the local file system 121 | # 122 | # @param [String] target 123 | # a URL to stream the response body from 124 | # @param [String] destination 125 | # a location on disk to stream the content of the response body to 126 | # 127 | # @return [Boolean] true when the destination file exists 128 | def stream(target, destination) 129 | FileUtils.mkdir_p(File.dirname(destination)) 130 | 131 | target = Addressable::URI.parse(target) 132 | headers = Middleware::ChefAuth.authentication_headers( 133 | client_name, 134 | client_key, 135 | http_method: "GET", 136 | host: target.host, 137 | path: target.path 138 | ) 139 | 140 | unless ssl[:verify] 141 | headers.merge!(ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE) 142 | end 143 | 144 | local = Tempfile.new('ridley-stream') 145 | local.binmode 146 | 147 | Retryable.retryable(tries: retries, on: OpenURI::HTTPError, sleep: retry_interval) do 148 | open(target, 'rb', headers) do |remote| 149 | body = remote.read 150 | case remote.content_encoding 151 | when ['gzip'] 152 | body = Zlib::GzipReader.new(StringIO.new(body), encoding: 'ASCII-8BIT').read 153 | when ['deflate'] 154 | body = Zlib::Inflate.inflate(body) 155 | end 156 | local.write(body) 157 | end 158 | end 159 | 160 | local.flush 161 | 162 | FileUtils.cp(local.path, destination) 163 | File.exists?(destination) 164 | rescue OpenURI::HTTPError => ex 165 | abort(ex) 166 | ensure 167 | local.close(true) unless local.nil? 168 | end 169 | end 170 | end 171 | -------------------------------------------------------------------------------- /lib/ridley/errors.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | module Errors 3 | class RidleyError < StandardError; end 4 | class InternalError < RidleyError; end 5 | class ArgumentError < InternalError; end 6 | 7 | class ClientError < RidleyError; end 8 | class ConnectionFailed < ClientError; end 9 | class TimeoutError < ClientError; end 10 | 11 | class ResourceNotFound < RidleyError; end 12 | class ValidatorNotFound < RidleyError; end 13 | class ValidationFailed < RidleyError; end 14 | 15 | class InvalidResource < RidleyError 16 | attr_reader :errors 17 | 18 | def initialize(errors) 19 | @errors = errors 20 | end 21 | 22 | def message 23 | errors.values 24 | end 25 | alias_method :to_s, :message 26 | end 27 | 28 | class UnknownCookbookFileType < RidleyError 29 | attr_reader :type 30 | 31 | def initialize(type) 32 | @type = type 33 | end 34 | 35 | def to_s 36 | "filetype: '#{type}'" 37 | end 38 | end 39 | 40 | class CookbookSyntaxError < RidleyError; end 41 | class EncryptedDataBagSecretNotSet < RidleyError 42 | def message 43 | "no encrypted data bag secret was set for this Ridley connection" 44 | end 45 | end 46 | class FromFileParserError < RidleyError 47 | def initialize(filename, error) 48 | super "Could not parse `#{filename}': #{error.message}" 49 | 50 | # Populate the backtrace with the actual error though 51 | set_backtrace(error.backtrace) 52 | end 53 | end 54 | 55 | class MissingNameAttribute < RidleyError 56 | def initialize(path) 57 | @path = path 58 | end 59 | 60 | def to_s 61 | out = "The metadata at '#{@path}' does not contain a 'name' " 62 | out << "attribute. While Chef does not strictly enforce this " 63 | out << "requirement, Ridley cannot continue without a valid metadata " 64 | out << "'name' entry." 65 | out 66 | end 67 | alias_method :message, :to_s 68 | end 69 | 70 | class ClientKeyFileNotFoundOrInvalid < RidleyError; end 71 | class EncryptedDataBagSecretNotFound < RidleyError; end 72 | 73 | # Exception thrown when the maximum amount of requests is exceeded. 74 | class RedirectLimitReached < RidleyError 75 | attr_reader :response 76 | 77 | def initialize(response) 78 | super "too many redirects; last one to: #{response['location']}" 79 | @response = response 80 | end 81 | end 82 | 83 | class FrozenCookbook < RidleyError; end 84 | class SandboxCommitError < RidleyError; end 85 | class PermissionDenied < RidleyError; end 86 | 87 | class SandboxUploadError < RidleyError; end 88 | class ChecksumMismatch < RidleyError; end 89 | 90 | class HTTPError < RidleyError 91 | class << self 92 | def fabricate(env) 93 | klass = lookup_error(env[:status].to_i) 94 | klass.new(env) 95 | end 96 | 97 | def register_error(status) 98 | error_map[status.to_i] = self 99 | end 100 | 101 | def lookup_error(status) 102 | error_map.fetch(status.to_i) 103 | rescue KeyError 104 | HTTPUnknownStatus 105 | end 106 | 107 | def error_map 108 | @@error_map ||= Hash.new 109 | end 110 | end 111 | 112 | attr_reader :env 113 | attr_reader :errors 114 | 115 | attr_reader :message 116 | alias_method :to_s, :message 117 | 118 | def initialize(env) 119 | @env = env 120 | @errors = env[:body].is_a?(Hash) ? Array(env[:body][:error]) : [] 121 | 122 | if errors.empty? 123 | @message = env[:body] || "no content body" 124 | else 125 | @message = "errors: " 126 | @message << errors.collect { |e| "'#{e}'" }.join(', ') 127 | end 128 | end 129 | end 130 | 131 | class HTTPUnknownStatus < HTTPError 132 | def initialize(env) 133 | super(env) 134 | @message = "status: #{env[:status]} is an unknown HTTP status code or not an error." 135 | end 136 | end 137 | 138 | class HTTPUnknownMethod < HTTPError 139 | attr_reader :method 140 | 141 | def initialize(method) 142 | @method = method 143 | @message = "unknown http method: #{method}" 144 | end 145 | end 146 | 147 | class HTTP3XXError < HTTPError; end 148 | class HTTP4XXError < HTTPError; end 149 | class HTTP5XXError < HTTPError; end 150 | 151 | # 3XX 152 | class HTTPMultipleChoices < HTTP3XXError; register_error(300); end 153 | class HTTPMovedPermanently < HTTP3XXError; register_error(301); end 154 | class HTTPFound < HTTP3XXError; register_error(302); end 155 | class HTTPSeeOther < HTTP3XXError; register_error(303); end 156 | class HTTPNotModified < HTTP3XXError; register_error(304); end 157 | class HTTPUseProxy < HTTP3XXError; register_error(305); end 158 | class HTTPTemporaryRedirect < HTTP3XXError; register_error(307); end 159 | 160 | # 4XX 161 | class HTTPBadRequest < HTTP4XXError; register_error(400); end 162 | class HTTPUnauthorized < HTTP4XXError; register_error(401); end 163 | class HTTPPaymentRequired < HTTP4XXError; register_error(402); end 164 | class HTTPForbidden < HTTP4XXError; register_error(403); end 165 | class HTTPNotFound < HTTP4XXError; register_error(404); end 166 | class HTTPMethodNotAllowed < HTTP4XXError; register_error(405); end 167 | class HTTPNotAcceptable < HTTP4XXError; register_error(406); end 168 | class HTTPProxyAuthenticationRequired < HTTP4XXError; register_error(407); end 169 | class HTTPRequestTimeout < HTTP4XXError; register_error(408); end 170 | class HTTPConflict < HTTP4XXError; register_error(409); end 171 | class HTTPGone < HTTP4XXError; register_error(410); end 172 | class HTTPLengthRequired < HTTP4XXError; register_error(411); end 173 | class HTTPPreconditionFailed < HTTP4XXError; register_error(412); end 174 | class HTTPRequestEntityTooLarge < HTTP4XXError; register_error(413); end 175 | class HTTPRequestURITooLong < HTTP4XXError; register_error(414); end 176 | class HTTPUnsupportedMediaType < HTTP4XXError; register_error(415); end 177 | 178 | # 5XX 179 | class HTTPInternalServerError < HTTP5XXError; register_error(500); end 180 | class HTTPNotImplemented < HTTP5XXError; register_error(501); end 181 | class HTTPBadGateway < HTTP5XXError; register_error(502); end 182 | class HTTPServiceUnavailable < HTTP5XXError; register_error(503); end 183 | class HTTPGatewayTimeout < HTTP5XXError; register_error(504); end 184 | end 185 | end 186 | -------------------------------------------------------------------------------- /lib/ridley/helpers.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | module Helpers 3 | def self.options_slice(options, *keys) 4 | keys.inject({}) do |memo, key| 5 | memo[key] = options[key] if options.include?(key) 6 | memo 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/ridley/httpclient_ext.rb: -------------------------------------------------------------------------------- 1 | require_relative 'httpclient_ext/cookie' 2 | 3 | ::WebAgent::Cookie.send(:include, ::Ridley::HTTPClientExt::WebAgent::Cookie) 4 | -------------------------------------------------------------------------------- /lib/ridley/httpclient_ext/cookie.rb: -------------------------------------------------------------------------------- 1 | require 'httpclient/webagent-cookie' 2 | 3 | module Ridley 4 | module HTTPClientExt 5 | module WebAgent 6 | module Cookie 7 | def domain 8 | self.original_domain 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/ridley/logger.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | module Logging 3 | class Logger < Logger 4 | def initialize(device = STDOUT) 5 | super 6 | self.level = Logger::WARN 7 | @filter_params = Array.new 8 | end 9 | 10 | # Reimplements Logger#add adding message filtering. The info, 11 | # warn, debug, error, and fatal methods all call add. 12 | # 13 | # @param [Fixnum] severity 14 | # an integer measuing the severity - Logger::INFO, etc. 15 | # @param [String] message = nil 16 | # the message to log 17 | # @param [String] progname = nil 18 | # the program name performing the logging 19 | # @param &block 20 | # a block that will be evaluated (for complicated logging) 21 | # 22 | # @example 23 | # log.filter_param("hello") 24 | # log.info("hello world!") => "FILTERED world!" 25 | # 26 | # @return [Boolean] 27 | def add(severity, message = nil, progname = nil, &block) 28 | severity ||= Logger::UNKNOWN 29 | if @logdev.nil? or severity < @level 30 | return true 31 | end 32 | progname ||= @progname 33 | if message.nil? 34 | if block_given? 35 | message = yield 36 | else 37 | message = progname 38 | progname = @progname 39 | end 40 | end 41 | @logdev.write( 42 | format_message(format_severity(severity), Time.now, progname, filter(message))) 43 | true 44 | end 45 | 46 | def filter_params 47 | @filter_params.dup 48 | end 49 | 50 | def filter_param(param) 51 | @filter_params << param unless filter_params.include?(param) 52 | end 53 | 54 | def clear_filter_params 55 | @filter_params.clear 56 | end 57 | 58 | def filter(message) 59 | filter_params.each do |param| 60 | message.gsub!(param.to_s, 'FILTERED') 61 | end 62 | message 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /lib/ridley/logging.rb: -------------------------------------------------------------------------------- 1 | require 'logger' 2 | 3 | module Ridley 4 | module Logging 5 | class << self 6 | # @return [Logger] 7 | def logger 8 | @logger ||= begin 9 | Ridley::Logging::Logger.new 10 | end 11 | end 12 | 13 | # @param [Logger, nil] obj 14 | # 15 | # @return [Logger] 16 | def set_logger(obj) 17 | @logger = (obj.nil? ? Logger.new(File::NULL) : obj) 18 | end 19 | alias_method :logger=, :set_logger 20 | end 21 | 22 | # @return [Logger] 23 | def logger 24 | Ridley::Logging.logger 25 | end 26 | alias_method :log, :logger 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/ridley/middleware.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | module Middleware 3 | CONTENT_TYPE = 'content-type'.freeze 4 | CONTENT_ENCODING = 'content-encoding'.freeze 5 | end 6 | end 7 | 8 | Dir["#{File.dirname(__FILE__)}/middleware/*.rb"].sort.each do |path| 9 | require_relative "middleware/#{File.basename(path, '.rb')}" 10 | end 11 | -------------------------------------------------------------------------------- /lib/ridley/middleware/chef_auth.rb: -------------------------------------------------------------------------------- 1 | require 'mixlib/authentication/signedheaderauth' 2 | 3 | module Ridley 4 | module Middleware 5 | class ChefAuth < Faraday::Middleware 6 | class << self 7 | include Mixlib::Authentication 8 | 9 | # Generate authentication headers for a request to a Chef Server 10 | # 11 | # @param [String] client_name 12 | # @param [String] client_key 13 | # the path OR actual client key 14 | # 15 | # @option options [String] :host 16 | # 17 | # @see {#signing_object} for options 18 | def authentication_headers(client_name, client_key, options = {}) 19 | contents = File.exists?(client_key) ? File.read(client_key) : client_key.to_s 20 | rsa_key = OpenSSL::PKey::RSA.new(contents) 21 | 22 | headers = signing_object(client_name, options).sign(rsa_key).merge(host: options[:host]) 23 | headers.inject({}) { |memo, kv| memo["#{kv[0].to_s.upcase}"] = kv[1];memo } 24 | end 25 | 26 | # Create a signing object for a Request to a Chef Server 27 | # 28 | # @param [String] client_name 29 | # 30 | # @option options [String] :http_method 31 | # @option options [String] :path 32 | # @option options [String] :body 33 | # @option options [Time] :timestamp 34 | # 35 | # @return [SigningObject] 36 | def signing_object(client_name, options = {}) 37 | options = options.reverse_merge( 38 | body: String.new, 39 | timestamp: Time.now.utc.iso8601 40 | ) 41 | options[:user_id] = client_name 42 | options[:proto_version] = "1.0" 43 | 44 | SignedHeaderAuth.signing_object(options) 45 | end 46 | end 47 | 48 | include Ridley::Logging 49 | 50 | attr_reader :client_name 51 | attr_reader :client_key 52 | 53 | def initialize(app, client_name, client_key) 54 | super(app) 55 | @client_name = client_name 56 | @client_key = client_key 57 | end 58 | 59 | def call(env) 60 | signing_options = { 61 | http_method: env[:method], 62 | host: "#{env[:url].host}:#{env[:url].port}", 63 | path: env[:url].path, 64 | body: env[:body] || '' 65 | } 66 | authentication_headers = self.class.authentication_headers(client_name, client_key, signing_options) 67 | 68 | env[:request_headers] = default_headers.merge(env[:request_headers]).merge(authentication_headers) 69 | env[:request_headers] = env[:request_headers].merge('Content-Length' => env[:body].bytesize.to_s) if env[:body] 70 | 71 | log.debug { "==> performing authenticated Chef request as '#{client_name}'"} 72 | log.debug { "request env: #{env}"} 73 | 74 | @app.call(env) 75 | end 76 | 77 | private 78 | 79 | def default_headers 80 | { 81 | 'Accept' => 'application/json', 82 | 'Content-Type' => 'application/json', 83 | 'X-Chef-Version' => Ridley::CHEF_VERSION 84 | } 85 | end 86 | end 87 | end 88 | end 89 | 90 | Faraday::Request.register_middleware chef_auth: Ridley::Middleware::ChefAuth 91 | -------------------------------------------------------------------------------- /lib/ridley/middleware/chef_response.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | module Middleware 3 | class ChefResponse < Faraday::Response::Middleware 4 | class << self 5 | # Determines if a response from the Chef server was successful 6 | # 7 | # @param [Hash] env 8 | # a faraday request env 9 | # 10 | # @return [Boolean] 11 | def success?(env) 12 | (200..210).to_a.index(env[:status].to_i) ? true : false 13 | end 14 | end 15 | 16 | include Ridley::Logging 17 | 18 | def on_complete(env) 19 | log.debug { "==> handling Chef response" } 20 | log.debug { "request env: #{env}" } 21 | 22 | unless self.class.success?(env) 23 | log.debug { "** error encounted in Chef response" } 24 | raise Errors::HTTPError.fabricate(env) 25 | end 26 | end 27 | end 28 | end 29 | end 30 | 31 | Faraday::Response.register_middleware chef_response: Ridley::Middleware::ChefResponse 32 | -------------------------------------------------------------------------------- /lib/ridley/middleware/follow_redirects.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Ridley 4 | module Middleware 5 | # Borrowed and modified from: 6 | # {https://github.com/lostisland/faraday_middleware/blob/master/lib/faraday_middleware/response/follow_redirects.rb} 7 | # 8 | # Public: Follow HTTP 301, 302, 303, and 307 redirects for GET, PATCH, POST, 9 | # PUT, and DELETE requests. 10 | # 11 | # This middleware does not follow the HTTP specification for HTTP 302, by 12 | # default, in that it follows the improper implementation used by most major 13 | # web browsers which forces the redirected request to become a GET request 14 | # regardless of the original request method. 15 | # 16 | # For HTTP 301, 302, and 303, the original request is transformed into a 17 | # GET request to the response Location, by default. However, with standards 18 | # compliance enabled, a 302 will instead act in accordance with the HTTP 19 | # specification, which will replay the original request to the received 20 | # Location, just as with a 307. 21 | # 22 | # For HTTP 307, the original request is replayed to the response Location, 23 | # including original HTTP request method (GET, POST, PUT, DELETE, PATCH), 24 | # original headers, and original body. 25 | # 26 | # This middleware currently only works with synchronous requests; in other 27 | # words, it doesn't support parallelism. 28 | class FollowRedirects < Faraday::Middleware 29 | include Ridley::Logging 30 | 31 | # HTTP methods for which 30x redirects can be followed 32 | ALLOWED_METHODS = Set.new [:head, :options, :get, :post, :put, :patch, :delete] 33 | # HTTP redirect status codes that this middleware implements 34 | REDIRECT_CODES = Set.new [301, 302, 303, 307] 35 | # Keys in env hash which will get cleared between requests 36 | ENV_TO_CLEAR = Set.new [:status, :response, :response_headers] 37 | 38 | # Default value for max redirects followed 39 | FOLLOW_LIMIT = 3 40 | 41 | # Public: Initialize the middleware. 42 | # 43 | # options - An options Hash (default: {}): 44 | # limit - A Numeric redirect limit (default: 3) 45 | # standards_compliant - A Boolean indicating whether to respect 46 | # the HTTP spec when following 302 47 | # (default: false) 48 | # cookie - Use either an array of strings 49 | # (e.g. ['cookie1', 'cookie2']) to choose kept cookies 50 | # or :all to keep all cookies. 51 | def initialize(app, options = {}) 52 | super(app) 53 | @options = options 54 | 55 | @replay_request_codes = Set.new [307] 56 | @replay_request_codes << 302 if standards_compliant? 57 | end 58 | 59 | def call(env) 60 | perform_with_redirection(env, follow_limit) 61 | end 62 | 63 | private 64 | 65 | def perform_with_redirection(env, follows) 66 | request_body = env[:body] 67 | response = @app.call(env) 68 | 69 | response.on_complete do |env| 70 | if follow_redirect?(env, response) 71 | log.debug { "==> request redirected to #{response['location']}" } 72 | log.debug { "request env: #{env}" } 73 | 74 | if follows.zero? 75 | log.debug { "==> too many redirects" } 76 | raise Ridley::Errors::RedirectLimitReached, response 77 | end 78 | 79 | env = update_env(env, request_body, response) 80 | response = perform_with_redirection(env, follows - 1) 81 | end 82 | end 83 | response 84 | end 85 | 86 | def update_env(env, request_body, response) 87 | env[:url] += response['location'] 88 | if @options[:cookies] 89 | cookies = keep_cookies(env) 90 | env[:request_headers][:cookies] = cookies unless cookies.nil? 91 | end 92 | 93 | env[:body] = request_body 94 | 95 | ENV_TO_CLEAR.each {|key| env.delete key } 96 | 97 | env 98 | end 99 | 100 | def follow_redirect?(env, response) 101 | ALLOWED_METHODS.include? env[:method] and 102 | REDIRECT_CODES.include? response.status 103 | end 104 | 105 | def follow_limit 106 | @options.fetch(:limit, FOLLOW_LIMIT) 107 | end 108 | 109 | def keep_cookies(env) 110 | cookies = @options.fetch(:cookies, []) 111 | response_cookies = env[:response_headers][:cookies] 112 | cookies == :all ? response_cookies : selected_request_cookies(response_cookies) 113 | end 114 | 115 | def selected_request_cookies(cookies) 116 | selected_cookies(cookies)[0...-1] 117 | end 118 | 119 | def selected_cookies(cookies) 120 | "".tap do |cookie_string| 121 | @options[:cookies].each do |cookie| 122 | string = /#{cookie}=?[^;]*/.match(cookies)[0] + ';' 123 | cookie_string << string 124 | end 125 | end 126 | end 127 | 128 | def standards_compliant? 129 | @options.fetch(:standards_compliant, false) 130 | end 131 | end 132 | end 133 | end 134 | 135 | Faraday::Response.register_middleware follow_redirects: Ridley::Middleware::FollowRedirects 136 | -------------------------------------------------------------------------------- /lib/ridley/middleware/parse_json.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | module Middleware 3 | class ParseJson < Faraday::Response::Middleware 4 | include Ridley::Logging 5 | 6 | JSON_TYPE = 'application/json'.freeze 7 | 8 | BRACKETS = [ 9 | "[", 10 | "{" 11 | ].freeze 12 | 13 | WHITESPACE = [ 14 | " ", 15 | "\n", 16 | "\r", 17 | "\t" 18 | ].freeze 19 | 20 | class << self 21 | include Ridley::Logging 22 | 23 | # Takes a string containing JSON and converts it to a Ruby hash 24 | # symbols for keys 25 | # 26 | # @param [String] body 27 | # 28 | # @return [Hash] 29 | def parse(body) 30 | result = JSON.parse(body) 31 | result.is_a?(Hash) ? Hashie::Mash.new(result) : result 32 | end 33 | 34 | # Extracts the type of the response from the response headers 35 | # of a Faraday request env. 'text/html' will be returned if no 36 | # content-type is specified in the response 37 | # 38 | # @example 39 | # env = { 40 | # :response_headers => { 41 | # 'content-type' => 'text/html; charset=utf-8' 42 | # } 43 | # ... 44 | # } 45 | # 46 | # ParseJson.response_type(env) => 'application/json' 47 | # 48 | # @param [Hash] env 49 | # a Faraday request env 50 | # 51 | # @return [String] 52 | def response_type(env) 53 | if env[:response_headers][CONTENT_TYPE].nil? 54 | log.debug { "response did not specify a content type" } 55 | return "text/html" 56 | end 57 | 58 | env[:response_headers][CONTENT_TYPE].split(';', 2).first 59 | end 60 | 61 | # Determines if the response of the given Faraday request env 62 | # contains JSON 63 | # 64 | # @param [Hash] env 65 | # a Faraday request env 66 | # 67 | # @return [Boolean] 68 | def json_response?(env) 69 | response_type(env) == JSON_TYPE && looks_like_json?(env) 70 | end 71 | 72 | # Examines the body of a request env and returns true if it appears 73 | # to contain JSON or false if it does not 74 | # 75 | # @param [Hash] env 76 | # a Faraday request env 77 | # @return [Boolean] 78 | def looks_like_json?(env) 79 | return false unless env[:body].present? 80 | 81 | BRACKETS.include?(first_char(env[:body])) 82 | end 83 | 84 | private 85 | 86 | def first_char(body) 87 | idx = -1 88 | begin 89 | char = body[idx += 1] 90 | char = char.chr if char 91 | end while char && WHITESPACE.include?(char) 92 | 93 | char 94 | end 95 | end 96 | 97 | def on_complete(env) 98 | if self.class.json_response?(env) 99 | log.debug { "==> parsing Chef response body as JSON" } 100 | env[:body] = self.class.parse(env[:body]) 101 | else 102 | log.debug { "==> Chef response did not contain a JSON body" } 103 | end 104 | end 105 | end 106 | end 107 | end 108 | 109 | Faraday::Response.register_middleware parse_json: Ridley::Middleware::ParseJson 110 | -------------------------------------------------------------------------------- /lib/ridley/mixin.rb: -------------------------------------------------------------------------------- 1 | Dir["#{File.dirname(__FILE__)}/mixin/*.rb"].sort.each do |path| 2 | require_relative "mixin/#{File.basename(path, '.rb')}" 3 | end 4 | -------------------------------------------------------------------------------- /lib/ridley/mixin/checksum.rb: -------------------------------------------------------------------------------- 1 | module Ridley::Mixin 2 | # Inspired by and dependency-free replacement for 3 | # {https://github.com/opscode/chef/blob/11.4.0/lib/chef/mixin/checksum.rb} 4 | module Checksum 5 | # @param [String] file 6 | # 7 | # @return [String] 8 | def checksum(file) 9 | Ridley::Chef::Digester.checksum_for_file(file) 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/ridley/mixin/from_file.rb: -------------------------------------------------------------------------------- 1 | module Ridley::Mixin 2 | module FromFile 3 | module ClassMethods 4 | def from_file(filename, *args) 5 | new(*args).from_file(filename) 6 | end 7 | 8 | def class_from_file(filename, *args) 9 | new(*args).class_from_file(filename) 10 | end 11 | end 12 | 13 | class << self 14 | def included(base) 15 | base.extend(ClassMethods) 16 | end 17 | end 18 | 19 | # Loads the contents of a file within the context of the current object 20 | # 21 | # @param [#to_s] filename 22 | # path to the file to load 23 | # 24 | # @raise [IOError] if the file does not exist or cannot be read 25 | def from_file(filename) 26 | filename = filename.to_s 27 | 28 | ensure_presence!(filename) 29 | 30 | with_error_handling(filename) do 31 | self.instance_eval(IO.read(filename), filename, 1) 32 | self 33 | end 34 | end 35 | 36 | # Loads the contents of a file within the context of the current object's class 37 | # 38 | # @param [#to_s] filename 39 | # path to the file to load 40 | # 41 | # @raise [IOError] if the file does not exist or cannot be read 42 | def class_from_file(filename) 43 | filename = filename.to_s 44 | 45 | ensure_presence!(filename) 46 | 47 | with_error_handling(filename) do 48 | self.class_eval(IO.read(filename), filename, 1) 49 | self 50 | end 51 | end 52 | 53 | private 54 | 55 | # Ensure the given filename and path is readable 56 | # 57 | # @param [String] filename 58 | # 59 | # @raise [IOError] 60 | # if the target file does not exist or is not readable 61 | def ensure_presence!(filename) 62 | unless File.exists?(filename) && File.readable?(filename) 63 | raise IOError, "Could not open or read: '#{filename}'" 64 | end 65 | end 66 | 67 | # Execute the given block, handling any exceptions that occur 68 | # 69 | # @param [String] filename 70 | # 71 | # @raise [Ridley::Errors::FromFileParserError] 72 | # if any exceptions if raised 73 | def with_error_handling(filename) 74 | yield 75 | rescue => e 76 | raise Ridley::Errors::FromFileParserError.new(filename, e) 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/ridley/resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class Resource 3 | class << self 4 | # @return [String] 5 | def resource_path 6 | @resource_path ||= representation.chef_type 7 | end 8 | 9 | # @param [String] path 10 | # 11 | # @return [String] 12 | def set_resource_path(path) 13 | @resource_path = path 14 | end 15 | 16 | def representation 17 | return @representation if @representation 18 | raise RuntimeError.new("no representation set") 19 | end 20 | 21 | def represented_by(klass) 22 | @representation = klass 23 | end 24 | end 25 | 26 | include Celluloid 27 | 28 | # @param [Celluloid::Registry] connection_registry 29 | def initialize(connection_registry) 30 | @connection_registry = connection_registry 31 | end 32 | 33 | def new(*args) 34 | self.class.representation.new(Actor.current, *args) 35 | end 36 | 37 | # Used to build a representation from a file with the current Actor's resource 38 | # 39 | # @param [String] filename 40 | # a full filename from which to build this representation (currently only supports .json files) 41 | # 42 | # @return [representation.class] 43 | def from_file(filename) 44 | from_json(File.read(filename)) 45 | end 46 | 47 | # Used to build a representation from a serialized json string with the current Actor's resource 48 | # 49 | # @param [String] json 50 | # a representation serialized into json 51 | # 52 | # @return [representation.class] 53 | def from_json(json) 54 | new(JSON.parse(json)) 55 | end 56 | 57 | # @return [Ridley::Connection] 58 | def connection 59 | @connection_registry[:connection_pool] 60 | end 61 | 62 | # @param [Ridley::Client] client 63 | # 64 | # @return [Array] 65 | def all 66 | request(:get, self.class.resource_path).collect do |identity, location| 67 | new(self.class.representation.chef_id.to_s => identity) 68 | end 69 | end 70 | 71 | # @param [String, #chef_id] object 72 | # 73 | # @return [Object, nil] 74 | def find(object) 75 | chef_id = object.respond_to?(:chef_id) ? object.chef_id : object 76 | new(request(:get, "#{self.class.resource_path}/#{chef_id}")) 77 | rescue AbortError => ex 78 | return nil if ex.cause.is_a?(Errors::HTTPNotFound) 79 | abort(ex.cause) 80 | end 81 | 82 | # @param [#to_hash] object 83 | # 84 | # @return [Object] 85 | def create(object) 86 | resource = new(object.to_hash) 87 | new_attributes = request(:post, self.class.resource_path, resource.to_json) 88 | resource.mass_assign(resource._attributes_.deep_merge(new_attributes)) 89 | resource 90 | end 91 | 92 | # @param [String, #chef_id] object 93 | # 94 | # @return [Object, nil] 95 | def delete(object) 96 | chef_id = object.respond_to?(:chef_id) ? object.chef_id : object 97 | new(request(:delete, "#{self.class.resource_path}/#{chef_id}")) 98 | rescue AbortError => ex 99 | return nil if ex.cause.is_a?(Errors::HTTPNotFound) 100 | abort(ex.cause) 101 | end 102 | 103 | # @return [Array] 104 | def delete_all 105 | all.collect { |resource| future(:delete, resource) }.map(&:value) 106 | end 107 | 108 | # @param [#to_hash] object 109 | # 110 | # @return [Object, nil] 111 | def update(object) 112 | resource = new(object.to_hash) 113 | new(request(:put, "#{self.class.resource_path}/#{resource.chef_id}", resource.to_json)) 114 | rescue AbortError => ex 115 | return nil if ex.cause.is_a?(Errors::HTTPConflict) 116 | abort(ex.cause) 117 | end 118 | 119 | private 120 | 121 | # @param [Symbol] method 122 | def request(method, *args) 123 | raw_request(method, *args).body 124 | end 125 | 126 | # @param [Symbol] method 127 | def raw_request(method, *args) 128 | unless Connection::METHODS.include?(method) 129 | raise Errors::HTTPUnknownMethod, "unknown http method: #{method}" 130 | end 131 | 132 | connection.send(method, *args) 133 | rescue Errors::HTTPError, Errors::ClientError => ex 134 | abort(ex) 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/ridley/resources.rb: -------------------------------------------------------------------------------- 1 | Dir["#{File.dirname(__FILE__)}/resources/*.rb"].sort.each do |path| 2 | require_relative "resources/#{File.basename(path, '.rb')}" 3 | end 4 | -------------------------------------------------------------------------------- /lib/ridley/resources/client_resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | # @example listing all clients 3 | # conn = Ridley.new(...) 4 | # conn.client.all #=> [ 5 | # #, 6 | # # 7 | # ] 8 | class ClientResource < Ridley::Resource 9 | set_resource_path "clients" 10 | represented_by Ridley::ClientObject 11 | 12 | # Retrieves a client from the remote connection matching the given chef_id 13 | # and regenerates its private key. An instance of the updated object will 14 | # be returned and will have a value set for the 'private_key' accessor. 15 | # 16 | # @param [String, #chef_id] chef_client 17 | # 18 | # @raise [Errors::ResourceNotFound] 19 | # if a client with the given chef_id is not found 20 | # 21 | # @return [Ridley::ClientObject] 22 | def regenerate_key(chef_client) 23 | unless chef_client = find(chef_client) 24 | abort Errors::ResourceNotFound.new("client '#{chef_client}' not found") 25 | end 26 | 27 | chef_client.private_key = true 28 | update(chef_client) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/ridley/resources/data_bag_item_resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class DataBagItemResource < Ridley::Resource 3 | represented_by Ridley::DataBagItemObject 4 | 5 | attr_reader :encrypted_data_bag_secret 6 | 7 | # @param [Celluloid::Registry] connection_registry 8 | # @param [String] encrypted_data_bag_secret 9 | def initialize(connection_registry, encrypted_data_bag_secret) 10 | super(connection_registry) 11 | @encrypted_data_bag_secret = encrypted_data_bag_secret 12 | end 13 | 14 | # @param [Ridley::DataBagObject] data_bag 15 | # 16 | # @return [Array] 17 | def all(data_bag) 18 | request(:get, "#{DataBagResource.resource_path}/#{data_bag.name}").collect do |id, location| 19 | new(data_bag, id: id) 20 | end 21 | end 22 | 23 | # @param [Ridley::DataBagObject] data_bag 24 | # @param [String, #chef_id] object 25 | # 26 | # @return [Ridley::DataBagItemObject, nil] 27 | def find(data_bag, object) 28 | chef_id = object.respond_to?(:chef_id) ? object.chef_id : object 29 | new(data_bag).from_hash(request(:get, "#{DataBagResource.resource_path}/#{data_bag.name}/#{chef_id}")) 30 | rescue AbortError => ex 31 | return nil if ex.cause.is_a?(Errors::HTTPNotFound) 32 | abort(ex.cause) 33 | end 34 | 35 | # @param [Ridley::DataBagObject] data_bag 36 | # @param [#to_hash] object 37 | # 38 | # @return [Ridley::DataBagItemObject, nil] 39 | def create(data_bag, object) 40 | resource = new(data_bag, object.to_hash) 41 | unless resource.valid? 42 | abort Errors::InvalidResource.new(resource.errors) 43 | end 44 | 45 | new_attributes = request(:post, "#{DataBagResource.resource_path}/#{data_bag.name}", resource.to_json) 46 | resource.mass_assign(new_attributes) 47 | resource 48 | end 49 | 50 | # @param [Ridley::DataBagObject] data_bag 51 | # @param [String, #chef_id] object 52 | # 53 | # @return [Ridley::DataBagItemObject, nil] 54 | def delete(data_bag, object) 55 | chef_id = object.respond_to?(:chef_id) ? object.chef_id : object 56 | new(data_bag).from_hash(request(:delete, "#{DataBagResource.resource_path}/#{data_bag.name}/#{chef_id}")) 57 | rescue AbortError => ex 58 | return nil if ex.cause.is_a?(Errors::HTTPNotFound) 59 | abort(ex.cause) 60 | end 61 | 62 | # @param [Ridley::DataBagObject] data_bag 63 | # 64 | # @return [Array] 65 | def delete_all(data_bag) 66 | all(data_bag).collect { |resource| future(:delete, data_bag, resource) }.map(&:value) 67 | end 68 | 69 | # @param [Ridley::DataBagObject] data_bag 70 | # @param [#to_hash] object 71 | # 72 | # @return [Ridley::DataBagItemObject, nil] 73 | def update(data_bag, object) 74 | resource = new(data_bag, object.to_hash) 75 | new(data_bag).from_hash( 76 | request(:put, "#{DataBagResource.resource_path}/#{data_bag.name}/#{resource.chef_id}", resource.to_json) 77 | ) 78 | rescue AbortError => ex 79 | return nil if ex.cause.is_a?(Errors::HTTPConflict) 80 | abort(ex.cause) 81 | end 82 | end 83 | end 84 | -------------------------------------------------------------------------------- /lib/ridley/resources/data_bag_resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class DataBagResource < Ridley::Resource 3 | require_relative 'data_bag_item_resource' 4 | 5 | set_resource_path "data" 6 | represented_by Ridley::DataBagObject 7 | 8 | attr_reader :item_resource 9 | 10 | finalizer :finalize_callback 11 | 12 | # @param [Celluloid::Registry] connection_registry 13 | # @param [String] data_bag_secret 14 | def initialize(connection_registry, data_bag_secret) 15 | super(connection_registry) 16 | @item_resource = DataBagItemResource.new_link(connection_registry, data_bag_secret) 17 | end 18 | 19 | # @param [String, #chef_id] object 20 | # 21 | # @return [nil, Ridley::DataBagResource] 22 | def find(object) 23 | chef_id = object.respond_to?(:chef_id) ? object.chef_id : object 24 | request(:get, "#{self.class.resource_path}/#{chef_id}") 25 | new(name: chef_id) 26 | rescue AbortError => ex 27 | return nil if ex.cause.is_a?(Errors::HTTPNotFound) 28 | abort(ex.cause) 29 | end 30 | 31 | private 32 | 33 | def finalize_callback 34 | item_resource.async.terminate if item_resource 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/ridley/resources/environment_resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class EnvironmentResource < Ridley::Resource 3 | set_resource_path "environments" 4 | represented_by Ridley::EnvironmentObject 5 | 6 | # Used to return a hash of the cookbooks and cookbook versions (including all dependencies) 7 | # that are required by the run_list array. 8 | # 9 | # @param [String] environment 10 | # name of the environment to run against 11 | # @param [Array] run_list 12 | # an array of cookbooks to satisfy 13 | # 14 | # @raise [Errors::ResourceNotFound] if the given environment is not found 15 | # 16 | # @return [Hash] 17 | def cookbook_versions(environment, run_list = []) 18 | run_list = Array(run_list).flatten 19 | chef_id = environment.respond_to?(:chef_id) ? environment.chef_id : environment 20 | request(:post, "#{self.class.resource_path}/#{chef_id}/cookbook_versions", JSON.fast_generate(run_list: run_list)) 21 | rescue AbortError => ex 22 | if ex.cause.is_a?(Errors::HTTPNotFound) 23 | abort Errors::ResourceNotFound.new(ex) 24 | end 25 | abort(ex.cause) 26 | end 27 | 28 | # Delete all of the environments on the client. The '_default' environment 29 | # will never be deleted. 30 | # 31 | # @return [Array] 32 | def delete_all 33 | envs = all.reject { |env| env.name.to_s == '_default' } 34 | envs.collect { |resource| future(:delete, resource) }.map(&:value) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/ridley/resources/node_resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class NodeResource < Ridley::Resource 3 | include Ridley::Logging 4 | 5 | set_resource_path "nodes" 6 | represented_by Ridley::NodeObject 7 | 8 | # @param [Celluloid::Registry] connection_registry 9 | def initialize(connection_registry, options = {}) 10 | super(connection_registry) 11 | end 12 | 13 | # Merges the given data with the the data of the target node on the remote 14 | # 15 | # @param [Ridley::NodeResource, String] target 16 | # node or identifier of the node to merge 17 | # 18 | # @option options [Array] :run_list 19 | # run list items to merge 20 | # @option options [Hash] :attributes 21 | # attributes of normal precedence to merge 22 | # 23 | # @raise [Errors::ResourceNotFound] 24 | # if the target node is not found 25 | # 26 | # @return [Ridley::NodeObject] 27 | def merge_data(target, options = {}) 28 | unless node = find(target) 29 | abort Errors::ResourceNotFound.new 30 | end 31 | 32 | update(node.merge_data(options)) 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/ridley/resources/role_resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class RoleResource < Ridley::Resource 3 | set_resource_path "roles" 4 | represented_by Ridley::RoleObject 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/ridley/resources/sandbox_resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | class SandboxResource < Ridley::Resource 3 | set_resource_path "sandboxes" 4 | represented_by Ridley::SandboxObject 5 | 6 | finalizer :finalize_callback 7 | 8 | def initialize(connection_registry, client_name, client_key, options = {}) 9 | super(connection_registry) 10 | options = options.reverse_merge(pool_size: 4) 11 | @uploader = SandboxUploader.pool(size: options.delete(:pool_size), args: [ client_name, client_key, options ]) 12 | end 13 | 14 | # Create a new Sandbox on the client's Chef Server. A Sandbox requires an 15 | # array of file checksums which lets the Chef Server know what the signature 16 | # of the contents to be uploaded will look like. 17 | # 18 | # @param [Ridley::Client] client 19 | # @param [Array] checksums 20 | # a hash of file checksums 21 | # 22 | # @example using the Ridley client to create a sandbox 23 | # client.sandbox.create([ 24 | # "385ea5490c86570c7de71070bce9384a", 25 | # "f6f73175e979bd90af6184ec277f760c", 26 | # "2e03dd7e5b2e6c8eab1cf41ac61396d5" 27 | # ]) 28 | # 29 | # @return [Array] 30 | def create(checksums = []) 31 | sumhash = { checksums: Hash.new }.tap do |chks| 32 | Array(checksums).each { |chk| chks[:checksums][chk] = nil } 33 | end 34 | new(request(:post, self.class.resource_path, JSON.fast_generate(sumhash))) 35 | end 36 | 37 | # @param [#chef_id] object 38 | # 39 | # @raise [Ridley::Errors::SandboxCommitError] 40 | # @raise [Ridley::Errors::ResourceNotFound] 41 | # @raise [Ridley::Errors::PermissionDenied] 42 | # 43 | # @return [Hash] 44 | def commit(object) 45 | chef_id = object.respond_to?(:chef_id) ? object.chef_id : object 46 | request(:put, "#{self.class.resource_path}/#{chef_id}", JSON.fast_generate(is_completed: true)) 47 | rescue AbortError => ex 48 | case ex.cause 49 | when Ridley::Errors::HTTPBadRequest; abort Ridley::Errors::SandboxCommitError.new(ex.message) 50 | when Ridley::Errors::HTTPNotFound; abort Ridley::Errors::ResourceNotFound.new(ex.message) 51 | when Ridley::Errors::HTTPUnauthorized, Ridley::Errors::HTTPForbidden 52 | abort Ridley::Errors::PermissionDenied.new(ex.message) 53 | else; abort(ex.cause) 54 | end 55 | end 56 | 57 | # Concurrently upload all of the files in the given sandbox 58 | # 59 | # @param [Ridley::SandboxObject] sandbox 60 | # @param [Hash] checksums 61 | # a hash of file checksums and file paths 62 | # 63 | # @example 64 | # SandboxUploader.upload(sandbox, 65 | # "e5a0f6b48d0712382295ff30bec1f9cc" => "/Users/reset/code/rbenv-cookbook/recipes/default.rb", 66 | # "de6532a7fbe717d52020dc9f3ae47dbe" => "/Users/reset/code/rbenv-cookbook/recipes/ohai_plugin.rb" 67 | # ) 68 | # 69 | # @return [Array] 70 | def upload(object, checksums) 71 | checksums.collect do |chk_id, path| 72 | uploader.future(:upload, object, chk_id, path) 73 | end.map(&:value) 74 | end 75 | 76 | def update(*args) 77 | abort RuntimeError.new("action not supported") 78 | end 79 | 80 | def all(*args) 81 | abort RuntimeError.new("action not supported") 82 | end 83 | 84 | def find(*args) 85 | abort RuntimeError.new("action not supported") 86 | end 87 | 88 | def delete(*args) 89 | abort RuntimeError.new("action not supported") 90 | end 91 | 92 | def delete_all(*args) 93 | abort RuntimeError.new("action not supported") 94 | end 95 | 96 | private 97 | 98 | attr_reader :uploader 99 | 100 | def finalize_callback 101 | uploader.async.terminate if uploader 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /lib/ridley/resources/user_resource.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | # @example listing all users 3 | # conn = Ridley.new(...) 4 | # conn.user.all #=> [ 5 | # # 6 | # ] 7 | class UserResource < Ridley::Resource 8 | set_resource_path "users" 9 | represented_by Ridley::UserObject 10 | 11 | # Retrieves a user from the remote connection matching the given chef_id 12 | # and regenerates its private key. An instance of the updated object will 13 | # be returned and will have a value set for the 'private_key' accessor. 14 | # 15 | # @param [String, #chef_id] chef_user 16 | # 17 | # @raise [Errors::ResourceNotFound] 18 | # if a user with the given chef_id is not found 19 | # 20 | # @return [Ridley::UserObject] 21 | def regenerate_key(chef_user) 22 | unless chef_user = find(chef_user) 23 | abort Errors::ResourceNotFound.new("user '#{chef_user}' not found") 24 | end 25 | 26 | chef_user.private_key = true 27 | update(chef_user) 28 | end 29 | 30 | def authenticate(username, password) 31 | resp = request(:post, '/authenticate_user', {'name' => username, 'password' => password}.to_json) 32 | abort("Username mismatch: sent #{username}, received #{resp['name']}") unless resp['name'] == username 33 | resp['verified'] 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/ridley/sandbox_uploader.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | # @api private 3 | class SandboxUploader 4 | class << self 5 | # Return the checksum of the contents of the file at the given filepath 6 | # 7 | # @param [String] io 8 | # a filepath or an IO 9 | # @param [Digest::Base] digest 10 | # 11 | # @return [String] 12 | # the binary checksum of the contents of the file 13 | def checksum(io, digest = Digest::MD5.new) 14 | while chunk = io.read(1024 * 8) 15 | digest.update(chunk) 16 | end 17 | digest.hexdigest 18 | end 19 | 20 | # Return a base64 encoded checksum of the contents of the given file. This is the expected 21 | # format of sandbox checksums given to the Chef Server. 22 | # 23 | # @param [String] io 24 | # a filepath or an IO 25 | # 26 | # @return [String] 27 | # a base64 encoded checksum 28 | def checksum64(io) 29 | Base64.encode64([checksum(io)].pack("H*")).strip 30 | end 31 | end 32 | 33 | include Celluloid 34 | 35 | attr_reader :client_name 36 | attr_reader :client_key 37 | attr_reader :options 38 | 39 | def initialize(client_name, client_key, options = {}) 40 | @client_name = client_name 41 | @client_key = client_key 42 | @options = options 43 | end 44 | 45 | # Upload one file into the sandbox for the given checksum id 46 | # 47 | # @param [Ridley::SandboxObject] sandbox 48 | # @param [String] chk_id 49 | # checksum of the file being uploaded 50 | # @param [String, #read] file 51 | # a filepath or an IO 52 | # 53 | # @raise [Errors::ChecksumMismatch] 54 | # if the given file does not match the expected checksum 55 | # 56 | # @return [Hash, nil] 57 | def upload(sandbox, chk_id, file) 58 | checksum = sandbox.checksum(chk_id) 59 | 60 | unless checksum[:needs_upload] 61 | return nil 62 | end 63 | 64 | io = file.respond_to?(:read) ? file : File.new(file, 'rb') 65 | calculated_checksum = self.class.checksum64(io) 66 | expected_checksum = Base64.encode64([chk_id].pack('H*')).strip 67 | 68 | unless calculated_checksum == expected_checksum 69 | raise Errors::ChecksumMismatch, 70 | "Error uploading #{chk_id}. Expected #{expected_checksum} but got #{calculated_checksum}" 71 | end 72 | 73 | headers = { 74 | 'Content-Type' => 'application/x-binary', 75 | 'content-md5' => calculated_checksum 76 | } 77 | 78 | url = URI(checksum[:url]) 79 | upload_path = url.path 80 | url.path = "" 81 | 82 | # versions prior to OSS Chef 11 will strip the port to upload the file to in the checksum 83 | # url returned. This will ensure we are uploading to the proper location. 84 | if sandbox.send(:resource).connection.foss? 85 | url.port = URI(sandbox.send(:resource).connection.server_url).port 86 | end 87 | 88 | begin 89 | io.rewind 90 | 91 | Faraday.new(url, self.options) do |c| 92 | c.response :chef_response 93 | c.response :follow_redirects 94 | c.request :chef_auth, self.client_name, self.client_key 95 | c.adapter :httpclient 96 | end.put(upload_path, io.read, headers) 97 | rescue Ridley::Errors::HTTPError => ex 98 | abort(ex) 99 | end 100 | end 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /lib/ridley/version.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | VERSION = "5.1.1" 3 | end 4 | -------------------------------------------------------------------------------- /ridley.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/ridley/version', __FILE__) 3 | 4 | Gem::Specification.new do |s| 5 | s.authors = ["Jamie Winsor", "Kyle Allan"] 6 | s.email = ["jamie@vialstudios.com", "kallan@riotgames.com"] 7 | s.description = %q{A reliable Chef API client with a clean syntax} 8 | s.summary = s.description 9 | s.homepage = "https://github.com/berkshelf/ridley" 10 | s.license = "Apache 2.0" 11 | 12 | s.files = `git ls-files`.split($\) 13 | s.executables = Array.new 14 | s.test_files = s.files.grep(%r{^(spec)/}) 15 | s.name = "ridley" 16 | s.require_paths = ["lib"] 17 | s.version = Ridley::VERSION 18 | s.required_ruby_version = ">= 2.2" 19 | 20 | s.add_dependency 'addressable' 21 | s.add_dependency 'varia_model', '~> 0.6' 22 | s.add_dependency 'buff-config', '~> 2.0' 23 | s.add_dependency 'buff-extensions', '~> 2.0' 24 | s.add_dependency 'buff-ignore', '~> 1.2' 25 | s.add_dependency 'buff-shell_out', '~> 1.0' 26 | s.add_dependency 'celluloid', '~> 0.16.0' 27 | s.add_dependency 'celluloid-io', '~> 0.16.1' 28 | s.add_dependency 'chef-config', '>= 12.5.0' 29 | s.add_dependency 'erubis' 30 | s.add_dependency 'faraday', '~> 0.9' 31 | s.add_dependency 'hashie', '>= 2.0.2', '< 4.0.0' 32 | s.add_dependency 'httpclient', '~> 2.7' 33 | s.add_dependency 'json', '>= 1.7.7' 34 | s.add_dependency 'mixlib-authentication', '>= 1.3.0' 35 | s.add_dependency 'retryable', '~> 2.0' 36 | s.add_dependency 'semverse', '~> 2.0' 37 | 38 | s.add_development_dependency 'buff-ruby_engine', '~> 0.1' 39 | end 40 | -------------------------------------------------------------------------------- /spec/acceptance/client_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Client API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | describe "finding a client" do 10 | context "when the server has a client of the given name" do 11 | before { chef_client("reset", admin: false) } 12 | 13 | it "returns a ClientObject" do 14 | expect(connection.client.find("reset")).to be_a(Ridley::ClientObject) 15 | end 16 | end 17 | 18 | context "when the server does not have the client" do 19 | it "returns a nil value" do 20 | expect(connection.client.find("not_there")).to be_nil 21 | end 22 | end 23 | end 24 | 25 | describe "creating a client" do 26 | it "returns a Ridley::ClientObject" do 27 | expect(connection.client.create(name: "reset")).to be_a(Ridley::ClientObject) 28 | end 29 | 30 | it "adds a client to the chef server" do 31 | old = connection.client.all.length 32 | connection.client.create(name: "reset") 33 | expect(connection.client.all.size).to eq(old + 1) 34 | end 35 | 36 | it "has a value for #private_key" do 37 | expect(connection.client.create(name: "reset").private_key).not_to be_nil 38 | end 39 | end 40 | 41 | describe "deleting a client" do 42 | before { chef_client("reset", admin: false) } 43 | 44 | it "returns a Ridley::ClientObject object" do 45 | expect(connection.client.delete("reset")).to be_a(Ridley::ClientObject) 46 | end 47 | 48 | it "removes the client from the server" do 49 | connection.client.delete("reset") 50 | 51 | expect(connection.client.find("reset")).to be_nil 52 | end 53 | end 54 | 55 | describe "deleting all clients" do 56 | before(:each) do 57 | chef_client("reset", admin: false) 58 | chef_client("jwinsor", admin: false) 59 | end 60 | 61 | it "returns an array of Ridley::ClientObject objects" do 62 | expect(connection.client.delete_all).to each be_a(Ridley::ClientObject) 63 | end 64 | 65 | it "deletes all clients from the remote" do 66 | connection.client.delete_all 67 | expect(connection.client.all.size).to eq(0) 68 | end 69 | end 70 | 71 | describe "listing all clients" do 72 | before(:each) do 73 | chef_client("reset", admin: false) 74 | chef_client("jwinsor", admin: false) 75 | end 76 | 77 | it "returns an array of Ridley::ClientObject objects" do 78 | expect(connection.client.all).to each be_a(Ridley::ClientObject) 79 | end 80 | 81 | it "returns all of the clients on the server" do 82 | expect(connection.client.all.size).to eq(4) 83 | end 84 | end 85 | 86 | describe "regenerating a client's private key" do 87 | before { chef_client("reset", admin: false) } 88 | 89 | it "returns a Ridley::ClientObject object with a value for #private_key" do 90 | expect(connection.client.regenerate_key("reset").private_key).to match(/^-----BEGIN RSA PRIVATE KEY-----/) 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /spec/acceptance/cookbook_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Client API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | subject { connection.cookbook } 10 | 11 | describe "downloading a cookbook" do 12 | before { subject.upload(fixtures_path.join('example_cookbook')) } 13 | let(:name) { "example_cookbook" } 14 | let(:version) { "0.1.0" } 15 | let(:destination) { tmp_path.join("example_cookbook-0.1.0") } 16 | 17 | context "when the cookbook of the name/version is found" do 18 | before { subject.download(name, version, destination) } 19 | 20 | it "downloads the cookbook to the destination" do 21 | expect(File.exist?(destination.join("metadata.json"))).to be_truthy 22 | end 23 | end 24 | end 25 | 26 | describe "uploading a cookbook" do 27 | let(:path) { fixtures_path.join("example_cookbook") } 28 | 29 | it "uploads the entire contents of the cookbook in the given path, applying chefignore" do 30 | subject.upload(path) 31 | cookbook = subject.find("example_cookbook", "0.1.0") 32 | 33 | expect(cookbook.attributes.size).to eq(1) 34 | expect(cookbook.definitions.size).to eq(1) 35 | expect(cookbook.files.size).to eq(2) 36 | expect(cookbook.libraries.size).to eq(1) 37 | expect(cookbook.providers.size).to eq(1) 38 | expect(cookbook.recipes.size).to eq(1) 39 | expect(cookbook.resources.size).to eq(1) 40 | expect(cookbook.templates.size).to eq(1) 41 | expect(cookbook.root_files.size).to eq(1) 42 | end 43 | 44 | it "does not contain a raw metadata.rb but does contain a compiled metadata.json" do 45 | subject.upload(path) 46 | cookbook = subject.find("example_cookbook", "0.1.0") 47 | 48 | expect(cookbook.root_files.any? { |f| f[:name] == "metadata.json" }).to be_truthy 49 | expect(cookbook.root_files.any? { |f| f[:name] == "metadata.rb" }).to be_falsey 50 | end 51 | end 52 | 53 | describe "listing cookbooks" do 54 | before do 55 | chef_cookbook("ruby", "1.0.0") 56 | chef_cookbook("ruby", "2.0.0") 57 | chef_cookbook("elixir", "3.0.0") 58 | chef_cookbook("elixir", "3.0.1") 59 | end 60 | 61 | it "returns all of the cookbooks on the server" do 62 | all_cookbooks = subject.all 63 | expect(all_cookbooks.size).to eq(2) 64 | expect(all_cookbooks["ruby"].size).to eq(2) 65 | expect(all_cookbooks["elixir"].size).to eq(2) 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /spec/acceptance/data_bag_item_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "DataBag API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | let(:data_bag) do 10 | chef_data_bag("ridley-test") 11 | connection.data_bag.find("ridley-test") 12 | end 13 | 14 | describe "listing data bag items" do 15 | context "when the data bag has no items" do 16 | it "returns an empty array" do 17 | expect(data_bag.item.all.size).to eq(0) 18 | end 19 | end 20 | 21 | context "when the data bag has items" do 22 | before(:each) do 23 | data_bag.item.create(id: "one") 24 | data_bag.item.create(id: "two") 25 | end 26 | 27 | it "returns an array with each item" do 28 | expect(data_bag.item.all.size).to eq(2) 29 | end 30 | end 31 | end 32 | 33 | describe "creating a data bag item" do 34 | it "adds a data bag item to the collection of data bag items" do 35 | data_bag.item.create(id: "appconfig", host: "host.local", port: 80, admin: false, servers: ["one"]) 36 | 37 | expect(data_bag.item.all.size).to eq(1) 38 | end 39 | 40 | context "when an 'id' field is missing" do 41 | it "raises an Ridley::Errors::InvalidResource error" do 42 | expect { 43 | data_bag.item.create(name: "jamie") 44 | }.to raise_error(Ridley::Errors::InvalidResource) 45 | end 46 | end 47 | end 48 | 49 | describe "retrieving a data bag item" do 50 | it "returns the desired item in the data bag" do 51 | attributes = { 52 | "id" => "appconfig", 53 | "host" => "host.local", 54 | "port" => 80, 55 | "admin" => false, 56 | "servers" => [ 57 | "one" 58 | ] 59 | } 60 | data_bag.item.create(attributes) 61 | 62 | expect(data_bag.item.find("appconfig").to_hash).to eql(attributes) 63 | end 64 | end 65 | 66 | describe "deleting a data bag item" do 67 | let(:attributes) do 68 | { 69 | "id" => "appconfig", 70 | "host" => "host.local" 71 | } 72 | end 73 | 74 | before { data_bag.item.create(attributes) } 75 | 76 | it "returns the deleted data bag item" do 77 | dbi = data_bag.item.delete(attributes["id"]) 78 | 79 | expect(dbi).to be_a(Ridley::DataBagItemObject) 80 | expect(dbi.attributes).to eql(attributes) 81 | end 82 | 83 | it "deletes the data bag item from the server" do 84 | data_bag.item.delete(attributes["id"]) 85 | 86 | expect(data_bag.item.find(attributes["id"])).to be_nil 87 | end 88 | end 89 | 90 | describe "deleting all data bag items in a data bag" do 91 | before do 92 | data_bag.item.create(id: "one") 93 | data_bag.item.create(id: "two") 94 | end 95 | 96 | it "returns the array of deleted data bag items" do 97 | expect(data_bag.item.delete_all).to each be_a(Ridley::DataBagItemObject) 98 | end 99 | 100 | it "removes all data bag items from the data bag" do 101 | data_bag.item.delete_all 102 | 103 | expect(data_bag.item.all.size).to eq(0) 104 | end 105 | end 106 | 107 | describe "updating a data bag item" do 108 | before { data_bag.item.create(id: "one") } 109 | 110 | it "returns the updated data bag item" do 111 | dbi = data_bag.item.update(id: "one", name: "brooke") 112 | 113 | expect(dbi[:name]).to eql("brooke") 114 | end 115 | end 116 | 117 | describe "saving a data bag item" do 118 | context "when the data bag item exists" do 119 | let(:dbi) { data_bag.item.create(id: "ridley-test") } 120 | 121 | it "returns true if successful" do 122 | dbi[:name] = "brooke" 123 | expect(dbi.save).to be_truthy 124 | end 125 | 126 | it "creates a new data bag item on the remote" do 127 | dbi[:name] = "brooke" 128 | dbi.save 129 | 130 | expect(data_bag.item.all.size).to eq(1) 131 | end 132 | end 133 | 134 | context "when the data bag item does not exist" do 135 | it "returns true if successful" do 136 | dbi = data_bag.item.new 137 | 138 | dbi.attributes = { id: "not-there", name: "brooke" } 139 | expect(dbi.save).to be_truthy 140 | end 141 | 142 | it "creates a new data bag item on the remote" do 143 | dbi = data_bag.item.new 144 | dbi.attributes = { id: "not-there", name: "brooke" } 145 | dbi.save 146 | 147 | expect(data_bag.item.all.size).to eq(1) 148 | end 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/acceptance/data_bag_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "DataBag API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | describe "listing data bags" do 10 | context "when no data bags exist" do 11 | it "returns an empty array" do 12 | expect(connection.data_bag.all.size).to eq(0) 13 | end 14 | end 15 | 16 | context "when the server has data bags" do 17 | before do 18 | chef_data_bag("ridley-one") 19 | chef_data_bag("ridley-two") 20 | end 21 | 22 | it "returns an array of data bags" do 23 | expect(connection.data_bag.all).to each be_a(Ridley::DataBagObject) 24 | end 25 | 26 | it "returns all of the data bags on the server" do 27 | expect(connection.data_bag.all.size).to eq(2) 28 | end 29 | end 30 | end 31 | 32 | describe "creating a data bag" do 33 | it "returns a Ridley::DataBagObject" do 34 | expect(connection.data_bag.create(name: "ridley-one")).to be_a(Ridley::DataBagObject) 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/acceptance/environment_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Environment API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | describe "finding an environment" do 10 | before { chef_environment("ridley-test-env") } 11 | 12 | it "returns a valid Ridley::EnvironmentObject object" do 13 | expect(connection.environment.find("ridley-test-env")).to be_a(Ridley::EnvironmentObject) 14 | end 15 | end 16 | 17 | describe "creating an environment" do 18 | it "returns a valid Ridley::EnvironmentObject object" do 19 | obj = connection.environment.create(name: "ridley-test-env", description: "a testing env for ridley") 20 | 21 | expect(obj).to be_a(Ridley::EnvironmentObject) 22 | end 23 | 24 | it "adds an environment to the chef server" do 25 | old = connection.environment.all.length 26 | connection.environment.create(name: "ridley") 27 | expect(connection.environment.all.size).to eq(old + 1) 28 | end 29 | end 30 | 31 | describe "deleting an environment" do 32 | before { chef_environment("ridley-env") } 33 | 34 | it "returns a Ridley::EnvironmentObject object" do 35 | expect(connection.environment.delete("ridley-env")).to be_a(Ridley::EnvironmentObject) 36 | end 37 | 38 | it "removes the environment from the server" do 39 | connection.environment.delete("ridley-env") 40 | 41 | expect(connection.environment.find("ridley-env")).to be_nil 42 | end 43 | 44 | it "raises Ridley::Errors::HTTPMethodNotAllowed when attempting to delete the '_default' environment" do 45 | expect { 46 | connection.environment.delete("_default") 47 | }.to raise_error(Ridley::Errors::HTTPMethodNotAllowed) 48 | end 49 | end 50 | 51 | describe "deleting all environments" do 52 | before do 53 | chef_environment("ridley-one") 54 | chef_environment("ridley-two") 55 | end 56 | 57 | it "returns an array of Ridley::EnvironmentObject objects" do 58 | expect(connection.environment.delete_all).to each be_a(Ridley::EnvironmentObject) 59 | end 60 | 61 | it "deletes all environments but '_default' from the remote" do 62 | connection.environment.delete_all 63 | 64 | expect(connection.environment.all.size).to eq(1) 65 | end 66 | end 67 | 68 | describe "listing all environments" do 69 | it "should return an array of Ridley::EnvironmentObject objects" do 70 | expect(connection.environment.all).to each be_a(Ridley::EnvironmentObject) 71 | end 72 | end 73 | 74 | describe "updating an environment" do 75 | before { chef_environment("ridley-env") } 76 | let(:target ) { connection.environment.find("ridley-env") } 77 | 78 | it "saves a new #description" do 79 | target.description = description = "ridley testing environment" 80 | 81 | connection.environment.update(target) 82 | expect(target.reload.description).to eql(description) 83 | end 84 | 85 | it "saves a new set of 'default_attributes'" do 86 | target.default_attributes = default_attributes = { 87 | "attribute_one" => "val_one", 88 | "nested" => { 89 | "other" => "val" 90 | } 91 | } 92 | 93 | connection.environment.update(target) 94 | obj = connection.environment.find(target) 95 | expect(obj.default_attributes).to eql(default_attributes) 96 | end 97 | 98 | it "saves a new set of 'override_attributes'" do 99 | target.override_attributes = override_attributes = { 100 | "attribute_one" => "val_one", 101 | "nested" => { 102 | "other" => "val" 103 | } 104 | } 105 | 106 | connection.environment.update(target) 107 | obj = connection.environment.find(target) 108 | expect(obj.override_attributes).to eql(override_attributes) 109 | end 110 | 111 | it "saves a new set of 'cookbook_versions'" do 112 | target.cookbook_versions = cookbook_versions = { 113 | "nginx" => "1.2.0", 114 | "tomcat" => "1.3.0" 115 | } 116 | 117 | connection.environment.update(target) 118 | obj = connection.environment.find(target) 119 | expect(obj.cookbook_versions).to eql(cookbook_versions) 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /spec/acceptance/node_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Node API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | describe "finding a node" do 10 | let(:node_name) { "ridley.localhost" } 11 | before { chef_node(node_name) } 12 | 13 | it "returns a Ridley::NodeObject" do 14 | expect(connection.node.find(node_name)).to be_a(Ridley::NodeObject) 15 | end 16 | end 17 | 18 | describe "creating a node" do 19 | let(:node_name) { "ridley.localhost" } 20 | 21 | it "returns a new Ridley::NodeObject object" do 22 | expect(connection.node.create(name: node_name)).to be_a(Ridley::NodeObject) 23 | end 24 | 25 | it "adds a new node to the server" do 26 | connection.node.create(name: node_name) 27 | 28 | expect(connection.node.all.size).to eq(1) 29 | end 30 | end 31 | 32 | describe "deleting a node" do 33 | let(:node_name) { "ridley.localhost" } 34 | before { chef_node(node_name) } 35 | 36 | it "returns a Ridley::NodeObject" do 37 | expect(connection.node.delete(node_name)).to be_a(Ridley::NodeObject) 38 | end 39 | 40 | it "removes the node from the server" do 41 | connection.node.delete(node_name) 42 | 43 | expect(connection.node.find(node_name)).to be_nil 44 | end 45 | end 46 | 47 | describe "deleting all nodes" do 48 | before do 49 | chef_node("ridley.localhost") 50 | chef_node("motherbrain.localhost") 51 | end 52 | 53 | it "deletes all nodes from the remote server" do 54 | connection.node.delete_all 55 | 56 | expect(connection.node.all.size).to eq(0) 57 | end 58 | end 59 | 60 | describe "listing all nodes" do 61 | before do 62 | chef_node("ridley.localhost") 63 | chef_node("motherbrain.localhost") 64 | end 65 | 66 | it "returns an array of Ridley::NodeObject" do 67 | obj = connection.node.all 68 | 69 | expect(obj).to each be_a(Ridley::NodeObject) 70 | expect(obj.size).to eq(2) 71 | end 72 | end 73 | 74 | describe "updating a node" do 75 | let(:node_name) { "ridley.localhost" } 76 | before { chef_node(node_name) } 77 | let(:target) { connection.node.find(node_name) } 78 | 79 | it "returns the updated node" do 80 | expect(connection.node.update(target)).to eql(target) 81 | end 82 | 83 | it "saves a new set of 'normal' attributes" do 84 | target.normal = normal = { 85 | "attribute_one" => "value_one", 86 | "nested" => { 87 | "other" => "val" 88 | } 89 | } 90 | 91 | connection.node.update(target) 92 | obj = connection.node.find(target) 93 | 94 | expect(obj.normal).to eql(normal) 95 | end 96 | 97 | it "saves a new set of 'default' attributes" do 98 | target.default = defaults = { 99 | "attribute_one" => "val_one", 100 | "nested" => { 101 | "other" => "val" 102 | } 103 | } 104 | 105 | connection.node.update(target) 106 | obj = connection.node.find(target) 107 | 108 | expect(obj.default).to eql(defaults) 109 | end 110 | 111 | it "saves a new set of 'automatic' attributes" do 112 | target.automatic = automatics = { 113 | "attribute_one" => "val_one", 114 | "nested" => { 115 | "other" => "val" 116 | } 117 | } 118 | 119 | connection.node.update(target) 120 | obj = connection.node.find(target) 121 | 122 | expect(obj.automatic).to eql(automatics) 123 | end 124 | 125 | it "saves a new set of 'override' attributes" do 126 | target.override = overrides = { 127 | "attribute_one" => "val_one", 128 | "nested" => { 129 | "other" => "val" 130 | } 131 | } 132 | 133 | connection.node.update(target) 134 | obj = connection.node.find(target) 135 | 136 | expect(obj.override).to eql(overrides) 137 | end 138 | 139 | it "places a node in a new 'chef_environment'" do 140 | target.chef_environment = environment = "ridley" 141 | 142 | connection.node.update(target) 143 | obj = connection.node.find(target) 144 | 145 | expect(obj.chef_environment).to eql(environment) 146 | end 147 | 148 | it "saves a new 'run_list' for the node" do 149 | target.run_list = run_list = ["recipe[one]", "recipe[two]"] 150 | 151 | connection.node.update(target) 152 | obj = connection.node.find(target) 153 | 154 | expect(obj.run_list).to eql(run_list) 155 | end 156 | end 157 | end 158 | -------------------------------------------------------------------------------- /spec/acceptance/role_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Role API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | describe "finding a role" do 10 | let(:role_name) { "ridley-role" } 11 | before { chef_role(role_name) } 12 | 13 | it "returns a Ridley::RoleObject" do 14 | expect(connection.role.find(role_name)).to be_a(Ridley::RoleObject) 15 | end 16 | end 17 | 18 | describe "creating a role" do 19 | let(:role_name) { "ridley-role" } 20 | 21 | it "returns a new Ridley::RoleObject" do 22 | expect(connection.role.create(name: role_name)).to be_a(Ridley::RoleObject) 23 | end 24 | 25 | it "adds a new role to the server" do 26 | connection.role.create(name: role_name) 27 | expect(connection.role.all.size).to eq(1) 28 | end 29 | end 30 | 31 | describe "deleting a role" do 32 | let(:role_name) { "ridley-role" } 33 | before { chef_role(role_name) } 34 | 35 | it "returns the deleted Ridley::RoleObject resource" do 36 | expect(connection.role.delete(role_name)).to be_a(Ridley::RoleObject) 37 | end 38 | 39 | it "removes the role from the server" do 40 | connection.role.delete(role_name) 41 | 42 | expect(connection.role.find(role_name)).to be_nil 43 | end 44 | end 45 | 46 | describe "deleting all roles" do 47 | before do 48 | chef_role("role_one") 49 | chef_role("role_two") 50 | end 51 | 52 | it "deletes all nodes from the remote server" do 53 | connection.role.delete_all 54 | 55 | expect(connection.role.all.size).to eq(0) 56 | end 57 | end 58 | 59 | describe "listing all roles" do 60 | before do 61 | chef_role("role_one") 62 | chef_role("role_two") 63 | end 64 | 65 | it "should return an array of Ridley::RoleObject" do 66 | obj = connection.role.all 67 | 68 | expect(obj.size).to eq(2) 69 | expect(obj).to each be_a(Ridley::RoleObject) 70 | end 71 | end 72 | 73 | describe "updating a role" do 74 | let(:role_name) { "ridley-role" } 75 | before { chef_role(role_name) } 76 | let(:target) { connection.role.find(role_name) } 77 | 78 | it "returns an updated Ridley::RoleObject object" do 79 | expect(connection.role.update(target)).to eql(target) 80 | end 81 | 82 | it "saves a new run_list" do 83 | target.run_list = run_list = ["recipe[one]", "recipe[two]"] 84 | 85 | connection.role.update(target) 86 | obj = connection.role.find(target) 87 | 88 | expect(obj.run_list).to eql(run_list) 89 | end 90 | 91 | it "saves a new env_run_lists" do 92 | target.env_run_lists = env_run_lists = { 93 | "production" => ["recipe[one]"], 94 | "development" => ["recipe[two]"] 95 | } 96 | 97 | connection.role.update(target) 98 | obj = connection.role.find(target) 99 | 100 | expect(obj.env_run_lists).to eql(env_run_lists) 101 | end 102 | 103 | it "saves a new description" do 104 | target.description = description = "a new description!" 105 | 106 | connection.role.update(target) 107 | obj = connection.role.find(target) 108 | 109 | expect(obj.description).to eql(description) 110 | end 111 | 112 | it "saves a new default_attributes" do 113 | target.default_attributes = defaults = { 114 | "attribute_one" => "value_one", 115 | "nested" => { 116 | "other" => false 117 | } 118 | } 119 | 120 | connection.role.update(target) 121 | obj = connection.role.find(target) 122 | 123 | expect(obj.default_attributes).to eql(defaults) 124 | end 125 | 126 | it "saves a new override_attributes" do 127 | target.override_attributes = overrides = { 128 | "attribute_two" => "value_two", 129 | "nested" => { 130 | "other" => false 131 | } 132 | } 133 | 134 | connection.role.update(target) 135 | obj = connection.role.find(target) 136 | 137 | expect(obj.override_attributes).to eql(overrides) 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /spec/acceptance/sandbox_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Sandbox API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | let(:checksums) do 10 | [ 11 | Ridley::SandboxUploader.checksum(File.open(fixtures_path.join("recipe_one.rb"))), 12 | Ridley::SandboxUploader.checksum(File.open(fixtures_path.join("recipe_two.rb"))) 13 | ] 14 | end 15 | 16 | describe "creating a new sandbox" do 17 | it "returns an instance of Ridley::SandboxObject" do 18 | expect(connection.sandbox.create(checksums)).to be_a(Ridley::SandboxObject) 19 | end 20 | 21 | it "contains a value for sandbox_id" do 22 | expect(connection.sandbox.create(checksums).sandbox_id).not_to be_nil 23 | end 24 | 25 | it "returns an instance with the same amount of checksums given to create" do 26 | expect(connection.sandbox.create(checksums).checksums.size).to eq(2) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/acceptance/search_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "Search API operations", type: "acceptance" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: client_name, client_key: client_key) } 8 | 9 | describe "listing indexes" do 10 | it "returns an array of indexes" do 11 | indexes = connection.search_indexes 12 | 13 | expect(indexes).to include("role") 14 | expect(indexes).to include("node") 15 | expect(indexes).to include("client") 16 | expect(indexes).to include("environment") 17 | end 18 | end 19 | 20 | describe "searching an index that doesn't exist" do 21 | it "it raises a Ridley::Errors::HTTPNotFound error" do 22 | expect { 23 | connection.search(:notthere) 24 | }.to raise_error(Ridley::Errors::HTTPNotFound) 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/acceptance/user_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "User API operations", type: "wip" do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:user_name) { "reset" } 6 | let(:user_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley.new(server_url: server_url, client_name: user_name, client_key: user_key) } 8 | 9 | describe "finding a user" do 10 | context "when the server has a user of the given name" do 11 | before { chef_user("reset", admin: false) } 12 | 13 | it "returns a UserObject" do 14 | expect(connection.user.find("reset")).to be_a(Ridley::UserObject) 15 | end 16 | end 17 | 18 | context "when the server does not have the user" do 19 | it "returns a nil value" do 20 | expect(connection.user.find("not_there")).to be_nil 21 | end 22 | end 23 | end 24 | 25 | describe "creating a user" do 26 | it "returns a Ridley::UserObject" do 27 | expect(connection.user.create(name: "reset")).to be_a(Ridley::UserObject) 28 | end 29 | 30 | it "adds a user to the chef server" do 31 | old = connection.user.all.length 32 | connection.user.create(name: "reset") 33 | expect(connection.user.all.size).to eq(old + 1) 34 | end 35 | 36 | it "has a value for #private_key" do 37 | expect(connection.user.create(name: "reset").private_key).not_to be_nil 38 | end 39 | end 40 | 41 | describe "deleting a user" do 42 | before { chef_user("reset", admin: false) } 43 | 44 | it "returns a Ridley::UserObject object" do 45 | expect(connection.user.delete("reset")).to be_a(Ridley::UserObject) 46 | end 47 | 48 | it "removes the user from the server" do 49 | connection.user.delete("reset") 50 | 51 | expect(connection.user.find("reset")).to be_nil 52 | end 53 | end 54 | 55 | describe "deleting all users" do 56 | before(:each) do 57 | chef_user("reset", admin: false) 58 | chef_user("jwinsor", admin: false) 59 | end 60 | 61 | it "returns an array of Ridley::UserObject objects" do 62 | expect(connection.user.delete_all).to each be_a(Ridley::UserObject) 63 | end 64 | 65 | it "deletes all users from the remote" do 66 | connection.user.delete_all 67 | expect(connection.user.all.size).to eq(0) 68 | end 69 | end 70 | 71 | describe "listing all users" do 72 | before(:each) do 73 | chef_user("reset", admin: false) 74 | chef_user("jwinsor", admin: false) 75 | end 76 | 77 | it "returns an array of Ridley::UserObject objects" do 78 | expect(connection.user.all).to each be_a(Ridley::UserObject) 79 | end 80 | 81 | it "returns all of the users on the server" do 82 | expect(connection.user.all.size).to eq(3) 83 | end 84 | end 85 | 86 | describe "regenerating a user's private key" do 87 | before { chef_user("reset", admin: false) } 88 | 89 | it "returns a Ridley::UserObject object with a value for #private_key" do 90 | expect(connection.user.regenerate_key("reset").private_key).to match(/^-----BEGIN RSA PRIVATE KEY-----/) 91 | end 92 | end 93 | 94 | describe "authenticating a user" do 95 | before { chef_user('reset', password: 'swordfish') } 96 | 97 | it "returns true when given valid username & password" do 98 | expect(connection.user.authenticate('reset', 'swordfish')).to be_truthy 99 | end 100 | 101 | it "returns false when given valid username & invalid password" do 102 | expect(connection.user.authenticate('reset', "not a swordfish")).to be_falsey 103 | end 104 | 105 | it "returns false when given invalid username & valid password" do 106 | expect(connection.user.authenticate("someone-else", 'swordfish')).to be_falsey 107 | end 108 | 109 | it "works also on a User object level" do 110 | expect(connection.user.find('reset').authenticate('swordfish')).to be_truthy 111 | expect(connection.user.find('reset').authenticate('not a swordfish')).to be_falsey 112 | end 113 | end 114 | 115 | describe "changing user's password" do 116 | before { chef_user('reset', password: 'swordfish') } 117 | subject { connection.user.find('reset') } 118 | 119 | it "changes the password with which user can authenticate" do 120 | expect(subject.authenticate('swordfish')).to be_truthy 121 | expect(subject.authenticate('salmon')).to be_falsey 122 | 123 | subject.password = 'salmon' 124 | subject.save 125 | 126 | expect(subject.authenticate('swordfish')).to be_falsey 127 | expect(subject.authenticate('salmon')).to be_truthy 128 | end 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /spec/fixtures/chefignore: -------------------------------------------------------------------------------- 1 | README.md 2 | Guardfile 3 | ignores/*.rb 4 | ignores/*.erb 5 | -------------------------------------------------------------------------------- /spec/fixtures/encrypted_data_bag_secret: -------------------------------------------------------------------------------- 1 | NTE5C5gzbe3F7fGBlrT/YTuMQuHlxaOWhY81KsgYXJ0mzDsDMFvCpfi4CUXu5M7n/Umgsf8jdHw/IIkJLZcXgk+Ll75kDU/VI5FyRzib0U0SX4JB8JLM7wRFgRpuk3GD27LnYR1APmLncE7R6ZSJc6iaFHcEL2MdR+hv0nhUZPUqITxYHyrYvqNSfroyxldQ/cvnrIMBS8JrpjIsLdYhcgL6mPJiakl4fM36cFk6Wl2Mxw7vOvGXRSR5l+t42pGDhtOjE3os5reLVoWkYoiQ1fpx3NrOdxsVuz17+3jMLBlmni2SGf2wncui2r9PqCrVbUbaCi6aNV1+SRbeh5kxBxjWSzw59BNXtna4vSK6hFPsT6tfXlOi67Q2vwjjAqltAVStGas/VZyU7DRzxMkbnPPtue+7Pajqe/TfSNWA5SX2cHtkG2X3EqZ8ftOa9p+b/VJlUnnnV2ilVfgjCW2q6XXMbC0C5yIbrDZm+aCJyhueA0j+ZHWM4k07OAuB7FRcuJJBs8H2StEx2o22OdAYUBcN5PRGlOAEBemL+sZAztbex2NjjOYV90806UFvSJLPixVWJgDTSA5OvXtNvUQYDYSGRQ/BmH86aA5gJ60AM9vEVB0BfPi946m9D4LZ/2uK6fqq3zVIV1s0EgfYHYUVz0oaf3srofc5YUAP3Ge/VLE= -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/Guardfile: -------------------------------------------------------------------------------- 1 | # This should be ignored by chefignore 2 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/README.md: -------------------------------------------------------------------------------- 1 | Description 2 | =========== 3 | 4 | Requirements 5 | ============ 6 | 7 | Attributes 8 | ========== 9 | 10 | Usage 11 | ===== 12 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/attributes/default.rb: -------------------------------------------------------------------------------- 1 | # Attribute:: default 2 | # 3 | # Copyright 2012, YOUR_COMPANY_NAME 4 | # 5 | # All rights reserved - Do Not Redistribute 6 | # 7 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/definitions/bad_def.rb: -------------------------------------------------------------------------------- 1 | # Definition: bad_def 2 | # 3 | # Copyright 2012, YOUR_COMPANY_NAME 4 | # 5 | # All rights reserved - Do Not Redistribute 6 | # 7 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/files/default/file.h: -------------------------------------------------------------------------------- 1 | # file.h 2 | hello 3 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/files/ubuntu/file.h: -------------------------------------------------------------------------------- 1 | # file.h 2 | hello, ubuntu 3 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/ignores/magic.erb: -------------------------------------------------------------------------------- 1 | # I should never see this file 2 | # It has bad ruby in it... 3 | 4 | <%= this is not { valid %> 5 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/ignores/magic.rb: -------------------------------------------------------------------------------- 1 | # I should never see this file 2 | # It has bad ruby in it... 3 | 4 | this is not { valid 5 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/ignores/ok.txt: -------------------------------------------------------------------------------- 1 | This one is okay, though 2 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/libraries/my_lib.rb: -------------------------------------------------------------------------------- 1 | # Library: my_lib 2 | # 3 | # Copyright 2012, YOUR_COMPANY_NAME 4 | # 5 | # All rights reserved - Do Not Redistribute 6 | # 7 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/metadata.rb: -------------------------------------------------------------------------------- 1 | name "example_cookbook" 2 | maintainer "Jamie Winsor" 3 | maintainer_email "jamie@vialstudios.com" 4 | license "Apache 2.0" 5 | description "Installs/Configures example_cookbook" 6 | long_description IO.read(File.join(File.dirname(__FILE__), "README.md")) 7 | version "0.1.0" 8 | 9 | attribute "example_cookbook/test", 10 | :display_name => "Test", 11 | :description => "Test Attribute", 12 | :choice => [ 13 | "test1", 14 | "test2" ], 15 | :type => "string", 16 | :required => "recommended", 17 | :recipes => [ 'example_cookbook::default' ], 18 | :default => "test1" 19 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/providers/defprovider.rb: -------------------------------------------------------------------------------- 1 | # Provider: defprovider 2 | # 3 | # Copyright 2012, YOUR_COMPANY_NAME 4 | # 5 | # All rights reserved - Do Not Redistribute 6 | # 7 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/recipes/default.rb: -------------------------------------------------------------------------------- 1 | # Recipe: default 2 | # 3 | # Copyright 2012, YOUR_COMPANY_NAME 4 | # 5 | # All rights reserved - Do Not Redistribute 6 | # 7 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/resources/defresource.rb: -------------------------------------------------------------------------------- 1 | # Resource: defresource 2 | # 3 | # Copyright 2012, YOUR_COMPANY_NAME 4 | # 5 | # All rights reserved - Do Not Redistribute 6 | # 7 | -------------------------------------------------------------------------------- /spec/fixtures/example_cookbook/templates/default/temp.txt.erb: -------------------------------------------------------------------------------- 1 | <%= 'hello' %> -------------------------------------------------------------------------------- /spec/fixtures/recipe_one.rb: -------------------------------------------------------------------------------- 1 | testfile -------------------------------------------------------------------------------- /spec/fixtures/recipe_two.rb: -------------------------------------------------------------------------------- 1 | testfile two -------------------------------------------------------------------------------- /spec/fixtures/reset.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpQIBAAKCAQEAyyUMqrTh1IzKOyE0fvXEWC7m0AdMI8/dr9JJMUKtK9vhhP0w 3 | rm6m95GoybFM2IRryukFsAxpcir3M1ungTU3Smq4MshhMJ7H9FbvZVfQoknTbCsR 4 | w6scg2fBepxT2+fcGRufr8nAh92M3uUkN9bMMTAkt18D4br6035YvdmvHDJERxYq 5 | ByA/720AdI9VNSIvw+x8oqsIkXLEdF6dgT9MpG5iWZT66pbFsnNZpRrd4/bFNWBY 6 | +13aOqdmjiTL08/EdgQFKMT5qimpos1TuQhA7mwInOjQgzVu9uCDkMiYejaLbUz0 7 | lGyS8y4uxu6z2hA900Jg/z+JJuXymH5QAX3GZQIDAQABAoIBAQCtFXkwbYPI1Nht 8 | /wG6du5+8B9K+hy+mppY9wPTy+q+Zs9Ev3Fd/fuXDm1QxBckl9c8AMUO1dR2KPOM 9 | t7gFl/DvH/SnmCFvCqp1nijFIUgrLlnMXPn6zG0z7RBlxpKQ2IGohufNIEpBuNwR 10 | Ag2U4hgChPGTp4ooJ2cVEh7MS5AupYPDbC62dWEdW68aRTWhh2BCGAWBb6s16yl9 11 | aZ7+OcxW2eeRJVbRfLkLQEDutJZi5TfOEn5QPc86ZgxcCmnvwulnpnhpz6QCkgQt 12 | OP/+KRqDhWSDVCFREVT30fUIj1EWvK7NFWASZQxueZStuIvMEKeFebYfrbHxRFzJ 13 | UmaxJnWVAoGBAPbKLpeky6ClccBaHHrCgjzakoDfGgyNKDQ9g753lJxB8nn7d9X4 14 | HQpkWpfqAGFRZp1hI2H+VxyUXLh2Ob5OUeTm0OZJll35vycOaQEtfgIScXTcvzn0 15 | 16J9eX2YY4wIHEEMh85nKk8BEGgiNP5nuEviHocCeYXoi/Zq3+qj6v63AoGBANK5 16 | 4nyi6LBQFs1CUc7Sh7vjtOE3ia7KeRmOr7gS6QhS3iK3Oa8FzBLJ6ETjN2a9Bw8N 17 | cF7I/+cr4s7DUJjxdb53D/J6TVSYORNNCUVnpF/uB2LqqdXDYmpO0PvFkXFoYTnJ 18 | kaLAN8uCoLKr6JH9tq3DfXIfDIHiZ+BOIvI070fDAoGBAMDyzEDFmGruTyRLj66u 19 | +rJnVVmqlKwxhLhrS+CTj74nlVOnt0a0KMhiM65IRqnPwcHUG5zXBPaUTHXwAS93 20 | /nFPwQ37hLPOupPnoVNJZRZrowbyPBQtCJbDMURv64ylHqoBCQDoCd0hANnZvMMX 21 | BrFVhfaaibaXXS542r6SD/27AoGAECadHE5kJTdOOBcwK/jo3Fa8g1J9Y/8yvum3 22 | wBT69V9clS6T5j08geglvDnqAh7UzquKBEnFi1NKw+wmXkKLcrivaTdEfApavYb3 23 | AfHKoGue907jC3Y5Mcquq81ds2J7qTEwz1eKLzfo1yjj32ShvrmwALIuhDn1GjUC 24 | 6qtx938CgYEApEqvu0nocR1jmVVlLe5uKQBj949dh6NGq0R5Lztz6xufaTYzMC3d 25 | AZG9XPPjRqSLs+ylSXJpwHEwoeyLFDaJcO+GgW1/ut4MC2HppOx6aImwDdXMHUWR 26 | KYGIFF4AU/IYoBcanAm4s078EH/Oz01B2c7tR2TqabisPgLYe7PXSCw= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler' 3 | require 'buff/ruby_engine' 4 | 5 | def setup_rspec 6 | require 'rspec' 7 | require 'json_spec' 8 | require 'webmock/rspec' 9 | 10 | Dir[File.join(File.expand_path("../../spec/support/**/*.rb", __FILE__))].each { |f| require f } 11 | 12 | RSpec.configure do |config| 13 | config.include Ridley::SpecHelpers 14 | config.include Ridley::RSpec::ChefServer 15 | config.include JsonSpec::Helpers 16 | 17 | config.mock_with :rspec 18 | config.treat_symbols_as_metadata_keys_with_true_values = true 19 | config.filter_run focus: true 20 | config.run_all_when_everything_filtered = true 21 | 22 | config.before(:suite) do 23 | WebMock.disable_net_connect!(allow_localhost: true, net_http_connect_on_start: true) 24 | Ridley::RSpec::ChefServer.start 25 | end 26 | 27 | config.before(:all) { Ridley.logger = Celluloid.logger = nil } 28 | 29 | config.before(:each) do 30 | Celluloid.shutdown 31 | Celluloid.boot 32 | clean_tmp_path 33 | Ridley::RSpec::ChefServer.server.clear_data 34 | end 35 | end 36 | end 37 | 38 | if Buff::RubyEngine.mri? && ENV['CI'] != 'true' 39 | require 'spork' 40 | 41 | Spork.prefork do 42 | setup_rspec 43 | end 44 | 45 | Spork.each_run do 46 | require 'ridley' 47 | end 48 | else 49 | require 'ridley' 50 | setup_rspec 51 | end 52 | -------------------------------------------------------------------------------- /spec/support/actor_mocking.rb: -------------------------------------------------------------------------------- 1 | RSpec.configuration.before(:each) do 2 | class Celluloid::CellProxy 3 | unless @rspec_compatible 4 | @rspec_compatible = true 5 | undef_method :should_receive if method_defined?(:should_receive) 6 | undef_method :stub if method_defined?(:stub) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/support/chef_server.rb: -------------------------------------------------------------------------------- 1 | require 'chef_zero/server' 2 | require_relative 'spec_helpers' 3 | 4 | module Ridley::RSpec 5 | module ChefServer 6 | class << self 7 | include Ridley::SpecHelpers 8 | 9 | def clear_request_log 10 | @request_log = Array.new 11 | end 12 | 13 | def request_log 14 | @request_log ||= Array.new 15 | end 16 | 17 | def server 18 | @server ||= ChefZero::Server.new(port: PORT, generate_real_keys: false) 19 | end 20 | 21 | def server_url 22 | (@server && @server.url) || "http://localhost/#{PORT}" 23 | end 24 | 25 | def start 26 | server.start_background 27 | server.on_response do |request, response| 28 | request_log << [ request, response ] 29 | end 30 | clear_request_log 31 | 32 | server 33 | end 34 | 35 | def stop 36 | @server.stop if @server 37 | end 38 | 39 | def running? 40 | @server && @server.running? 41 | end 42 | end 43 | 44 | include Ridley::SpecHelpers 45 | 46 | PORT = 8889 47 | 48 | def chef_client(name, hash = Hash.new) 49 | load_data(:clients, name, hash) 50 | end 51 | 52 | def chef_cookbook(name, version, cookbook = Hash.new) 53 | ChefServer.server.load_data("cookbooks" => { "#{name}-#{version}" => cookbook }) 54 | end 55 | 56 | def chef_data_bag(name, hash = Hash.new) 57 | ChefServer.server.load_data({ 'data' => { name => hash }}) 58 | end 59 | 60 | def chef_environment(name, hash = Hash.new) 61 | load_data(:environments, name, hash) 62 | end 63 | 64 | def chef_node(name, hash = Hash.new) 65 | load_data(:nodes, name, hash) 66 | end 67 | 68 | def chef_role(name, hash = Hash.new) 69 | load_data(:roles, name, hash) 70 | end 71 | 72 | def chef_user(name, hash = Hash.new) 73 | load_data(:users, name, hash) 74 | end 75 | 76 | def chef_zero_connection 77 | Ridley::Connection.new(ChefServer.server_url, "reset", fixtures_path.join('reset.pem').to_s) 78 | end 79 | 80 | private 81 | 82 | def load_data(key, name, hash) 83 | ChefServer.server.load_data(key.to_s => { name => JSON.fast_generate(hash) }) 84 | end 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /spec/support/each_matcher.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :each do |check| 2 | match do |actual| 3 | actual.each_with_index do |index, o| 4 | @object = o 5 | expect(index).to check 6 | end 7 | end 8 | 9 | failure_message do |actual| 10 | "at[#{@object}] #{check.failure_message_for_should}" 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/support/filepath_matchers.rb: -------------------------------------------------------------------------------- 1 | require 'pathname' 2 | 3 | RSpec::Matchers.define :be_relative_path do 4 | match do |given| 5 | if given.nil? 6 | false 7 | else 8 | Pathname.new(given).relative? 9 | end 10 | end 11 | 12 | failure_message do |given| 13 | "Expected '#{given}' to be a relative path but got an absolute path." 14 | end 15 | 16 | failure_message_when_negated do |given| 17 | "Expected '#{given}' to not be a relative path but got an absolute path." 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/support/spec_helpers.rb: -------------------------------------------------------------------------------- 1 | module Ridley 2 | module SpecHelpers 3 | def app_root_path 4 | Pathname.new(File.expand_path('../../../', __FILE__)) 5 | end 6 | 7 | def clean_tmp_path 8 | FileUtils.rm_rf(tmp_path) 9 | FileUtils.mkdir_p(tmp_path) 10 | end 11 | 12 | def fixtures_path 13 | app_root_path.join('spec/fixtures') 14 | end 15 | 16 | def tmp_path 17 | app_root_path.join('spec/tmp') 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef/chefignore_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Chef::Chefignore do 4 | describe '.initialize' do 5 | let(:path) { tmp_path.join('chefignore-test') } 6 | before { FileUtils.mkdir_p(path) } 7 | 8 | it 'finds the nearest chefignore' do 9 | target = path.join('chefignore').to_s 10 | FileUtils.touch(target) 11 | expect(described_class.new(path).filepath).to eq(target) 12 | end 13 | 14 | it 'finds a chefignore in the `cookbooks` directory' do 15 | target = path.join('cookbooks', 'chefignore').to_s 16 | FileUtils.mkdir_p(path.join('cookbooks')) 17 | FileUtils.touch(target) 18 | expect(described_class.new(path).filepath).to eq(target) 19 | end 20 | 21 | it 'finds a chefignore in the `.chef` directory' do 22 | target = path.join('.chef', 'chefignore').to_s 23 | FileUtils.mkdir_p(path.join('.chef')) 24 | FileUtils.touch(target) 25 | expect(described_class.new(path).filepath).to eq(target) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef/cookbook/metadata_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Chef::Cookbook::Metadata do 4 | 5 | let(:metadata) do 6 | described_class.new 7 | end 8 | 9 | before(:each) do 10 | subject { metadata } 11 | end 12 | 13 | describe "#validate_choice_array" do 14 | it "should limit the types allowed in the choice array." do 15 | options = { 16 | :type => "string", 17 | :choice => [ "test1", "test2" ], 18 | :default => "test1" 19 | } 20 | expect { 21 | subject.attribute("test_cookbook/test", options) 22 | }.not_to raise_error 23 | 24 | options = { 25 | :type => "boolean", 26 | :choice => [ true, false ], 27 | :default => true 28 | } 29 | expect { 30 | subject.attribute("test_cookbook/test", options) 31 | }.not_to raise_error 32 | 33 | options = { 34 | :type => "numeric", 35 | :choice => [ 1337, 420 ], 36 | :default => 1337 37 | } 38 | expect { 39 | subject.attribute("test_cookbook/test", options) 40 | }.not_to raise_error 41 | 42 | options = { 43 | :type => "numeric", 44 | :choice => [ true, "false" ], 45 | :default => false 46 | } 47 | expect { 48 | subject.attribute("test_cookbook/test", options) 49 | }.to raise_error 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef/cookbook/syntax_check_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Chef::Cookbook::SyntaxCheck do 4 | 5 | let(:cookbook_dir) { fixtures_path.join('example_cookbook')} 6 | let(:chefignore) { Ridley::Chef::Chefignore.new(cookbook_dir) } 7 | 8 | let(:syntax_check) do 9 | described_class.new(fixtures_path, chefignore) 10 | end 11 | 12 | subject { syntax_check } 13 | 14 | before(:each) do 15 | allow(subject).to receive(:chefignore) { chefignore } 16 | end 17 | 18 | describe "#ruby_files" do 19 | it "lists the rb files in a cookbook" do 20 | expect(subject.ruby_files).to include(cookbook_dir.join("libraries/my_lib.rb").to_s) 21 | end 22 | 23 | it "does not list the rb files in a cookbook that are ignored" do 24 | expect(subject.ruby_files).not_to include(cookbook_dir.join("ignores/magic.rb").to_s) 25 | end 26 | end 27 | 28 | describe "#untested_ruby_files" do 29 | it "filters out validated rb files" do 30 | valid_ruby_file = cookbook_dir.join("libraries/my_lib.rb").to_s 31 | subject.validated(valid_ruby_file) 32 | expect(subject.untested_ruby_files).not_to include(valid_ruby_file) 33 | end 34 | end 35 | 36 | describe "#template_files" do 37 | it "lists the erb files in a cookbook" do 38 | expect(subject.template_files).to include(cookbook_dir.join("templates/default/temp.txt.erb").to_s) 39 | end 40 | 41 | it "does not list the erb files in a cookbook that are ignored" do 42 | expect(subject.template_files).not_to include(cookbook_dir.join("ignores/magic.erb").to_s) 43 | end 44 | end 45 | 46 | describe "#untested_template_files" do 47 | it "filters out validated erb files" do 48 | valid_template_file = cookbook_dir.join("templates/default/temp.txt.erb").to_s 49 | subject.validated(valid_template_file) 50 | expect(subject.untested_template_files).not_to include(valid_template_file) 51 | end 52 | end 53 | 54 | describe "#validated?" do 55 | it "checks if a file has already been validated" do 56 | valid_template_file = cookbook_dir.join("templates/default/temp.txt.erb").to_s 57 | subject.validated(valid_template_file) 58 | expect(subject.validated?(valid_template_file)).to be_truthy 59 | end 60 | end 61 | 62 | describe "#validated" do 63 | let(:validated_files) { double('validated_files') } 64 | 65 | before(:each) do 66 | allow(subject).to receive(:validated_files) { validated_files } 67 | end 68 | 69 | it "records a file as validated" do 70 | template_file = cookbook_dir.join("templates/default/temp.txt.erb").to_s 71 | file_checksum = Ridley::Chef::Digester.checksum_for_file(template_file) 72 | 73 | expect(validated_files).to receive(:add).with(file_checksum) 74 | expect(subject.validated(template_file)).to be_nil 75 | end 76 | end 77 | 78 | describe "#validate_ruby_files" do 79 | it "asks #untested_ruby_files for a list of files and calls #validate_ruby_file on each" do 80 | allow(subject).to receive(:validate_ruby_file).with(anything()).exactly(9).times { true } 81 | expect(subject.validate_ruby_files).to be_truthy 82 | end 83 | 84 | it "marks the successfully validated ruby files" do 85 | allow(subject).to receive(:validated).with(anything()).exactly(9).times 86 | expect(subject.validate_ruby_files).to be_truthy 87 | end 88 | 89 | it "returns false if any ruby file fails to validate" do 90 | allow(subject).to receive(:validate_ruby_file).with(/\.rb$/) { false } 91 | expect(subject.validate_ruby_files).to be_falsey 92 | end 93 | end 94 | 95 | describe "#validate_templates" do 96 | it "asks #untested_template_files for a list of erb files and calls #validate_template on each" do 97 | allow(subject).to receive(:validate_template).with(anything()).exactly(9).times { true } 98 | expect(subject.validate_templates).to be_truthy 99 | end 100 | 101 | it "marks the successfully validated erb files" do 102 | allow(subject).to receive(:validated).with(anything()).exactly(9).times 103 | expect(subject.validate_templates).to be_truthy 104 | end 105 | 106 | it "returns false if any erb file fails to validate" do 107 | allow(subject).to receive(:validate_template).with(/\.erb$/) { false } 108 | expect(subject.validate_templates).to be_falsey 109 | end 110 | end 111 | 112 | describe "#validate_template" do 113 | it "asks #shell_out to check the files syntax" 114 | end 115 | 116 | describe "#validate_ruby_file" do 117 | it "asks #shell_out to check the files syntax" 118 | end 119 | 120 | describe "without a chefignore" do 121 | let(:chefignore) { nil } 122 | 123 | it "the file listing still works" do 124 | expect(subject.ruby_files).to include(cookbook_dir.join("libraries/my_lib.rb").to_s) 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef/digester_spec.rb: -------------------------------------------------------------------------------- 1 | # Borrowed and modified from: {https://github.com/opscode/chef/blob/11.4.0/spec/unit/digester_spec.rb} 2 | 3 | require 'spec_helper' 4 | 5 | describe Ridley::Chef::Digester do 6 | before(:each) do 7 | @cache = described_class.instance 8 | end 9 | 10 | describe "when computing checksums of cookbook files and templates" do 11 | it "proxies the class method checksum_for_file to the instance" do 12 | expect(@cache).to receive(:checksum_for_file).with("a_file_or_a_fail") 13 | described_class.checksum_for_file("a_file_or_a_fail") 14 | end 15 | 16 | it "generates a checksum from a non-file IO object" do 17 | io = StringIO.new("riseofthemachines\nriseofthechefs\n") 18 | expected_md5 = '0e157ac1e2dd73191b76067fb6b4bceb' 19 | expect(@cache.generate_md5_checksum(io)).to eq(expected_md5) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef_objects/client_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::ClientObject do 4 | describe "#to_json" do 5 | skip 6 | end 7 | 8 | describe "#regenerate_key" do 9 | skip 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef_objects/cookbook_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::CookbookObject do 4 | let(:connection) { double('connection') } 5 | let(:resource) { double('resource', connection: connection) } 6 | subject { described_class.new(resource) } 7 | 8 | describe "#download" do 9 | it "downloads each file" do 10 | allow(subject).to receive(:manifest) do 11 | { 12 | resources: [], 13 | providers: [], 14 | recipes: [ 15 | { 16 | checksum: "aa3505d3eb8ce328ea84a4333df05b07", 17 | name: "default.rb", 18 | path: "recipes/default.rb", 19 | specificity: "default", 20 | url: "https://chef.lax1.riotgames.com/organizations/reset/cookbooks/ohai/1.0.2/files/aa3505d3eb8ce328ea84a4333df05b07" 21 | } 22 | ], 23 | definitions: [], 24 | libraries: [], 25 | attributes: [], 26 | files: [ 27 | { 28 | checksum: "85bc3bb921efade3f2566a668ab4b639", 29 | name: "README", 30 | path: "files/default/plugins/README", 31 | specificity: "plugins", 32 | url: "https://chef.lax1.riotgames.com/organizations/reset/cookbooks/ohai/1.0.2/files/85bc3bb921efade3f2566a668ab4b639" 33 | } 34 | ], 35 | templates: [], 36 | root_files: [] 37 | } 38 | end 39 | 40 | expect(subject).to receive(:download_file).with(:recipes, "recipes/default.rb", anything) 41 | expect(subject).to receive(:download_file).with(:files, "files/default/plugins/README", anything) 42 | 43 | subject.download 44 | end 45 | end 46 | 47 | describe "#download_file" do 48 | let(:destination) { tmp_path.join('fake.file').to_s } 49 | 50 | before(:each) do 51 | allow(subject).to receive(:root_files) { [ { path: 'metadata.rb', url: "http://test.it/file" } ] } 52 | end 53 | 54 | it "downloads the file from the file's url" do 55 | expect(connection).to receive(:stream).with("http://test.it/file", destination) 56 | 57 | subject.download_file(:root_file, "metadata.rb", destination) 58 | end 59 | 60 | context "when given an unknown filetype" do 61 | it "raises an UnknownCookbookFileType error" do 62 | expect { 63 | subject.download_file(:not_existant, "default.rb", destination) 64 | }.to raise_error(Ridley::Errors::UnknownCookbookFileType) 65 | end 66 | end 67 | 68 | context "when the cookbook doesn't have the specified file" do 69 | before(:each) do 70 | allow(subject).to receive(:root_files) { Array.new } 71 | end 72 | 73 | it "returns nil" do 74 | expect(subject.download_file(:root_file, "metadata.rb", destination)).to be_nil 75 | end 76 | end 77 | end 78 | 79 | describe "#manifest" do 80 | it "returns a Hash" do 81 | expect(subject.manifest).to be_a(Hash) 82 | end 83 | 84 | it "has a key for each item in FILE_TYPES" do 85 | expect(subject.manifest.keys).to match_array(described_class::FILE_TYPES) 86 | end 87 | 88 | it "contains an empty array for each key" do 89 | expect(subject.manifest).to each be_a(Array) 90 | expect(subject.manifest.values).to each be_empty 91 | end 92 | end 93 | 94 | describe "#reload" do 95 | it "returns the updated self" do 96 | other = subject.dup 97 | other.version = "1.2.3" 98 | expect(resource).to receive(:find).with(subject, subject.version).and_return(other) 99 | 100 | expect(subject.reload).to eq(other) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef_objects/data_bag_item_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::DataBagItemObject do 4 | let(:resource) { double('chef-resource') } 5 | let(:data_bag) { double('data-bag') } 6 | subject { described_class.new(resource, data_bag) } 7 | 8 | describe "#from_hash" do 9 | context "when JSON has a 'raw_data' field" do 10 | let(:response) do 11 | { 12 | "name" => "data_bag_item_ridley-test_appconfig", 13 | "raw_data" => { 14 | "id" => "appconfig", 15 | "host" => "host.local" 16 | }, 17 | "json_class" => "Chef::DataBagItem", 18 | "data_bag" => "ridley-test", 19 | "chef_type" => "data_bag_item" 20 | } 21 | end 22 | 23 | it "returns a new object from attributes in the 'raw_data' field" do 24 | expect(subject.from_hash(response).attributes).to eql(response["raw_data"]) 25 | end 26 | end 27 | 28 | context "when JSON does not contain a 'raw_data' field" do 29 | let(:response) do 30 | { 31 | "id" => "appconfig", 32 | "host" => "host.local" 33 | } 34 | end 35 | 36 | it "returns a new object from the hash" do 37 | expect(subject.from_hash(response).attributes).to eql(response) 38 | end 39 | end 40 | end 41 | 42 | describe "#decrypt" do 43 | before(:each) do 44 | allow(resource).to receive_messages(encrypted_data_bag_secret: File.read(fixtures_path.join("encrypted_data_bag_secret").to_s)) 45 | end 46 | 47 | it "decrypts an encrypted v0 value" do 48 | subject.attributes[:test] = "Xk0E8lV9r4BhZzcg4wal0X4w9ZexN3azxMjZ9r1MCZc=" 49 | subject.decrypt 50 | expect(subject.attributes[:test][:database][:username]).to eq("test") 51 | end 52 | 53 | it "decrypts an encrypted v1 value" do 54 | subject.attributes[:password] = Hashie::Mash.new 55 | subject.attributes[:password][:version] = 1 56 | subject.attributes[:password][:cipher] = "aes-256-cbc" 57 | subject.attributes[:password][:encrypted_data] = "zG+tTjtwOWA4vEYDoUwPYreXLZ1pFyKoWDGezEejmKs=" 58 | subject.attributes[:password][:iv] = "URVhHxv/ZrnABJBvl82qsg==" 59 | subject.decrypt 60 | expect(subject.attributes[:password]).to eq("password123") 61 | end 62 | 63 | it "does not decrypt the id field" do 64 | id = "dbi_id" 65 | subject.attributes[:id] = id 66 | subject.decrypt 67 | expect(subject.attributes[:id]).to eq(id) 68 | end 69 | end 70 | 71 | describe "#decrypt_value" do 72 | context "when no encrypted_data_bag_secret has been configured" do 73 | before do 74 | allow(resource).to receive_messages(encrypted_data_bag_secret: nil) 75 | end 76 | 77 | it "raises an EncryptedDataBagSecretNotSet error" do 78 | expect { 79 | subject.decrypt_value("Xk0E8lV9r4BhZzcg4wal0X4w9ZexN3azxMjZ9r1MCZc=") 80 | }.to raise_error(Ridley::Errors::EncryptedDataBagSecretNotSet) 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef_objects/data_bag_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::DataBagObject do 4 | let(:item_resource) { double('item-resource') } 5 | let(:resource) { double('db-resource', item_resource: item_resource) } 6 | subject { described_class.new(resource) } 7 | 8 | describe '#item' do 9 | subject { super().item } 10 | it { is_expected.to be_a(Ridley::DataBagObject::DataBagItemProxy) } 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef_objects/environment_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::EnvironmentObject do 4 | subject { described_class.new(double('registry')) } 5 | 6 | describe "#set_override_attribute" do 7 | it "sets an override node attribute at the nested path" do 8 | subject.set_override_attribute('deep.nested.item', true) 9 | 10 | expect(subject.override_attributes).to have_key("deep") 11 | expect(subject.override_attributes["deep"]).to have_key("nested") 12 | expect(subject.override_attributes["deep"]["nested"]).to have_key("item") 13 | expect(subject.override_attributes["deep"]["nested"]["item"]).to be_truthy 14 | end 15 | 16 | context "when the override attribute is already set" do 17 | it "test" do 18 | subject.override_attributes = { 19 | deep: { 20 | nested: { 21 | item: false 22 | } 23 | } 24 | } 25 | subject.set_override_attribute('deep.nested.item', true) 26 | 27 | expect(subject.override_attributes["deep"]["nested"]["item"]).to be_truthy 28 | end 29 | end 30 | end 31 | 32 | describe "#set_default_attribute" do 33 | it "sets an override node attribute at the nested path" do 34 | subject.set_default_attribute('deep.nested.item', true) 35 | 36 | expect(subject.default_attributes).to have_key("deep") 37 | expect(subject.default_attributes["deep"]).to have_key("nested") 38 | expect(subject.default_attributes["deep"]["nested"]).to have_key("item") 39 | expect(subject.default_attributes["deep"]["nested"]["item"]).to be_truthy 40 | end 41 | 42 | context "when the override attribute is already set" do 43 | it "test" do 44 | subject.default_attributes = { 45 | deep: { 46 | nested: { 47 | item: false 48 | } 49 | } 50 | } 51 | subject.set_default_attribute('deep.nested.item', true) 52 | 53 | expect(subject.default_attributes["deep"]["nested"]["item"]).to be_truthy 54 | end 55 | end 56 | 57 | shared_examples_for "attribute deleter" do 58 | let(:precedence) { raise "You must provide the precedence level (let(:precedence) { \"default\" } in the shared example context" } 59 | let(:delete_attribute) { subject.send(:"delete_#{precedence}_attribute", delete_attribute_key) } 60 | let(:set_attribute_value) { true } 61 | let(:attributes) { { "hello" => { "world" => set_attribute_value } } } 62 | let(:delete_attribute_key) { "hello.world" } 63 | 64 | before do 65 | subject.send(:"#{precedence}_attributes=", attributes) 66 | end 67 | 68 | it "removes the attribute" do 69 | delete_attribute 70 | expect(subject.send(:"#{precedence}_attributes")[:hello][:world]).to be_nil 71 | end 72 | 73 | context "when the attribute does not exist" do 74 | let(:delete_attribute_key) { "not.existing" } 75 | 76 | it "does not delete anything" do 77 | delete_attribute 78 | expect(subject.send(:"#{precedence}_attributes")[:hello][:world]).to eq(set_attribute_value) 79 | end 80 | end 81 | 82 | context "when an internal hash is nil" do 83 | let(:delete_attribute_key) { "never.not.existing" } 84 | 85 | before do 86 | subject.send(:"#{precedence}_attributes=", Hash.new) 87 | end 88 | 89 | it "does not delete anything" do 90 | delete_attribute 91 | expect(subject.send(:"#{precedence}_attributes")).to be_empty 92 | end 93 | end 94 | 95 | ["string", true, :symbol, ["array"], Object.new].each do |nonattrs| 96 | context "when the attribute chain is partially set, interrupted by a #{nonattrs.class}" do 97 | let(:attributes) { { "hello" => set_attribute_value } } 98 | let(:set_attribute_value) { nonattrs } 99 | 100 | it "leaves the attributes unchanged" do 101 | expect(subject.send(:"unset_#{precedence}_attribute", delete_attribute_key).to_hash).to eq(attributes) 102 | end 103 | end 104 | end 105 | end 106 | 107 | describe "#delete_default_attribute" do 108 | it_behaves_like "attribute deleter" do 109 | let(:precedence) { "default" } 110 | end 111 | end 112 | 113 | describe "#delete_override_attribute" do 114 | it_behaves_like "attribute deleter" do 115 | let(:precedence) { "override" } 116 | end 117 | end 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef_objects/role_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::RoleObject do 4 | subject { described_class.new(double('registry')) } 5 | 6 | describe "#set_override_attribute" do 7 | it "sets an override node attribute at the nested path" do 8 | subject.set_override_attribute('deep.nested.item', true) 9 | 10 | expect(subject.override_attributes).to have_key("deep") 11 | expect(subject.override_attributes["deep"]).to have_key("nested") 12 | expect(subject.override_attributes["deep"]["nested"]).to have_key("item") 13 | expect(subject.override_attributes["deep"]["nested"]["item"]).to be_truthy 14 | end 15 | 16 | context "when the override attribute is already set" do 17 | it "test" do 18 | subject.override_attributes = { 19 | deep: { 20 | nested: { 21 | item: false 22 | } 23 | } 24 | } 25 | subject.set_override_attribute('deep.nested.item', true) 26 | 27 | expect(subject.override_attributes["deep"]["nested"]["item"]).to be_truthy 28 | end 29 | end 30 | end 31 | 32 | describe "#set_default_attribute" do 33 | it "sets an override node attribute at the nested path" do 34 | subject.set_default_attribute('deep.nested.item', true) 35 | 36 | expect(subject.default_attributes).to have_key("deep") 37 | expect(subject.default_attributes["deep"]).to have_key("nested") 38 | expect(subject.default_attributes["deep"]["nested"]).to have_key("item") 39 | expect(subject.default_attributes["deep"]["nested"]["item"]).to be_truthy 40 | end 41 | 42 | context "when the override attribute is already set" do 43 | it "test" do 44 | subject.default_attributes = { 45 | deep: { 46 | nested: { 47 | item: false 48 | } 49 | } 50 | } 51 | subject.set_default_attribute('deep.nested.item', true) 52 | 53 | expect(subject.default_attributes["deep"]["nested"]["item"]).to be_truthy 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /spec/unit/ridley/chef_objects/sandbox_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::SandboxObject do 4 | let(:resource) { double('chef-resource') } 5 | 6 | subject do 7 | described_class.new(double('registry'), 8 | "uri" => "https://api.opscode.com/organizations/vialstudios/sandboxes/bd091b150b0a4578b97771af6abf3e05", 9 | "checksums" => { 10 | "385ea5490c86570c7de71070bce9384a" => { 11 | "url" => "https://s3.amazonaws.com/opscode-platform-production-data/organization", 12 | "needs_upload" => true 13 | }, 14 | "f6f73175e979bd90af6184ec277f760c" => { 15 | "url" => "https://s3.amazonaws.com/opscode-platform-production-data/organization", 16 | "needs_upload" => true 17 | }, 18 | "2e03dd7e5b2e6c8eab1cf41ac61396d5" => { 19 | "url" => "https://s3.amazonaws.com/opscode-platform-production-data/organization", 20 | "needs_upload" => true 21 | }, 22 | }, 23 | "sandbox_id" => "bd091b150b0a4578b97771af6abf3e05" 24 | ) 25 | end 26 | 27 | before { allow(subject).to receive_messages(resource: resource) } 28 | 29 | describe "#checksums" do 30 | skip 31 | end 32 | 33 | describe "#commit" do 34 | let(:response) { { is_completed: nil} } 35 | before { expect(resource).to receive(:commit).with(subject).and_return(response) } 36 | 37 | context "when the commit is successful" do 38 | before { response[:is_completed] = true } 39 | 40 | it "has an 'is_completed' value of true" do 41 | subject.commit 42 | 43 | expect(subject.is_completed).to be_truthy 44 | end 45 | end 46 | 47 | context "when the commit is a failure" do 48 | before { response[:is_completed] = false } 49 | 50 | it "has an 'is_completed' value of false" do 51 | subject.commit 52 | 53 | expect(subject.is_completed).to be_falsey 54 | end 55 | end 56 | end 57 | 58 | describe "#upload" do 59 | it "delegates to resource#upload" do 60 | checksums = double('checksums') 61 | expect(resource).to receive(:upload).with(subject, checksums) 62 | 63 | subject.upload(checksums) 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/unit/ridley/connection_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Connection do 4 | let(:server_url) { "https://api.opscode.com" } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join("reset.pem").to_s } 7 | 8 | subject do 9 | described_class.new(server_url, client_name, client_key) 10 | end 11 | 12 | context " when proxy environment variables are set" do 13 | subject do 14 | described_class.new('http://127.0.0.1:8889', client_name, client_key) 15 | end 16 | 17 | it "fails with http_proxy set without no_proxy" do 18 | stub_const('ENV', ENV.to_hash.merge( 19 | 'http_proxy' => 'http://i.am.an.http.proxy') 20 | ) 21 | expect { subject.get('/nodes') }.to raise_error(Ridley::Errors::ConnectionFailed) 22 | end 23 | 24 | it "works with http_proxy and no_proxy set" do 25 | stub_const('ENV', ENV.to_hash.merge( 26 | 'http_proxy' => 'http://i.am.an.http.proxy', 27 | 'no_proxy' => '127.0.0.1:8889') 28 | ) 29 | expect(subject.get('/nodes').status).to eq(200) 30 | end 31 | end 32 | 33 | describe "configurable retries" do 34 | before(:each) do 35 | stub_request(:get, "https://api.opscode.com/organizations/vialstudios").to_return(status: 500, body: "") 36 | end 37 | 38 | it "attempts five (5) retries by default" do 39 | expect { 40 | subject.get('organizations/vialstudios') 41 | }.to raise_error(Ridley::Errors::HTTPInternalServerError) 42 | expect(a_request(:get, "https://api.opscode.com/organizations/vialstudios")).to have_been_made.times(6) 43 | end 44 | 45 | context "given a configured count of two (2) retries" do 46 | subject do 47 | described_class.new(server_url, client_name, client_key, retries: 2) 48 | end 49 | 50 | it "attempts two (2) retries" do 51 | expect { 52 | subject.get('organizations/vialstudios') 53 | }.to raise_error(Ridley::Errors::HTTPInternalServerError) 54 | 55 | expect(a_request(:get, "https://api.opscode.com/organizations/vialstudios")).to have_been_made.times(3) 56 | end 57 | end 58 | end 59 | 60 | describe "#api_type" do 61 | it "returns :foss if the organization is not set" do 62 | subject.stub(:organization).and_return(nil) 63 | 64 | expect(subject.api_type).to eql(:foss) 65 | end 66 | 67 | it "returns :hosted if the organization is set" do 68 | subject.stub(:organization).and_return("vialstudios") 69 | 70 | expect(subject.api_type).to eql(:hosted) 71 | end 72 | end 73 | 74 | describe "#stream" do 75 | let(:target) { "http://test.it/file" } 76 | let(:destination) { tmp_path.join("test.file") } 77 | let(:contents) { "SOME STRING STUFF\nHERE.\n" } 78 | 79 | before(:each) do 80 | stub_request(:get, "http://test.it/file").to_return(status: 200, body: contents) 81 | end 82 | 83 | it "creates a destination file on disk" do 84 | subject.stream(target, destination) 85 | 86 | expect(File.exist?(destination)).to be_truthy 87 | end 88 | 89 | it "returns true when the file was copied" do 90 | expect(subject.stream(target, destination)).to be_truthy 91 | end 92 | 93 | it "contains the contents of the response body" do 94 | subject.stream(target, destination) 95 | 96 | expect(File.read(destination)).to include(contents) 97 | end 98 | end 99 | end 100 | -------------------------------------------------------------------------------- /spec/unit/ridley/errors_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Errors do 4 | describe Ridley::Errors::HTTPError do 5 | describe "ClassMethods" do 6 | subject { Ridley::Errors::HTTPError } 7 | 8 | before(:each) do 9 | @original = Ridley::Errors::HTTPError.class_variable_get :@@error_map 10 | Ridley::Errors::HTTPError.class_variable_set :@@error_map, Hash.new 11 | end 12 | 13 | after(:each) do 14 | Ridley::Errors::HTTPError.class_variable_set :@@error_map, @original 15 | end 16 | 17 | describe "::register_error" do 18 | it "adds an item to the error map" do 19 | subject.register_error(400) 20 | 21 | expect(subject.error_map.size).to eq(1) 22 | end 23 | 24 | it "adds a key of the given status code with a value of the class inheriting from HTTPError" do 25 | class RidleyTestHTTPError < Ridley::Errors::HTTPError 26 | register_error(400) 27 | end 28 | 29 | expect(subject.error_map[400]).to eql(RidleyTestHTTPError) 30 | end 31 | end 32 | end 33 | 34 | context "with an HTML error payload" do 35 | subject { Ridley::Errors::HTTPError.new(:body => "

Redirected

") } 36 | 37 | it "has an HTML body" do 38 | expect(subject.message).to eq("

Redirected

") 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/unit/ridley/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Logging::Logger do 4 | subject { described_class.new(File::NULL) } 5 | let(:message) { "my message" } 6 | let(:filtered_param) { "message" } 7 | 8 | describe "::initialize" do 9 | it "defaults to info" do 10 | expect(subject.level).to eq(Logger::WARN) 11 | end 12 | end 13 | 14 | describe "#info" do 15 | 16 | before do 17 | subject.level = Logger::INFO 18 | subject.filter_param filtered_param 19 | end 20 | 21 | it "supports filtering" do 22 | expect(subject).to receive(:filter).with("my message").and_return("my FILTERED") 23 | subject.info message 24 | end 25 | end 26 | 27 | describe "#filter_params" do 28 | it "returns an array" do 29 | expect(subject.filter_params).to be_a(Array) 30 | end 31 | end 32 | 33 | describe "#filter_param" do 34 | let(:param) { "hello" } 35 | 36 | before do 37 | subject.clear_filter_params 38 | end 39 | 40 | it "adds an element to the array" do 41 | subject.filter_param(param) 42 | expect(subject.filter_params).to include(param) 43 | expect(subject.filter_params.size).to eq(1) 44 | end 45 | 46 | context "when the element is already in the array" do 47 | 48 | before do 49 | subject.filter_param(param) 50 | end 51 | it "does not duplicate the element" do 52 | subject.filter_param(param) 53 | expect(subject.filter_params.size).to eq(1) 54 | end 55 | end 56 | end 57 | 58 | describe "#filter" do 59 | 60 | before do 61 | subject.filter_param(filtered_param) 62 | end 63 | 64 | it "replaces entries in filter_params" do 65 | expect(subject.filter(message)).to eq("my FILTERED") 66 | end 67 | 68 | context "when there are multiple filter_params" do 69 | before do 70 | subject.filter_param("fake param") 71 | subject.filter_param(filtered_param) 72 | end 73 | 74 | it "replaces only matching filter_params" do 75 | expect(subject.filter(message)).to eq("my FILTERED") 76 | end 77 | end 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /spec/unit/ridley/middleware/chef_auth_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Middleware::ChefAuth do 4 | let(:server_url) { "https://api.opscode.com/organizations/vialstudios/" } 5 | 6 | describe "ClassMethods" do 7 | subject { described_class } 8 | 9 | describe "#authentication_headers" do 10 | let(:client_name) { "reset" } 11 | let(:client_key) { fixtures_path.join("reset.pem") } 12 | 13 | it "returns a Hash of authentication headers" do 14 | options = { 15 | http_method: "GET", 16 | host: "https://api.opscode.com", 17 | path: "/something.file" 18 | } 19 | expect(subject.authentication_headers(client_name, client_key, options)).to be_a(Hash) 20 | end 21 | 22 | context "when the :client_key is an actual key" do 23 | let(:client_key) { File.read(fixtures_path.join("reset.pem")) } 24 | 25 | it "returns a Hash of authentication headers" do 26 | options = { 27 | http_method: "GET", 28 | host: "https://api.opscode.com", 29 | path: "/something.file" 30 | } 31 | expect(subject.authentication_headers(client_name, client_key, options)).to be_a(Hash) 32 | end 33 | end 34 | end 35 | end 36 | 37 | subject do 38 | Faraday.new(server_url) do |conn| 39 | conn.request :chef_auth, "reset", "/Users/reset/.chef/reset.pem" 40 | conn.adapter Faraday.default_adapter 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/unit/ridley/middleware/parse_json_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Middleware::ParseJson do 4 | let(:server_url) { "https://api.opscode.com/organizations/vialstudios/" } 5 | 6 | describe "ClassMethods" do 7 | subject { Ridley::Middleware::ParseJson } 8 | 9 | describe "::response_type" do 10 | it "returns the first element of the response content-type" do 11 | env = double('env') 12 | allow(env).to receive(:[]).with(:response_headers).and_return( 13 | 'content-type' => 'text/html; charset=utf-8' 14 | ) 15 | 16 | expect(subject.response_type(env)).to eql("text/html") 17 | end 18 | end 19 | 20 | describe "::json_response?" do 21 | it "returns true if the value of content-type includes 'application/json' and the body looks like JSON" do 22 | env = double('env') 23 | allow(env).to receive(:[]).with(:response_headers).and_return( 24 | 'content-type' => 'application/json; charset=utf8' 25 | ) 26 | expect(subject).to receive(:looks_like_json?).with(env).and_return(true) 27 | 28 | expect(subject.json_response?(env)).to be_truthy 29 | end 30 | 31 | it "returns false if the value of content-type includes 'application/json' but the body does not look like JSON" do 32 | env = double('env') 33 | allow(env).to receive(:[]).with(:response_headers).and_return( 34 | 'content-type' => 'application/json; charset=utf8' 35 | ) 36 | expect(subject).to receive(:looks_like_json?).with(env).and_return(false) 37 | 38 | expect(subject.json_response?(env)).to be_falsey 39 | end 40 | 41 | it "returns false if the value of content-type does not include 'application/json'" do 42 | env = double('env') 43 | allow(env).to receive(:[]).with(:response_headers).and_return( 44 | 'content-type' => 'text/plain' 45 | ) 46 | 47 | expect(subject.json_response?(env)).to be_falsey 48 | end 49 | end 50 | 51 | describe "::looks_like_json?" do 52 | let(:env) { double('env') } 53 | 54 | it "returns true if the given string contains JSON brackets" do 55 | allow(env).to receive(:[]).with(:body).and_return("{\"name\":\"jamie\"}") 56 | 57 | expect(subject.looks_like_json?(env)).to be_truthy 58 | end 59 | 60 | it "returns false if the given string does not contain JSON brackets" do 61 | allow(env).to receive(:[]).with(:body).and_return("name") 62 | 63 | expect(subject.looks_like_json?(env)).to be_falsey 64 | end 65 | end 66 | end 67 | 68 | subject do 69 | Faraday.new(server_url) do |conn| 70 | conn.response :json 71 | conn.adapter Faraday.default_adapter 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/unit/ridley/mixins/from_file_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Ridley 4 | describe Mixin::FromFile do 5 | describe '.from_file' do 6 | context 'when there is bad Ruby in the file' do 7 | let(:instance) { Class.new { include Ridley::Mixin::FromFile }.new } 8 | 9 | before do 10 | allow(File).to receive(:exists?).and_return(true) 11 | allow(File).to receive(:readable?).and_return(true) 12 | allow(IO).to receive(:read).and_return('invalid Ruby code') 13 | end 14 | 15 | it 'raises a FromFileParserError' do 16 | expect { 17 | instance.from_file('/path') 18 | }.to raise_error(Errors::FromFileParserError) 19 | end 20 | 21 | it 'includes the backtrace from the original error' do 22 | expect { instance.from_file('/path') }.to raise_error { |error| 23 | expect(error.message).to include("undefined local variable or method `code' for") 24 | expect(error.backtrace).to include("/path:1:in `block in from_file'") 25 | } 26 | end 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /spec/unit/ridley/resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::Resource do 4 | let(:representation) do 5 | Class.new(Ridley::ChefObject) do 6 | set_chef_id "id" 7 | set_chef_type "thing" 8 | set_chef_json_class "Chef::Thing" 9 | end 10 | end 11 | 12 | let(:resource_class) do 13 | Class.new(Ridley::Resource) do 14 | set_resource_path "rspecs" 15 | end 16 | end 17 | 18 | describe "ClassMethods" do 19 | subject { resource_class } 20 | 21 | describe "::set_resource_path" do 22 | it "sets the resource_path attr on the class" do 23 | subject.set_resource_path("environments") 24 | 25 | expect(subject.resource_path).to eql("environments") 26 | end 27 | end 28 | 29 | describe "::resource_path" do 30 | context "when not explicitly set" do 31 | before { subject.set_resource_path(nil) } 32 | 33 | it "returns the representation's chef type" do 34 | expect(subject.resource_path).to eql(representation.chef_type) 35 | end 36 | end 37 | 38 | context "when explicitly set" do 39 | let(:set_path) { "hello" } 40 | before { subject.set_resource_path(set_path) } 41 | 42 | it "returns the set value" do 43 | expect(subject.resource_path).to eql(set_path) 44 | end 45 | end 46 | end 47 | end 48 | 49 | let(:connection) { double('chef-connection') } 50 | let(:response) { double('chef-response', body: Hash.new) } 51 | let(:resource_json) { '{"some":"valid json"}' } 52 | 53 | subject { resource_class.new(double('registry')) } 54 | 55 | before do 56 | resource_class.stub(representation: representation) 57 | subject.stub(connection: connection) 58 | end 59 | 60 | describe "::from_file" do 61 | it "reads the file and calls ::from_json with contents" do 62 | File.stub(:read) { resource_json } 63 | subject.should_receive(:from_json).with(resource_json) 64 | subject.from_file('/bogus/filename.json') 65 | end 66 | end 67 | 68 | describe "::from_json" do 69 | it "parses the argument and calls ::new with newly built hash" do 70 | hashed_json = JSON.parse(resource_json) 71 | subject.should_receive(:new).with(hashed_json).and_return representation 72 | subject.from_json(resource_json) 73 | end 74 | end 75 | 76 | describe "::all" do 77 | it "sends GET to /{resource_path}" do 78 | connection.should_receive(:get).with(subject.class.resource_path).and_return(response) 79 | 80 | subject.all 81 | end 82 | end 83 | 84 | describe "::find" do 85 | let(:id) { "some_id" } 86 | 87 | it "sends GET to /{resource_path}/{id} where {id} is the given ID" do 88 | connection.should_receive(:get).with("#{subject.class.resource_path}/#{id}").and_return(response) 89 | 90 | subject.find(id) 91 | end 92 | 93 | context "when the resource is not found" do 94 | before do 95 | connection.should_receive(:get).with("#{subject.class.resource_path}/#{id}"). 96 | and_raise(Ridley::Errors::HTTPNotFound.new({})) 97 | end 98 | 99 | it "returns nil" do 100 | expect(subject.find(id)).to be_nil 101 | end 102 | end 103 | end 104 | 105 | describe "::create" do 106 | let(:attrs) do 107 | { 108 | first_name: "jamie", 109 | last_name: "winsor" 110 | } 111 | end 112 | 113 | it "sends a post request to the given client using the includer's resource_path" do 114 | connection.should_receive(:post).with(subject.class.resource_path, duck_type(:to_json)).and_return(response) 115 | 116 | subject.create(attrs) 117 | end 118 | end 119 | 120 | describe "::delete" do 121 | it "sends a delete request to the given client using the includer's resource_path for the given string" do 122 | connection.should_receive(:delete).with("#{subject.class.resource_path}/ridley-test").and_return(response) 123 | 124 | subject.delete("ridley-test") 125 | end 126 | 127 | it "accepts an object that responds to 'chef_id'" do 128 | object = double("obj") 129 | object.stub(:chef_id) { "hello" } 130 | connection.should_receive(:delete).with("#{subject.class.resource_path}/#{object.chef_id}").and_return(response) 131 | 132 | subject.delete( object) 133 | end 134 | end 135 | 136 | describe "::delete_all" do 137 | it "sends a delete request for every object in the collection" do 138 | skip 139 | end 140 | end 141 | 142 | describe "::update" do 143 | it "sends a put request to the given client using the includer's resource_path with the given object" do 144 | object = subject.new(name: "hello") 145 | connection.should_receive(:put). 146 | with("#{subject.class.resource_path}/#{object.chef_id}", duck_type(:to_json)).and_return(response) 147 | 148 | subject.update(object) 149 | end 150 | end 151 | end 152 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/client_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::ClientResource do 4 | subject { described_class.new(double('registry')) } 5 | 6 | describe "#regenerate_key" do 7 | let(:client_id) { "rspec-client" } 8 | before { subject.stub(find: nil) } 9 | 10 | context "when a client with the given ID exists" do 11 | let(:client) { double('chef-client') } 12 | before { subject.should_receive(:find).with(client_id).and_return(client) } 13 | 14 | it "sets the private key to true and updates the client" do 15 | client.should_receive(:private_key=).with(true) 16 | subject.should_receive(:update).with(client) 17 | 18 | subject.regenerate_key(client_id) 19 | end 20 | end 21 | 22 | context "when a client with the given ID does not exist" do 23 | before { subject.should_receive(:find).with(client_id).and_return(nil) } 24 | 25 | it "raises a ResourceNotFound error" do 26 | expect { 27 | subject.regenerate_key(client_id) 28 | }.to raise_error(Ridley::Errors::ResourceNotFound) 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/cookbook_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::CookbookResource do 4 | let(:client_name) { "reset" } 5 | let(:client_key) { fixtures_path.join('reset.pem') } 6 | let(:connection) { Ridley::Connection.new("http://localhost:8889", "reset", fixtures_path.join("reset.pem").to_s) } 7 | subject { described_class.new(double('registry'), client_name, client_key) } 8 | before { subject.stub(connection: connection) } 9 | 10 | describe "#download" do 11 | let(:name) { "example_cookbook" } 12 | let(:version) { "0.1.0" } 13 | let(:destination) { tmp_path.join("example_cookbook-0.1.0").to_s } 14 | 15 | context "when the cookbook of the name/version is not found" do 16 | before { subject.should_receive(:find).with(name, version).and_return(nil) } 17 | 18 | it "raises a ResourceNotFound error" do 19 | expect { 20 | subject.download(name, version, destination) 21 | }.to raise_error(Ridley::Errors::ResourceNotFound) 22 | end 23 | end 24 | end 25 | 26 | describe "#latest_version" do 27 | let(:name) { "ant" } 28 | 29 | context "when the cookbook has no versions" do 30 | it "returns a ResourceNotFound error" do 31 | expect { 32 | subject.latest_version(name) 33 | }.to raise_error(Ridley::Errors::ResourceNotFound) 34 | end 35 | end 36 | 37 | context "when the cookbook has versions" do 38 | before do 39 | chef_cookbook(name, "1.0.0") 40 | chef_cookbook(name, "1.2.0") 41 | chef_cookbook(name, "3.0.0") 42 | end 43 | 44 | it "returns the latest version" do 45 | expect(subject.latest_version(name)).to eql("3.0.0") 46 | end 47 | end 48 | end 49 | 50 | describe "#versions" do 51 | let(:name) { "artifact" } 52 | 53 | context "when the cookbook has versions" do 54 | before do 55 | chef_cookbook(name, "1.0.0") 56 | chef_cookbook(name, "1.1.0") 57 | chef_cookbook(name, "1.2.0") 58 | end 59 | 60 | it "returns an array" do 61 | expect(subject.versions(name)).to be_a(Array) 62 | end 63 | 64 | it "contains a version string for each cookbook version available" do 65 | result = subject.versions(name) 66 | 67 | expect(result.size).to eq(3) 68 | expect(result).to include("1.0.0") 69 | expect(result).to include("1.1.0") 70 | expect(result).to include("1.2.0") 71 | end 72 | end 73 | 74 | context "when the cookbook has no versions" do 75 | it "raises a ResourceNotFound error" do 76 | expect { 77 | subject.versions(name) 78 | }.to raise_error(Ridley::Errors::ResourceNotFound) 79 | end 80 | end 81 | end 82 | 83 | describe "#satisfy" do 84 | let(:name) { "ridley_test" } 85 | 86 | context "when there is a solution" do 87 | before do 88 | chef_cookbook(name, "2.0.0") 89 | chef_cookbook(name, "3.0.0") 90 | end 91 | 92 | it "returns a CookbookObject" do 93 | expect(subject.satisfy(name, ">= 2.0.0")).to be_a(Ridley::CookbookObject) 94 | end 95 | 96 | it "is the best solution" do 97 | expect(subject.satisfy(name, ">= 2.0.0").version).to eql("3.0.0") 98 | end 99 | end 100 | 101 | context "when there is no solution" do 102 | before { chef_cookbook(name, "1.0.0") } 103 | 104 | it "returns nil" do 105 | expect(subject.satisfy(name, ">= 2.0.0")).to be_nil 106 | end 107 | end 108 | 109 | context "when the cookbook does not exist" do 110 | it "raises a ResourceNotFound error" do 111 | expect { 112 | subject.satisfy(name, ">= 1.2.3") 113 | }.to raise_error(Ridley::Errors::ResourceNotFound) 114 | end 115 | end 116 | end 117 | 118 | describe "#upload" do 119 | let(:name) { "upload_test" } 120 | let(:cookbook_path) { fixtures_path.join('example_cookbook') } 121 | let(:sandbox_resource) { double('sandbox_resource') } 122 | let(:sandbox) { double('sandbox', upload: nil, commit: nil) } 123 | 124 | before do 125 | subject.stub(:sandbox_resource).and_return(sandbox_resource) 126 | end 127 | 128 | it 'does not include files that are ignored' do 129 | # These are the MD5s for the files. It's not possible to check that 130 | # the ignored files weren't uploaded, so we just check that the 131 | # non-ignored files are the ONLY thing uploaded 132 | sandbox_resource.should_receive(:create).with([ 133 | "211a3a8798d4acd424af15ff8a2e28a5", 134 | "5f025b0817442ec087c4e0172a6d1e67", 135 | "75077ba33d2887cc1746d1ef716bf8b7", 136 | "7b1ebd2ff580ca9dc46fb27ec1653bf2", 137 | "84e12365e6f4ebe7db6a0e6a92473b16", 138 | "a39eb80def9804f4b118099697cc2cd2", 139 | "b70ba735f3af47e5d6fc71b91775b34c", 140 | "cafb6869fca13f5c36f24a60de8fb982", 141 | "dbf3a6c4ab68a86172be748aced9f46e", 142 | "dc6461b5da25775f3ef6a9cc1f6cff9f", 143 | "e9a2e24281cfbd6be0a6b1af3b6d277e" 144 | ]).and_return(sandbox) 145 | 146 | subject.upload(cookbook_path, validate: false) 147 | end 148 | end 149 | 150 | describe "#update" do 151 | skip 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/data_bag_item_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::DataBagItemResource do 4 | subject { described_class.new(double) } 5 | 6 | skip 7 | end 8 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/data_bag_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::DataBagResource do 4 | let(:secret) { "supersecretkey" } 5 | let(:instance) { described_class.new(double, secret) } 6 | 7 | describe "#item_resource" do 8 | subject { instance.item_resource } 9 | 10 | it "returns a DataBagItemResource" do 11 | expect(subject).to be_a(Ridley::DataBagItemResource) 12 | end 13 | 14 | describe '#encrypted_data_bag_secret' do 15 | subject { super().encrypted_data_bag_secret } 16 | it { is_expected.to eql(secret) } 17 | end 18 | end 19 | 20 | describe "#find" do 21 | skip 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/environment_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::EnvironmentResource do 4 | let(:server_url) { Ridley::RSpec::ChefServer.server_url } 5 | let(:client_name) { "reset" } 6 | let(:client_key) { fixtures_path.join('reset.pem').to_s } 7 | let(:connection) { Ridley::Connection.new(server_url, client_name, client_key) } 8 | 9 | let(:resource) do 10 | resource = described_class.new(double('registry')) 11 | resource.stub(connection: connection) 12 | resource 13 | end 14 | 15 | subject { resource } 16 | 17 | describe "#cookbook_versions" do 18 | let(:name) { "rspec-test" } 19 | let(:run_list) { ["hello", "there"] } 20 | 21 | subject { resource.cookbook_versions(name, run_list) } 22 | 23 | context "when the chef server has the given cookbooks" do 24 | before do 25 | chef_environment("rspec-test") 26 | chef_cookbook("hello", "1.2.3") 27 | chef_cookbook("there", "1.0.0") 28 | end 29 | 30 | it "returns a Hash" do 31 | is_expected.to be_a(Hash) 32 | end 33 | 34 | it "contains a key for each cookbook" do 35 | expect(subject.keys.size).to eq(2) 36 | expect(subject).to have_key("hello") 37 | expect(subject).to have_key("there") 38 | end 39 | end 40 | 41 | context "when the chef server does not have the environment" do 42 | before do 43 | chef_cookbook("hello", "1.2.3") 44 | chef_cookbook("there", "1.0.0") 45 | end 46 | 47 | it "raises a ResourceNotFound error" do 48 | expect { subject }.to raise_error(Ridley::Errors::ResourceNotFound) 49 | end 50 | end 51 | 52 | context "when the chef server does not have one or more of the cookbooks" do 53 | it "raises a precondition failed error" do 54 | expect { subject }.to raise_error(Ridley::Errors::HTTPPreconditionFailed) 55 | end 56 | end 57 | end 58 | 59 | describe "#delete_all" do 60 | let(:default_env) { double(name: "_default") } 61 | let(:destroy_env) { double(name: "destroy_me") } 62 | 63 | before do 64 | subject.stub(all: [ default_env, destroy_env ]) 65 | end 66 | 67 | it "does not destroy the '_default' environment" do 68 | subject.stub(future: double('future', value: nil)) 69 | subject.should_not_receive(:future).with(:delete, default_env) 70 | 71 | subject.delete_all 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/node_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::NodeResource do 4 | let(:instance) do 5 | inst = described_class.new(double) 6 | inst.stub(connection: chef_zero_connection) 7 | inst 8 | end 9 | 10 | describe "#merge_data" do 11 | let(:node_name) { "rspec-test" } 12 | let(:run_list) { [ "recipe[one]", "recipe[two]" ] } 13 | let(:attributes) { { deep: { two: "val" } } } 14 | 15 | subject(:result) { instance.merge_data(node_name, run_list: run_list, attributes: attributes) } 16 | 17 | context "when a node of the given name exists" do 18 | before do 19 | chef_node(node_name, 20 | run_list: [ "recipe[one]", "recipe[three]" ], 21 | normal: { deep: { one: "val" } } 22 | ) 23 | end 24 | 25 | it "returns a Ridley::NodeObject" do 26 | expect(result).to be_a(Ridley::NodeObject) 27 | end 28 | 29 | it "has a union between the run list of the original node and the new run list" do 30 | expect(result.run_list).to eql(["recipe[one]","recipe[three]","recipe[two]"]) 31 | end 32 | 33 | it "has a deep merge between the attributes of the original node and the new attributes" do 34 | expect(result.normal.to_hash).to eql("deep" => { "one" => "val", "two" => "val" }) 35 | end 36 | end 37 | 38 | context "when a node with the given name does not exist" do 39 | let(:node_name) { "does_not_exist" } 40 | 41 | it "raises a ResourceNotFound error" do 42 | expect { result }.to raise_error(Ridley::Errors::ResourceNotFound) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/role_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::RoleResource do 4 | subject { described_class.new(double) } 5 | 6 | skip 7 | end 8 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/sandbox_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::SandboxResource do 4 | let(:client_name) { "reset" } 5 | let(:client_key) { fixtures_path.join('reset.pem') } 6 | let(:connection) { double('chef-connection') } 7 | subject { described_class.new(double, client_name, client_key) } 8 | before { subject.stub(connection: connection) } 9 | 10 | describe "#create" do 11 | let(:sandbox_id) { "bd091b150b0a4578b97771af6abf3e05" } 12 | let(:sandbox_uri) { "https://api.opscode.com/organizations/vialstudios/sandboxes/bd091b150b0a4578b97771af6abf3e05" } 13 | let(:checksums) { Hash.new } 14 | let(:response) do 15 | double(body: { uri: sandbox_uri, checksums: checksums, sandbox_id: sandbox_id }) 16 | end 17 | 18 | before(:each) do 19 | connection.stub(:post). 20 | with(subject.class.resource_path, JSON.fast_generate(checksums: checksums)). 21 | and_return(response) 22 | end 23 | 24 | it "returns a Ridley::SandboxObject" do 25 | expect(subject.create).to be_a(Ridley::SandboxObject) 26 | end 27 | 28 | it "has a value of 'false' for :is_completed" do 29 | expect(subject.create.is_completed).to be_falsey 30 | end 31 | 32 | it "has an empty Hash of checksums" do 33 | expect(subject.create.checksums).to be_a(Hash) 34 | expect(subject.create.checksums).to be_empty 35 | end 36 | 37 | it "has a value for :uri" do 38 | expect(subject.create.uri).to eql(sandbox_uri) 39 | end 40 | 41 | it "has a value for :sandbox_id" do 42 | expect(subject.create.sandbox_id).to eql(sandbox_id) 43 | end 44 | 45 | context "when given an array of checksums" do 46 | let(:checksums) do 47 | { 48 | "385ea5490c86570c7de71070bce9384a" => nil, 49 | "f6f73175e979bd90af6184ec277f760c" => nil, 50 | "2e03dd7e5b2e6c8eab1cf41ac61396d5" => nil 51 | } 52 | end 53 | 54 | let(:checksum_array) { checksums.keys } 55 | 56 | it "has a Hash of checksums with each of the given checksum ids" do 57 | expect(subject.create(checksum_array).checksums.size).to eq(checksum_array.length) 58 | end 59 | end 60 | end 61 | 62 | describe "#commit" do 63 | let(:sandbox_id) { "bd091b150b0a4578b97771af6abf3e05" } 64 | let(:sandbox_path) { "#{described_class.resource_path}/#{sandbox_id}" } 65 | 66 | let(:response) do 67 | double(body: { 68 | is_completed: true, 69 | _rev: "1-bbc8a96f7486aeba2b562d382142fd68", 70 | create_time: "2013-01-16T01:43:43+00:00", 71 | guid: "bd091b150b0a4578b97771af6abf3e05", 72 | json_class: "Chef::Sandbox", 73 | name: "bd091b150b0a4578b97771af6abf3e05", 74 | checksums: [], 75 | chef_type: "sandbox" 76 | }) 77 | end 78 | 79 | it "sends a /PUT to the sandbox resource with is_complete set to true" do 80 | connection.should_receive(:put).with(sandbox_path, JSON.fast_generate(is_completed: true)).and_return(response) 81 | 82 | subject.commit(sandbox_id) 83 | end 84 | 85 | context "when a sandbox of the given ID is not found" do 86 | before do 87 | connection.should_receive(:put).and_raise(Ridley::Errors::HTTPNotFound.new({})) 88 | end 89 | 90 | it "raises a ResourceNotFound error" do 91 | expect { 92 | subject.commit(sandbox_id) 93 | }.to raise_error(Ridley::Errors::ResourceNotFound) 94 | end 95 | end 96 | 97 | context "when the given sandbox contents are malformed" do 98 | before do 99 | connection.should_receive(:put).and_raise(Ridley::Errors::HTTPBadRequest.new({})) 100 | end 101 | 102 | it "raises a SandboxCommitError error" do 103 | expect { 104 | subject.commit(sandbox_id) 105 | }.to raise_error(Ridley::Errors::SandboxCommitError) 106 | end 107 | end 108 | 109 | context "when the user who made the request is not authorized" do 110 | it "raises a PermissionDenied error on unauthorized" do 111 | connection.should_receive(:put).and_raise(Ridley::Errors::HTTPUnauthorized.new({})) 112 | 113 | expect { 114 | subject.commit(sandbox_id) 115 | }.to raise_error(Ridley::Errors::PermissionDenied) 116 | end 117 | 118 | it "raises a PermissionDenied error on forbidden" do 119 | connection.should_receive(:put).and_raise(Ridley::Errors::HTTPForbidden.new({})) 120 | 121 | expect { 122 | subject.commit(sandbox_id) 123 | }.to raise_error(Ridley::Errors::PermissionDenied) 124 | end 125 | end 126 | end 127 | 128 | describe "#update" do 129 | it "is not a supported action" do 130 | expect { 131 | subject.update(anything) 132 | }.to raise_error(RuntimeError, "action not supported") 133 | end 134 | end 135 | 136 | describe "#update" do 137 | it "is not a supported action" do 138 | expect { 139 | subject.update 140 | }.to raise_error(RuntimeError, "action not supported") 141 | end 142 | end 143 | 144 | describe "#all" do 145 | it "is not a supported action" do 146 | expect { 147 | subject.all 148 | }.to raise_error(RuntimeError, "action not supported") 149 | end 150 | end 151 | 152 | describe "#find" do 153 | it "is not a supported action" do 154 | expect { 155 | subject.find 156 | }.to raise_error(RuntimeError, "action not supported") 157 | end 158 | end 159 | 160 | describe "#delete" do 161 | it "is not a supported action" do 162 | expect { 163 | subject.delete 164 | }.to raise_error(RuntimeError, "action not supported") 165 | end 166 | end 167 | 168 | describe "#delete_all" do 169 | it "is not a supported action" do 170 | expect { 171 | subject.delete_all 172 | }.to raise_error(RuntimeError, "action not supported") 173 | end 174 | end 175 | end 176 | -------------------------------------------------------------------------------- /spec/unit/ridley/resources/user_resource_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::UserResource, type: 'wip' do 4 | subject { described_class.new(double('registry')) } 5 | let(:user_id) { "rspec-user" } 6 | let(:user_password) { "swordfish" } 7 | 8 | describe "#regenerate_key" do 9 | before { subject.stub(find: nil) } 10 | 11 | context "when a user with the given ID exists" do 12 | let(:user) { double('chef-user') } 13 | before { subject.should_receive(:find).with(user_id).and_return(user) } 14 | 15 | it "sets the private key to true and updates the user" do 16 | user.should_receive(:private_key=).with(true) 17 | subject.should_receive(:update).with(user) 18 | 19 | subject.regenerate_key(user_id) 20 | end 21 | end 22 | 23 | context "when a user with the given ID does not exist" do 24 | before { subject.should_receive(:find).with(user_id).and_return(nil) } 25 | 26 | it "raises a ResourceNotFound error" do 27 | expect { 28 | subject.regenerate_key(user_id) 29 | }.to raise_error(Ridley::Errors::ResourceNotFound) 30 | end 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/unit/ridley/sandbox_uploader_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley::SandboxUploader do 4 | describe "ClassMethods" do 5 | subject { described_class } 6 | 7 | describe "::checksum" do 8 | let(:io) { StringIO.new("some long string") } 9 | subject { described_class.checksum(io) } 10 | 11 | it { is_expected.to eq("2fb66bbfb88cdf9e07a3f1d1dfad71ab") } 12 | end 13 | 14 | describe "::checksum64" do 15 | let(:io) { StringIO.new("some long string") } 16 | subject { described_class.checksum64(io) } 17 | 18 | it { is_expected.to eq("L7Zrv7iM354Ho/HR361xqw==") } 19 | end 20 | end 21 | 22 | let(:client_name) { "reset" } 23 | let(:client_key) { fixtures_path.join('reset.pem') } 24 | let(:connection) do 25 | double('connection', 26 | client_name: client_name, 27 | client_key: client_key, 28 | options: {} 29 | ) 30 | end 31 | let(:resource) { double('resource', connection: connection) } 32 | let(:checksums) do 33 | { 34 | "oGCPHrQ+5MylEL+V+NIJ9w==" => { 35 | needs_upload: true, 36 | url: "https://api.opscode.com/organizations/vialstudios/sandboxes/bd091b150b0a4578b97771af6abf3e05" 37 | } 38 | } 39 | end 40 | 41 | let(:sandbox) { Ridley::SandboxObject.new(resource, checksums: checksums) } 42 | 43 | subject { described_class.new(client_name, client_key, {}) } 44 | 45 | describe "#upload" do 46 | let(:chk_id) { "a0608f1eb43ee4cca510bf95f8d209f7" } 47 | let(:path) { fixtures_path.join('reset.pem').to_s } 48 | let(:different_path) { fixtures_path.join('recipe_one.rb').to_s } 49 | 50 | before { connection.stub(foss?: false) } 51 | 52 | context "when the checksum needs uploading" do 53 | let(:checksums) do 54 | { 55 | chk_id => { 56 | url: "https://api.opscode.com/organizations/vialstudios/sandboxes/bd091b150b0a4578b97771af6abf3e05", 57 | needs_upload: true 58 | } 59 | } 60 | end 61 | 62 | it "uploads each checksum to their target URL" do 63 | stub_request(:put, checksums[chk_id][:url]) 64 | 65 | subject.upload(sandbox, chk_id, path) 66 | end 67 | 68 | it "raises an exception when the calcuated checksum does not match the expected checksum" do 69 | expect { subject.upload(sandbox, chk_id, different_path) }.to raise_error(Ridley::Errors::ChecksumMismatch) 70 | end 71 | end 72 | 73 | context "when the checksum doesn't need uploading" do 74 | let(:checksums) do 75 | { 76 | chk_id => { 77 | needs_upload: false 78 | } 79 | } 80 | end 81 | 82 | it "returns nil" do 83 | expect(subject.upload(sandbox, chk_id, path)).to be_nil 84 | end 85 | end 86 | 87 | context "when the connection is an open source server connection with a non-80 port" do 88 | before do 89 | connection.stub(foss?: true, server_url: "http://localhost:8889") 90 | end 91 | 92 | let(:checksums) do 93 | { 94 | chk_id => { 95 | url: "http://localhost/sandboxes/bd091b150b0a4578b97771af6abf3e05", 96 | needs_upload: true 97 | } 98 | } 99 | end 100 | 101 | it "does not strip the port from the target to upload to" do 102 | stub_request(:put, "http://localhost:8889/sandboxes/bd091b150b0a4578b97771af6abf3e05") 103 | 104 | subject.upload(sandbox, chk_id, path) 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/unit/ridley_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Ridley do 4 | let(:config) { double("config") } 5 | 6 | describe "ClassMethods" do 7 | subject { Ridley } 8 | 9 | describe "::new" do 10 | it "creates a new Ridley::Connection" do 11 | client = double('client') 12 | expect(Ridley::Client).to receive(:new).with(config).and_return(client) 13 | 14 | expect(subject.new(config)).to eql(client) 15 | end 16 | end 17 | 18 | describe "from_chef_config" do 19 | let(:chef_config) do 20 | %( 21 | node_name "username" 22 | client_key "username.pem" 23 | validation_client_name "validator" 24 | validation_key "validator.pem" 25 | chef_server_url "https://api.opscode.com" 26 | cache_options(:path => "~/.chef/checksums") 27 | syntax_check_cache_path "/foo/bar" 28 | ssl_verify_mode :verify_none 29 | ) 30 | end 31 | 32 | let(:client) { double('client') } 33 | let(:path) { tmp_path.join('config.rb').to_s } 34 | 35 | before do 36 | allow(Ridley::Client).to receive(:new).and_return(client) 37 | File.open(path, 'w') { |f| f.write(chef_config) } 38 | end 39 | 40 | it "creates a Ridley connection from the Chef config" do 41 | expect(Ridley::Client).to receive(:new).with(hash_including( 42 | client_key: 'username.pem', 43 | client_name: 'username', 44 | validator_client: 'validator', 45 | validator_path: 'validator.pem', 46 | server_url: 'https://api.opscode.com', 47 | syntax_check_cache_path: "/foo/bar", 48 | cache_options: { path: "~/.chef/checksums" }, 49 | ssl: {verify: false}, 50 | )).and_return(nil) 51 | 52 | subject.from_chef_config(path) 53 | end 54 | 55 | it "allows the user to override attributes" do 56 | expect(Ridley::Client).to receive(:new).with(hash_including( 57 | client_key: 'bacon.pem', 58 | client_name: 'bacon', 59 | validator_client: 'validator', 60 | validator_path: 'validator.pem', 61 | server_url: 'https://api.opscode.com', 62 | syntax_check_cache_path: "/foo/bar", 63 | cache_options: { path: "~/.chef/checksums" }, 64 | ssl: {verify: false}, 65 | )) 66 | 67 | subject.from_chef_config(path, client_key: 'bacon.pem', client_name: 'bacon') 68 | end 69 | 70 | context "when the config location isn't explicitly specified" do 71 | before do 72 | dot_chef = tmp_path.join('.chef') 73 | knife_rb = dot_chef.join('knife.rb') 74 | 75 | FileUtils.mkdir_p(dot_chef) 76 | File.open(knife_rb, 'w') { |f| f.write(chef_config) } 77 | end 78 | 79 | it "does a knife.rb search" do 80 | expect(Ridley::Client).to receive(:new).with(hash_including( 81 | client_key: 'username.pem', 82 | client_name: 'username', 83 | validator_client: 'validator', 84 | validator_path: 'validator.pem', 85 | server_url: 'https://api.opscode.com', 86 | syntax_check_cache_path: "/foo/bar", 87 | cache_options: { path: "~/.chef/checksums" }, 88 | )).and_return(nil) 89 | 90 | Dir.chdir(tmp_path) do 91 | ENV['PWD'] = Dir.pwd 92 | subject.from_chef_config 93 | end 94 | end 95 | end 96 | end 97 | end 98 | end 99 | --------------------------------------------------------------------------------