ridley 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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