ridley 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. data/.gitignore +17 -0
  2. data/.travis.yml +5 -0
  3. data/Gemfile +3 -0
  4. data/Guardfile +20 -0
  5. data/LICENSE +201 -0
  6. data/README.md +273 -0
  7. data/Thorfile +48 -0
  8. data/lib/ridley.rb +48 -0
  9. data/lib/ridley/connection.rb +131 -0
  10. data/lib/ridley/context.rb +25 -0
  11. data/lib/ridley/dsl.rb +58 -0
  12. data/lib/ridley/errors.rb +82 -0
  13. data/lib/ridley/log.rb +10 -0
  14. data/lib/ridley/middleware.rb +19 -0
  15. data/lib/ridley/middleware/chef_auth.rb +45 -0
  16. data/lib/ridley/middleware/chef_response.rb +28 -0
  17. data/lib/ridley/middleware/parse_json.rb +107 -0
  18. data/lib/ridley/resource.rb +305 -0
  19. data/lib/ridley/resources/client.rb +75 -0
  20. data/lib/ridley/resources/cookbook.rb +27 -0
  21. data/lib/ridley/resources/data_bag.rb +75 -0
  22. data/lib/ridley/resources/data_bag_item.rb +186 -0
  23. data/lib/ridley/resources/environment.rb +45 -0
  24. data/lib/ridley/resources/node.rb +34 -0
  25. data/lib/ridley/resources/role.rb +33 -0
  26. data/lib/ridley/version.rb +3 -0
  27. data/ridley.gemspec +39 -0
  28. data/spec/acceptance/client_resource_spec.rb +135 -0
  29. data/spec/acceptance/cookbook_resource_spec.rb +46 -0
  30. data/spec/acceptance/data_bag_item_resource_spec.rb +171 -0
  31. data/spec/acceptance/data_bag_resource_spec.rb +51 -0
  32. data/spec/acceptance/environment_resource_spec.rb +171 -0
  33. data/spec/acceptance/node_resource_spec.rb +218 -0
  34. data/spec/acceptance/role_resource_spec.rb +200 -0
  35. data/spec/fixtures/reset.pem +27 -0
  36. data/spec/spec_helper.rb +25 -0
  37. data/spec/support/each_matcher.rb +12 -0
  38. data/spec/support/shared_examples/ridley_resource.rb +237 -0
  39. data/spec/support/spec_helpers.rb +11 -0
  40. data/spec/unit/ridley/connection_spec.rb +167 -0
  41. data/spec/unit/ridley/errors_spec.rb +34 -0
  42. data/spec/unit/ridley/middleware/chef_auth_spec.rb +14 -0
  43. data/spec/unit/ridley/middleware/chef_response_spec.rb +213 -0
  44. data/spec/unit/ridley/middleware/parse_json_spec.rb +74 -0
  45. data/spec/unit/ridley/resource_spec.rb +214 -0
  46. data/spec/unit/ridley/resources/client_spec.rb +47 -0
  47. data/spec/unit/ridley/resources/cookbook_spec.rb +5 -0
  48. data/spec/unit/ridley/resources/data_bag_item_spec.rb +42 -0
  49. data/spec/unit/ridley/resources/data_bag_spec.rb +15 -0
  50. data/spec/unit/ridley/resources/environment_spec.rb +73 -0
  51. data/spec/unit/ridley/resources/node_spec.rb +5 -0
  52. data/spec/unit/ridley/resources/role_spec.rb +5 -0
  53. data/spec/unit/ridley_spec.rb +32 -0
  54. metadata +451 -0
