chef-api 0.2.0

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +20 -0
  3. data/.travis.yml +14 -0
  4. data/Gemfile +12 -0
  5. data/LICENSE +201 -0
  6. data/README.md +264 -0
  7. data/Rakefile +1 -0
  8. data/chef-api.gemspec +25 -0
  9. data/lib/chef-api/boolean.rb +6 -0
  10. data/lib/chef-api/configurable.rb +78 -0
  11. data/lib/chef-api/connection.rb +466 -0
  12. data/lib/chef-api/defaults.rb +130 -0
  13. data/lib/chef-api/error_collection.rb +44 -0
  14. data/lib/chef-api/errors.rb +35 -0
  15. data/lib/chef-api/logger.rb +160 -0
  16. data/lib/chef-api/proxy.rb +72 -0
  17. data/lib/chef-api/resource.rb +16 -0
  18. data/lib/chef-api/resources/base.rb +951 -0
  19. data/lib/chef-api/resources/client.rb +85 -0
  20. data/lib/chef-api/resources/collection_proxy.rb +217 -0
  21. data/lib/chef-api/resources/cookbook.rb +24 -0
  22. data/lib/chef-api/resources/cookbook_version.rb +23 -0
  23. data/lib/chef-api/resources/data_bag.rb +136 -0
  24. data/lib/chef-api/resources/data_bag_item.rb +35 -0
  25. data/lib/chef-api/resources/environment.rb +16 -0
  26. data/lib/chef-api/resources/node.rb +17 -0
  27. data/lib/chef-api/resources/principal.rb +11 -0
  28. data/lib/chef-api/resources/role.rb +16 -0
  29. data/lib/chef-api/resources/user.rb +11 -0
  30. data/lib/chef-api/schema.rb +112 -0
  31. data/lib/chef-api/util.rb +119 -0
  32. data/lib/chef-api/validator.rb +16 -0
  33. data/lib/chef-api/validators/base.rb +82 -0
  34. data/lib/chef-api/validators/required.rb +11 -0
  35. data/lib/chef-api/validators/type.rb +23 -0
  36. data/lib/chef-api/version.rb +3 -0
  37. data/lib/chef-api.rb +76 -0
  38. data/locales/en.yml +89 -0
  39. data/spec/integration/resources/client_spec.rb +8 -0
  40. data/spec/integration/resources/environment_spec.rb +8 -0
  41. data/spec/integration/resources/node_spec.rb +8 -0
  42. data/spec/integration/resources/role_spec.rb +8 -0
  43. data/spec/spec_helper.rb +26 -0
  44. data/spec/support/chef_server.rb +115 -0
  45. data/spec/support/shared/chef_api_resource.rb +91 -0
  46. data/spec/unit/resources/base_spec.rb +47 -0
  47. data/spec/unit/resources/client_spec.rb +69 -0
  48. metadata +128 -0
