diff --git a/lib/puppet/resource_api.rb b/lib/puppet/resource_api.rb index 07b97edf..1d69ac0a 100644 --- a/lib/puppet/resource_api.rb +++ b/lib/puppet/resource_api.rb @@ -5,6 +5,7 @@ require 'puppet/resource_api/property' require 'puppet/resource_api/puppet_context' unless RUBY_PLATFORM == 'java' require 'puppet/resource_api/read_only_parameter' +require 'puppet/resource_api/transport' require 'puppet/resource_api/type_definition' require 'puppet/resource_api/value_creator' require 'puppet/resource_api/version' @@ -451,6 +452,12 @@ def load_device_provider(class_name, type_name_sym, device_class_name, device_na end module_function :load_device_provider # rubocop:disable Style/AccessModifierDeclarations + # keeps the existing register API format. e.g. Puppet::ResourceApi.register_type + def register_transport(schema) + Puppet::ResourceApi::Transport.register(schema) + end + module_function :register_transport # rubocop:disable Style/AccessModifierDeclarations + def self.class_name_from_type_name(type_name) type_name.to_s.split('_').map(&:capitalize).join end diff --git a/lib/puppet/resource_api/transport.rb b/lib/puppet/resource_api/transport.rb new file mode 100644 index 00000000..8e4c0935 --- /dev/null +++ b/lib/puppet/resource_api/transport.rb @@ -0,0 +1,15 @@ +# Remote target transport API +module Puppet::ResourceApi::Transport + def register(schema) + raise Puppet::DevError, 'requires a hash as schema, not `%{other_type}`' % { other_type: schema.class } unless schema.is_a? Hash + raise Puppet::DevError, 'requires a `:name`' unless schema.key? :name + raise Puppet::DevError, 'requires `:desc`' unless schema.key? :desc + raise Puppet::DevError, 'requires `:connection_info`' unless schema.key? :connection_info + raise Puppet::DevError, '`:connection_info` must be a hash, not `%{other_type}`' % { other_type: schema[:connection_info].class } unless schema[:connection_info].is_a?(Hash) + + @transports ||= {} + raise Puppet::DevError, 'Transport `%{name}` is already registered.' % { name: schema[:name] } unless @transports[schema[:name]].nil? + @transports[schema[:name]] = Puppet::ResourceApi::TransportSchemaDef.new(schema) + end + module_function :register # rubocop:disable Style/AccessModifierDeclarations +end diff --git a/lib/puppet/resource_api/type_definition.rb b/lib/puppet/resource_api/type_definition.rb index e23a077a..a5bf2ccb 100644 --- a/lib/puppet/resource_api/type_definition.rb +++ b/lib/puppet/resource_api/type_definition.rb @@ -1,144 +1,168 @@ # Provides accessor methods for the type being provided -class Puppet::ResourceApi::TypeDefinition - attr_reader :definition +module Puppet::ResourceApi + # pre-declare class + class BaseTypeDefinition; end + + # RSAPI Resource Type + class TypeDefinition < BaseTypeDefinition + def initialize(definition) + super(definition, :attributes) + end - def initialize(definition) - @data_type_cache = {} - validate_schema(definition) - end + def ensurable? + attributes.key?(:ensure) + end - def name - @definition[:name] - end + # rubocop complains when this is named has_feature? + def feature?(feature) + supported = (definition[:features] && definition[:features].include?(feature)) + if supported + Puppet.debug("#{definition[:name]} supports `#{feature}`") + else + Puppet.debug("#{definition[:name]} does not support `#{feature}`") + end + supported + end - def attributes - @definition[:attributes] - end + def validate_schema(definition, attr_key) + super(definition, attr_key) + [:title, :provider, :alias, :audit, :before, :consume, :export, :loglevel, :noop, :notify, :require, :schedule, :stage, :subscribe, :tag].each do |name| + raise Puppet::DevError, 'must not define an attribute called `%{name}`' % { name: name.inspect } if definition[attr_key].key? name + end + if definition.key?(:title_patterns) && !definition[:title_patterns].is_a?(Array) + raise Puppet::DevError, '`:title_patterns` must be an array, not `%{other_type}`' % { other_type: definition[:title_patterns].class } + end - def ensurable? - @definition[:attributes].key?(:ensure) - end + Puppet::ResourceApi::DataTypeHandling.validate_ensure(definition) - def namevars - @namevars ||= @definition[:attributes].select { |_name, options| - options.key?(:behaviour) && options[:behaviour] == :namevar - }.keys - end + definition[:features] ||= [] + supported_features = %w[supports_noop canonicalize remote_resource simple_get_filter].freeze + unknown_features = definition[:features] - supported_features + Puppet.warning("Unknown feature detected: #{unknown_features.inspect}") unless unknown_features.empty? - # rubocop complains when this is named has_feature? - def feature?(feature) - supported = (definition[:features] && definition[:features].include?(feature)) - if supported - Puppet.debug("#{definition[:name]} supports `#{feature}`") - else - Puppet.debug("#{definition[:name]} does not support `#{feature}`") + # store the validated definition + @definition = definition end - supported end - def validate_schema(definition) - raise Puppet::DevError, 'Type definition must be a Hash, not `%{other_type}`' % { other_type: definition.class } unless definition.is_a?(Hash) - raise Puppet::DevError, 'Type definition must have a name' unless definition.key? :name - raise Puppet::DevError, 'Type definition must have `:attributes`' unless definition.key? :attributes - unless definition[:attributes].is_a?(Hash) - raise Puppet::DevError, '`%{name}.attributes` must be a hash, not `%{other_type}`' % { - name: definition[:name], other_type: definition[:attributes].class - } - end - [:title, :provider, :alias, :audit, :before, :consume, :export, :loglevel, :noop, :notify, :require, :schedule, :stage, :subscribe, :tag].each do |name| - raise Puppet::DevError, 'must not define an attribute called `%{name}`' % { name: name.inspect } if definition[:attributes].key? name - end - if definition.key?(:title_patterns) && !definition[:title_patterns].is_a?(Array) - raise Puppet::DevError, '`:title_patterns` must be an array, not `%{other_type}`' % { other_type: definition[:title_patterns].class } + # RSAPI Transport schema + class TransportSchemaDef < BaseTypeDefinition + def initialize(definition) + super(definition, :connection_info) end + end - Puppet::ResourceApi::DataTypeHandling.validate_ensure(definition) + # Base RSAPI schema Object + class BaseTypeDefinition + attr_reader :definition, :attributes + + def initialize(definition, attr_key) + @data_type_cache = {} + validate_schema(definition, attr_key) + end - definition[:features] ||= [] - supported_features = %w[supports_noop canonicalize remote_resource simple_get_filter].freeze - unknown_features = definition[:features] - supported_features - Puppet.warning("Unknown feature detected: #{unknown_features.inspect}") unless unknown_features.empty? + def name + @definition[:name] + end - definition[:attributes].each do |key, attr| - raise Puppet::DevError, "`#{definition[:name]}.#{key}` must be a Hash, not a #{attr.class}" unless attr.is_a? Hash - raise Puppet::DevError, "`#{definition[:name]}.#{key}` has no type" unless attr.key? :type - Puppet.warning("`#{definition[:name]}.#{key}` has no docs") unless attr.key? :desc + def namevars + @namevars ||= attributes.select { |_name, options| + options.key?(:behaviour) && options[:behaviour] == :namevar + }.keys + end - # validate the type by attempting to parse into a puppet type - @data_type_cache[definition[:attributes][key][:type]] ||= - Puppet::ResourceApi::DataTypeHandling.parse_puppet_type( - key, - definition[:attributes][key][:type], - ) + def validate_schema(definition, attr_key) + raise Puppet::DevError, '%{type_class} must be a Hash, not `%{other_type}`' % { type_class: self.class.name, other_type: definition.class } unless definition.is_a?(Hash) + @attributes = definition[attr_key] + raise Puppet::DevError, '%{type_class} must have a name' % { type_class: self.class.name } unless definition.key? :name + raise Puppet::DevError, '%{type_class} must have `%{attr_key}`' % { type_class: self.class.name, attrs: attr_key } unless definition.key? attr_key + unless attributes.is_a?(Hash) + raise Puppet::DevError, '`%{name}.%{attrs}` must be a hash, not `%{other_type}`' % { + name: definition[:name], attrs: attr_key, other_type: attributes.class + } + end - # fixup any weird behavior ;-) - next unless attr[:behavior] - if attr[:behaviour] - raise Puppet::DevError, "the '#{key}' attribute has both a `behavior` and a `behaviour`, only use one" + attributes.each do |key, attr| + raise Puppet::DevError, "`#{definition[:name]}.#{key}` must be a Hash, not a #{attr.class}" unless attr.is_a? Hash + raise Puppet::DevError, "`#{definition[:name]}.#{key}` has no type" unless attr.key? :type + Puppet.warning("`#{definition[:name]}.#{key}` has no docs") unless attr.key? :desc + + # validate the type by attempting to parse into a puppet type + @data_type_cache[attributes[key][:type]] ||= + Puppet::ResourceApi::DataTypeHandling.parse_puppet_type( + key, + attributes[key][:type], + ) + + # fixup any weird behavior ;-) + next unless attr[:behavior] + if attr[:behaviour] + raise Puppet::DevError, "the '#{key}' attribute has both a `behavior` and a `behaviour`, only use one" + end + attr[:behaviour] = attr[:behavior] + attr.delete(:behavior) end - attr[:behaviour] = attr[:behavior] - attr.delete(:behavior) + # store the validated definition + @definition = definition end - # store the validated definition - @definition = definition - end - # validates a resource hash against its type schema - def check_schema(resource) - namevars.each do |namevar| - if resource[namevar].nil? - raise Puppet::ResourceError, "`#{name}.get` did not return a value for the `#{namevar}` namevar attribute" + # validates a resource hash against its type schema + def check_schema(resource) + namevars.each do |namevar| + if resource[namevar].nil? + raise Puppet::ResourceError, "`#{name}.get` did not return a value for the `#{namevar}` namevar attribute" + end end - end - message = "Provider returned data that does not match the Type Schema for `#{name}[#{resource[namevars.first]}]`" + message = "Provider returned data that does not match the Type Schema for `#{name}[#{resource[namevars.first]}]`" - rejected_keys = check_schema_keys(resource) # removes bad keys - bad_values = check_schema_values(resource) + rejected_keys = check_schema_keys(resource) # removes bad keys + bad_values = check_schema_values(resource) - unless rejected_keys.empty? - message += "\n Unknown attribute:\n" - rejected_keys.each { |key, _value| message += " * #{key}\n" } - end - unless bad_values.empty? - message += "\n Value type mismatch:\n" - bad_values.each { |key, value| message += " * #{key}: #{value}\n" } - end + unless rejected_keys.empty? + message += "\n Unknown attribute:\n" + rejected_keys.each { |key, _value| message += " * #{key}\n" } + end + unless bad_values.empty? + message += "\n Value type mismatch:\n" + bad_values.each { |key, value| message += " * #{key}: #{value}\n" } + end - return if rejected_keys.empty? && bad_values.empty? + return if rejected_keys.empty? && bad_values.empty? - if Puppet.settings[:strict] == :off - Puppet.debug(message) - elsif Puppet.settings[:strict] == :warning - Puppet::ResourceApi.warning_count += 1 - Puppet.warning(message) if Puppet::ResourceApi.warning_count <= 100 # maximum number of schema warnings to display in a run - elsif Puppet.settings[:strict] == :error - raise Puppet::DevError, message + if Puppet.settings[:strict] == :off + Puppet.debug(message) + elsif Puppet.settings[:strict] == :warning + Puppet::ResourceApi.warning_count += 1 + Puppet.warning(message) if Puppet::ResourceApi.warning_count <= 100 # maximum number of schema warnings to display in a run + elsif Puppet.settings[:strict] == :error + raise Puppet::DevError, message + end end - end - # Returns an array of keys that where not found in the type schema - # Modifies the resource passed in, leaving only valid attributes - def check_schema_keys(resource) - rejected = [] - resource.reject! { |key| rejected << key if key != :title && attributes.key?(key) == false } - rejected - end + # Returns an array of keys that where not found in the type schema + # Modifies the resource passed in, leaving only valid attributes + def check_schema_keys(resource) + rejected = [] + resource.reject! { |key| rejected << key if key != :title && attributes.key?(key) == false } + rejected + end - # Returns a hash of keys and values that are not valid - # does not modify the resource passed in - def check_schema_values(resource) - bad_vals = {} - resource.each do |key, value| - next unless attributes[key] - type = @data_type_cache[attributes[key][:type]] - error_message = Puppet::ResourceApi::DataTypeHandling.try_validate( - type, - value, - '', - ) - bad_vals[key] = value unless error_message.nil? + # Returns a hash of keys and values that are not valid + # does not modify the resource passed in + def check_schema_values(resource) + bad_vals = {} + resource.each do |key, value| + next unless attributes[key] + type = @data_type_cache[attributes[key][:type]] + error_message = Puppet::ResourceApi::DataTypeHandling.try_validate( + type, + value, + '', + ) + bad_vals[key] = value unless error_message.nil? + end + bad_vals end - bad_vals end end diff --git a/spec/puppet/resource_api/base_type_definition_spec.rb b/spec/puppet/resource_api/base_type_definition_spec.rb new file mode 100644 index 00000000..2c432dc0 --- /dev/null +++ b/spec/puppet/resource_api/base_type_definition_spec.rb @@ -0,0 +1,220 @@ +require 'spec_helper' + +RSpec.describe Puppet::ResourceApi::BaseTypeDefinition do + subject(:type) { described_class.new(definition, :attributes) } + + let(:definition) do + { name: 'some_resource', attributes: { + ensure: { + type: 'Enum[present, absent]', + desc: 'Whether this resource should be present or absent on the target system.', + default: 'present', + }, + name: { + type: 'String', + desc: 'The name of the resource you want to manage.', + behaviour: :namevar, + }, + prop: { + type: 'Integer', + desc: 'A mandatory property, that MUST NOT be validated on deleting.', + }, + }, features: feature_support } + end + let(:feature_support) { [] } + + it { expect { described_class.new(nil, :attributes) }.to raise_error Puppet::DevError, %r{BaseTypeDefinition must be a Hash} } + + describe '.name' do + it { expect(type.name).to eq 'some_resource' } + end + + describe '#check_schema_keys' do + context 'when resource contains only valid keys' do + it 'returns an empty array' do + expect(type.check_schema_keys(definition[:attributes])).to eq([]) + end + end + + context 'when resource contains invalid keys' do + let(:resource) { { name: 'test_string', wibble: '1', foo: '2' } } + + it 'returns an array containing the bad keys' do + expect(type.check_schema_keys(resource)).to eq([:wibble, :foo]) + end + end + end + + describe '#check_schema_values' do + context 'when resource contains only valid values' do + let(:resource) { { name: 'some_resource', prop: 1, ensure: 'present' } } + + it 'returns an empty array' do + expect(type.check_schema_values(resource)).to eq({}) + end + end + + context 'when resource contains invalid values' do + let(:resource) { { name: 'test_string', prop: 'foo', ensure: 1 } } + + it 'returns a hash of the keys that have invalid values' do + expect(type.check_schema_values(resource)).to eq(prop: 'foo', ensure: 1) + end + end + end + + describe '#check_schema' do + context 'when resource does not contain its namevar' do + let(:resource) { { nom: 'some_resource', prop: 1, ensure: 'present' } } + + it { expect { type.check_schema(resource) }.to raise_error Puppet::ResourceError, %r{`some_resource.get` did not return a value for the `name` namevar attribute} } + end + + context 'when a resource contains unknown attributes' do + let(:resource) { { name: 'wibble', prop: 1, ensure: 'present', foo: 'bar' } } + let(:message) { %r{Provider returned data that does not match the Type Schema for `some_resource\[wibble\]`\n\s*Unknown attribute:\n\s*\* foo} } + let(:strict_level) { :warning } + + before(:each) do + Puppet::ResourceApi.warning_count = 0 + Puppet.settings[:strict] = strict_level + end + + context 'when puppet strict is set to default (warning)' do + it 'displays up to 100 warnings' do + expect(Puppet).to receive(:warning).with(message).exactly(100).times + 110.times do + type.check_schema(resource.dup) + end + end + end + + context 'when puppet strict is set to error' do + let(:strict_level) { :error } + + it 'raises a DevError' do + expect { type.check_schema(resource) }.to raise_error Puppet::DevError, message + end + end + + context 'when puppet strict is set to off' do + let(:strict_level) { :off } + + it 'logs to Debug console' do + expect(Puppet).to receive(:debug).with(message) + type.check_schema(resource) + end + end + end + + context 'when a resource contains invalid value' do + let(:resource) { { name: 'wibble', prop: 'foo', ensure: 'present' } } + let(:message) { %r{Provider returned data that does not match the Type Schema for `some_resource\[wibble\]`\n\s*Value type mismatch:\n\s*\* prop: foo} } + let(:strict_level) { :warning } + + before(:each) do + Puppet::ResourceApi.warning_count = 0 + Puppet.settings[:strict] = strict_level + end + + context 'when puppet strict is set to default (warning)' do + it 'displays up to 100 warnings' do + expect(Puppet).to receive(:warning).with(message).exactly(100).times + 110.times do + type.check_schema(resource.dup) + end + end + end + + context 'when puppet strict is set to error' do + let(:strict_level) { :error } + + it 'raises a DevError' do + expect { type.check_schema(resource) }.to raise_error Puppet::DevError, message + end + end + + context 'when puppet strict is set to off' do + let(:strict_level) { :off } + + it 'logs to Debug console' do + expect(Puppet).to receive(:debug).with(message) + type.check_schema(resource) + end + end + end + end + + describe '#validate_schema' do + context 'when the type definition does not have a name' do + let(:definition) { { attributes: 'some_string' } } + + it { expect { type }.to raise_error Puppet::DevError, %r{must have a name} } + end + + context 'when attributes is not a hash' do + let(:definition) { { name: 'some_resource', attributes: 'some_string' } } + + it { expect { type }.to raise_error Puppet::DevError, %r{`some_resource.attributes` must be a hash} } + end + + context 'when an attribute is not a hash' do + let(:definition) { { name: 'some_resource', attributes: { name: 'some_string' } } } + + it { expect { type }.to raise_error Puppet::DevError, %r{`some_resource.name` must be a Hash} } + end + + context 'when an attribute has no type' do + let(:definition) { { name: 'some_resource', attributes: { name: { desc: 'message' } } } } + + it { expect { type }.to raise_error Puppet::DevError, %r{has no type} } + end + + context 'when an attribute has no descrption' do + let(:definition) { { name: 'some_resource', attributes: { name: { type: 'String' } } } } + + it 'Raises a warning message' do + expect(Puppet).to receive(:warning).with('`some_resource.name` has no docs') + type + end + end + + context 'when an attribute has an unsupported type' do + let(:definition) { { name: 'some_resource', attributes: { name: { type: 'basic' } } } } + + it { expect { type }.to raise_error %r{ is not a valid type specification} } + end + + context 'with both behavior and behaviour' do + let(:definition) do + { + name: 'bad_behaviour', + attributes: { + name: { + type: 'String', + behaviour: :namevar, + behavior: :namevar, + }, + }, + } + end + + it { expect { type }.to raise_error Puppet::DevError, %r{name.*attribute has both} } + end + + context 'when registering a type with badly formed attribute type' do + let(:definition) do + { + name: 'bad_syntax', + attributes: { + name: { + type: 'Optional[String', + }, + }, + } + end + + it { expect { type }.to raise_error Puppet::DevError, %r{The type of the `name` attribute `Optional\[String` could not be parsed:} } + end + end +end diff --git a/spec/puppet/resource_api/transport_schema_def_spec.rb b/spec/puppet/resource_api/transport_schema_def_spec.rb new file mode 100644 index 00000000..e88bb0e8 --- /dev/null +++ b/spec/puppet/resource_api/transport_schema_def_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +RSpec.describe Puppet::ResourceApi::TransportSchemaDef do + subject(:type) { described_class.new(definition) } + + let(:definition) do + { name: 'some_target', + connection_info: { + host: { + type: 'String', + desc: 'The IP address or hostname', + }, + user: { + type: 'String', + desc: 'The user to connect as', + }, + } } + end + + it { expect { described_class.new(nil) }.to raise_error Puppet::DevError, %r{TransportSchemaDef must be a Hash} } + + describe '#attributes' do + context 'when type has attributes' do + it { expect(type.attributes).to be_key(:host) } + it { expect(type.attributes).to be_key(:user) } + end + end +end diff --git a/spec/puppet/resource_api/transport_spec.rb b/spec/puppet/resource_api/transport_spec.rb new file mode 100644 index 00000000..7bbeca6a --- /dev/null +++ b/spec/puppet/resource_api/transport_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +RSpec.describe Puppet::ResourceApi::Transport do + let(:strict_level) { :error } + + before(:each) do + # set default to strictest setting + # by default Puppet runs at warning level + Puppet.settings[:strict] = strict_level + # Enable debug logging + Puppet.debug = true + end + + context 'when registering a schema with missing keys' do + it { expect { described_class.register([]) }.to raise_error(Puppet::DevError, %r{requires a hash as schema}) } + it { expect { described_class.register({}) }.to raise_error(Puppet::DevError, %r{requires a `:name`}) } + it { expect { described_class.register(name: 'no connection info', desc: 'some description') }.to raise_error(Puppet::DevError, %r{requires `:connection_info`}) } + it { expect { described_class.register(name: 'no description') }.to raise_error(Puppet::DevError, %r{requires `:desc`}) } + it { expect { described_class.register(name: 'no hash attributes', desc: 'some description', connection_info: []) }.to raise_error(Puppet::DevError, %r{`:connection_info` must be a hash, not}) } + end + + context 'when registering a minimal transport' do + let(:schema) { { name: 'minimal', desc: 'a minimal connection', connection_info: {} } } + + it { expect { described_class.register(schema) }.not_to raise_error } + + context 'when re-registering a transport' do + it { expect { described_class.register(schema) }.to raise_error(Puppet::DevError, %r{`minimal` is already registered}) } + end + end + + context 'when registering a transport' do + let(:schema) do + { + name: 'a_remote_thing', + desc: 'basic transport', + connection_info: { + host: { + type: 'String', + desc: 'the host ip address or hostname', + }, + user: { + type: 'String', + desc: 'the user to connect as', + }, + password: { + type: 'Sensitive[String]', + desc: 'the password to make the connection', + }, + }, + } + end + + it 'adds to the transports register' do + expect { described_class.register(schema) }.not_to raise_error + end + end + + context 'when registering a transport with a bad type' do + let(:schema) do + { + name: 'a_bad_thing', + desc: 'basic transport', + connection_info: { + host: { + type: 'garbage', + desc: 'the host ip address or hostname', + }, + }, + } + end + + it { + expect { described_class.register(schema) }.to raise_error( + Puppet::DevError, %r{ is not a valid type specification} + ) + } + end +end diff --git a/spec/puppet/resource_api/type_definition_spec.rb b/spec/puppet/resource_api/type_definition_spec.rb index a2df5d99..121745a6 100644 --- a/spec/puppet/resource_api/type_definition_spec.rb +++ b/spec/puppet/resource_api/type_definition_spec.rb @@ -23,22 +23,18 @@ end let(:feature_support) { [] } - it { expect { described_class.new(nil) }.to raise_error Puppet::DevError, %r{Type definition must be a Hash} } - - describe '.name' do - it { expect(type.name).to eq 'some_resource' } - end + it { expect { described_class.new(nil) }.to raise_error Puppet::DevError, %r{TypeDefinition must be a Hash} } describe '#ensurable?' do context 'when type is ensurable' do - let(:definition) { { name: 'some_resource', attributes: { ensure: { type: 'Enum[absent, present]' } } } } + let(:definition) { { name: 'ensurable', attributes: { ensure: { type: 'Enum[absent, present]' } } } } it { expect(type).to be_ensurable } it { expect(type.attributes).to be_key(:ensure) } end context 'when type is not ensurable' do - let(:definition) { { name: 'some_resource', attributes: { name: { type: 'String' } } } } + let(:definition) { { name: 'ensurable', attributes: { name: { type: 'String' } } } } it { expect(type).not_to be_ensurable } it { expect(type.attributes).to be_key(:name) } @@ -61,204 +57,17 @@ describe '#attributes' do context 'when type has attributes' do - let(:definition) { { name: 'some_resource', attributes: { wibble: { type: 'String' } } } } - - it { expect(type.attributes).to be_key(:wibble) } - end - end - - describe '#check_schema_keys' do - context 'when resource contains only valid keys' do - it 'returns an empty array' do - expect(type.check_schema_keys(definition[:attributes])).to eq([]) - end - end - - context 'when resource contains invalid keys' do - let(:resource) { { name: 'test_string', wibble: '1', foo: '2' } } - - it 'returns an array containing the bad keys' do - expect(type.check_schema_keys(resource)).to eq([:wibble, :foo]) - end - end - end - - describe '#check_schema_values' do - context 'when resource contains only valid values' do - let(:resource) { { name: 'some_resource', prop: 1, ensure: 'present' } } - - it 'returns an empty array' do - expect(type.check_schema_values(resource)).to eq({}) - end - end - - context 'when resource contains invalid values' do - let(:resource) { { name: 'test_string', prop: 'foo', ensure: 1 } } - - it 'returns a hash of the keys that have invalid values' do - expect(type.check_schema_values(resource)).to eq(prop: 'foo', ensure: 1) - end - end - end - - describe '#check_schema' do - context 'when resource does not contain its namevar' do - let(:resource) { { nom: 'some_resource', prop: 1, ensure: 'present' } } - - it { expect { type.check_schema(resource) }.to raise_error Puppet::ResourceError, %r{`some_resource.get` did not return a value for the `name` namevar attribute} } - end - - context 'when a resource contains unknown attributes' do - let(:resource) { { name: 'wibble', prop: 1, ensure: 'present', foo: 'bar' } } - let(:message) { %r{Provider returned data that does not match the Type Schema for `some_resource\[wibble\]`\n\s*Unknown attribute:\n\s*\* foo} } - let(:strict_level) { :warning } - - before(:each) do - Puppet::ResourceApi.warning_count = 0 - Puppet.settings[:strict] = strict_level - end - - context 'when puppet strict is set to default (warning)' do - it 'displays up to 100 warnings' do - expect(Puppet).to receive(:warning).with(message).exactly(100).times - 110.times do - type.check_schema(resource.dup) - end - end - end - - context 'when puppet strict is set to error' do - let(:strict_level) { :error } - - it 'raises a DevError' do - expect { type.check_schema(resource) }.to raise_error Puppet::DevError, message - end - end - - context 'when puppet strict is set to off' do - let(:strict_level) { :off } - - it 'logs to Debug console' do - expect(Puppet).to receive(:debug).with(message) - type.check_schema(resource) - end - end - end - - context 'when a resource contains invalid value' do - let(:resource) { { name: 'wibble', prop: 'foo', ensure: 'present' } } - let(:message) { %r{Provider returned data that does not match the Type Schema for `some_resource\[wibble\]`\n\s*Value type mismatch:\n\s*\* prop: foo} } - let(:strict_level) { :warning } - - before(:each) do - Puppet::ResourceApi.warning_count = 0 - Puppet.settings[:strict] = strict_level - end - - context 'when puppet strict is set to default (warning)' do - it 'displays up to 100 warnings' do - expect(Puppet).to receive(:warning).with(message).exactly(100).times - 110.times do - type.check_schema(resource.dup) - end - end - end - - context 'when puppet strict is set to error' do - let(:strict_level) { :error } - - it 'raises a DevError' do - expect { type.check_schema(resource) }.to raise_error Puppet::DevError, message - end - end - - context 'when puppet strict is set to off' do - let(:strict_level) { :off } - - it 'logs to Debug console' do - expect(Puppet).to receive(:debug).with(message) - type.check_schema(resource) - end - end + it { expect(type.attributes).to be_key(:ensure) } + it { expect(type.attributes).to be_key(:name) } + it { expect(type.attributes).to be_key(:prop) } end end describe '#validate_schema' do - context 'when the type definition does not have a name' do - let(:definition) { { attributes: 'some_string' } } - - it { expect { type }.to raise_error Puppet::DevError, %r{Type definition must have a name} } - end - - context 'when attributes is not a hash' do - let(:definition) { { name: 'some_resource', attributes: 'some_string' } } - - it { expect { type }.to raise_error Puppet::DevError, %r{`some_resource.attributes` must be a hash} } - end - context 'when the schema contains title_patterns and it is not an array' do let(:definition) { { name: 'some_resource', title_patterns: {}, attributes: {} } } it { expect { type }.to raise_error Puppet::DevError, %r{`:title_patterns` must be an array} } end - - context 'when an attribute is not a hash' do - let(:definition) { { name: 'some_resource', attributes: { name: 'some_string' } } } - - it { expect { type }.to raise_error Puppet::DevError, %r{`some_resource.name` must be a Hash} } - end - - context 'when an attribute has no type' do - let(:definition) { { name: 'some_resource', attributes: { name: { desc: 'message' } } } } - - it { expect { type }.to raise_error Puppet::DevError, %r{has no type} } - end - - context 'when an attribute has no descrption' do - let(:definition) { { name: 'some_resource', attributes: { name: { type: 'String' } } } } - - it 'Raises a warning message' do - expect(Puppet).to receive(:warning).with('`some_resource.name` has no docs') - type - end - end - - context 'when an attribute has an unsupported type' do - let(:definition) { { name: 'some_resource', attributes: { name: { type: 'basic' } } } } - - it { expect { type }.to raise_error %r{ is not a valid type specification} } - end - - context 'with both behavior and behaviour' do - let(:definition) do - { - name: 'bad_behaviour', - attributes: { - name: { - type: 'String', - behaviour: :namevar, - behavior: :namevar, - }, - }, - } - end - - it { expect { type }.to raise_error Puppet::DevError, %r{name.*attribute has both} } - end - - context 'when registering a type with badly formed attribute type' do - let(:definition) do - { - name: 'bad_syntax', - attributes: { - name: { - type: 'Optional[String', - }, - }, - } - end - - it { expect { type }.to raise_error Puppet::DevError, %r{The type of the `name` attribute `Optional\[String` could not be parsed:} } - end end end diff --git a/spec/puppet/resource_api_spec.rb b/spec/puppet/resource_api_spec.rb index 83fd72c3..7daa44e6 100644 --- a/spec/puppet/resource_api_spec.rb +++ b/spec/puppet/resource_api_spec.rb @@ -1825,4 +1825,24 @@ def set(_context, changes) end it { expect { described_class.register_type(definition) }.to raise_error Puppet::ResourceError, %r{^`bad` is not a valid behaviour value$} } end end + + describe '#register_transport' do + let(:schema) do + { + name: 'test_transport', + desc: 'a demo transport', + connection_info: { + host: { + type: 'String', + desc: 'hostname', + }, + }, + } + end + + it 'calls Puppet::ResourceApi::Transport.register' do + expect(Puppet::ResourceApi::Transport).to receive(:register).with(schema) + described_class.register_transport(schema) + end + end end