data/Thorfile ADDED
@@ -0,0 +1,48 @@
1
+ # encoding: utf-8
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require 'bundler'
5
+ require 'bundler/setup'
6
+
7
+ require 'ridley'
8
+ require 'thor/rake_compat'
9
+
10
+ class Default < Thor
11
+ include Thor::RakeCompat
12
+ Bundler::GemHelper.install_tasks
13
+
14
+ desc "build", "Build berkshelf-#{Ridley::VERSION}.gem into the pkg directory"
15
+ def build
16
+ Rake::Task["build"].execute
17
+ end
18
+
19
+ desc "install", "Build and install berkshelf-#{Ridley::VERSION}.gem into system gems"
20
+ def install
21
+ Rake::Task["install"].execute
22
+ end
23
+
24
+ desc "release", "Create tag v#{Ridley::VERSION} and build and push berkshelf-#{Ridley::VERSION}.gem to Rubygems"
25
+ def release
26
+ Rake::Task["release"].execute
27
+ end
28
+
29
+ class Spec < Thor
30
+ namespace :spec
31
+ default_task :all
32
+
33
+ desc "all", "run all tests"
34
+ def all
35
+ exec "rspec --color --format=documentation spec"
36
+ end
37
+
38
+ desc "unit", "run only unit tests"
39
+ def unit
40
+ exec "rspec --color --format=documentation spec --tag ~type:acceptance"
41
+ end
42
+
43
+ desc "acceptance", "run only acceptance tests"
44
+ def acceptance
45
+ exec "rspec --color --format=documentation spec --tag type:acceptance"
46
+ end
47
+ end
48
+ end
data/lib/ridley.rb ADDED
@@ -0,0 +1,48 @@
1
+ require 'faraday'
2
+ require 'addressable/uri'
3
+ require 'yajl'
4
+ require 'multi_json'
5
+ require 'active_model'
6
+ require 'active_support/inflector'
7
+ require 'active_support/core_ext'
8
+ require 'forwardable'
9
+ require 'set'
10
+ require 'thread'
11
+
12
+ require 'ridley/errors'
13
+
14
+ # @author Jamie Winsor <jamie@vialstudios.com>
15
+ module Ridley
16
+ CHEF_VERSION = '10.12.0'.freeze
17
+
18
+ autoload :Log, 'ridley/log'
19
+ autoload :Connection, 'ridley/connection'
20
+ autoload :DSL, 'ridley/dsl'
21
+ autoload :Context, 'ridley/context'
22
+ autoload :Resource, 'ridley/resource'
23
+ autoload :Environment, 'ridley/resources/environment'
24
+ autoload :Role, 'ridley/resources/role'
25
+ autoload :Client, 'ridley/resources/client'
26
+ autoload :Node, 'ridley/resources/node'
27
+ autoload :DataBag, 'ridley/resources/data_bag'
28
+ autoload :DataBagItem, 'ridley/resources/data_bag_item'
29
+ autoload :Cookbook, 'ridley/resources/cookbook'
30
+
31
+ class << self
32
+ def connection(*args)
33
+ Connection.new(*args)
34
+ end
35
+
36
+ def sync(*args, &block)
37
+ Connection.sync(*args, &block)
38
+ end
39
+
40
+ # @return [Ridley::Log]
41
+ def log
42
+ Ridley::Log
43
+ end
44
+ alias_method :logger, :log
45
+ end
46
+ end
47
+
48
+ require 'ridley/middleware'
@@ -0,0 +1,131 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ class Connection
4
+ class << self
5
+ def sync(options, &block)
6
+ new(options).sync(&block)
7
+ end
8
+ end
9
+
10
+ extend Forwardable
11
+ include Ridley::DSL
12
+
13
+ attr_reader :client_name
14
+ attr_reader :client_key
15
+ attr_reader :organization
16
+
17
+ attr_accessor :thread_count
18
+
19
+ def_delegator :conn, :build_url
20
+ def_delegator :conn, :scheme
21
+ def_delegator :conn, :host
22
+ def_delegator :conn, :port
23
+ def_delegator :conn, :path_prefix
24
+
25
+ def_delegator :conn, :get
26
+ def_delegator :conn, :put
27
+ def_delegator :conn, :post
28
+ def_delegator :conn, :delete
29
+ def_delegator :conn, :head
30
+
31
+ def_delegator :conn, :in_parallel
32
+
33
+ REQUIRED_OPTIONS = [
34
+ :server_url,
35
+ :client_name,
36
+ :client_key
37
+ ]
38
+
39
+ DEFAULT_THREAD_COUNT = 8
40
+
41
+ # @option options [String] :server_url
42
+ # @option options [String] :client_name
43
+ # @option options [String] :client_key
44
+ # @option options [Integer] :thread_count
45
+ # @option options [Hash] :params
46
+ # URI query unencoded key/value pairs
47
+ # @option options [Hash] :headers
48
+ # unencoded HTTP header key/value pairs
49
+ # @option options [Hash] :request
50
+ # request options
51
+ # @option options [Hash] :ssl
52
+ # SSL options
53
+ # @option options [URI, String, Hash] :proxy
54
+ # URI, String, or Hash of HTTP proxy options
55
+ def initialize(options = {})
56
+ options[:thread_count] ||= DEFAULT_THREAD_COUNT
57
+
58
+ validate_options(options)
59
+
60
+ @client_name = options[:client_name]
61
+ @client_key = options[:client_key]
62
+ @organization = options[:organization]
63
+ @thread_count = options[:thread_count]
64
+
65
+ faraday_options = options.slice(:params, :headers, :request, :ssl, :proxy)
66
+ uri_hash = Addressable::URI.parse(options[:server_url]).to_hash.slice(:scheme, :host, :port)
67
+
68
+ unless uri_hash[:port]
69
+ uri_hash[:port] = (uri_hash[:scheme] == "https" ? 443 : 80)
70
+ end
71
+
72
+ if organization
73
+ uri_hash[:path] = "/organizations/#{organization}"
74
+ end
75
+
76
+ server_uri = Addressable::URI.new(uri_hash)
77
+
78
+ @conn = Faraday.new(server_uri, faraday_options) do |c|
79
+ c.request :chef_auth, client_name, client_key
80
+ c.response :chef_response
81
+ c.response :json
82
+
83
+ c.adapter Faraday.default_adapter
84
+ end
85
+ end
86
+
87
+ def sync(&block)
88
+ unless block
89
+ raise Errors::InternalError, "A block must be given to synchronously process requests."
90
+ end
91
+
92
+ evaluate(&block)
93
+ end
94
+
95
+ # @return [Symbol]
96
+ def api_type
97
+ organization.nil? ? :foss : :hosted
98
+ end
99
+
100
+ # @return [Boolean]
101
+ def hosted?
102
+ api_type == :hosted
103
+ end
104
+
105
+ # @return [Boolean]
106
+ def foss?
107
+ api_type == :foss
108
+ end
109
+
110
+ private
111
+
112
+ attr_reader :conn
113
+
114
+ def evaluate(&block)
115
+ @self_before_instance_eval = eval("self", block.binding)
116
+ instance_eval(&block)
117
+ end
118
+
119
+ def method_missing(method, *args, &block)
120
+ @self_before_instance_eval.send(method, *args, &block)
121
+ end
122
+
123
+ def validate_options(options)
124
+ missing = REQUIRED_OPTIONS - options.keys
125
+ unless missing.empty?
126
+ missing.collect! { |opt| "'#{opt}'" }
127
+ raise ArgumentError, "missing required option(s): #{missing.join(', ')}"
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,25 @@
1
+ module Ridley
2
+ # @api private
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ class Context
5
+ attr_reader :resource
6
+ attr_reader :connection
7
+
8
+ # @param [Constant] resource
9
+ # the constant of the class to send class functions to
10
+ # @param [Ridley::Connection] connection
11
+ # the connection to use when sending class functions to resources
12
+ def initialize(resource, connection)
13
+ @resource = resource
14
+ @connection = connection
15
+ end
16
+
17
+ def new(*args)
18
+ resource.send(:new, connection, *args)
19
+ end
20
+
21
+ def method_missing(fun, *args, &block)
22
+ resource.send(fun, connection, *args, &block)
23
+ end
24
+ end
25
+ end
data/lib/ridley/dsl.rb ADDED
@@ -0,0 +1,58 @@
1
+ module Ridley
2
+ # @api public
3
+ # @author Jamie Winsor <jamie@vialstudios.com>
4
+ #
5
+ # A DSL to be included into Ridley::Connection. Instance functions of the same name as
6
+ # Chef a resource are coerced into class functions of a class of the same name.
7
+ #
8
+ # This is accomplished by returning a Ridley::Context object and coercing any messages sent
9
+ # to it into a message to the Chef resource's class in Ridley.
10
+ #
11
+ # @example
12
+ # class Connection
13
+ # include Ridley::DSL
14
+ # end
15
+ #
16
+ # connection = Ridley::Connection.new
17
+ # connection.role.all
18
+ #
19
+ # The 'role' function is made available to the instance of Ridley::Connection by including
20
+ # Ridley::DSL. This function returns a Ridley::Context object which receives the 'all' message.
21
+ # The Ridley::Context coerces the 'all' message into a message to the Ridley::Role class and
22
+ # sends along the instance of Ridley::Connection that is chaining 'role.all'
23
+ #
24
+ # connection.role.all => Ridley::Role.all(connection)
25
+ #
26
+ # Any additional arguments will also be passed to the class function of the Chef resource's class
27
+ #
28
+ # connection.role.find("reset") => Ridley::Role.find(connection, "reset")
29
+ #
30
+ # @example instantiating new resources
31
+ # class connection
32
+ # include Ridley::DSL
33
+ # end
34
+ #
35
+ # connection = Ridley::Connection.new
36
+ # connection.role.new(name: "hello") => <#Ridley::Role: @name="hello">
37
+ #
38
+ # New instances of resources can be instantiated by calling new on the Ridley::Context. These messages
39
+ # will be send to the Chef resource's class in Ridley and can be treated as a normal Ruby object. Each
40
+ # instantiated object will have the connection information contained within so you can do things like
41
+ # save a role after changing it's attributes.
42
+ #
43
+ # r = connection.role.new(name: "new-role")
44
+ # r.name => "new-role"
45
+ # r.name = "other-name"
46
+ # r.save
47
+ #
48
+ # connection.role.find("new-role") => <#Ridley::Role: @name="new-role">
49
+ #
50
+ # @see Ridley::Context
51
+ # @see Ridley::Role
52
+ # @see Ridley::Connection
53
+ module DSL; end
54
+ end
55
+
56
+ Dir["#{File.dirname(__FILE__)}/resources/*.rb"].sort.each do |path|
57
+ require "ridley/resources/#{File.basename(path, '.rb')}"
58
+ end
@@ -0,0 +1,82 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ module Errors
4
+ class RidleyError < StandardError; end
5
+ class InternalError < RidleyError; end
6
+
7
+ class InvalidResource < RidleyError
8
+ attr_reader :errors
9
+
10
+ def initialize(errors)
11
+ @errors = errors
12
+ end
13
+
14
+ def message
15
+ errors.full_messages.join(', ')
16
+ end
17
+ alias_method :to_s, :message
18
+ end
19
+
20
+ class HTTPError < RidleyError
21
+ class << self
22
+ def fabricate(env)
23
+ klass = lookup_error(env[:status].to_i)
24
+ klass.new(env)
25
+ end
26
+
27
+ def register_error(status)
28
+ error_map[status.to_i] = self
29
+ end
30
+
31
+ def lookup_error(status)
32
+ error_map.fetch(status.to_i)
33
+ rescue KeyError
34
+ HTTPUnknownStatus
35
+ end
36
+
37
+ def error_map
38
+ @@error_map ||= Hash.new
39
+ end
40
+ end
41
+
42
+ attr_reader :env
43
+ attr_reader :errors
44
+
45
+ attr_reader :message
46
+ alias_method :to_s, :message
47
+
48
+ def initialize(env)
49
+ @env = env
50
+ @errors = Array(env[:body][:error]) || []
51
+
52
+ if errors.empty?
53
+ @message = env[:body] || "no content body"
54
+ else
55
+ @message = "errors: "
56
+ @message << errors.collect { |e| "'#{e}'" }.join(', ')
57
+ end
58
+ end
59
+ end
60
+
61
+ class HTTPUnknownStatus < HTTPError
62
+ def initialize(env)
63
+ super(env)
64
+ @message = "status: #{env[:status]} is an unknown HTTP status code or not an error."
65
+ end
66
+ end
67
+
68
+ class HTTPBadRequest < HTTPError; register_error(400); end
69
+ class HTTPUnauthorized < HTTPError; register_error(401); end
70
+ class HTTPForbidden < HTTPError; register_error(403); end
71
+ class HTTPNotFound < HTTPError; register_error(404); end
72
+ class HTTPMethodNotAllowed < HTTPError; register_error(405); end
73
+ class HTTPRequestTimeout < HTTPError; register_error(408); end
74
+ class HTTPConflict < HTTPError; register_error(409); end
75
+
76
+ class HTTPInternalServerError < HTTPError; register_error(500); end
77
+ class HTTPNotImplemented < HTTPError; register_error(501); end
78
+ class HTTPBadGateway < HTTPError; register_error(502); end
79
+ class HTTPServiceUnavailable < HTTPError; register_error(503); end
80
+ class HTTPGatewayTimeout < HTTPError; register_error(504); end
81
+ end
82
+ end
data/lib/ridley/log.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'mixlib/log'
2
+
3
+ module Ridley
4
+ # @author Jamie Winsor <jamie@vialstudios.com>
5
+ class Log
6
+ extend Mixlib::Log
7
+
8
+ init
9
+ end
10
+ end
@@ -0,0 +1,19 @@
1
+ module Ridley
2
+ # @author Jamie Winsor <jamie@vialstudios.com>
3
+ module Middleware
4
+ CONTENT_TYPE = 'content-type'.freeze
5
+
6
+ require 'ridley/middleware/parse_json'
7
+ require 'ridley/middleware/chef_response'
8
+ require 'ridley/middleware/chef_auth'
9
+
10
+ Faraday.register_middleware :request,
11
+ chef_auth: -> { ChefAuth }
12
+
13
+ Faraday.register_middleware :response,
14
+ json: -> { ParseJson }
15
+
16
+ Faraday.register_middleware :response,
17
+ chef_response: -> { ChefResponse }
18
+ end
19
+ end
@@ -0,0 +1,45 @@
1
+ require 'mixlib/authentication/signedheaderauth'
2
+
3
+ module Ridley
4
+ module Middleware
5
+ # @author Jamie Winsor <jamie@vialstudios.com>
6
+ class ChefAuth < Faraday::Middleware
7
+ attr_reader :client_name
8
+ attr_reader :client_key
9
+
10
+ def initialize(app, client_name, client_key)
11
+ super(app)
12
+ @client_name = client_name
13
+ @client_key = OpenSSL::PKey::RSA.new(File.read(client_key))
14
+ end
15
+
16
+ def call(env)
17
+ sign_obj = Mixlib::Authentication::SignedHeaderAuth.signing_object(
18
+ http_method: env[:method],
19
+ host: env[:url].host,
20
+ path: env[:url].path,
21
+ body: env[:body] || '',
22
+ timestamp: Time.now.utc.iso8601,
23
+ user_id: client_name
24
+ )
25
+ authentication_headers = sign_obj.sign(client_key)
26
+ env[:request_headers] = env[:request_headers].merge(authentication_headers).merge(default_headers)
27
+ env[:request_headers] = env[:request_headers].merge('Content-Length' => env[:body].bytesize.to_s) if env[:body]
28
+
29
+ Ridley.log.debug(env)
30
+
31
+ @app.call(env)
32
+ end
33
+
34
+ private
35
+
36
+ def default_headers
37
+ {
38
+ 'Accept' => 'application/json',
39
+ 'Content-Type' => 'application/json',
40
+ 'X-Chef-Version' => Ridley::CHEF_VERSION
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end