Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(GH-225) Add support for custom insync #285

Merged
merged 3 commits into from Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .rubocop.yml
@@ -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
@@ -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
@@ -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)
DavidS marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
joshcooper marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,3 +1,5 @@
# frozen_string_literal: true

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