Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions lib/puppet/provider/pulpcore.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# frozen_string_literal: true

require 'puppet/provider'

# Base provider for logic common to all Pulpcore providers, regardless of the
# concrete implementation used to communicate with Pulp.
class Puppet::Provider::Pulpcore < Puppet::Provider
# Define property getters that read the current value from @property_hash,
# reporting an unset value as :absent (see #property_hash_value).
def self.mk_property_hash_getters(*property_names)
property_names.each do |property_name|
property_name = property_name.to_sym

define_method(property_name) do
property_hash_value(property_name)
end
end
end

# Define `property=` setters that record the new value in @property_flush.
def self.mk_property_flush_setters(*property_names)
property_names.each do |property_name|
property_name = property_name.to_sym

define_method("#{property_name}=") do |value|
@property_flush[property_name] = value
end
end
end

# Define `property=` setters for removable properties. Setting the property to
# `:absent` records an empty string in @property_flush, which concrete
# providers translate into "clear this field" when talking to Pulp.
def self.mk_absent_clearing_setters(*property_names)
property_names.each do |property_name|
property_name = property_name.to_sym

define_method("#{property_name}=") do |value|
@property_flush[property_name] = value == :absent ? '' : value
end
end
end

def self.resource_api_hashes
raise Puppet::DevError, "#{self} must implement .resource_api_hashes"
end

def self.resource_api_hash(_resource_name)
raise Puppet::DevError, "#{self} must implement .resource_api_hash"
end

def self.resource_properties_from_api_hash(_api_hash)
raise Puppet::DevError, "#{self} must implement .resource_properties_from_api_hash"
end

def self.api_hash_by_href(_href)
raise Puppet::DevError, "#{self} must implement .api_hash_by_href"
end

# Resolve a Pulp resource href to the referenced resource's name, returning
# :absent when no href is set. Lookups are memoised per provider class so that
# many resources referencing the same href only trigger one API call.
def self.name_by_href(href)
return :absent if href.nil?

@name_by_href ||= {}
@name_by_href[href] ||= api_hash_by_href(href)['name']
end

def initialize(value = {})
super(value)
@property_flush = {}
end

def self.prefetch(resources)
instances.each do |prov|
if (resource = resources[prov.name])
resource.provider = prov
end
end
end

def self.instances
resource_api_hashes.map do |api_hash|
properties = resource_properties_from_api_hash(api_hash)
next if properties.empty?

new(properties)
end.compact
end

# Fetch and map the current properties for a single resource. A failed `show`
# (most commonly because the resource does not exist) raises an
# ExecutionFailure; treat that as "no properties" so the resource reads as
# absent rather than aborting the whole run.
def self.resource_properties(resource_name)
resource_properties_from_api_hash(resource_api_hash(resource_name))
rescue Puppet::ExecutionFailure => e
warning "#resource_properties had an error -> #{e.inspect}"
{}
end

def exists?
@property_hash[:ensure] == :present
end

def create
@property_flush[:ensure] = :present
end

def destroy
@property_flush[:ensure] = :absent
end

def flush
case @property_flush[:ensure]
when :absent
delete_resource
@property_hash = {}
when :present
create_resource
update_property_hash
else
update_resource
update_property_hash
end

@property_flush.clear
end

def update_property_hash
@property_hash = self.class.resource_properties(resource[:name])
end

def create_resource
raise Puppet::DevError, "#{self.class} must implement #create_resource"
end

def update_resource
raise Puppet::DevError, "#{self.class} must implement #update_resource"
end

def delete_resource
raise Puppet::DevError, "#{self.class} must implement #delete_resource"
end

private

# Puppet treats :absent as "this property is not set", so map a missing or
# nil stored value to :absent for comparison against the desired state.
def property_hash_value(property_name)
value = @property_hash[property_name]
value.nil? ? :absent : value
end
end
60 changes: 60 additions & 0 deletions lib/puppet/provider/pulpcore_cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require 'json'
require 'tempfile'

# Mixin for concrete providers that manage Pulp resources through the `pulp` CLI.
module Puppet::Provider::PulpcoreCli
def self.included(provider_class)
provider_class.commands pulp_binary: '/usr/bin/pulp'
provider_class.extend(ClassMethods)
end

module ClassMethods
def pulp(*args)
pulp_binary('--format', 'json', *args)
end

def api_hash_by_href(href)
parse_pulp_json(pulp('show', '--href', href))
end

# Parse a JSON response from the Pulp CLI, failing hard with a useful error
# if the output is not valid JSON (e.g. a warning line or truncated list)
# rather than letting a bare JSON::ParserError surface or silently treating
# the resource as absent.
def parse_pulp_json(response)
JSON.parse(response)
rescue JSON::ParserError => e
raise Puppet::Error, "Unable to parse the Pulp CLI JSON response (#{e.message}). Response began with: #{response.to_s.strip[0, 200].inspect}"
end
end

def pulp(*args)
self.class.pulp(*args)
end

# Write sensitive or large option values to temporary files and pass them to
# the Pulp CLI as @file arguments. The block must run the command while the
# files are still open.
def with_temp_file_arguments(file_arguments)
tempfiles = []
command_arguments = []

begin
file_arguments.each do |option, content|
tempfile = Tempfile.new
tempfile.chmod(0o600)
tempfile.write(content)
tempfile.flush

