]> review.fuel-infra Code Review - puppet-modules/puppetlabs-apt.git/commitdiff
Start building out the next phase resource API shim
authorDavid Schmitt <david.schmitt@puppet.com>
Wed, 12 Apr 2017 15:23:04 +0000 (16:23 +0100)
committerDavid Schmitt <david.schmitt@puppet.com>
Mon, 11 Sep 2017 09:51:46 +0000 (10:51 +0100)
lib/puppet/provider/apt_key/apt_key2.rb [new file with mode: 0644]
lib/puppet/type/apt_key2.rb [new file with mode: 0644]
lib/puppet_x/apt_key/resource_api.rb [new file with mode: 0644]
spec/unit/puppet/type/apt_key_spec.rb

diff --git a/lib/puppet/provider/apt_key/apt_key2.rb b/lib/puppet/provider/apt_key/apt_key2.rb
new file mode 100644 (file)
index 0000000..ae7200a
--- /dev/null
@@ -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 (file)
index 0000000..4cccaf8
--- /dev/null
@@ -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 (file)
index 0000000..b947528
--- /dev/null
@@ -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
index 9c2dd91a4f5faf72fc84f31e108ff0c73dbf8542..330b8fa47d155d1416d3c4fac819ae30dc68dc68 100644 (file)
 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