From: David Schmitt Date: Wed, 12 Apr 2017 15:23:04 +0000 (+0100) Subject: Start building out the next phase resource API shim X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=c3b3063d5004732528b383a23b67c2f35e40505a;p=puppet-modules%2Fpuppetlabs-apt.git Start building out the next phase resource API shim --- diff --git a/lib/puppet/provider/apt_key/apt_key2.rb b/lib/puppet/provider/apt_key/apt_key2.rb new file mode 100644 index 0000000..ae7200a --- /dev/null +++ b/lib/puppet/provider/apt_key/apt_key2.rb @@ -0,0 +1,162 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', 'puppet_x', 'apt_key', 'resource_api.rb')) + +require 'open-uri' +require 'net/ftp' +require 'tempfile' + +if RUBY_VERSION == '1.8.7' + # Mothers cry, puppies die and Ruby 1.8.7's open-uri needs to be + # monkeypatched to support passing in :ftp_passive_mode. + require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', '..', + 'puppet_x', 'apt_key', 'patch_openuri.rb')) + OpenURI::Options.merge!({:ftp_active_mode => false,}) +end + +register_provider('apt_key2') do + commands apt_key: 'apt-key' + commands gpg: '/usr/bin/gpg' + + def canonicalize(resources) + resources.collect do |r| + r[:id] = r[:id].upcase + end + end + + def get(names = []) + cli_args = %w(adv --list-keys --with-colons --fingerprint --fixed-list-mode) + key_output = apt_key(cli_args).encode('UTF-8', 'binary', :invalid => :replace, :undef => :replace, :replace => '') + pub_line = nil + fpr_line = nil + + result = key_output.split("\n").collect do |line| + if line.start_with?('pub') + pub_line = line + elsif line.start_with?('fpr') + fpr_line = line + end + + next unless (pub_line and fpr_line) + + hash = key_line_to_hash(pub_line, fpr_line) + + # reset scanning + pub_line = nil + fpr_line = nil + + hash + end.compact! + + result + end + + def self.key_line_to_hash(pub_line, fpr_line) + pub_split = pub_line.split(':') + fpr_split = fpr_line.split(':') + + # set key type based on types defined in /usr/share/doc/gnupg/DETAILS.gz + key_type = case pub_split[3] + when '1' + :rsa + when '17' + :dsa + when '18' + :ecc + when '19' + :ecdsa + else + :unrecognized + end + + fingerprint = fpr_split.last + expiry = pub_split[6].empty? ? nil : Time.at(pub_split[6].to_i) + + { + ensure: 'present', + id: fingerprint, + fingerprint: fingerprint, + long: fingerprint[-16..-1], # last 16 characters of fingerprint + short: fingerprint[-8..-1], # last 8 characters of fingerprint + size: pub_split[2].to_i, + type: key_type, + created: Time.at(pub_split[5].to_i), + expiry: expiry, + expired: !!(expiry && Time.now >= expiry), + } + end + + def set(current_state, target_state, noop = false) + target_state.each do |title, resource| + logger.warning(title, 'The id should be a full fingerprint (40 characters) to avoid collision attacks, see the README for details.') if title.length < 40 + if resource[:source] and resource[:content] + logger.fail(title, 'The properties content and source are mutually exclusive') + next + end + + current = current_state[title] + if current && resource[:ensure].to_s == 'absent' + logger.deleting(title) do + begin + apt_key('del', resource[:short], noop: noop) + r = execute(["#{command(:apt_key)} list | grep '/#{resource[:short]}\s'"], :failonfail => false) + end while r.exitstatus == 0 + end + elsif current && resource[:ensure].to_s == 'present' + # No updating implemented + # update(key, noop: noop) + elsif !current && resource[:ensure].to_s == 'present' + create(title, resource, noop: noop) + end + end + end + + def create(title, resource, noop = false) + logger.creating(title) do |logger| + if resource[:source].nil? and resource[:content].nil? + # Breaking up the command like this is needed because it blows up + # if --recv-keys isn't the last argument. + args = ['adv', '--keyserver', resource[:server]] + if resource[:options] + args.push('--keyserver-options', resource[:options]) + end + args.push('--recv-keys', resource[:id]) + apt_key(*args, noop: noop) + elsif resource[:content] + temp_key_file(resource[:content], logger) do |key_file| + apt_key('add', key_file, noop: noop) + end + elsif resource[:source] + key_file = source_to_file(resource[:source]) + apt_key('add', key_file.path, noop: noop) + # In case we really screwed up, better safe than sorry. + else + logger.fail("an unexpected condition occurred while trying to add the key: #{title} (content: #{resource[:content].inspect}, source: #{resource[:source].inspect})") + end + end + end + + # This method writes out the specified contents to a temporary file and + # confirms that the fingerprint from the file, matches the long key that is in the manifest + def temp_key_file(resource, logger) + file = Tempfile.new('apt_key') + begin + file.write resource[:content] + file.close + if name.size == 40 + if File.executable? command(:gpg) + extracted_key = execute(["#{command(:gpg)} --with-fingerprint --with-colons #{file.path} | awk -F: '/^fpr:/ { print $10 }'"], :failonfail => false) + extracted_key = extracted_key.chomp + + unless extracted_key.match(/^#{name}$/) + logger.fail("The id in your manifest #{resource[:id]} and the fingerprint from content/source do not match. Please check there is not an error in the id or check the content/source is legitimate.") + end + else + logger.warning('/usr/bin/gpg cannot be found for verification of the id.') + end + end + yield file.path + ensure + file.close + file.unlink + end + end +end diff --git a/lib/puppet/type/apt_key2.rb b/lib/puppet/type/apt_key2.rb new file mode 100644 index 0000000..4cccaf8 --- /dev/null +++ b/lib/puppet/type/apt_key2.rb @@ -0,0 +1,91 @@ +require File.expand_path(File.join(File.dirname(__FILE__), '..', '..', 'puppet_x', 'apt_key', 'resource_api.rb')) + +register_type({ + name: 'apt_key2', + docs: <<-EOS, + This type provides Puppet with the capabilities to manage GPG keys needed + by apt to perform package validation. Apt has it's own GPG keyring that can + be manipulated through the `apt-key` command. + + apt_key { '6F6B15509CF8E59E6E469F327F438280EF8D349F': + source => 'http://apt.puppetlabs.com/pubkey.gpg' + } + + **Autorequires**: + If Puppet is given the location of a key file which looks like an absolute + path this type will autorequire that file. + EOS + attributes: { + ensure: { + type: 'Enum[present, absent]', + docs: 'Whether this apt key should be present or absent on the target system.' + }, + id: { + type: 'Variant[Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{16}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{40}\Z/]]', + docs: 'The ID of the key you want to manage.', + namevar: true, + }, + content: { + type: 'Optional[String]', + docs: 'The content of, or string representing, a GPG key.', + }, + source: { + type: 'Variant[Stdlib::Absolutepath, Pattern[/\A(https?|ftp):\/\//]]', + docs: 'Location of a GPG key file, /path/to/file, ftp://, http:// or https://', + }, + server: { + type: 'Pattern[/\A((hkp|http|https):\/\/)?([a-z\d])([a-z\d-]{0,61}\.)+[a-z\d]+(:\d{2,5})?$/]', + docs: 'The key server to fetch the key from based on the ID. It can either be a domain name or url.', + default: :'keyserver.ubuntu.com' + }, + options: { + type: 'Optional[String]', + docs: 'Additional options to pass to apt-key\'s --keyserver-options.', + }, + fingerprint: { + type: 'Pattern[/[a-f]{40}/]', + docs: 'The 40-digit hexadecimal fingerprint of the specified GPG key.', + read_only: true, + }, + long: { + type: 'Pattern[/[a-f]{16}/]', + docs: 'The 16-digit hexadecimal id of the specified GPG key.', + read_only: true, + }, + short: { + type: 'Pattern[/[a-f]{8}/]', + docs: 'The 8-digit hexadecimal id of the specified GPG key.', + read_only: true, + }, + expired: { + type: 'Boolean', + docs: 'Indicates if the key has expired.', + read_only: true, + }, + expiry: { + # TODO: should be DateTime + type: 'String', + docs: 'The date the key will expire, or nil if it has no expiry date, in ISO format.', + read_only: true, + }, + size: { + type: 'Integer', + docs: 'The key size, usually a multiple of 1024.', + read_only: true, + }, + type: { + type: 'String', + docs: 'The key type, one of: rsa, dsa, ecc, ecdsa.', + read_only: true, + }, + created: { + type: 'String', + docs: 'Date the key was created, in ISO format.', + read_only: true, + }, + }, + autorequires: { + file: '$source', # will evaluate to the value of the `source` attribute + package: 'apt', + }, +}) diff --git a/lib/puppet_x/apt_key/resource_api.rb b/lib/puppet_x/apt_key/resource_api.rb new file mode 100644 index 0000000..b947528 --- /dev/null +++ b/lib/puppet_x/apt_key/resource_api.rb @@ -0,0 +1,206 @@ +require 'pathname' +require 'pry' + +module Puppet::SimpleResource + class TypeShim + attr_reader :values + + def initialize(title, resource_hash) + # internalize and protect - needs to go deeper + @values = resource_hash.dup + # "name" is a privileged key + @values[:name] = title + @values.freeze + end + + def to_resource + ResourceShim.new(@values) + end + + def name + values[:name] + end + end + + class ResourceShim + attr_reader :values + + def initialize(resource_hash) + @values = resource_hash.dup.freeze # whatevs + end + + def title + values[:name] + end + + def prune_parameters(*args) + # puts "not pruning #{args.inspect}" if args.length > 0 + self + end + + def to_manifest + [ + "apt_key { #{values[:name].inspect}: ", + ] + values.keys.select { |k| k != :name }.collect { |k| " #{k} => #{values[k].inspect}," } + ['}'] + end + end +end + +def register_type(definition) + Puppet::Type.newtype(definition[:name].to_sym) do + @docs = definition[:docs] + has_namevar = false + namevar_name = nil + + definition[:attributes].each do |name, options| + # puts "#{name}: #{options.inspect}" + + # TODO: using newparam everywhere would suppress change reporting + # that would allow more fine-grained reporting through logger, + # but require more invest in hooking up the infrastructure to emulate existing data + param_or_property = if options[:read_only] || options[:namevar] + :newparam + else + :newproperty + end + send(param_or_property, name.to_sym) do + unless options[:type] + fail("#{definition[:name]}.#{name} has no type") + end + + if options[:docs] + desc "#{options[:docs]} (a #{options[:type]}" + else + warn("#{definition[:name]}.#{name} has no docs") + end + + if options[:namevar] + # puts 'setting namevar' + isnamevar + has_namevar = true + namevar_name = name + end + + # read-only values do not need type checking, but can have default values + if not options[:read_only] + # TODO: this should use Pops infrastructure to avoid hardcoding stuff, and enhance type fidelity + # validate do |v| + # type = Puppet::Pops::Types::TypeParser.singleton.parse(options[:type]).normalize + # if type.instance?(v) + # return true + # else + # inferred_type = Puppet::Pops::Types::TypeCalculator.infer_set(value) + # error_msg = Puppet::Pops::Types::TypeMismatchDescriber.new.describe_mismatch("#{DEFINITION[:name]}.#{name}", type, inferred_type) + # raise Puppet::ResourceError, error_msg + # end + # end + + if options.has_key? :default + defaultto options[:default] + end + + case options[:type] + when 'String' + # require any string value + newvalue // do + end + when 'Boolean' + ['true', 'false', :true, :false, true, false].each do |v| + newvalue v do + end + end + + munge do |v| + case v + when 'true', :true + true + when 'false', :false + false + else + v + end + end + when 'Integer' + newvalue /^\d+$/ do + end + munge do |v| + Puppet::Pops::Utils.to_n(v) + end + when 'Float', 'Numeric' + newvalue Puppet::Pops::Patterns::NUMERIC do + end + munge do |v| + Puppet::Pops::Utils.to_n(v) + end + when 'Enum[present, absent]' + newvalue :absent do + end + newvalue :present do + end + when 'Variant[Pattern[/\A(0x)?[0-9a-fA-F]{8}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{16}\Z/], Pattern[/\A(0x)?[0-9a-fA-F]{40}\Z/]]' + # the namevar needs to be a Parameter, which only has newvalue*s* + newvalues(/\A(0x)?[0-9a-fA-F]{8}\Z/, /\A(0x)?[0-9a-fA-F]{16}\Z/, /\A(0x)?[0-9a-fA-F]{40}\Z/) + when 'Optional[String]' + newvalue :undef do + end + newvalue // do + end + when 'Variant[Stdlib::Absolutepath, Pattern[/\A(https?|ftp):\/\//]]' + # TODO: this is wrong, but matches original implementation + [/^\//, /\A(https?|ftp):\/\//].each do |v| + newvalue v do + end + end + when /^(Enum|Optional|Variant)/ + fail("#{$1} is not currently supported") + end + end + end + end + + def self.instances + puts 'instances' + # klass = Puppet::Type.type(:api) + get.collect do |resource_hash| + Puppet::SimpleResource::TypeShim.new(resource_hash[namevar_name], resource_hash) + end + end + + def retrieve + puts 'retrieve' + result = Puppet::Resource.new(self.class, title) + current_state = self.class.get.find { |h| h[namevar_name] == title } + + if current_state + current_state.each do |k, v| + result[k]=v + end + else + result[:ensure] = :absent + end + + @rapi_current_state = current_state + result + end + + def flush + puts 'flush' + # binding.pry + target_state = self.class.canonicalize([Hash[@parameters.collect { |k, v| [k, v.value] }]]) + if @rapi_current_state != target_state + self.class.set({title => @rapi_current_state}, {title => target_state}, false) + else + puts 'no changes' + end + end + + def self.commands(*args) + puts "registering command: #{args.inspect}" + end + end +end + +def register_provider(typename, &block) + type = Puppet::Type.type(typename.to_sym) + type.instance_eval &block +end diff --git a/spec/unit/puppet/type/apt_key_spec.rb b/spec/unit/puppet/type/apt_key_spec.rb index 9c2dd91..330b8fa 100644 --- a/spec/unit/puppet/type/apt_key_spec.rb +++ b/spec/unit/puppet/type/apt_key_spec.rb @@ -1,188 +1,190 @@ require 'spec_helper' require 'puppet' -describe Puppet::Type::type(:apt_key) do - context 'only namevar 32bit key id' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F' - )} - it 'id is set' do - expect(resource[:id]).to eq 'EF8D349F' - end - - it 'name is set to id' do - expect(resource[:name]).to eq 'EF8D349F' - end - - it 'keyserver is default' do - expect(resource[:server]).to eq :'keyserver.ubuntu.com' - end - - it 'source is not set' do - expect(resource[:source]).to eq nil - end - - it 'content is not set' do - expect(resource[:content]).to eq nil - end - end - - context 'with a lowercase 32bit key id' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => 'ef8d349f' - )} - it 'id is set' do - expect(resource[:id]).to eq 'EF8D349F' - end - end - - context 'with a 64bit key id' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => 'FFFFFFFFEF8D349F' - )} - it 'id is set' do - expect(resource[:id]).to eq 'FFFFFFFFEF8D349F' - end - end - - context 'with a 0x formatted key id' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => '0xEF8D349F' - )} - it 'id is set' do - expect(resource[:id]).to eq 'EF8D349F' - end - end - - context 'with a 0x formatted lowercase key id' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => '0xef8d349f' - )} - it 'id is set' do - expect(resource[:id]).to eq 'EF8D349F' - end - end - - context 'with a 0x formatted 64bit key id' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => '0xFFFFFFFFEF8D349F' - )} - it 'id is set' do - expect(resource[:id]).to eq 'FFFFFFFFEF8D349F' - end - end - - context 'with source' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :source => 'http://apt.puppetlabs.com/pubkey.gpg' - )} - - it 'source is set to the URL' do - expect(resource[:source]).to eq 'http://apt.puppetlabs.com/pubkey.gpg' - end - end - - context 'with content' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :content => 'http://apt.puppetlabs.com/pubkey.gpg' - )} - - it 'content is set to the string' do - expect(resource[:content]).to eq 'http://apt.puppetlabs.com/pubkey.gpg' - end - end - - context 'with keyserver' do - let(:resource) { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :server => 'http://keyring.debian.org' - )} - - it 'keyserver is set to Debian' do - expect(resource[:server]).to eq 'http://keyring.debian.org' - end - end - - context 'validation' do - it 'raises an error if content and source are set' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :source => 'http://apt.puppetlabs.com/pubkey.gpg', - :content => 'Completely invalid as a GPG key' - )}.to raise_error(/content and source are mutually exclusive/) - end - - it 'raises an error if a weird length key is used' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'FEF8D349F', - :source => 'http://apt.puppetlabs.com/pubkey.gpg', - :content => 'Completely invalid as a GPG key' - )}.to raise_error(/Valid values match/) - end - - it 'raises an error when an invalid URI scheme is used in source' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :source => 'hkp://pgp.mit.edu' - )}.to raise_error(/Valid values match/) - end - - it 'allows the http URI scheme in source' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :source => 'http://pgp.mit.edu' - )}.to_not raise_error - end - - it 'allows the http URI with username and password' do - expect { Puppet::Type.type(:apt_key).new( - :id => '4BD6EC30', - :source => 'http://testme:Password2@pgp.mit.edu' - )}.to_not raise_error - end - - it 'allows the https URI scheme in source' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :source => 'https://pgp.mit.edu' - )}.to_not raise_error - end - - it 'allows the https URI with username and password' do - expect { Puppet::Type.type(:apt_key).new( +[:apt_key, :apt_key2].each do |typename| + describe Puppet::Type::type(typename) do + context 'only namevar 32bit key id' do + let(:resource) { Puppet::Type.type(typename).new( + :id => 'EF8D349F' + )} + it 'id is set' do + expect(resource[:id]).to eq 'EF8D349F' + end + + it 'name is set to id' do + expect(resource[:name]).to eq 'EF8D349F' + end + + it 'keyserver is default' do + expect(resource[:server]).to eq :'keyserver.ubuntu.com' + end + + it 'source is not set' do + expect(resource[:source]).to eq nil + end + + it 'content is not set' do + expect(resource[:content]).to eq nil + end + end + + context 'with a lowercase 32bit key id' do + let(:resource) { Puppet::Type.type(typename).new( + :id => 'ef8d349f' + )} + it 'id is set' do + expect(resource[:id]).to eq 'EF8D349F' + end + end + + context 'with a 64bit key id' do + let(:resource) { Puppet::Type.type(typename).new( + :id => 'FFFFFFFFEF8D349F' + )} + it 'id is set' do + expect(resource[:id]).to eq 'FFFFFFFFEF8D349F' + end + end + + context 'with a 0x formatted key id' do + let(:resource) { Puppet::Type.type(typename).new( + :id => '0xEF8D349F' + )} + it 'id is set' do + expect(resource[:id]).to eq 'EF8D349F' + end + end + + context 'with a 0x formatted lowercase key id' do + let(:resource) { Puppet::Type.type(typename).new( + :id => '0xef8d349f' + )} + it 'id is set' do + expect(resource[:id]).to eq 'EF8D349F' + end + end + + context 'with a 0x formatted 64bit key id' do + let(:resource) { Puppet::Type.type(typename).new( + :id => '0xFFFFFFFFEF8D349F' + )} + it 'id is set' do + expect(resource[:id]).to eq 'FFFFFFFFEF8D349F' + end + end + + context 'with source' do + let(:resource) { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :source => 'http://apt.puppetlabs.com/pubkey.gpg' + )} + + it 'source is set to the URL' do + expect(resource[:source]).to eq 'http://apt.puppetlabs.com/pubkey.gpg' + end + end + + context 'with content' do + let(:resource) { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :content => 'http://apt.puppetlabs.com/pubkey.gpg' + )} + + it 'content is set to the string' do + expect(resource[:content]).to eq 'http://apt.puppetlabs.com/pubkey.gpg' + end + end + + context 'with keyserver' do + let(:resource) { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :server => 'http://keyring.debian.org' + )} + + it 'keyserver is set to Debian' do + expect(resource[:server]).to eq 'http://keyring.debian.org' + end + end + + context 'validation' do + it 'raises an error if content and source are set' do + expect { Puppet::Type.type(typename).new( :id => 'EF8D349F', - :source => 'https://testme:Password2@pgp.mit.edu' - )}.to_not raise_error - end + :source => 'http://apt.puppetlabs.com/pubkey.gpg', + :content => 'Completely invalid as a GPG key' + )}.to raise_error(/content and source are mutually exclusive/) + end + + it 'raises an error if a weird length key is used' do + expect { Puppet::Type.type(typename).new( + :id => 'FEF8D349F', + :source => 'http://apt.puppetlabs.com/pubkey.gpg', + :content => 'Completely invalid as a GPG key' + )}.to raise_error(/Valid values match/) + end + + it 'raises an error when an invalid URI scheme is used in source' do + expect { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :source => 'hkp://pgp.mit.edu' + )}.to raise_error(/Valid values match/) + end - it 'allows the ftp URI scheme in source' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :source => 'ftp://pgp.mit.edu' - )}.to_not raise_error - end + it 'allows the http URI scheme in source' do + expect { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :source => 'http://pgp.mit.edu' + )}.to_not raise_error + end + + it 'allows the http URI with username and password' do + expect { Puppet::Type.type(typename).new( + :id => '4BD6EC30', + :source => 'http://testme:Password2@pgp.mit.edu' + )}.to_not raise_error + end + + it 'allows the https URI scheme in source' do + expect { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :source => 'https://pgp.mit.edu' + )}.to_not raise_error + end + + it 'allows the https URI with username and password' do + expect { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :source => 'https://testme:Password2@pgp.mit.edu' + )}.to_not raise_error + end + + it 'allows the ftp URI scheme in source' do + expect { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :source => 'ftp://pgp.mit.edu' + )}.to_not raise_error + end - it 'allows an absolute path in source' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :source => '/path/to/a/file' - )}.to_not raise_error - end + it 'allows an absolute path in source' do + expect { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :source => '/path/to/a/file' + )}.to_not raise_error + end - it 'allows 5-digit ports' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :source => 'http://pgp.mit.edu:12345/key' - )}.to_not raise_error - end + it 'allows 5-digit ports' do + expect { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :source => 'http://pgp.mit.edu:12345/key' + )}.to_not raise_error + end - it 'allows 5-digit ports when using key servers' do - expect { Puppet::Type.type(:apt_key).new( - :id => 'EF8D349F', - :server => 'http://pgp.mit.edu:12345' - )}.to_not raise_error + it 'allows 5-digit ports when using key servers' do + expect { Puppet::Type.type(typename).new( + :id => 'EF8D349F', + :server => 'http://pgp.mit.edu:12345' + )}.to_not raise_error + end end end end