tempfiles << tempfile
command_arguments << option << "@#{tempfile.path}"
end

yield command_arguments
ensure
tempfiles.each(&:close!)
end
end
end
34 changes: 34 additions & 0 deletions lib/puppet/provider/pulpcore_rpm_distribution.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require_relative 'pulpcore'

# Abstract provider for Pulpcore RPM distribution resources.
#
# This class contains behaviour common to all `pulpcore_rpm_distribution`
# providers. Concrete implementations, such as the `cli` provider, inherit
# from it.
class Puppet::Provider::PulpcoreRpmDistribution < Puppet::Provider::Pulpcore
mk_property_hash_getters(
:base_path,
:repo,
:checkpoint
)
mk_property_flush_setters(:base_path, :checkpoint)
mk_absent_clearing_setters(:repo)

def self.resource_properties_from_api_hash(distribution_properties)
resource_properties = {
name: distribution_properties['name'],
ensure: :present,
provider: name,

base_path: distribution_properties['base_path'],
repo: name_by_href(distribution_properties['repository']),
checkpoint: distribution_properties['checkpoint'] ? :true : :false
}

debug "Distribution resource properties: #{resource_properties.inspect}"

resource_properties
end
end
60 changes: 60 additions & 0 deletions lib/puppet/provider/pulpcore_rpm_distribution/cli.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# frozen_string_literal: true

require_relative '../pulpcore_cli'
require_relative '../pulpcore_rpm_distribution'

Puppet::Type.type(:pulpcore_rpm_distribution).provide(:cli, parent: Puppet::Provider::PulpcoreRpmDistribution) do
include Puppet::Provider::PulpcoreCli

def self.resource_api_hashes
parse_pulp_json(pulp('rpm', 'distribution', 'list', '--limit', 1_000_000))
end

def self.resource_api_hash(distribution_name)
parse_pulp_json(pulp('rpm', 'distribution', 'show', '--name', distribution_name))
end

# base_path is mandatory to create a distribution but optional once it exists
# (you may manage only a subset of properties), so it can't be a required
# type-level param. Enforce it here, at create time.
def create_resource
raise ArgumentError, '`base_path` is a required property when creating a `Pulpcore_rpm_distribution` resource.' unless resource[:base_path]

command_arguments = [
'rpm', 'distribution', 'create',
'--name', resource[:name],
'--base-path', resource[:base_path]
]

command_arguments << '--repository' << resource[:repo] if resource[:repo] && resource[:repo] != :absent

case resource[:checkpoint]
when :true
command_arguments << '--checkpoint'
when :false
command_arguments << '--not-checkpoint'
end

pulp(*command_arguments)
end

def update_resource
command_arguments = ['rpm', 'distribution', 'update', '--name', resource[:name]]

command_arguments << '--base-path' << @property_flush[:base_path] if @property_flush.key?(:base_path)
command_arguments << '--repository' << @property_flush[:repo] if @property_flush.key?(:repo)

case @property_flush[:checkpoint]
when :true
command_arguments << '--checkpoint'
when :false
command_arguments << '--not-checkpoint'
end

pulp(*command_arguments)
end

def delete_resource
pulp('rpm', 'distribution', 'destroy', '--name', resource[:name])
end
end
65 changes: 65 additions & 0 deletions lib/puppet/provider/pulpcore_rpm_remote.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# frozen_string_literal: true

require_relative 'pulpcore'

# Abstract provider for Pulpcore RPM remote resources.
#
# This class contains behaviour common to all `pulpcore_rpm_remote` providers.
# Concrete implementations, such as the `cli` provider, inherit from it.
class Puppet::Provider::PulpcoreRpmRemote < Puppet::Provider::Pulpcore
mk_property_hash_getters(
:url,
:policy,
:tls_validation,
:client_cert,
:ca_cert
)
mk_property_flush_setters(:url, :policy, :tls_validation)
mk_absent_clearing_setters(:client_cert, :ca_cert)

def self.resource_properties_from_api_hash(remote_properties)
resource_properties = {
name: remote_properties['name'],
ensure: :present,
provider: name,

url: remote_properties['url'],
policy: remote_properties['policy'],
tls_validation: remote_properties['tls_validation'] ? :true : :false,

client_cert: remote_properties['client_cert'] || :absent,
ca_cert: remote_properties['ca_cert'] || :absent,

# Internal provider state, not a Puppet property.
client_key_set: hidden_field_set?(remote_properties, 'client_key')
}

debug "Remote resource properties: #{resource_properties.inspect}"

resource_properties
end

# Pulp never returns secret values (such as client_key) in an API response.
# Instead each appears in the `hidden_fields` array with an `is_set` flag
# reporting whether a value is currently stored. Return that flag for the
# named field, raising if the response isn't shaped as expected.
def self.hidden_field_set?(api_hash, field_name)
hidden_fields = api_hash.fetch('hidden_fields') do
raise Puppet::Error, 'Pulp API response did not include hidden_fields.'
end

raise Puppet::Error, 'Pulp API response hidden_fields was nil.' if hidden_fields.nil?

hidden_field = hidden_fields.find do |field|
field['name'] == field_name
end

raise Puppet::Error, "Pulp API response did not include #{field_name} in hidden_fields." unless hidden_field

hidden_field['is_set']
end

def client_key_set?
@property_hash[:client_key_set] == true
end
end
Loading