@@ -0,0 +1,112 @@
1
+ module ChefAPI
2
+ #
3
+ # A wrapper class that describes a remote schema (such as the Chef Server
4
+ # API layer), with validation and other magic spinkled on top.
5
+ #
6
+ class Schema
7
+
8
+ #
9
+ # The full list of attributes defined on this schema.
10
+ #
11
+ # @return [Hash]
12
+ #
13
+ attr_reader :attributes
14
+
15
+ attr_reader :ignored_attributes
16
+ attr_reader :transformations
17
+
18
+ #
19
+ # The list of defined validators for this schema.
20
+ #
21
+ # @return [Array]
22
+ #
23
+ attr_reader :validators
24
+
25
+ #
26
+ # Create a new schema and evaulte the block contents in a clean room.
27
+ #
28
+ def initialize(&block)
29
+ @attributes = {}
30
+ @ignored_attributes = {}
31
+ @transformations = {}
32
+ @validators = []
33
+
34
+ instance_eval(&block) if block
35
+
36
+ @attributes.freeze
37
+ @ignored_attributes.freeze
38
+ @transformations.freeze
39
+ @validators.freeze
40
+ end
41
+
42
+ #
43
+ # The defined primary key for this schema. If no primary key is given, it
44
+ # is assumed to be the first item in the list.
45
+ #
46
+ # @return [Symbol]
47
+ #
48
+ def primary_key
49
+ @primary_key ||= @attributes.first[0]
50
+ end
51
+
52
+ #
53
+ # DSL method for defining an attribute.
54
+ #
55
+ # @param [Symbol] key
56
+ # the key to use
57
+ # @param [Hash] options
58
+ # a list of options to create the attribute with
59
+ #
60
+ # @return [Symbol]
61
+ # the attribute
62
+ #
63
+ def attribute(key, options = {})
64
+ if primary_key = options.delete(:primary)
65
+ @primary_key = key.to_sym
66
+ end
67
+
68
+ @attributes[key] = options.delete(:default)
69
+
70
+ # All remaining options are assumed to be validations
71
+ options.each do |validation, options|
72
+ if options
73
+ @validators << Validator.find(validation).new(key, options)
74
+ end
75
+ end
76
+
77
+ key
78
+ end
79
+
80
+ #
81
+ # Ignore an attribute. This is handy if you know there's an attribute that
82
+ # the remote server will return, but you don't want that information
83
+ # exposed to the user (or the data is sensitive).
84
+ #
85
+ # @param [Array<Symbol>] keys
86
+ # the list of attributes to ignore
87
+ #
88
+ def ignore(*keys)
89
+ keys.each do |key|
90
+ @ignored_attributes[key.to_sym] = true
91
+ end
92
+ end
93
+
94
+ #
95
+ # Transform an attribute onto another.
96
+ #
97
+ # @example Transform the +:bacon+ attribute onto the +:ham+ attribute
98
+ # transform :bacon, ham: true
99
+ #
100
+ # @example Transform an attribute with a complex transformation
101
+ # transform :bacon, ham: ->(value) { value.split('__', 2).last }
102
+ #
103
+ # @param [Symbol] key
104
+ # the attribute to transform
105
+ # @param [Hash] options
106
+ # the key-value pair of the transformations to make
107
+ #
108
+ def transform(key, options = {})
109
+ @transformations[key.to_sym] = options
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,119 @@
1
+ module ChefAPI
2
+ module Util
3
+ extend self
4
+
5
+ #
6
+ # Covert the given CaMelCaSeD string to under_score. Graciously borrowed
7
+ # from http://stackoverflow.com/questions/1509915.
8
+ #
9
+ # @param [String] string
10
+ # the string to use for transformation
11
+ #
12
+ # @return [String]
13
+ #
14
+ def underscore(string)
15
+ string
16
+ .to_s
17
+ .gsub(/::/, '/')
18
+ .gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
19
+ .gsub(/([a-z\d])([A-Z])/,'\1_\2')
20
+ .tr('-', '_')
21
+ .downcase
22
+ end
23
+
24
+ #
25
+ # Convert an underscored string to it's camelcase equivalent constant.
26
+ #
27
+ # @param [String] string
28
+ # the string to convert
29
+ #
30
+ # @return [String]
31
+ #
32
+ def camelize(string)
33
+ string
34
+ .to_s
35
+ .split('_')
36
+ .map { |e| e.capitalize }
37
+ .join
38
+ end
39
+
40
+ #
41
+ # Truncate the given string to a certain number of characters.
42
+ #
43
+ # @param [String] string
44
+ # the string to truncate
45
+ # @param [Hash] options
46
+ # the list of options (such as +length+)
47
+ #
48
+ def truncate(string, options = {})
49
+ length = options[:length] || 30
50
+
51
+ if string.length > length
52
+ string[0..length-3] + '...'
53
+ else
54
+ string
55
+ end
56
+ end
57
+
58
+ #
59
+ # "Safely" read the contents of a file on disk, catching any permission
60
+ # errors or not found errors and raising a nicer exception.
61
+ #
62
+ # @example Reading a file that does not exist
63
+ # safe_read('/non-existent/file') #=> Error::FileNotFound
64
+ #
65
+ # @example Reading a file with improper permissions
66
+ # safe_read('/bad-permissions') #=> Error::InsufficientFilePermissions
67
+ #
68
+ # @example Reading a regular file
69
+ # safe_read('my-file.txt') #=> ["my-file", "..."]
70
+ #
71
+ # @param [String] path
72
+ # the path to the file on disk
73
+ #
74
+ # @return [Array<String>]
75
+ # A array where the first value is the basename of the file and the
76
+ # second value is the literal contents from +File.read+.
77
+ #
78
+ def safe_read(path)
79
+ path = File.expand_path(path)
80
+ name = File.basename(path, '.*')
81
+ contents = File.read(path)
82
+
83
+ [name, contents]
84
+ rescue Errno::EACCES
85
+ raise Error::InsufficientFilePermissions.new(path: path)
86
+ rescue Errno::ENOENT
87
+ raise Error::FileNotFound.new(path: path)
88
+ end
89
+
90
+ #
91
+ # Quickly iterate over a collection using native Ruby threads, preserving
92
+ # the original order of elements and being all thread-safe and stuff.
93
+ #
94
+ # @example Parse a collection of JSON files
95
+ #
96
+ # fast_collect(Dir['**/*.json']) do |item|
97
+ # JSON.parse(File.read(item))
98
+ # end
99
+ #
100
+ # @param [#each] collection
101
+ # the collection to iterate
102
+ # @param [Proc] block
103
+ # the block to evaluate (typically an expensive operation)
104
+ #
105
+ # @return [Array]
106
+ # the result of the iteration
107
+ #
108
+ def fast_collect(collection, &block)
109
+ collection.map do |item|
110
+ Thread.new do
111
+ Thread.current[:result] = block.call(item)
112
+ end
113
+ end.collect do |thread|
114
+ thread.join
115
+ thread[:result]
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,16 @@
1
+ module ChefAPI
2
+ module Validator
3
+ autoload :Base, 'chef-api/validators/base'
4
+ autoload :Required, 'chef-api/validators/required'
5
+ autoload :Type, 'chef-api/validators/type'
6
+
7
+ #
8
+ # Find a validator by the given key.
9
+ #
10
+ def self.find(key)
11
+ const_get(Util.camelize(key))
12
+ rescue NameError
13
+ raise Error::InvalidValidator.new(key: key)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,82 @@
1
+ module ChefAPI
2
+ class Validator::Base
3
+ #
4
+ # @return [Symbol]
5
+ # the attribute to apply this validation on
6
+ #
7
+ attr_reader :attribute
8
+
9
+ #
10
+ # @return [Hash]
11
+ # the hash of additional arguments passed in
12
+ #
13
+ attr_reader :options
14
+
15
+ #
16
+ # Create anew validator.
17
+ #
18
+ # @param [Symbol] attribute
19
+ # the attribute to apply this validation on
20
+ # @param [Hash] options
21
+ # the list of options passed in
22
+ #
23
+ def initialize(attribute, options = {})
24
+ @attribute = attribute
25
+ @options = options.is_a?(Hash) ? options : {}
26
+ end
27
+
28
+ #
29
+ # Just in case someone forgets to define a key, this will return the
30
+ # class's underscored name without "validator" as a symbol.
31
+ #
32
+ # @example
33
+ # FooValidator.new.key #=> :foo
34
+ #
35
+ # @return [Symbol]
36
+ #
37
+ def key
38
+ name = self.class.name.split('::').last
39
+ Util.underscore(name).to_sym
40
+ end
41
+
42
+ #
43
+ # Execute the validations. This is an abstract class and must be
44
+ # overridden in custom validators.
45
+ #
46
+ # @param [Resource::Base::Base] resource
47
+ # the parent resource to validate against
48
+ #
49
+ def validate(resource)
50
+ raise Error::AbstractMethod.new(method: 'Validators::Base#validate')
51
+ end
52
+
53
+ #
54
+ # The string representation of this validation.
55
+ #
56
+ # @return [String]
57
+ #
58
+ def to_s
59
+ "#<#{classname}>"
60
+ end
61
+
62
+ #
63
+ # The string representation of this validation.
64
+ #
65
+ # @return [String]
66
+ #
67
+ def inspect
68
+ "#<#{classname} :#{attribute}>"
69
+ end
70
+
71
+ private
72
+
73
+ #
74
+ # The class name for this validator.
75
+ #
76
+ # @return [String]
77
+ #
78
+ def classname
79
+ @classname ||= self.class.name.split('::')[1..-1].join('::')
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,11 @@
1
+ module ChefAPI
2
+ class Validator::Required < Validator::Base
3
+ def validate(resource)
4
+ value = resource._attributes[attribute]
5
+
6
+ if value.to_s.strip.empty?
7
+ resource.errors.add(attribute, 'must be present')
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,23 @@
1
+ module ChefAPI
2
+ class Validator::Type < Validator::Base
3
+ attr_reader :types
4
+
5
+ #
6
+ # Overload the super method to capture the type attribute in the options
7
+ # hash.
8
+ #
9
+ def initialize(attribute, type)
10
+ super
11
+ @types = Array(type)
12
+ end
13
+
14
+ def validate(resource)
15
+ value = resource._attributes[attribute]
16
+
17
+ if value && !types.any? { |type| value.is_a?(type) }
18
+ short_name = type.to_s.split('::').last
19
+ resource.errors.add(attribute, "must be a kind of #{short_name}")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,3 @@
1
+ module ChefAPI
2
+ VERSION = '0.2.0'
3
+ end
data/lib/chef-api.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'json'
2
+ require 'pathname'
3
+ require 'chef-api/version'
4
+
5
+ module ChefAPI
6
+ autoload :Boolean, 'chef-api/boolean'
7
+ autoload :Configurable, 'chef-api/configurable'
8
+ autoload :Connection, 'chef-api/connection'
9
+ autoload :Defaults, 'chef-api/defaults'
10
+ autoload :Error, 'chef-api/errors'
11
+ autoload :ErrorCollection, 'chef-api/error_collection'
12
+ autoload :Logger, 'chef-api/logger'
13
+ autoload :Resource, 'chef-api/resource'
14
+ autoload :Schema, 'chef-api/schema'
15
+ autoload :Util, 'chef-api/util'
16
+ autoload :Validator, 'chef-api/validator'
17
+
18
+ #
19
+ # @todo Document this and why it's important
20
+ #
21
+ UNSET = Object.new
22
+
23
+ class << self
24
+ include ChefAPI::Configurable
25
+
26
+ #
27
+ # The source root of the ChefAPI gem. This is useful when requiring files
28
+ # that are relative to the root of the project.
29
+ #
30
+ # @return [Pathname]
31
+ #
32
+ def root
33
+ @root ||= Pathname.new(File.expand_path('../../', __FILE__))
34
+ end
35
+
36
+ #
37
+ # API connection object based off the configured options in {Configurable}.
38
+ #
39
+ # @return [ChefAPI::Connection]
40
+ #
41
+ def connection
42
+ unless @connection && @connection.same_options?(options)
43
+ @connection = ChefAPI::Connection.new(options)
44
+ end
45
+
46
+ @connection
47
+ end
48
+
49
+ #
50
+ # Delegate all methods to the connection object, essentially making the
51
+ # module object behave like a {Connection}.
52
+ #
53
+ def method_missing(m, *args, &block)
54
+ if connection.respond_to?(m)
55
+ connection.send(m, *args, &block)
56
+ else
57
+ super
58
+ end
59
+ end
60
+
61
+ #
62
+ # Delegating +respond_to+ to the {Connection}.
63
+ #
64
+ def respond_to_missing?(m, include_private = false)
65
+ connection.respond_to?(m) || super
66
+ end
67
+ end
68
+ end
69
+
70
+ require 'i18n'
71
+ I18n.enforce_available_locales = false
72
+ I18n.load_path << Dir[ChefAPI.root.join('locales', '*.yml').to_s]
73
+
74
+ # Load the initial default values
75
+ ChefAPI.setup
76
+
data/locales/en.yml ADDED
@@ -0,0 +1,89 @@
1
+ en:
2
+ chef_api:
3
+ errors:
4
+ abstract_method: >
5
+ `%{method}` is an abstract method. You must override this method in
6
+ your subclass with the proper implementation and logic. For more
7
+ information, please see the inline documentation for %{method}. If you
8
+ are not a developer, this is most likely a bug in the ChefAPI gem.
9
+ Please file a bug report at
10
+ https://github.com/sethvargo/chef-api/issues/new and include the
11
+ command(s) you ran to arrive at this error.
12
+ cannot_regenerate_key: >
13
+ You called `regenerate_key` on a client that does not exist on the
14
+ remote Chef Server. You can only regenerate a private key for a client
15
+ that is persisted. Try saving this record this client before
16
+ regenerating the private key.
17
+ file_not_found: >
18
+ I could not find a file at `%{path}`. Please make sure you have typed
19
+ the path correctly and that the resource at `%{path}` does actually
20
+ exist.
21
+ http_bad_request: >
22
+ The Chef Server did not understand the request because it was malformed.
23
+ The Chef Server returned this message:
24
+
25
+ %{message}
26
+ http_forbidden_request: >
27
+ The Chef Server actively refused to fulfill the request. The Chef Server
28
+ returned this message:
29
+
30
+ %{message}
31
+ http_method_not_allowed: >
32
+ That HTTP method is not allowed on this URL. The Chef Server returned
33
+ this message:
34
+
35
+ %{message}
36
+ http_not_acceptable: >
37
+ The Chef Server identified this request as unacceptable. This usually
38
+ means you have not specified the correct Accept or Content-Type headers
39
+ on the request object. The Chef Server returned this message:
40
+
41
+ %{message}
42
+ http_not_found: >
43
+ The requested URL does not exist on the Chef Server. The Chef Server
44
+ returned this message:
45
+
46
+ %{message}
47
+ http_server_unavailable: >
48
+ The Chef Server is currently unavailable or is not currently accepting
49
+ client connections. Please ensure the server is accessible via ping
50
+ or telnet on your local network. If this error persists, please contact
51
+ your network administrator.
52
+ http_unauthorized_request: >
53
+ The Chef Server requires authorization. Please ensure you have specified
54
+ the correct client name and private key. If this error continues, please
55
+ verify the given client has the proper permissions on the Chef Server.
56
+ The Chef Server returned this message:
57
+
58
+ %{message}
59
+ insufficient_file_permissions: >
60
+ I cannot read the file at `%{path}` because the permissions on the file
61
+ do not permit it. Please ensure the file has the correct permissions and
62
+ that this Ruby process is running as a user with access to `%{path}`.
63
+ invalid_resource: >
64
+ There were errors saving your resource: %{errors}
65
+ invalid_validator: >
66
+ `%{key}` is not a valid validator. Please make sure it is spelled
67
+ correctly and that the constant is properly defined. If you are using
68
+ a custom validator, please ensure the validator extends
69
+ ChefAPI::Validator::Base and is a subclass of ChefAPI::Validator.
70
+ missing_url_parameter: >
71
+ The required URL parameter `%{param}' was not present. Please specify
72
+ `%{param}' as an option, like Resource.new(id, %{param}: 'value').
73
+ not_a_directory: >
74
+ The given path `%{path}' is not a directory. Please make sure you have
75
+ passed the path to a directory on disk.
76
+ resource_already_exists: >
77
+ The %{type} `%{id}` already exists on the Chef Server. Each
78
+ %{type} must have a unique identifier and the Chef Server indicated
79
+ this %{type} already exists. If you are trying to update the %{type},
80
+ consider using the `update` method instead.
81
+ resource_not_found: >
82
+ There is no %{type} with an id of `%{id}` on the Chef Server. If you
83
+ are updating the %{type}, please make sure the %{type} exists and has
84
+ the correct Chef identifier (primary key).
85
+ resource_not_mutable: >
86
+ The %{type} `%{id}` is not mutable. It may be locked by the remote
87
+ Chef Server, or the Chef Server may not permit modifying the resource.
88
+ unknown_attribute: >
89
+ `%{attribute}` is not a valid attribute
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ module ChefAPI
4
+ describe Resource::Client do
5
+ it_behaves_like 'a Chef API resource', :client,
6
+ update: { validator: true }
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ module ChefAPI
4
+ describe Resource::Environment do
5
+ it_behaves_like 'a Chef API resource', :environment,
6
+ update: { description: 'This is the new description' }
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ module ChefAPI
4
+ describe Resource::Node do
5
+ it_behaves_like 'a Chef API resource', :node,
6
+ update: { chef_environment: 'my_environment' }
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ module ChefAPI
4
+ describe Resource::Role do
5
+ it_behaves_like 'a Chef API resource', :role,
6
+ update: { description: 'This is the new description' }
7
+ end
8
+ end
@@ -0,0 +1,26 @@
1
+ require 'chef-api'
2
+
3
+ RSpec.configure do |config|
4
+ # Chef Server
5
+ require 'support/chef_server'
6
+ config.include(RSpec::ChefServer::DSL)
7
+
8
+ # Shared Examples
9
+ Dir[ChefAPI.root.join('spec/support/shared/**/*.rb')].each { |file| require file }
10
+
11
+ # Basic configuraiton
12
+ config.treat_symbols_as_metadata_keys_with_true_values = true
13
+ config.run_all_when_everything_filtered = true
14
+ config.filter_run(:focus)
15
+
16
+ #
17
+ config.before(:each) do
18
+ ChefAPI::Logger.level = :fatal
19
+ end
20
+
21
+ # Run specs in random order to surface order dependencies. If you find an
22
+ # order dependency and want to debug it, you can fix the order by providing
23
+ # the seed, which is printed after each run.
24
+ # --seed 1234
25
+ config.order = 'random'
26
+ end