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.
- 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
|