Skip to content

Commit

Permalink
Merge pull request #285 from michaeltlombardi/gh-225/main/custom-insync
Browse files Browse the repository at this point in the history
(GH-225) Add support for custom insync
  • Loading branch information
joshcooper authored Jun 3, 2021
2 parents dab8aa9 + b213ec8 commit b388f03
Show file tree
Hide file tree
Showing 66 changed files with 1,497 additions and 76 deletions.
2 changes: 1 addition & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
require: rubocop-rspec
AllCops:
TargetRubyVersion: '2.1'
TargetRubyVersion: '2.5'
Include:
- "**/*.rb"
Exclude:
Expand Down
2 changes: 2 additions & 0 deletions contrib/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Code modified from: https://gist.github.com/hanloong/9849098
require 'English'

Expand Down
82 changes: 37 additions & 45 deletions lib/puppet/resource_api.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'pathname'
require 'puppet/resource_api/data_type_handling'
require 'puppet/resource_api/glue'
Expand Down Expand Up @@ -123,6 +125,22 @@ def rsapi_title
@rsapi_title
end

def rsapi_canonicalized_target_state
@rsapi_canonicalized_target_state ||= begin
# skip puppet's injected metaparams
actual_params = @parameters.select { |k, _v| type_definition.attributes.key? k }
target_state = Hash[actual_params.map { |k, v| [k, v.rs_value] }]
target_state = my_provider.canonicalize(context, [target_state]).first if type_definition.feature?('canonicalize')
target_state
end
@rsapi_canonicalized_target_state
end

def rsapi_current_state
refresh_current_state unless @rsapi_current_state
@rsapi_current_state
end

def to_resource
to_resource_shim(super)
end
Expand Down Expand Up @@ -166,6 +184,21 @@ def to_resource_shim(resource)
raise_missing_params if @missing_params.any?
end

# If the custom_insync feature is specified but no insyncable attributes are included
# in the definition, add the hidden rsapi_custom_insync_trigger property.
# This property exists *only* to allow a resource without properties to still execute an
# insync check; there's no point in specifying it in a manifest as it can only have one
# value; it cannot be specified in a type definition as it should only exist in this case.
if type_definition.feature?('custom_insync') && type_definition.insyncable_attributes.empty?
custom_insync_trigger_options = {
type: 'Enum[do_not_specify_in_manifest]',
desc: 'A hidden property which enables a type with custom insync to perform an insync check without specifying any insyncable properties',
default: 'do_not_specify_in_manifest',
}

type_definition.create_attribute_in(self, :rsapi_custom_insync_trigger, :newproperty, Puppet::ResourceApi::Property, custom_insync_trigger_options)
end

definition[:attributes].each do |name, options|
# puts "#{name}: #{options.inspect}"

Expand All @@ -189,43 +222,7 @@ def to_resource_shim(resource)
parent = Puppet::ResourceApi::Property
end

# This call creates a new parameter or property with all work-arounds or
# customizations required by the Resource API applied. Under the hood,
# this maps to the relevant DSL methods in Puppet::Type. See
# https://puppet.com/docs/puppet/6.0/custom_types.html#reference-5883
# for details.
send(param_or_property, name.to_sym, parent: parent) do
if options[:desc]
desc "#{options[:desc]} (a #{options[:type]})"
end

# The initialize method is called when puppet core starts building up
# type objects. The core passes in a hash of shape { resource:
# #<Puppet::Type::TypeName> }. We use this to pass through the
# required configuration data to the parent (see
# Puppet::ResourceApi::Property, Puppet::ResourceApi::Parameter and
# Puppet::ResourceApi::ReadOnlyParameter).
define_method(:initialize) do |resource_hash|
super(definition[:name], self.class.data_type, name, resource_hash)
end

# get pops data type object for this parameter or property
define_singleton_method(:data_type) do
@rsapi_data_type ||= Puppet::ResourceApi::DataTypeHandling.parse_puppet_type(
name,
options[:type],
)
end

# from ValueCreator call create_values which makes alias values and
# default values for properties and params
Puppet::ResourceApi::ValueCreator.create_values(
self,
data_type,
param_or_property,
options,
)
end
type_definition.create_attribute_in(self, name, param_or_property, parent, options)
end

