ridley 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.travis.yml +5 -0
- data/Gemfile +3 -0
- data/Guardfile +20 -0
- data/LICENSE +201 -0
- data/README.md +273 -0
- data/Thorfile +48 -0
- data/lib/ridley.rb +48 -0
- data/lib/ridley/connection.rb +131 -0
- data/lib/ridley/context.rb +25 -0
- data/lib/ridley/dsl.rb +58 -0
- data/lib/ridley/errors.rb +82 -0
- data/lib/ridley/log.rb +10 -0
- data/lib/ridley/middleware.rb +19 -0
- data/lib/ridley/middleware/chef_auth.rb +45 -0
- data/lib/ridley/middleware/chef_response.rb +28 -0
- data/lib/ridley/middleware/parse_json.rb +107 -0
- data/lib/ridley/resource.rb +305 -0
- data/lib/ridley/resources/client.rb +75 -0
- data/lib/ridley/resources/cookbook.rb +27 -0
- data/lib/ridley/resources/data_bag.rb +75 -0
- data/lib/ridley/resources/data_bag_item.rb +186 -0
- data/lib/ridley/resources/environment.rb +45 -0
- data/lib/ridley/resources/node.rb +34 -0
- data/lib/ridley/resources/role.rb +33 -0
- data/lib/ridley/version.rb +3 -0
- data/ridley.gemspec +39 -0
- data/spec/acceptance/client_resource_spec.rb +135 -0
- data/spec/acceptance/cookbook_resource_spec.rb +46 -0
- data/spec/acceptance/data_bag_item_resource_spec.rb +171 -0
- data/spec/acceptance/data_bag_resource_spec.rb +51 -0
- data/spec/acceptance/environment_resource_spec.rb +171 -0
- data/spec/acceptance/node_resource_spec.rb +218 -0
- data/spec/acceptance/role_resource_spec.rb +200 -0
- data/spec/fixtures/reset.pem +27 -0
- data/spec/spec_helper.rb +25 -0
- data/spec/support/each_matcher.rb +12 -0
- data/spec/support/shared_examples/ridley_resource.rb +237 -0
- data/spec/support/spec_helpers.rb +11 -0
- data/spec/unit/ridley/connection_spec.rb +167 -0
- data/spec/unit/ridley/errors_spec.rb +34 -0
- data/spec/unit/ridley/middleware/chef_auth_spec.rb +14 -0
- data/spec/unit/ridley/middleware/chef_response_spec.rb +213 -0
- data/spec/unit/ridley/middleware/parse_json_spec.rb +74 -0
- data/spec/unit/ridley/resource_spec.rb +214 -0
- data/spec/unit/ridley/resources/client_spec.rb +47 -0
- data/spec/unit/ridley/resources/cookbook_spec.rb +5 -0
- data/spec/unit/ridley/resources/data_bag_item_spec.rb +42 -0
- data/spec/unit/ridley/resources/data_bag_spec.rb +15 -0
- data/spec/unit/ridley/resources/environment_spec.rb +73 -0
- data/spec/unit/ridley/resources/node_spec.rb +5 -0
- data/spec/unit/ridley/resources/role_spec.rb +5 -0
- data/spec/unit/ridley_spec.rb +32 -0
- 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,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
|