Skip to content

(FM-7597) RSAPI Transport register function and Schema validation #141

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions lib/puppet/resource_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -195,14 +196,8 @@ def to_resource
# https://puppet.com/docs/puppet/6.0/custom_types.html#reference-5883
# for details.
send(param_or_property, name.to_sym, parent: parent) do
unless options[:type]
raise Puppet::DevError, "#{definition[:name]}.#{name} has no type"
end

if options[:desc]
desc "#{options[:desc]} (a #{options[:type]})"
else
warn("#{definition[:name]}.#{name} has no docs")
end

# The initialize method is called when puppet core starts building up
Expand Down
97 changes: 97 additions & 0 deletions lib/puppet/resource_api/base_type_definition.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Provides accessor methods for the type being provided
class Puppet::ResourceApi::BaseTypeDefinition
attr_reader :definition, :attributes

def initialize(definition, attr_key)
raise Puppet::DevError, 'BaseTypeDefinition requires definition to be a Hash' unless definition.is_a?(Hash)
@definition = definition
@attributes = definition[attr_key]
@data_type_cache = {}
validate_schema
end

def name
@definition[:name]
end

def namevars
@namevars ||= attributes.select { |_name, options|
options.key?(:behaviour) && options[:behaviour] == :namevar
}.keys
end

def validate_schema
return if attributes.nil?
attributes.each do |key, type_hash|
raise Puppet::DevError, "`#{name}.#{key}` should be a Hash, not a #{type_hash.class}" unless type_hash.is_a? Hash
raise Puppet::DevError, "`#{name}.#{key}` has no type" unless type_hash.key? :type
warn("#{name}.#{key} has no docs") unless type_hash.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],
)
end
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"
end
end

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)

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?

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

# 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?
end
bad_vals
end
end
20 changes: 20 additions & 0 deletions lib/puppet/resource_api/transport.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
require 'puppet/resource_api/transport_schema_def'
# Remote target transport API
module Puppet::ResourceApi::Transport
class << self
attr_reader :transports
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exposing the transports like this will allow folks to directly interact with the hash and add/modify/delete entries without validation. there is no need for the accessor yet.

end

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
8 changes: 8 additions & 0 deletions lib/puppet/resource_api/transport_schema_def.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
require 'puppet/resource_api/base_type_definition'
# Provides accessor methods for the type being provided
class Puppet::ResourceApi::TransportSchemaDef < Puppet::ResourceApi::BaseTypeDefinition
def initialize(definition)
raise Puppet::DevError, 'TransportSchemaDef requires definition to be a Hash' unless definition.is_a?(Hash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate check - if you're doing this because of the class name in the error message, use self.class.name in the base class.

super(definition, :connection_info)
end
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given how small TransportSchemaDef and TypeDefinition have become I'd pref to have them all in one file:

module Puppet::ResourceApi
  # pre-declare class
  class BaseTypeDefinition; end
 
  class Puppet::ResourceApi::TypeDefinition < Puppet::ResourceApi::BaseTypeDefinition
    # ...
  ...
  ..
  class BaseTypeDefinition
    # the rest

85 changes: 4 additions & 81 deletions lib/puppet/resource_api/type_definition.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,13 @@
require 'puppet/resource_api/base_type_definition'
# Provides accessor methods for the type being provided
class Puppet::ResourceApi::TypeDefinition
attr_reader :definition

class Puppet::ResourceApi::TypeDefinition < Puppet::ResourceApi::BaseTypeDefinition
def initialize(definition)
raise Puppet::DevError, 'TypeDefinition requires definition to be a Hash' unless definition.is_a?(Hash)
@definition = definition
end

def name
@definition[:name]
end

def attributes
@definition[:attributes]
super(definition, :attributes)
end

def ensurable?
@definition[:attributes].key?(:ensure)
end

def namevars
@namevars ||= @definition[:attributes].select { |_name, options|
options.key?(:behaviour) && options[:behaviour] == :namevar
}.keys
attributes.key?(:ensure)
end

# rubocop complains when this is named has_feature?
Expand All @@ -35,66 +20,4 @@ def feature?(feature)
end
supported
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"
end
end

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)

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?

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

# 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 = Puppet::ResourceApi::DataTypeHandling.parse_puppet_type(
key,
attributes[key][:type],
)
error_message = Puppet::ResourceApi::DataTypeHandling.try_validate(
type,
value,
'',
)
bad_vals[key] = value unless error_message.nil?
end
bad_vals
end
end
2 changes: 1 addition & 1 deletion spec/puppet/resource_api/base_context_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ def send_log(log, msg)
TestContext.new(definition)
end

let(:definition) { { name: 'some_resource', attributes: { name: 'some_resource' }, features: feature_support } }
let(:definition) { { name: 'some_resource', attributes: { name: { type: 'String' } }, features: feature_support } }
let(:feature_support) { [] }

it { expect { described_class.new(nil) }.to raise_error ArgumentError, %r{BaseContext requires definition to be a Hash} }
Expand Down
Loading