diff --git a/.rubocop.yml b/.rubocop.yml
index 6d831a74..5457a9ea 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,6 +1,5 @@
---
require: rubocop-rspec
-inherit_from: .rubocop_todo.yml
AllCops:
TargetRubyVersion: '2.1'
Include:
@@ -157,4 +156,4 @@ Naming/UncommunicativeMethodParamName:
# This cop breaks syntax highlighting in VSCode
Layout/ClosingHeredocIndentation:
- Enabled: false
\ No newline at end of file
+ Enabled: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
deleted file mode 100644
index 63295575..00000000
--- a/.rubocop_todo.yml
+++ /dev/null
@@ -1,17 +0,0 @@
-# This configuration was generated by
-# `rubocop --auto-gen-config`
-# on 2017-09-05 11:31:11 +0100 using RuboCop version 0.49.1.
-# The point is for the user to remove these configuration records
-# one by one as the offenses are removed from the code base.
-# Note that changes in the inspected code, or installation of new
-# versions of RuboCop, may require this file to be generated again.
-
-# Offense count: 6
-Style/Documentation:
- Exclude:
- - 'spec/**/*'
- - 'test/**/*'
- - 'lib/puppet/resource_api.rb'
- - 'lib/puppet/resource_api/base_context.rb'
- - 'lib/puppet/resource_api/io_context.rb'
- - 'lib/puppet/resource_api/puppet_context.rb'
diff --git a/.travis.yml b/.travis.yml
index b2619621..7b214550 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -25,7 +25,7 @@ matrix:
- env: RVM="jruby-9.1.9.0" PUPPET_GEM_VERSION='~> 5' JRUBY_OPTS="--debug"
before_cache: pushd ~/.rvm && rm -rf archives rubies/ruby-2.2.7 rubies/ruby-2.3.4 && popd
cache:
- bundler: true
+ bundler: false
directories: ~/.rvm
before_install: rvm use jruby-9.1.9.0 --install --binary --fuzzy
- rvm: 2.4.3
@@ -65,10 +65,5 @@ matrix:
- rvm: 2.5.1
env: PUPPET_GEM_VERSION='https://github.com/puppetlabs/puppet.git#6.0.x'
notifications:
- hipchat:
- rooms:
- secure: 10a49kkZcghKHNnef8x7eBG+KjScL3i1VpygFg6DPAOK2YNbEoyEx1Kv9KLC7GSRYov/SQZOsZrvHZtDhEtFSKhhiAjOwxl1jV1t6aAMGMnN1IoZBOvdAJKrZsm54/bBeYp+je2wqnnoFNtLVFSoOX0LkFaDEWT+zGZ5xKJIH25GpeQEZf1eDxs/d8YX/m+RwbGXHVA//hOpvZo0ntvznh2EbW5OPODKSeUXbWZ+W4ndODTsKWFc/WLMSSgFDzW/Y2/9V40D4IC8lvSx6eKFryMfAQy6pO/d1aTB468awzyVcdYAMMCOITm7hlKGRKxNgq6NkOsXs5KLg6ifpn+a/Rhapbz6Qxbpjjho/7Wxngl4B3T+i35ap/mFrS/fOfKCq3gEQlYn29its9bEFArNGbr+/sXKABb+sRpgW4RTPWYDHJyHJendbevd5tZ+fd0JUBOi0Cb4PcXxQxM8IQrbuu2zso0K5MV05kL0S1DE/VsuUrPaK0RsF+b1+i6NfvtN8kgbYs1eiVku+guIG2ec3xIefQ1hsEOFPFNqSDfHp7nANnRVIbBCt8qw8DhmNEczsfN5Dp21euJUsO9qpau++NzD3jRhkE5Zki5cwsakU7hIQzw82BIb0eSQJCHygieExeEboWRqtDgy/IKIWPgIvEuU68ppR2bl2reKCHLCnWc=
- template:
- - '%{repository}#%{build_number} (%{branch} - %{commit} : %{author}): %{message}
- (Details | PR/Diff)'
- format: html
+ slack:
+ secure: aPXZYNow8LsmmlS8PQU3FjL0bc7FqUUA95d++wZfIu7YAjGboIUiekxYouQ0XnY+Aig8InvbTOIgBHgGNheyr/YFbFS90/jtulbF8oW7BitW+imgjeAHDCwlQZTCc4FFYde/2pI7QTT8H5NpLR9mKxlTU77Sqr8gFAIybuPdHcKMYQZdEZS07ma2pUp7+GyKS6PDQpzW2+mDCz/wfi3/JdsUvc0mclCZ8vxySc66j5P1E6nFDMzuakBOjwJHpgeDpreapbmSUQLAX0a3ZsFP+N+SNduLotlV2BWnJK2gcO6rGFP4Fz1D0bGXuBnYYdIiB+9OgI3wtXg9y1SifNHUG3IrOBAA8CGNyrebTGKtH0TS2O+HZLbaNX2g6udD5e3156vys9wScmJuQ/rSkVtQfXf1qUm5eijvlXI+DIbssbZHqm6QQGyM4p3NoULmNmF1C85bQoZ4GF7b1P/8mstsVE/HUfnzRPNbwD0r6j1aE/ck3PKMi7ZAhIi0Ja9RnAgP3wi0t62uERYcJGGYEycWohMWnrf2w6GFwGeuoiwAkASdHOLX0/AOMPc4mBOjlc621o8uYMrrZqfF5CrOAvJ151USSsWn2AhXaibIvnHo6X91paNvvNpU/GYu3CUAl6q8OhYovvjtRVPVnhs2DrpgoRB+6NWHnzjRG/wr6Z9U+vA=
diff --git a/lib/puppet/resource_api.rb b/lib/puppet/resource_api.rb
index 07b97edf..5e84b628 100644
--- a/lib/puppet/resource_api.rb
+++ b/lib/puppet/resource_api.rb
@@ -5,12 +5,15 @@
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/transport/wrapper'
require 'puppet/resource_api/type_definition'
require 'puppet/resource_api/value_creator'
require 'puppet/resource_api/version'
require 'puppet/type'
require 'puppet/util/network_device'
+# This module contains the main API to register and access types, providers and transports.
module Puppet::ResourceApi
@warning_count = 0
@@ -38,7 +41,12 @@ def register_type(definition)
# Keeps a copy of the provider around. Weird naming to avoid clashes with puppet's own `provider` member
define_singleton_method(:my_provider) do
@my_provider ||= Hash.new { |hash, key| hash[key] = Puppet::ResourceApi.load_provider(definition[:name]).new }
- @my_provider[Puppet::Util::NetworkDevice.current.class]
+
+ if Puppet::Util::NetworkDevice.current.is_a? Puppet::ResourceApi::Transport::Wrapper
+ @my_provider[Puppet::Util::NetworkDevice.current.transport.class]
+ else
+ @my_provider[Puppet::Util::NetworkDevice.current.class]
+ end
end
# make the provider available in the instance's namespace
@@ -352,7 +360,7 @@ def cache_current_state(resource_hash)
end
define_singleton_method(:context) do
- @context ||= PuppetContext.new(definition)
+ @context ||= PuppetContext.new(TypeDefinition.new(definition))
end
def context
@@ -410,8 +418,10 @@ def load_provider(type_name)
type_name_sym = type_name.to_sym
device_name = if Puppet::Util::NetworkDevice.current.nil?
nil
- else
+ elsif Puppet::Util::NetworkDevice.current.is_a? Puppet::ResourceApi::Transport::Wrapper
# extract the device type from the currently loaded device's class
+ Puppet::Util::NetworkDevice.current.schema.name
+ else
Puppet::Util::NetworkDevice.current.class.name.split('::')[-2].downcase
end
device_class_name = class_name_from_type_name(device_name)
@@ -451,6 +461,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/base_context.rb b/lib/puppet/resource_api/base_context.rb
index bf877680..1c44d1f0 100644
--- a/lib/puppet/resource_api/base_context.rb
+++ b/lib/puppet/resource_api/base_context.rb
@@ -1,20 +1,34 @@
require 'puppet/resource_api/type_definition'
+# rubocop:disable Style/Documentation
module Puppet; end
module Puppet::ResourceApi; end
+# rubocop:enable Style/Documentation
+
+# This class provides access to all common external dependencies of providers and transports.
+# The runtime environment will inject an appropriate implementation.
class Puppet::ResourceApi::BaseContext
attr_reader :type
def initialize(definition)
- raise ArgumentError, 'BaseContext requires definition to be a Hash' unless definition.is_a?(Hash)
- @typename = definition[:name]
- @type = Puppet::ResourceApi::TypeDefinition.new(definition)
+ if definition.is_a?(Hash)
+ # this is only for backwards compatibility
+ @type = Puppet::ResourceApi::TypeDefinition.new(definition)
+ elsif definition.is_a? Puppet::ResourceApi::BaseTypeDefinition
+ @type = definition
+ else
+ raise ArgumentError, 'BaseContext requires definition to be a child of Puppet::ResourceApi::BaseTypeDefinition, not <%{actual_type}>' % { actual_type: definition.class }
+ end
end
def device
raise 'Received device() on an unprepared BaseContext. Use a PuppetContext instead.'
end
+ def transport
+ raise 'No transport available.'
+ end
+
def failed?
@failed
end
@@ -27,7 +41,7 @@ def feature_support?(feature)
[:debug, :info, :notice, :warning, :err].each do |level|
define_method(level) do |*args|
if args.length == 1
- message = "#{@context || @typename}: #{args.last}"
+ message = "#{@context || @type.name}: #{args.last}"
elsif args.length == 2
resources = format_titles(args.first)
message = "#{resources}: #{args.last}"
@@ -137,9 +151,9 @@ def send_log(_level, _message)
def format_titles(titles)
if titles.length.zero? && !titles.is_a?(String)
- @typename
+ @type.name
else
- "#{@typename}[#{[titles].flatten.compact.join(', ')}]"
+ "#{@type.name}[#{[titles].flatten.compact.join(', ')}]"
end
end
diff --git a/lib/puppet/resource_api/io_context.rb b/lib/puppet/resource_api/io_context.rb
index 3cfb4afb..3efdde63 100644
--- a/lib/puppet/resource_api/io_context.rb
+++ b/lib/puppet/resource_api/io_context.rb
@@ -1,9 +1,14 @@
require 'puppet/resource_api/base_context'
+# Implement Resource API Conext to log through an IO object, defaulting to `$stderr`.
+# There is no access to a device here. You can supply a transport if necessary.
class Puppet::ResourceApi::IOContext < Puppet::ResourceApi::BaseContext
- def initialize(definition, target = $stderr)
+ attr_reader :transport
+
+ def initialize(definition, target = $stderr, transport = nil)
super(definition)
@target = target
+ @transport = transport
end
protected
diff --git a/lib/puppet/resource_api/puppet_context.rb b/lib/puppet/resource_api/puppet_context.rb
index 770c7692..be057bcc 100644
--- a/lib/puppet/resource_api/puppet_context.rb
+++ b/lib/puppet/resource_api/puppet_context.rb
@@ -1,6 +1,8 @@
require 'puppet/resource_api/base_context'
require 'puppet/util/logging'
+# Implement Resource API Context to log through Puppet facilities
+# and access/expose the puppet process' current device/transport
class Puppet::ResourceApi::PuppetContext < Puppet::ResourceApi::BaseContext
def device
# TODO: evaluate facter_url setting for loading config if there is no `current` NetworkDevice
@@ -8,6 +10,10 @@ def device
Puppet::Util::NetworkDevice.current
end
+ def transport
+ device.transport
+ end
+
def log_exception(exception, message: 'Error encountered', trace: false)
super(exception, message: message, trace: trace || Puppet[:trace])
end
diff --git a/lib/puppet/resource_api/transport.rb b/lib/puppet/resource_api/transport.rb
new file mode 100644
index 00000000..438333f1
--- /dev/null
+++ b/lib/puppet/resource_api/transport.rb
@@ -0,0 +1,95 @@
+module Puppet::ResourceApi; end # rubocop:disable Style/Documentation
+
+# 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)
+
+ init_transports
+ unless @transports[@environment][schema[:name]].nil?
+ raise Puppet::DevError, 'Transport `%{name}` is already registered for `%{environment}`' % {
+ name: schema[:name],
+ environment: @environment,
+ }
+ end
+ @transports[@environment][schema[:name]] = Puppet::ResourceApi::TransportSchemaDef.new(schema)
+ end
+ module_function :register # rubocop:disable Style/AccessModifierDeclarations
+
+ # retrieve a Hash of transport schemas, keyed by their name.
+ def list
+ init_transports
+ Marshal.load(Marshal.dump(@transports[@environment]))
+ end
+ module_function :list # rubocop:disable Style/AccessModifierDeclarations
+
+ def connect(name, connection_info)
+ validate(name, connection_info)
+ require "puppet/transport/#{name}"
+ class_name = name.split('_').map { |e| e.capitalize }.join
+ Puppet::Transport.const_get(class_name).new(get_context(name), wrap_sensitive(name, connection_info))
+ end
+ module_function :connect # rubocop:disable Style/AccessModifierDeclarations
+
+ def inject_device(name, transport)
+ transport_wrapper = Puppet::ResourceApi::Transport::Wrapper.new(name, transport)
+
+ if Puppet::Util::NetworkDevice.respond_to?(:set_device)
+ Puppet::Util::NetworkDevice.set_device(name, transport_wrapper)
+ else
+ Puppet::Util::NetworkDevice.instance_variable_set(:@current, transport_wrapper)
+ end
+ end
+ module_function :inject_device # rubocop:disable Style/AccessModifierDeclarations
+
+ def self.validate(name, connection_info)
+ init_transports
+ require "puppet/transport/schema/#{name}" unless @transports[@environment].key? name
+ transport_schema = @transports[@environment][name]
+ if transport_schema.nil?
+ raise Puppet::DevError, 'Transport for `%{target}` not registered with `%{environment}`' % {
+ target: name,
+ environment: @environment,
+ }
+ end
+ message_prefix = 'The connection info provided does not match the Transport Schema'
+ transport_schema.check_schema(connection_info, message_prefix)
+ transport_schema.validate(connection_info)
+ end
+ private_class_method :validate
+
+ def self.get_context(name)
+ require 'puppet/resource_api/puppet_context'
+ Puppet::ResourceApi::PuppetContext.new(@transports[@environment][name])
+ end
+ private_class_method :get_context
+
+ def self.init_transports
+ lookup = Puppet.lookup(:current_environment) if Puppet.respond_to? :lookup
+ @environment = if lookup.nil?
+ :transports_default
+ else
+ lookup.name
+ end
+ @transports ||= {}
+ @transports[@environment] ||= {}
+ end
+ private_class_method :init_transports
+
+ def self.wrap_sensitive(name, connection_info)
+ transport_schema = @transports[@environment][name]
+ if transport_schema
+ transport_schema.definition[:connection_info].each do |attr_name, options|
+ if options.key?(:sensitive) && (options[:sensitive] == true)
+ connection_info[attr_name] = Puppet::Pops::Types::PSensitiveType::Sensitive.new(connection_info[attr_name])
+ end
+ end
+ end
+ connection_info
+ end
+ private_class_method :wrap_sensitive
+end
diff --git a/lib/puppet/resource_api/transport/wrapper.rb b/lib/puppet/resource_api/transport/wrapper.rb
new file mode 100644
index 00000000..fe4a41e2
--- /dev/null
+++ b/lib/puppet/resource_api/transport/wrapper.rb
@@ -0,0 +1,55 @@
+require 'puppet/resource_api/transport'
+require 'hocon'
+require 'hocon/config_syntax'
+
+# Puppet::ResourceApi::Transport::Wrapper` to interface between the Util::NetworkDevice
+class Puppet::ResourceApi::Transport::Wrapper
+ attr_reader :transport, :schema
+
+ def initialize(name, url_or_config_or_transport)
+ if url_or_config_or_transport.is_a? String
+ url = URI.parse(url_or_config_or_transport)
+ raise "Unexpected url '#{url_or_config_or_transport}' found. Only file:/// URLs for configuration supported at the moment." unless url.scheme == 'file'
+ raise "Trying to load config from '#{url.path}, but file does not exist." if url && !File.exist?(url.path)
+ config = self.class.deep_symbolize(Hocon.load(url.path, syntax: Hocon::ConfigSyntax::HOCON) || {})
+ elsif url_or_config_or_transport.is_a? Hash
+ config = url_or_config_or_transport
+ elsif transport_class?(name, url_or_config_or_transport)
+ @transport = url_or_config_or_transport
+ end
+
+ @transport ||= Puppet::ResourceApi::Transport.connect(name, config)
+ @schema = Puppet::ResourceApi::Transport.list[name]
+ end
+
+ def transport_class?(name, transport)
+ class_name = name.split('_').map { |e| e.capitalize }.join
+ expected = Puppet::Transport.const_get(class_name).to_s
+ expected == transport.class.to_s
+ end
+
+ def facts
+ context = Puppet::ResourceApi::PuppetContext.new(@schema)
+ # @transport.facts + custom_facts # look into custom facts work by TP
+ @transport.facts(context)
+ end
+
+ def respond_to_missing?(name, _include_private)
+ (@transport.respond_to? name) || super
+ end
+
+ def method_missing(method_name, *args, &block)
+ if @transport.respond_to? method_name
+ @transport.send(method_name, *args, &block)
+ else
+ super
+ end
+ end
+
+ # From https://stackoverflow.com/a/11788082/4918
+ def self.deep_symbolize(obj)
+ return obj.each_with_object({}) { |(k, v), memo| memo[k.to_sym] = deep_symbolize(v); } if obj.is_a? Hash
+ return obj.each_with_object([]) { |v, memo| memo << deep_symbolize(v); } if obj.is_a? Array
+ obj
+ end
+end
diff --git a/lib/puppet/resource_api/type_definition.rb b/lib/puppet/resource_api/type_definition.rb
index e23a077a..40bd3713 100644
--- a/lib/puppet/resource_api/type_definition.rb
+++ b/lib/puppet/resource_api/type_definition.rb
@@ -1,144 +1,187 @@
# Provides accessor methods for the type being provided
-class Puppet::ResourceApi::TypeDefinition
- attr_reader :definition
-
- def initialize(definition)
- @data_type_cache = {}
- validate_schema(definition)
- end
+module Puppet::ResourceApi
+ # pre-declare class
+ class BaseTypeDefinition; end
+
+ # RSAPI Resource Type
+ class TypeDefinition < BaseTypeDefinition
+ def initialize(definition)
+ super(definition, :attributes)
+ end
- def name
- @definition[:name]
- end
+ def ensurable?
+ attributes.key?(:ensure)
+ end
- def attributes
- @definition[:attributes]
- 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 ensurable?
- @definition[:attributes].key?(:ensure)
- 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 namevars
- @namevars ||= @definition[:attributes].select { |_name, options|
- options.key?(:behaviour) && options[:behaviour] == :namevar
- }.keys
- end
+ Puppet::ResourceApi::DataTypeHandling.validate_ensure(definition)
- # 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}`")
+ 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?
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
+ # RSAPI Transport schema
+ class TransportSchemaDef < BaseTypeDefinition
+ def initialize(definition)
+ super(definition, :connection_info)
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 }
+
+ def validate(resource)
+ # enforce mandatory attributes
+ missing_attrs = []
+
+ attributes.each do |name, _options|
+ type = @data_type_cache[attributes[name][:type]]
+
+ if resource[name].nil? && !(type.instance_of? Puppet::Pops::Types::POptionalType)
+ missing_attrs << name
+ end
+ end
+
+ error_msg = "The following mandatory attributes were not provided:\n * " + missing_attrs.join(", \n * ")
+ raise Puppet::ResourceError, error_msg if missing_attrs.any?
end
+ end
- Puppet::ResourceApi::DataTypeHandling.validate_ensure(definition)
+ # Base RSAPI schema Object
+ class BaseTypeDefinition
+ attr_reader :definition, :attributes
- 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 initialize(definition, attr_key)
+ @data_type_cache = {}
+ validate_schema(definition, attr_key)
+ # store the validated definition
+ @definition = definition
+ 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 name
+ definition[:name]
+ 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 namevars
+ @namevars ||= attributes.select { |_name, options|
+ options.key?(:behaviour) && options[:behaviour] == :namevar
+ }.keys
+ 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"
+ 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
- attr[:behaviour] = attr[:behavior]
- attr.delete(:behavior)
- 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"
+ 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
end
- message = "Provider returned data that does not match the Type Schema for `#{name}[#{resource[namevars.first]}]`"
+ # validates a resource hash against its type schema
+ def check_schema(resource, message_prefix = nil)
+ 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
- rejected_keys = check_schema_keys(resource) # removes bad keys
- bad_values = check_schema_values(resource)
+ message_prefix = 'Provider returned data that does not match the Type Schema' if message_prefix.nil?
+ message = "#{message_prefix} for `#{name}[#{resource[namevars.first]}]`"
- 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
+ rejected_keys = check_schema_keys(resource)
+ bad_values = check_schema_values(resource)
- return if rejected_keys.empty? && bad_values.empty?
+ 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
+ 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
+ # No longer modifies the resource passed in
+ 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]]
+ is_sensitive = (attributes[key].key?(:sensitive) && (attributes[key][:sensitive] == true))
+ error_message = Puppet::ResourceApi::DataTypeHandling.try_validate(
+ type,
+ value,
+ '',
+ )
+ if is_sensitive
+ bad_vals[key] = '<< redacted value >> ' + error_message unless error_message.nil?
+ else
+ bad_vals[key] = value unless error_message.nil?
+ end
+ end
+ bad_vals
end
- bad_vals
end
end
diff --git a/spec/acceptance/device_spec.rb b/spec/acceptance/device_spec.rb
index 69302007..788244dd 100644
--- a/spec/acceptance/device_spec.rb
+++ b/spec/acceptance/device_spec.rb
@@ -74,9 +74,18 @@
<= Gem::Version.new('5.3.6') && Gem::Version.new(Puppet::PUPPETVERSION) != Gem::Version.new('5.4.0')
@@ -86,10 +95,14 @@ def is_device_apply_supported?
skip "No device --apply in puppet before v5.3.6 nor in v5.4.0 (v#{Puppet::PUPPETVERSION} is installed)" unless is_device_apply_supported?
device_conf.write(device_conf_content)
device_conf.close
+
+ device_credentials.write(device_credentials_content)
+ device_credentials.close
end
after(:each) do
device_conf.unlink
+ device_credentials.unlink
end
context 'with no config specified' do
diff --git a/spec/acceptance/sensitive_spec.rb b/spec/acceptance/sensitive_spec.rb
index f48341fb..e23c4088 100644
--- a/spec/acceptance/sensitive_spec.rb
+++ b/spec/acceptance/sensitive_spec.rb
@@ -21,6 +21,22 @@
expect(stdout_str).not_to match %r{foo}
expect(stdout_str).not_to match %r{warn|error}i
end
+
+ context 'when a sensitive value is not the top level type' do
+ it 'is not exposed by a provider' do
+ stdout_str, _status = Open3.capture2e("puppet apply #{common_args} -e \"test_sensitive { bar: secret => Sensitive('foo'), "\
+ "optional_secret => Sensitive('optional foo'), variant_secret => [Sensitive('variant foo')] }\"")
+ expect(stdout_str).to match %r{redacted}
+ expect(stdout_str).not_to match %r{variant foo}
+ expect(stdout_str).not_to match %r{warn|error}i
+ end
+ it 'properly validates the sensitive type value' do
+ stdout_str, _status = Open3.capture2e("puppet apply #{common_args} -e \"test_sensitive { bar: secret => Sensitive('foo'), "\
+ "optional_secret => Sensitive('optional foo'), variant_secret => [Sensitive(134679)] }\"")
+ expect(stdout_str).to match %r{Sensitive\[String\]( value)?, got Sensitive\[Integer\]}
+ expect(stdout_str).not_to match %r{134679}
+ end
+ end
end
describe 'using `puppet resource`' do
diff --git a/spec/acceptance/transport/transport_spec.rb b/spec/acceptance/transport/transport_spec.rb
new file mode 100644
index 00000000..3fe7c0b5
--- /dev/null
+++ b/spec/acceptance/transport/transport_spec.rb
@@ -0,0 +1,185 @@
+require 'spec_helper'
+require 'tempfile'
+require 'open3'
+
+RSpec.describe 'a transport' do
+ let(:common_args) { '--verbose --trace --debug --strict=error --modulepath spec/fixtures' }
+
+ before(:all) do
+ FileUtils.mkdir_p(File.expand_path('~/.puppetlabs/opt/puppet/cache/devices/the_node/state'))
+ end
+
+ describe 'using `puppet device`' do
+ let(:common_args) { super() + ' --target the_node' }
+ let(:device_conf) { Tempfile.new('device.conf') }
+ let(:device_conf_content) do
+ <= Gem::Version.new('5.3.6') && Gem::Version.new(Puppet::PUPPETVERSION) != Gem::Version.new('5.4.0')
+ end
+
+ before(:each) do
+ skip "No device --apply in puppet before v5.3.6 nor in v5.4.0 (v#{Puppet::PUPPETVERSION} is installed)" unless is_device_apply_supported?
+ device_conf.write(device_conf_content)
+ device_conf.close
+
+ device_credentials.write(device_credentials_content)
+ device_credentials.close
+ end
+
+ after(:each) do
+ device_conf.unlink
+ device_credentials.unlink
+ end
+
+ context 'when all sensitive values are valid' do
+ let(:device_credentials_content) do
+ <"foo"}
+ expect(stdout_str).not_to match %r{wibble}
+ expect(stdout_str).not_to match %r{bar}
+ expect(stdout_str).not_to match %r{meep}
+ expect(stdout_str).not_to match %r{1234567890}
+ end
+ end
+ end
+
+ context 'with a sensitive string value that is invalid' do
+ let(:device_credentials_content) do
+ <> }
+ expect(stdout_str).not_to match %r{optional_secret}
+ expect(stdout_str).not_to match %r{array_secret}
+ expect(stdout_str).not_to match %r{variant_secret}
+ end
+ end
+ end
+
+ context 'with an optional sensitive string value that is invalid' do
+ let(:device_credentials_content) do
+ <>}
+ expect(stdout_str).not_to match %r{array_secret}
+ expect(stdout_str).not_to match %r{variant_secret}
+ end
+ end
+ end
+
+ context 'with an array of sensitive strings that is invalid' do
+ let(:device_credentials_content) do
+ <>}
+ expect(stdout_str).not_to match %r{variant_secret}
+ end
+ end
+ end
+
+ context 'with an variant containing a sensitive value that is invalid' do
+ let(:device_credentials_content) do
+ <>}
+ end
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/test_module/lib/puppet/transport/schema/test_device.rb b/spec/fixtures/test_module/lib/puppet/transport/schema/test_device.rb
new file mode 100644
index 00000000..df2f7a9b
--- /dev/null
+++ b/spec/fixtures/test_module/lib/puppet/transport/schema/test_device.rb
@@ -0,0 +1,32 @@
+require 'puppet/resource_api'
+
+Puppet::ResourceApi.register_transport(
+ name: 'test_device', # points at class Puppet::Transport::TestDevice
+ desc: 'Connects to a device',
+ connection_info: {
+ username: {
+ type: 'String',
+ desc: 'The name of the resource you want to manage.',
+ },
+ secret: {
+ type: 'String',
+ desc: 'A secret to protect.',
+ sensitive: true,
+ },
+ optional_secret: {
+ type: 'Optional[String]',
+ desc: 'An optional secret to protect.',
+ sensitive: true,
+ },
+ array_secret: {
+ type: 'Optional[Array[String]]',
+ desc: 'An array secret to protect.',
+ sensitive: true,
+ },
+ variant_secret: {
+ type: 'Optional[Variant[Array[String], Integer]]',
+ desc: 'An array secret to protect.',
+ sensitive: true,
+ },
+ },
+)
diff --git a/spec/fixtures/test_module/lib/puppet/transport/schema/test_device_sensitive.rb b/spec/fixtures/test_module/lib/puppet/transport/schema/test_device_sensitive.rb
new file mode 100644
index 00000000..a22c43aa
--- /dev/null
+++ b/spec/fixtures/test_module/lib/puppet/transport/schema/test_device_sensitive.rb
@@ -0,0 +1,32 @@
+require 'puppet/resource_api'
+
+Puppet::ResourceApi.register_transport(
+ name: 'test_device_sensitive', # points at class Puppet::Transport::TestDevice
+ desc: 'Connects to a device',
+ connection_info: {
+ username: {
+ type: 'String',
+ desc: 'The name of the resource you want to manage.',
+ },
+ secret_string: {
+ type: 'String',
+ desc: 'A secret to protect.',
+ sensitive: true,
+ },
+ optional_secret: {
+ type: 'Optional[String]',
+ desc: 'An optional secret to protect.',
+ sensitive: true,
+ },
+ array_secret: {
+ type: 'Optional[Array[String]]',
+ desc: 'An array secret to protect.',
+ sensitive: true,
+ },
+ variant_secret: {
+ type: 'Optional[Variant[Array[String], Integer]]',
+ desc: 'An array secret to protect.',
+ sensitive: true,
+ },
+ },
+)
diff --git a/spec/fixtures/test_module/lib/puppet/transport/test_device.rb b/spec/fixtures/test_module/lib/puppet/transport/test_device.rb
new file mode 100644
index 00000000..7974f1d4
--- /dev/null
+++ b/spec/fixtures/test_module/lib/puppet/transport/test_device.rb
@@ -0,0 +1,21 @@
+module Puppet::Transport
+# a transport for a test_device
+class TestDevice
+ def initialize(_context, connection_info);
+ puts connection_info
+ end
+
+ def facts(_context)
+ { 'foo' => 'bar' }
+ end
+
+ def verify(_context)
+ return true
+ end
+
+ def close(_context)
+ return
+ end
+end
+
+end
diff --git a/spec/fixtures/test_module/lib/puppet/transport/test_device_sensitive.rb b/spec/fixtures/test_module/lib/puppet/transport/test_device_sensitive.rb
new file mode 100644
index 00000000..c2a898a2
--- /dev/null
+++ b/spec/fixtures/test_module/lib/puppet/transport/test_device_sensitive.rb
@@ -0,0 +1,21 @@
+module Puppet::Transport
+# a transport for a test_device
+class TestDeviceSensitive
+ def initialize(_context, connection_info);
+ puts "transport connection_info: #{connection_info}"
+ end
+
+ def facts(_context)
+ { 'foo' => 'bar' }
+ end
+
+ def verify(_context)
+ return true
+ end
+
+ def close(_context)
+ return
+ end
+end
+
+end
diff --git a/spec/fixtures/test_module/lib/puppet/type/test_sensitive.rb b/spec/fixtures/test_module/lib/puppet/type/test_sensitive.rb
index ce74363d..f42f89c9 100644
--- a/spec/fixtures/test_module/lib/puppet/type/test_sensitive.rb
+++ b/spec/fixtures/test_module/lib/puppet/type/test_sensitive.rb
@@ -26,7 +26,11 @@
desc: 'An optional secret to protect.',
},
array_secret: {
- type: 'Array[Sensitive[String]]',
+ type: 'Optional[Array[Sensitive[String]]]',
+ desc: 'An array secret to protect.',
+ },
+ variant_secret: {
+ type: 'Optional[Variant[Array[Sensitive[String]], Integer]]',
desc: 'An array secret to protect.',
},
},
diff --git a/spec/fixtures/test_module/lib/puppet/util/network_device/test_device/device.rb b/spec/fixtures/test_module/lib/puppet/util/network_device/test_device/device.rb
index 7fb6ebd6..505209a5 100644
--- a/spec/fixtures/test_module/lib/puppet/util/network_device/test_device/device.rb
+++ b/spec/fixtures/test_module/lib/puppet/util/network_device/test_device/device.rb
@@ -1,10 +1,10 @@
-require 'puppet/util/network_device/simple/device'
+require 'puppet/resource_api/transport/wrapper'
module Puppet::Util::NetworkDevice::Test_device # rubocop:disable Style/ClassAndModuleCamelCase
- # A simple test device returning hardcoded facts
- class Device < Puppet::Util::NetworkDevice::Simple::Device
- def facts
- { 'foo' => 'bar' }
+ class Device < Puppet::ResourceApi::Transport::Wrapper
+ def initialize(url_or_config, _options = {})
+ puts url_or_config.inspect
+ super('test_device', url_or_config)
end
end
end
diff --git a/spec/fixtures/test_module/lib/puppet/util/network_device/test_device_sensitive/device.rb b/spec/fixtures/test_module/lib/puppet/util/network_device/test_device_sensitive/device.rb
new file mode 100644
index 00000000..4c04b7cf
--- /dev/null
+++ b/spec/fixtures/test_module/lib/puppet/util/network_device/test_device_sensitive/device.rb
@@ -0,0 +1,12 @@
+require 'puppet/resource_api/transport/wrapper'
+
+class Puppet::Util::NetworkDevice; end
+
+module Puppet::Util::NetworkDevice::Test_device_sensitive # rubocop:disable Style/ClassAndModuleCamelCase
+ # The main class for handling the connection and command parsing to the IOS Catalyst device
+ class Device < Puppet::ResourceApi::Transport::Wrapper
+ def initialize(url_or_config, _options = {})
+ super('test_device_sensitive', url_or_config)
+ end
+ end
+end
\ No newline at end of file
diff --git a/spec/puppet/resource_api/base_context_spec.rb b/spec/puppet/resource_api/base_context_spec.rb
index 34869f75..9569faf3 100644
--- a/spec/puppet/resource_api/base_context_spec.rb
+++ b/spec/puppet/resource_api/base_context_spec.rb
@@ -13,10 +13,17 @@ def send_log(log, msg)
TestContext.new(definition)
end
- let(:definition) { { name: 'some_resource', attributes: { name: { type: 'String', desc: 'message' } }, features: feature_support } }
+ let(:definition_hash) { { name: 'some_resource', attributes: { name: { type: 'String', desc: 'message' } }, features: feature_support } }
+ let(:definition) { Puppet::ResourceApi::TypeDefinition.new(definition_hash) }
let(:feature_support) { [] }
- it { expect { described_class.new(nil) }.to raise_error ArgumentError, %r{BaseContext requires definition to be a Hash} }
+ it { expect { described_class.new(nil) }.to raise_error ArgumentError, %r{BaseContext requires definition to be a child of Puppet::ResourceApi::BaseTypeDefinition} }
+ describe 'legacy hash definition support' do
+ let(:definition) { definition_hash }
+
+ it { expect { context }.not_to raise_error }
+ it { expect(context.type.name).to eq 'some_resource' }
+ end
describe '#failed?' do
it('defaults to false') { is_expected.not_to be_failed }
@@ -334,6 +341,10 @@ def send_log(log, msg)
it { expect { described_class.new(definition).device }.to raise_error RuntimeError, %r{Received device\(\) on an unprepared BaseContext\. Use a PuppetContext instead} }
end
+ describe '#transport' do
+ it { expect { described_class.new(definition).transport }.to raise_error RuntimeError, %r{No transport available\.} }
+ end
+
describe '#send_log' do
it { expect { described_class.new(definition).send_log(nil, nil) }.to raise_error RuntimeError, %r{Received send_log\(\) on an unprepared BaseContext\. Use IOContext, or PuppetContext instead} }
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..62a918fd
--- /dev/null
+++ b/spec/puppet/resource_api/base_type_definition_spec.rb
@@ -0,0 +1,265 @@
+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' } }
+ let(:resource_copy) { { 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
+
+ it 'does not modify the resource passed in' do
+ type.check_schema_keys(resource)
+ expect(resource).to eq(resource_copy)
+ end
+ end
+ end
+
+ describe '#check_schema_values' do
+ context 'when the definition is a type' 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
+
+ context 'when the definition is a transport' do
+ subject(:type) { described_class.new(definition, :connection_info) }
+
+ let(:definition) do
+ {
+ name: 'some_transport',
+ connection_info: {
+ username: {
+ type: 'String',
+ desc: 'The username to connect with',
+ },
+ secret: {
+ type: 'String',
+ desc: 'A sensitive value',
+ sensitive: true,
+ },
+ },
+ }
+ end
+
+ context 'when resource contains only valid values' do
+ let(:resource) { { username: 'wibble', secret: 'foo' } }
+
+ 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) { { username: 'wibble', secret: 12_345 } }
+
+ it 'returns a hash of the keys that have invalid values' do
+ expect(type.check_schema_values(resource)).to match(secret: %r{<< redacted value >> expect(s|ed) a String value, got Integer})
+ end
+ 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)
+ 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)
+ 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/io_context_spec.rb b/spec/puppet/resource_api/io_context_spec.rb
index 266095d0..a4cb8aa5 100644
--- a/spec/puppet/resource_api/io_context_spec.rb
+++ b/spec/puppet/resource_api/io_context_spec.rb
@@ -2,9 +2,10 @@
require 'puppet/resource_api/io_context'
RSpec.describe Puppet::ResourceApi::IOContext do
- subject(:context) { described_class.new(definition, io) }
+ subject(:context) { described_class.new(definition, io, transport) }
let(:definition) { { name: 'some_resource', attributes: {} } }
+ let(:transport) { nil }
let(:io) { StringIO.new('', 'w') }
@@ -18,4 +19,13 @@
expect(io.string).to match %r{warning}i
end
end
+
+ describe '#transport' do
+ it { expect(context.transport).to be_nil }
+ context 'when passing in a transport' do
+ let(:transport) { instance_double(Object, 'transport') }
+
+ it { expect(context.transport).to eq transport }
+ end
+ end
end
diff --git a/spec/puppet/resource_api/puppet_context_spec.rb b/spec/puppet/resource_api/puppet_context_spec.rb
index 96a3683e..dbc70c8b 100644
--- a/spec/puppet/resource_api/puppet_context_spec.rb
+++ b/spec/puppet/resource_api/puppet_context_spec.rb
@@ -18,7 +18,21 @@
end
end
- context 'with no NetworkDevice configured' do
+ context 'when a Transport::Wrapper device is configured' do
+ let(:device) { instance_double('Puppet::Util::NetworkDevice::Test_device::Device', 'device') }
+ let(:transport) { instance_double('Puppet::Transport::TestDevice', 'transport') }
+
+ before(:each) do
+ allow(Puppet::Util::NetworkDevice).to receive(:current).and_return(device)
+ allow(device).to receive(:transport).and_return(transport)
+ end
+
+ it 'returns the transport' do
+ expect(context.transport).to eq(transport)
+ end
+ end
+
+ context 'with nothing configured' do
before(:each) do
allow(Puppet::Util::NetworkDevice).to receive(:current).and_return(nil)
end
diff --git a/spec/puppet/resource_api/transport/wrapper_spec.rb b/spec/puppet/resource_api/transport/wrapper_spec.rb
new file mode 100644
index 00000000..1b56c20d
--- /dev/null
+++ b/spec/puppet/resource_api/transport/wrapper_spec.rb
@@ -0,0 +1,135 @@
+require 'spec_helper'
+require 'puppet/resource_api/transport/wrapper'
+require_relative '../../../fixtures/test_module/lib/puppet/transport/test_device'
+
+RSpec.describe Puppet::ResourceApi::Transport::Wrapper, agent_test: true do
+ describe '#initialize(name, url_or_config)' do
+ context 'when called with a url' do
+ context 'with a file:// prefix' do
+ let(:url) { 'file:///etc/credentials' }
+
+ it 'will not throw an error' do
+ allow(File).to receive(:exist?).and_return(true)
+ expect(Puppet::ResourceApi::Transport).to receive(:connect)
+ expect(Hocon).to receive(:load).with('/etc/credentials', any_args).and_return('foo' => %w[a b], 'bar' => 2)
+ expect { described_class.new('wibble', url) }.not_to raise_error
+ end
+ end
+
+ context 'with an http:// prefix' do
+ let(:url) { 'http://www.puppet.com' }
+
+ it { expect { described_class.new('wibble', url) }.to raise_error RuntimeError, %r{Only file:/// URLs for configuration supported} }
+ end
+ end
+
+ context 'when called with a config hash' do
+ let(:config) { {} }
+
+ it 'will use the configuration directly' do
+ expect(Hocon).not_to receive(:load)
+ expect(Puppet::ResourceApi::Transport).to receive(:connect)
+ described_class.new('wibble', config)
+ end
+ end
+
+ before(:each) do
+ module Puppet::Transport
+ class SomethingSomethingDarkside; end
+ end
+ end
+
+ context 'when called with a transport class' do
+ let(:transport) { Puppet::Transport::SomethingSomethingDarkside.new }
+ let(:instance) { described_class.new('something_something_darkside', transport) }
+
+ it 'will set the @transport class variable' do
+ expect(instance.instance_variable_get(:@transport)).to eq(transport)
+ end
+ end
+ end
+
+ describe '#facts' do
+ context 'when called' do
+ let(:instance) { described_class.new('wibble', {}) }
+ let(:context) { instance_double(Puppet::ResourceApi::PuppetContext, 'context') }
+ let(:facts) { { 'foo' => 'bar' } }
+ let(:transport) { instance_double(Puppet::Transport::TestDevice, 'transport') }
+
+ it 'will return the facts provided by the transport' do
+ allow(Puppet::ResourceApi::Transport).to receive(:connect).and_return(transport)
+ allow(Puppet::ResourceApi::Transport).to receive(:list).and_return(schema: :dummy)
+ allow(Puppet::ResourceApi::PuppetContext).to receive(:new).and_return(context)
+ allow(transport).to receive(:facts).with(context).and_return(facts)
+
+ expect(instance.facts).to eq(facts)
+ end
+ end
+ end
+
+ context 'when an unsupported method is called' do
+ context 'when the transport can handle the method' do
+ let(:instance) { described_class.new('wibble', {}) }
+ let(:transport) { instance_double(Puppet::Transport::TestDevice, 'transport') }
+ let(:context) { instance_double(Puppet::ResourceApi::PuppetContext, 'context') }
+
+ it 'will return the facts provided by the transport' do
+ allow(Puppet::ResourceApi::Transport).to receive(:connect).and_return(transport)
+ expect(transport).to receive(:close)
+
+ instance.close(context)
+ end
+ end
+
+ context 'when the transport cannot handle the method' do
+ let(:instance) { described_class.new('wibble', {}) }
+ let(:transport) { instance_double(Puppet::Transport::TestDevice, 'transport') }
+
+ it 'will raise a NoMethodError' do
+ allow(Puppet::ResourceApi::Transport).to receive(:connect).and_return(transport)
+ expect { instance.wibble }.to raise_error NoMethodError
+ end
+ end
+ end
+
+ context 'when a method is checked for' do
+ let(:instance) { described_class.new('wibble', {}) }
+ let(:transport) { instance_double(Puppet::Transport::TestDevice, 'transport') }
+
+ before(:each) do
+ allow(Puppet::ResourceApi::Transport).to receive(:connect).and_return(transport)
+ end
+
+ context 'when the transport does not support the function' do
+ context 'when using respond_to?' do
+ it 'will return false' do
+ expect(instance.respond_to?(:wibble)).to eq(false)
+ end
+ end
+
+ context 'when using method?' do
+ it 'will return false' do
+ expect { instance.method :wibble }.to raise_error
+ end
+ end
+ end
+
+ context 'when the transport does support the function' do
+ before(:each) do
+ allow(transport).to receive(:close)
+ end
+
+ context 'when using respond_to?' do
+ it 'will return true' do
+ expect(instance.respond_to?(:close)).to eq(true)
+ end
+ end
+
+ context 'when using method?' do
+ it 'will return the method' do
+ expect(instance.method(:close)).to be_a(Method)
+ end
+ end
+ 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..92cac1fa
--- /dev/null
+++ b/spec/puppet/resource_api/transport_schema_def_spec.rb
@@ -0,0 +1,45 @@
+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
+
+ describe '#validate' do
+ context 'when resource is missing attributes' do
+ let(:resource) { {} }
+
+ it 'raises an error listing the missing attributes' do
+ expect { type.validate(resource) }.to raise_error Puppet::ResourceError, %r{host}
+ expect { type.validate(resource) }.to raise_error Puppet::ResourceError, %r{user}
+ end
+ end
+
+ context 'when resource has all its attributes' do
+ let(:resource) { { host: '1234', user: '4321' } }
+
+ it { expect { type.validate(resource) }.not_to raise_error }
+ 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..ad59697d
--- /dev/null
+++ b/spec/puppet/resource_api/transport_spec.rb
@@ -0,0 +1,371 @@
+require 'spec_helper'
+
+RSpec.describe Puppet::ResourceApi::Transport do
+ def change_environment(name = nil)
+ environment = class_double(Puppet::Node::Environment)
+
+ if name.nil?
+ allow(Puppet).to receive(:respond_to?).and_return(false)
+ else
+ allow(Puppet).to receive(:respond_to?).and_return(true)
+ end
+
+ allow(Puppet).to receive(:lookup).with(:current_environment).and_return(environment)
+
+ # allow clean up scripts to run unhindered
+ allow(Puppet).to receive(:lookup).with(:root_environment).and_call_original
+ allow(Puppet).to receive(:lookup).with(:environments).and_call_original
+
+ allow(environment).to receive(:name).and_return(name)
+ end
+
+ 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
+
+ after(:each) do
+ # reset registered transports between tests to reduce cross-test poisoning
+ described_class.instance_variable_set(:@transports, nil)
+ end
+
+ describe '#register(schema)' do
+ 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 {
+ described_class.register(schema)
+ 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: 'String',
+ sensitive: true,
+ desc: 'the password to make the connection',
+ },
+ },
+ }
+ end
+ let(:schema2) do
+ {
+ name: 'schema2',
+ desc: 'basic transport',
+ connection_info: {
+ host: {
+ type: 'String',
+ desc: 'the host ip address or hostname',
+ },
+ },
+ }
+ end
+ let(:schema3) do
+ {
+ name: 'schema3',
+ desc: 'basic transport',
+ connection_info: {
+ user: {
+ type: 'String',
+ desc: 'the user to connect as',
+ },
+ password: {
+ type: 'String',
+ sensitive: true,
+ 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
+
+ context 'when a transports are added to multiple environments' do
+ it 'will record the schemas in the correct structure' do
+ change_environment(:wibble)
+ described_class.register(schema)
+ expect(described_class.instance_variable_get(:@transports)).to be_key(:wibble)
+ expect(described_class.instance_variable_get(:@transports)[:wibble][schema[:name]]).to be_a_kind_of(Puppet::ResourceApi::TransportSchemaDef)
+ expect(described_class.instance_variable_get(:@transports)[:wibble][schema[:name]].definition).to eq(schema)
+
+ change_environment(:foo)
+ described_class.register(schema)
+ described_class.register(schema2)
+ expect(described_class.instance_variable_get(:@transports)).to be_key(:foo)
+ expect(described_class.instance_variable_get(:@transports)[:foo][schema[:name]]).to be_a_kind_of(Puppet::ResourceApi::TransportSchemaDef)
+ expect(described_class.instance_variable_get(:@transports)[:foo][schema[:name]].definition).to eq(schema)
+ expect(described_class.instance_variable_get(:@transports)[:foo][schema2[:name]]).to be_a_kind_of(Puppet::ResourceApi::TransportSchemaDef)
+ expect(described_class.instance_variable_get(:@transports)[:foo][schema2[:name]].definition).to eq(schema2)
+
+ change_environment(:bar)
+ described_class.register(schema3)
+ expect(described_class.instance_variable_get(:@transports)).to be_key(:bar)
+ expect(described_class.instance_variable_get(:@transports)[:bar][schema3[:name]]).to be_a_kind_of(Puppet::ResourceApi::TransportSchemaDef)
+ expect(described_class.instance_variable_get(:@transports)[:bar][schema3[:name]].definition).to eq(schema3)
+ end
+ 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
+
+ describe '#list' do
+ subject { described_class.list }
+
+ context 'with no transports registered' do
+ it { is_expected.to eq({}) }
+ end
+
+ context 'with a transport registered' do
+ let(:schema) do
+ {
+ name: 'test_target',
+ desc: 'a basic transport',
+ connection_info: {
+ host: {
+ type: 'String',
+ desc: 'the host ip address or hostname',
+ },
+ },
+ }
+ end
+
+ before(:each) do
+ described_class.register(schema)
+ end
+
+ it { expect(described_class.list['test_target'].definition).to eq schema }
+ it 'returns a new object' do
+ expect(described_class.list['test_target'].definition.object_id).not_to eq schema.object_id
+ end
+ end
+ end
+
+ describe '#connect(name, connection_info)', agent_test: true do
+ let(:name) { 'test_target' }
+ let(:schema) do
+ {
+ name: 'test_target',
+ desc: 'a basic transport',
+ connection_info: {
+ host: {
+ type: 'String',
+ desc: 'the host ip address or hostname',
+ },
+ },
+ }
+ end
+
+ context 'when the transport file does not exist' do
+ it 'throws a LoadError' do
+ expect(described_class).to receive(:validate).with(name, host: 'example.com')
+ expect { described_class.connect(name, host: 'example.com') }.to raise_error LoadError, %r{(no such file to load|cannot load such file) -- puppet/transport/test_target}
+ end
+ end
+
+ context 'when the transport file does exist' do
+ context 'with an incorrectly defined transport' do
+ it 'throws a NameError' do
+ described_class.register(schema)
+
+ expect(described_class).to receive(:validate).with(name, host: 'example.com')
+ expect(described_class).to receive(:require).with('puppet/transport/test_target')
+ expect { described_class.connect(name, host: 'example.com') }.to raise_error NameError,
+ %r{uninitialized constant (Puppet::Transport|TestTarget)}
+ end
+ end
+
+ context 'with a correctly defined transport' do
+ let(:test_target) { double('Puppet::Transport::TestTarget') } # rubocop:disable RSpec/VerifiedDoubles
+ let(:context) { instance_double(Puppet::ResourceApi::PuppetContext, 'context') }
+
+ it 'loads initiates the class successfully' do
+ described_class.register(schema)
+
+ allow(described_class).to receive(:require).with('puppet/resource_api/puppet_context').and_call_original
+ expect(described_class).to receive(:require).with('puppet/transport/test_target')
+ expect(described_class).to receive(:validate).with(name, host: 'example.com')
+ expect(Puppet::ResourceApi::PuppetContext).to receive(:new).with(kind_of(Puppet::ResourceApi::TransportSchemaDef)).and_return(context)
+
+ stub_const('Puppet::Transport::TestTarget', test_target)
+ expect(test_target).to receive(:new).with(context, host: 'example.com')
+
+ described_class.connect(name, host: 'example.com')
+ end
+ end
+ end
+ end
+
+ describe '#inject_device(name, transport)' do
+ let(:device_name) { 'wibble' }
+ let(:transport) { instance_double(Puppet::Transport::Wibble, 'transport') }
+ let(:wrapper) { instance_double(Puppet::ResourceApi::Transport::Wrapper, 'wrapper') }
+
+ before(:each) do
+ module Puppet::Transport
+ class Wibble; end
+ end
+ end
+
+ context 'when puppet has set_device' do
+ it 'wraps the transport and calls set_device within NetworkDevice' do
+ expect(Puppet::ResourceApi::Transport::Wrapper).to receive(:new).with(device_name, transport).and_return(wrapper)
+ allow(Puppet::Util::NetworkDevice).to receive(:respond_to?).with(:set_device).and_return(true)
+ expect(Puppet::Util::NetworkDevice).to receive(:set_device).with(device_name, wrapper)
+
+ described_class.inject_device(device_name, transport)
+ end
+ end
+
+ context 'when puppet does not have set_device' do
+ it 'wraps the transport and sets it as current in NetworkDevice' do
+ expect(Puppet::ResourceApi::Transport::Wrapper).to receive(:new).with(device_name, transport).and_return(wrapper)
+ expect(Puppet::Util::NetworkDevice).to receive(:respond_to?).with(:set_device).and_return(false)
+
+ described_class.inject_device(device_name, transport)
+
+ expect(Puppet::Util::NetworkDevice.current).to eq(wrapper)
+ end
+ end
+ end
+
+ describe '#validate(name, connection_info)', agent_test: true do
+ context 'when the transport does not exist' do
+ it { expect { described_class.send(:validate, 'wibble', {}) }.to raise_error LoadError, %r{(no such file to load|cannot load such file) -- puppet/transport/schema/wibble} }
+ end
+
+ context 'when the transport being validated has not be registered' do
+ it 'will throw an unregistered error message' do
+ expect(described_class).to receive(:require).with('puppet/transport/schema/wibble')
+ expect { described_class.send(:validate, 'wibble', {}) }.to raise_error Puppet::DevError, %r{ not registered with }
+ end
+ end
+
+ context 'when the transport being validated has been registered' do
+ let(:schema) { { name: 'validate', desc: 'a minimal connection', connection_info: {} } }
+ let(:schema_def) { instance_double('Puppet::ResourceApi::TransportSchemaDef', 'schema_def') }
+
+ it 'validates the connection_info' do
+ allow(Puppet::ResourceApi::TransportSchemaDef).to receive(:new).with(schema).and_return(schema_def)
+
+ described_class.register(schema)
+
+ expect(described_class).not_to receive(:require).with('puppet/transport/schema/validate')
+ expect(schema_def).to receive(:check_schema).with('connection_info', kind_of(String)).and_return(nil)
+ expect(schema_def).to receive(:validate).with('connection_info').and_return(nil)
+
+ described_class.send :validate, 'validate', 'connection_info'
+ end
+ end
+ end
+
+ describe '#init_transports' do
+ context 'when there is not a current_environment' do
+ it 'will return the default transport environment name' do
+ change_environment
+
+ described_class.send :init_transports
+
+ expect(described_class.instance_variable_get(:@environment)).to eq(:transports_default)
+ end
+ end
+
+ context 'when there is a current_environment' do
+ it 'will return the set environment name' do
+ change_environment(:something)
+
+ described_class.send :init_transports
+
+ expect(described_class.instance_variable_get(:@environment)).to eq(:something)
+ end
+ end
+ end
+
+ describe '#wrap_sensitive(name, connection_info)' do
+ context 'when the connection info contains a `Sensitive` type' do
+ let(:schema) do
+ {
+ name: 'sensitive_transport',
+ desc: 'a secret',
+ connection_info: {
+ secret: {
+ type: 'String',
+ desc: 'A secret to protect.',
+ sensitive: true,
+ },
+ },
+ }
+ end
+ let(:schema_def) { instance_double('Puppet::ResourceApi::TransportSchemaDef', 'schema_def') }
+ let(:connection_info) do
+ {
+ secret: 'sup3r_secret_str1ng',
+ }
+ end
+
+ before(:each) do
+ allow(Puppet::ResourceApi::TransportSchemaDef).to receive(:new).with(schema).and_return(schema_def)
+ described_class.register(schema)
+ end
+
+ it 'wraps the value in a PSensitiveType' do
+ allow(schema_def).to receive(:definition).and_return(schema)
+
+ conn_info = described_class.send :wrap_sensitive, 'sensitive_transport', connection_info
+ expect(conn_info[:secret]).to be_a(Puppet::Pops::Types::PSensitiveType::Sensitive)
+ expect(conn_info[:secret].unwrap).to eq('sup3r_secret_str1ng')
+ end
+ end
+ 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..d8db9afc 100644
--- a/spec/puppet/resource_api_spec.rb
+++ b/spec/puppet/resource_api_spec.rb
@@ -1216,6 +1216,37 @@ class OtherDevice; end
it('loads the device provider') { expect(described_class.load_provider('multi_provider').name).to eq 'Puppet::Provider::MultiProvider::SomeDevice' }
end
end
+
+ context 'with a transport configured' do
+ let(:definition) { { name: 'multi_provider', attributes: {} } }
+ let(:transport) { instance_double('Puppet::ResourceApi::Transport::Wrapper', 'transport') }
+ let(:schema_def) { instance_double('Puppet::ResourceApi::TransportSchemaDef', 'schema_def') }
+
+ before(:each) do
+ allow(Puppet::Util::NetworkDevice).to receive(:current).with(no_args).and_return(transport)
+ allow(transport).to receive(:is_a?).with(Puppet::ResourceApi::Transport::Wrapper).and_return(true)
+ allow(transport).to receive(:schema).and_return(schema_def)
+ allow(schema_def).to receive(:name).and_return(schema_name)
+
+ module ::Puppet::Provider::MultiProvider
+ class MultiProvider; end
+ class SomeDevice; end
+ class OtherDevice; end
+ end
+ end
+
+ context 'with no device-specific provider' do
+ let(:schema_name) { 'multi_provider' }
+
+ it('loads the default provider') { expect(described_class.load_provider('multi_provider').name).to eq 'Puppet::Provider::MultiProvider::MultiProvider' }
+ end
+
+ context 'with a device-specific provider' do
+ let(:schema_name) { 'some_device' }
+
+ it('loads the device provider') { expect(described_class.load_provider('multi_provider').name).to eq 'Puppet::Provider::MultiProvider::SomeDevice' }
+ end
+ end
end
context 'with a provider that does canonicalization', agent_test: true do
@@ -1601,6 +1632,10 @@ def set(_context, changes)
stub_const('Puppet::Provider::Remoter::Remoter', provider_class)
allow(provider_class).to receive(:new).and_return(provider)
Puppet.settings[:strict] = :warning
+
+ module ::Puppet::Transport
+ class Wibble; end
+ end
end
it 'is seen as a supported feature' do
@@ -1618,6 +1653,30 @@ def set(_context, changes)
expect(type.context.type).to be_feature('remote_resource')
end
end
+
+ describe '#self.my_provider' do
+ subject(:type) { Puppet::Type.type(:remoter) }
+
+ let(:instance) { type.new(name: 'remote_thing', test_string: 'wibble') }
+ let(:wrapper) { instance_double('Puppet::ResourceApi::Transport::Wrapper', 'wrapper') }
+ let(:transport) { instance_double('Puppet::Transport::Wibble', 'transport') }
+
+ before(:each) do
+ allow(described_class).to receive(:load_provider).and_return(provider)
+ allow(provider).to receive(:new).and_return(provider)
+ end
+
+ context 'when a transport is returned by NetworkDevice.current' do
+ it 'stores the provider with the the name of the transport' do
+ allow(Puppet::Util::NetworkDevice).to receive(:current).and_return(wrapper)
+ allow(wrapper).to receive(:is_a?).with(Puppet::ResourceApi::Transport::Wrapper).and_return(true)
+ allow(wrapper).to receive(:transport).and_return(transport)
+ allow(transport).to receive(:class).and_return(Puppet::Transport::Wibble)
+
+ expect(instance.my_provider).to eq provider
+ end
+ end
+ end
end
context 'with a `supports_noop` provider', agent_test: true do
@@ -1825,4 +1884,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