def self.instances
Expand Down Expand Up @@ -279,11 +276,9 @@ def cache_current_state(resource_hash)
end

def retrieve
refresh_current_state unless @rsapi_current_state

Puppet.debug("Current State: #{@rsapi_current_state.inspect}")
Puppet.debug("Current State: #{rsapi_current_state.inspect}")

result = Puppet::Resource.new(self.class, title, parameters: @rsapi_current_state)
result = Puppet::Resource.new(self.class, title, parameters: rsapi_current_state)
# puppet needs ensure to be a symbol
result[:ensure] = result[:ensure].to_sym if type_definition.ensurable? && result[:ensure].is_a?(String)

Expand All @@ -302,10 +297,7 @@ def flush
raise_missing_attrs

# puts 'flush'
# skip puppet's injected metaparams
actual_params = @parameters.select { |k, _v| type_definition.attributes.key? k }
target_state = Hash[actual_params.map { |k, v| [k, v.rs_value] }]
target_state = my_provider.canonicalize(context, [target_state]).first if type_definition.feature?('canonicalize')
target_state = rsapi_canonicalized_target_state

retrieve unless @rsapi_current_state

Expand Down
2 changes: 2 additions & 0 deletions lib/puppet/resource_api/base_context.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'puppet/resource_api/type_definition'

# rubocop:disable Style/Documentation
Expand Down
2 changes: 2 additions & 0 deletions lib/puppet/resource_api/data_type_handling.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Puppet; module ResourceApi; end; end # predeclare the main module # rubocop:disable Style/Documentation,Style/ClassAndModuleChildren

# This module is used to handle data inside types, contains methods for munging
Expand Down
6 changes: 4 additions & 2 deletions lib/puppet/resource_api/glue.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'yaml'

module Puppet; end # rubocop:disable Style/Documentation
Expand Down Expand Up @@ -57,9 +59,9 @@ def to_hash
values
end

# attribute names that are not title or namevars
# attribute names that are not title, namevars, or rsapi_custom_insync_trigger
def filtered_keys
values.keys.reject { |k| k == :title || !attr_def[k] || (attr_def[k][:behaviour] == :namevar && @namevars.size == 1) }
values.keys.reject { |k| k == :title || k == :rsapi_custom_insync_trigger || !attr_def[k] || (attr_def[k][:behaviour] == :namevar && @namevars.size == 1) }
end
end

Expand Down
2 changes: 2 additions & 0 deletions lib/puppet/resource_api/io_context.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'puppet/resource_api/base_context'

# Implement Resource API Conext to log through an IO object, defaulting to `$stderr`.
Expand Down
4 changes: 3 additions & 1 deletion lib/puppet/resource_api/parameter.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'puppet/util'
require 'puppet/parameter'

Expand All @@ -13,7 +15,7 @@ class Puppet::ResourceApi::Parameter < Puppet::Parameter
# @param attribute_name the name of attribue of the parameter
# @param resource_hash the resource hash instance which is passed to the
# parent class.
def initialize(type_name, data_type, attribute_name, resource_hash)
def initialize(type_name, data_type, attribute_name, resource_hash, _referrable_type = nil)
@type_name = type_name
@data_type = data_type
@attribute_name = attribute_name
Expand Down
68 changes: 64 additions & 4 deletions lib/puppet/resource_api/property.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'puppet/util'
require 'puppet/property'

Expand All @@ -11,12 +13,29 @@ class Puppet::ResourceApi::Property < Puppet::Property
# @param attribute_name the name of attribue of the property
# @param resource_hash the resource hash instance which is passed to the
# parent class.
def initialize(type_name, data_type, attribute_name, resource_hash)
def initialize(type_name, data_type, attribute_name, resource_hash, referrable_type = nil)
@type_name = type_name
@data_type = data_type
@attribute_name = attribute_name
# Define class method insync?(is) if the name is :ensure
def_insync? if @attribute_name == :ensure && self.class != Puppet::ResourceApi::Property
@resource = resource_hash[:resource]
@referrable_type = referrable_type

# Do not want to define insync on the base class because
# this overrides for everything instead of only for the
# appropriate instance/class of the property.
if self.class != Puppet::ResourceApi::Property
# Define class method insync?(is) if the custom_insync feature flag is set
if referrable_type&.type_definition&.feature?('custom_insync')
def_custom_insync?
if @attribute_name == :rsapi_custom_insync_trigger
@change_to_s_value = 'Custom insync logic determined that this resource is out of sync'
end
# Define class method insync?(is) if the name is :ensure and custom_insync feature flag is not set
elsif @attribute_name == :ensure
def_ensure_insync?
end
end

# Pass resource to parent Puppet class.
super(**resource_hash)
end
Expand Down Expand Up @@ -69,10 +88,51 @@ def rs_value
# method overloaded only for the :ensure property, add option to check if the
# rs_value matches is. Only if the class is child of
# Puppet::ResourceApi::Property.
def def_insync?
def def_ensure_insync?
define_singleton_method(:insync?) { |is| rs_value.to_s == is.to_s }
end

def def_custom_insync?
define_singleton_method(:insync?) do |is|
provider = @referrable_type.my_provider
context = @referrable_type.context
should_hash = @resource.rsapi_canonicalized_target_state
is_hash = @resource.rsapi_current_state
title = @resource.rsapi_title

raise(Puppet::DevError, 'No insync? method defined in the provider; an insync? method must be defined if the custom_insync feature is defined for the type') unless provider.respond_to?(:insync?)

provider_insync_result, change_message = provider.insync?(context, title, @attribute_name, is_hash, should_hash)

unless provider_insync_result.nil? || change_message.nil? || change_message.empty?
@change_to_s_value = change_message
end

case provider_insync_result
when nil
# If validating ensure and no custom insync was used, check if rs_value matches is.
return rs_value.to_s == is.to_s if @attribute_name == :ensure
# Otherwise, super and rely on Puppet::Property.insync?
super(is)
when TrueClass, FalseClass
return provider_insync_result
else
# When returning anything else, raise a DevError for a non-idiomatic return
raise(Puppet::DevError, "Custom insync for #{@attribute_name} returned a #{provider_insync_result.class} with a value of #{provider_insync_result.inspect} instead of true/false; insync? MUST return nil or the boolean true or false") # rubocop:disable Metrics/LineLength
end
end

define_singleton_method(:change_to_s) do |current_value, newvalue|
# As defined in the custom insync? method, it is sometimes useful to overwrite the default change messaging;
# The enables a user to return a more useful change report than a strict "is to should" report.
# If @change_to_s_value is not set, Puppet writes a generic change notification, like:
# Notice: /Stage[main]/Main/<type_name>[<name_hash>]/<property name>: <property name> changed <is value> to <should value>
# If #change_to_s_value is *nil* Puppet writes a weird empty message like:
# Notice: /Stage[main]/Main/<type_name>[<name_hash>]/<property name>:
@change_to_s_value || super(current_value, newvalue)
end
end

# puppet symbolizes some values through puppet/parameter/value.rb
# (see .convert()), but (especially) Enums are strings. specifying a
# munge block here skips the value_collection fallback in
Expand Down
2 changes: 2 additions & 0 deletions lib/puppet/resource_api/puppet_context.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'puppet/resource_api/base_context'
require 'puppet/util/logging'

Expand Down
2 changes: 2 additions & 0 deletions lib/puppet/resource_api/read_only_parameter.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'puppet/util'
require 'puppet/resource_api/parameter'

Expand Down
2 changes: 2 additions & 0 deletions lib/puppet/resource_api/simple_provider.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Puppet; end # rubocop:disable Style/Documentation

module Puppet::ResourceApi
Expand Down
2 changes: 2 additions & 0 deletions lib/puppet/resource_api/transport.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

module Puppet::ResourceApi; end # rubocop:disable Style/Documentation

# Remote target transport API
Expand Down
2 changes: 2 additions & 0 deletions lib/puppet/resource_api/transport/wrapper.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# frozen_string_literal: true

require 'puppet/resource_api/transport'
require 'hocon'
require 'hocon/config_syntax'
Expand Down
Loading

0 comments on commit b388f03

Please sign in to comment.