cem_data_processor 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,741 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'deep_merge'
4
+ require 'rgl/adjacency'
5
+ require 'rgl/topsort'
6
+ require 'rgl/traversal'
7
+ require 'set'
8
+
9
+ module CemDataProcessor
10
+ # This module contains the logic for creating resource data from Hiera data.
11
+ module Parser
12
+ MAP_TYPES = %w[hiera_title hiera_title_num number title].freeze
13
+ METAPARAMS = %w[dependent before require subscribe notify].freeze
14
+
15
+ # Parse Hiera data into a resource data Hash
16
+ # @param hiera_data [Hash] Hiera data to parse
17
+ # @param control_maps [Array] Control maps to use
18
+ # @return [Hash] Parsed resource data
19
+ def self.parse(hiera_data, control_maps, control_configs: {}, ignore: [], only: [])
20
+ ResourceDataParser.new(
21
+ hiera_data,
22
+ control_maps,
23
+ control_configs: control_configs,
24
+ ignore: ignore,
25
+ only: only
26
+ ).parse
27
+ end
28
+
29
+ # This module handles data validation for the CIS data parser
30
+ module Validation
31
+ # Validates the hiera_data parameter and either raises an ArgumentError or returns the hiera_data parameter.
32
+ # @param hiera_data [Hash] The Hiera data to be parsed.
33
+ # @return [Hash] The Hiera data to be parsed.
34
+ # @raise [ArgumentError] If the hiera_data parameter is not a non-empty Hash.
35
+ def validate_hiera_data(hiera_data)
36
+ return hiera_data if hiera_data == :no_params
37
+
38
+ unless not_nil_or_empty?(hiera_data) && hiera_data.is_a?(Hash)
39
+ raise ArgumentError, 'hiera_data must be a non-nil, non-empty Hash'
40
+ end
41
+
42
+ hiera_data
43
+ end
44
+
45
+ # Validates the control_maps parameter and either raises an ArgumentError or returns the control_maps parameter.
46
+ # @param control_maps [Array] The control maps to be parsed.
47
+ # @return [Array] The control maps to be parsed.
48
+ # @raise [ArgumentError] If the control_maps parameter is not a non-empty Array of Hashes.
49
+ def validate_control_maps(control_maps)
50
+ unless not_nil_or_empty?(control_maps) && array_of_hashes?(control_maps)
51
+ raise ArgumentError, 'control_maps must be a non-nil, non-empty Array of Hashes'
52
+ end
53
+
54
+ control_maps
55
+ end
56
+
57
+ # Checks if the value is not nil or empty.
58
+ # @param value [Any] The value to be checked.
59
+ # @return [Boolean] True if the value is not nil or empty, false otherwise.
60
+ def not_nil_or_empty?(value)
61
+ !value.nil? && !value.empty?
62
+ end
63
+
64
+ # Checks if the value is an Array of Hashes.
65
+ # @param value [Any] The value to be checked.
66
+ # @return [Boolean] True if the value is an Array of Hashes, false otherwise.
67
+ def array_of_hashes?(value)
68
+ value.is_a?(Array) && value.all? { |h| h.is_a?(Hash) }
69
+ end
70
+ end
71
+
72
+ # Parser class for resource Hiera data.
73
+ # rubocop:disable Metrics/ClassLength
74
+ class ResourceDataParser
75
+ include Validation
76
+ attr_reader :hiera_data, :control_maps, :resources
77
+
78
+ def initialize(hiera_data, control_maps, control_configs: {}, ignore: [], only: [])
79
+ @hiera_data = validate_hiera_data(hiera_data)
80
+ @control_maps = validate_control_maps(control_maps)
81
+ @control_configs = control_configs
82
+ @ignore = ignore
83
+ @only = only
84
+ @resources = RGL::DirectedAdjacencyGraph.new
85
+ @controls = Set.new
86
+ @filtered = Set.new
87
+ @dependent = {}
88
+ end
89
+
90
+ # Parse the Hiera data into a Hash used by Puppet to create the resources.
91
+ # The way this works is by first creating a DAG and adding all resources to the graph
92
+ # as vertices, with an edge for each resource pointing from a dummy node, :root, to the
93
+ # resource. We then add edges to the graph based on the `before_me` and `after_me` lists
94
+ # of each resource and remove the :root-connected edges for each resource that has a
95
+ # `before_me` list, and remove the :root-connected edges for each resource in a `after_me`
96
+ # list. Finally, we sort the graph into an Array populated with a single Hash of ordered
97
+ # resources and return that Hash.
98
+ # @return [Array] A sorted array of resource hashes.
99
+ # rubocop:disable Metrics/MethodLength
100
+ def parse
101
+ @hiera_data.each do |name, data|
102
+ resource = CemDataProcessor::Parser.new_resource(name, data, @control_maps)
103
+ add_control_names(resource)
104
+ add_dependent_mapping(resource) # Map any controls this resource depends on
105
+ @resources.add_vertex(resource) # Add all the resources to the graph
106
+ @resources.add_edge(:root, resource) # Establish the root -> resource edges
107
+ add_edge_ordering(resource) # Add resource ordering edges
108
+ end
109
+ # If the resource should be filtered (i.e. only or ignore), remove it from the graph.
110
+ filter_resources!
111
+ # Verify that all dependent resources are in the graph, remove them if not.
112
+ remove_unsatisfied_dependents!
113
+ # Sort the graph and return the array of ordered resource hashes
114
+ sort_resources.map do |r|
115
+ r.add_control_configs(@control_configs)
116
+ resource_data(r)
117
+ end
118
+ end
119
+ # rubocop:enable Metrics/MethodLength
120
+
121
+ private
122
+
123
+ # Adds control neames for the given resource to the @controls set.
124
+ # @param resource [Resource] The resource to add control names for.
125
+ def add_control_names(resource)
126
+ return unless resource.controls
127
+
128
+ @controls.merge(resource.control_names).flatten!
129
+ @controls.merge(resource.mapped_control_names).flatten!
130
+ end
131
+
132
+ # Calls the given Resource's `resource_data` method, filters out any resource references
133
+ # in metaparameters that references filtered resources, and returns the result.
134
+ # @param resource [Resource] The resource to be filtered.
135
+ # @return [Hash] The filtered resource data.
136
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
137
+ def resource_data(resource)
138
+ data = resource.resource_data.dup
139
+ data.each do |_, res_data|
140
+ res_data.each do |_, params|
141
+ METAPARAMS.each do |param|
142
+ next unless params.key?(param)
143
+
144
+ params[param].reject! { |r| @filtered.to_a.map(&:resource_reference).include?(r) }
145
+ params.delete(param) if params[param].empty?
146
+ end
147
+ end
148
+ end
149
+ data
150
+ end
151
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
152
+
153
+ # Removes Resources from the graph if they should be filtered.
154
+ def filter_resources!
155
+ @resources.depth_first_search do |resource|
156
+ next if resource == :root
157
+
158
+ if filter_resource?(resource)
159
+ @resources.remove_vertex(resource) # Remove resource's graph vertex
160
+ @filtered.add(resource) # Add resource to filtered set
161
+ end
162
+ end
163
+ end
164
+
165
+ # Checks whether the resource should be filtered out based on the ignore and only lists.
166
+ # @param resource [Resource] The resource to check.
167
+ # @return [Boolean] True if the resource should be filtered out, false otherwise.
168
+ def filter_resource?(resource)
169
+ return true if control_in?(resource, @ignore)
170
+ return true unless @only.empty? || control_in?(resource, @only)
171
+
172
+ false
173
+ end
174
+
175
+ # Adds a mapping for a dependent control and the resources that depend on it.
176
+ # @param resource [Resource] The resource to add the mapping for.
177
+ def add_dependent_mapping(resource)
178
+ return unless resource.dependent
179
+
180
+ resource.dependent.each do |control_name|
181
+ @dependent[control_name] = [] unless @dependent.key?(control_name)
182
+ @dependent[control_name] << resource
183
+ end
184
+ end
185
+
186
+ # Checks the dependent controls against all controls after filtered resource controls are removed
187
+ # and removes any dependent resources that are not satisfied.
188
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
189
+ def remove_unsatisfied_dependents!
190
+ dependent_set = Set.new(@dependent.keys)
191
+ filtered_set = Set.new(@filtered.to_a.map(&:control_names)).flatten
192
+ filtered_mapped = Set.new(@filtered.to_a.map(&:mapped_control_names)).flatten
193
+
194
+ all_controls = @controls.subtract(filtered_set + filtered_mapped)
195
+ return if dependent_set.proper_subset?(all_controls) # All dependent controls exist in the graph
196
+
197
+ (dependent_set - all_controls).each do |control_name|
198
+ @dependent[control_name].each do |resource|
199
+ @resources.remove_vertex(resource)
200
+ @filtered.add(resource)
201
+ end
202
+ end
203
+ end
204
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
205
+
206
+ # Gets all verticies in the graph that have the associated control
207
+ # @param control_name [String] The name of the control to check.
208
+ # @return [Array] The verticies that have the associated control.
209
+ def collect_verticies_by_control(control_name)
210
+ @resources.vertices.select { |r| r.control?(control_name) }
211
+ end
212
+
213
+ # Checks if the given Resource has a control in the given list.
214
+ # @param resource [Resource] The resource to check.
215
+ # @param control_list [Array] The list of controls to check against.
216
+ # @return [Boolean] True if the resource is in the control list, false otherwise.
217
+ def control_in?(resource, control_list)
218
+ return false if control_list.empty?
219
+
220
+ control_list.each do |ignored_control|
221
+ return true if resource.control?(ignored_control)
222
+ end
223
+ false
224
+ end
225
+
226
+ # Adds edges to the graph based on the given Resource's `before_me` and `after_me` lists.
227
+ # @param resource [Resource] The Resource to add edges for.
228
+ def add_edge_ordering(resource)
229
+ add_before_me_edge(resource)
230
+ add_after_me_edge(resource)
231
+ end
232
+
233
+ # Adds edges to the graph based on the given Resource's `before_me` list.
234
+ # @param resource [Resource] The Resource to add edges for.
235
+ def add_before_me_edge(resource)
236
+ resource.before_me.flatten.each do |before|
237
+ next unless before # Skip if this `before` is nil, empty, or falsy (e.g. false, 0, etc.)
238
+ next if before.equal?(resource) # Skip if this `before` is the same as the current resource
239
+
240
+ # We remove the edge from root to this resource if it exists because this resource is no longer
241
+ # attached to the root of the graph as it has other resources before it.
242
+ @resources.remove_edge(:root, resource) if @resources.has_edge?(:root, resource)
243
+ # Add the edge from the before resource to this resource
244
+ @resources.add_edge(before, resource) unless @resources.has_edge?(before, resource)
245
+ end
246
+ end
247
+
248
+ # Adds edges to the graph based on the given Resource's `after_me` list.
249
+ # @param resource [Resource] The Resource to add edges for.
250
+ def add_after_me_edge(resource)
251
+ resource.after_me.flatten.each do |after|
252
+ next unless after # Skip if this `after` is nil, empty, or falsy (e.g. false, 0, etc.)
253
+ next if after.equal?(resource) # Skip if this `after` is the same as the current resource
254
+
255
+ # We remove the edge from root to the `after` resource if it exists because the `after` resource
256
+ # is no longer attached to the root of the graph as this resources comes before it.
257
+ @resources.remove_edge(:root, after) if @resources.has_edge?(:root, after)
258
+ # Add the edge from this resource to the after resource
259
+ @resources.add_edge(resource, after) unless @resources.has_edge?(resource, after)
260
+ end
261
+ end
262
+
263
+ # This method validates that the resources graph has no cycles and then returns a topological sort of the graph
264
+ # as an Array of Resource objects.
265
+ # @return [Array] The sorted Resources.
266
+ # @raise [ArgumentError] If the resources graph has any cycles.
267
+ def sort_resources
268
+ raise "Resource cyclic ordering detected: #{@resources.cycles}" unless @resources.acyclic?
269
+
270
+ # We call topsort on the graph to get the sorted list of resources, convert it to an array, and
271
+ # remove the root node.
272
+ @resources.topsort_iterator.to_a.flatten.uniq.reject { |r| r == :root }
273
+ end
274
+ end
275
+ # rubocop:enable Metrics/ClassLength
276
+
277
+ # This class holds all base attributes and methods for every syntax object.
278
+ # rubocop:disable Metrics/ClassLength
279
+ class ProcessorObject
280
+ include Validation
281
+ attr_reader :name, *METAPARAMS.map(&:to_sym)
282
+
283
+ def initialize(name, data, control_maps)
284
+ @name = name
285
+ @data = validate_hiera_data(data)
286
+ @control_maps = validate_control_maps(control_maps)
287
+ @dependent = Set.new
288
+ initialize_metaparams(@data, @control_maps)
289
+ end
290
+
291
+ # Determines if the name supplied is equal to the name of the object.
292
+ # This is overridden by subclasses to implement name mapped matches.
293
+ # @param name [String] The name to be compared to the object's name.
294
+ # @return [Boolean] True if the name is equal to the object's name, false otherwise.
295
+ def name?(_name)
296
+ raise NotImplementedError, 'This method must be implemented by a subclass'
297
+ end
298
+
299
+ # Abstract method to be implemented by subclasses.
300
+ # Returns a representation of this object as a Hash usable by Puppet's
301
+ # create_resources function.
302
+ def resource_data
303
+ raise NotImplementedError, 'This method must be implemented by a subclass'
304
+ end
305
+
306
+ # Returns any Resource objects that must be ordered before this object.
307
+ # @return [Array] The Resources that must be ordered before this object.
308
+ def before_me
309
+ defined?(@before_me) ? @before_me : initialize_before_me
310
+ end
311
+
312
+ # Returns any Resource objects that must be ordered after this object.
313
+ # @return [Array] The Resources that must be ordered after this object.
314
+ def after_me
315
+ defined?(@after_me) ? @after_me : initialize_after_me
316
+ end
317
+
318
+ # Converts this object to a String.
319
+ # @return [String] The class and name of this object.
320
+ def to_s
321
+ "#{self.class.name}('#{@name}')"
322
+ end
323
+
324
+ # Gives a more detailed String representation of this object.
325
+ # @return [String] The class, object id, and name of this object.
326
+ def inspect
327
+ "#<#{self.class.name}:#{object_id} '#{@name}'>"
328
+ end
329
+
330
+ private
331
+
332
+ # This method normalizes an array of Resources, or anything really, by
333
+ # flattening it, removing any nil values, removing any duplicates, and
334
+ # rejecting any empty objects if they respond to `empty?`. It then
335
+ # returns the new array.
336
+ # @param resources [Array] The array of Resources to be normalized.
337
+ # @return [Array] The normalized array of Resources.
338
+ def normalize_resource_array(array)
339
+ array.flatten.compact.uniq.reject { |r| r.empty? if r.respond_to?(:empty?) }
340
+ end
341
+
342
+ # This method normalizes an array of Resources, or anything really, by
343
+ # flattening it, removing any nil values, removing any duplicates, and
344
+ # rejecting any empty objects if they respond to `empty?`. It does this
345
+ # in place, directly modifying the input array.
346
+ # @param resources [Array] The array of Resources to be normalized.
347
+ def normalize_resource_array!(array)
348
+ array.flatten!
349
+ array.compact!
350
+ array.uniq!
351
+ array.reject! { |r| r.empty? if r.respond_to?(:empty?) }
352
+ end
353
+
354
+ # Initializes any relevant metaparameters based on the data supplied.
355
+ # @param data [Hash] The resource data to be parsed.
356
+ # @param control_maps [Array] The control maps to be used.
357
+ def initialize_metaparams(data, control_maps)
358
+ METAPARAMS.each do |param|
359
+ metaparam_data = data == :no_params ? {} : data[param]
360
+ raw_mdata = data == :no_params ? {} : data
361
+ value, bool_value = parse_metaparam(metaparam_data, control_maps)
362
+ raw_value, raw_bool_value = parse_raw_metaparam(raw_mdata, param)
363
+ set_metaparam_instance_vars(param, value, raw_value)
364
+ define_metaparam_bool_methods(param, raw_bool_value, bool_value)
365
+ end
366
+ end
367
+
368
+ # Initilizes the before_me instance variable with a list of Resources
369
+ # that must be ordered before this object.
370
+ # @return [Array] The list of Resources that must be ordered before this object.
371
+ def initialize_before_me
372
+ ctrls = @controls ? calculate_ordered_controls('before_me') : []
373
+ this = calculate_self_ordering('require', 'subscribe')
374
+ @before_me = normalize_resource_array(this.concat(ctrls))
375
+ @before_me
376
+ end
377
+
378
+ # Initializes the after_me instance variable with a list of Resources
379
+ # that must be ordered after this object.
380
+ # @return [Array] The list of Resources that must be ordered after this object.
381
+ def initialize_after_me
382
+ ctrls = @controls ? calculate_ordered_controls('after_me') : []
383
+ this = calculate_self_ordering('notify', 'subscribe')
384
+ @after_me = normalize_resource_array(this.concat(ctrls))
385
+ @after_me
386
+ end
387
+
388
+ # This method adds the supplied Resource to the inverse ordering list of this
389
+ # object based on the supplied metaparameter. This method is never directly used
390
+ # by an object on itself, rather it is called by other objects when they establish
391
+ # ordering relationships with this object. Because this method is private, other
392
+ # objects must use `send` to call it.
393
+ # @param metaparam [String] The metaparameter to inverse
394
+ # @param resource [Resource] The Resource to be added to the inverse ordering list.
395
+ def add_inverse_ordered_resource(metaparam, resource)
396
+ if %w[require subscribe].include?(metaparam)
397
+ add_after_me(resource)
398
+ elsif %w[before notify].include?(metaparam)
399
+ add_before_me(resource)
400
+ end
401
+ end
402
+
403
+ # This method calculates the ordering of this object based on the
404
+ # ordering of the controls that are defined for this object.
405
+ # @param order_function [String] The function to use to calculate the ordering (before_me or after_me).
406
+ # @return [Array] The list of Resources gathered from the order function return of all controls.
407
+ def calculate_ordered_controls(order_function)
408
+ @controls.each_with_object([]) do |control, ary|
409
+ ary << control.send(order_function.to_sym)
410
+ end
411
+ end
412
+
413
+ # This method calculates the ordering of this object based on the
414
+ # the supplied metaparameters. This function is used with "like pairs"
415
+ # of metaparameters, such as "require" and "subscribe".
416
+ # @param metaparameters [Array] The metaparameters to use to calculate the ordering.
417
+ # @return [Array] The list of Resources gathered from this object's metaparameters.
418
+ def calculate_self_ordering(*metaparams)
419
+ ordered = metaparams.each_with_object([]) do |mparam, ary|
420
+ next unless send("#{mparam}?".to_sym)
421
+
422
+ ordered_resources = send(mparam.to_sym)
423
+ ordered_resources.each { |r| r.send(:add_inverse_ordered_resource, mparam, self) }
424
+
425
+ ary << ordered_resources
426
+ end
427
+ normalize_resource_array(ordered)
428
+ end
429
+
430
+ # Returns appropriate values for instance variables of the given metaparam based off the supplied value.
431
+ # @param value [Array] The metaparameter declaration value from Hiera.
432
+ # @param control_maps [Array] The relevant control maps used in Resource creation.
433
+ # @return [Array] Values for the instance variables of the given metaparam. The order of the values
434
+ # is: Resource collection value, boolean value.
435
+ def parse_metaparam(value, control_maps)
436
+ return [nil, false] unless not_nil_or_empty?(value)
437
+
438
+ return parse_dependent_param(value) if value.is_a?(Array)
439
+
440
+ objects = value.each_with_object([]) do |(k, v), a|
441
+ a << CemDataProcessor::Parser.new_resource(k, v, control_maps)
442
+ end
443
+ [normalize_resource_array(objects), !objects.empty?]
444
+ end
445
+
446
+ # Adds a each dependent control from a list of dependent controls to the
447
+ # @dependent instance variable.
448
+ # @param value [Array] The dependent controls to be added to the @dependent instance variable.
449
+ def parse_dependent_param(value)
450
+ value.each { |x| @dependent.add(x) }
451
+ end
452
+
453
+ # Returns appropriate raw value for instance variables of the given metaparam based off the supplied value.
454
+ # The raw value is the text values for the metaparameter declaration supplied via the resource data.
455
+ # @param data [Hash] The resource data to be parsed.
456
+ # @param param [String] The metaparameter to be parsed.
457
+ # @return [Array] Values for the instance variables of the given metaparam. The order of the values
458
+ # is: raw value, boolean raw value.
459
+ def parse_raw_metaparam(data, param)
460
+ raw_value = data.fetch(param, nil)
461
+ [raw_value, (!raw_value.nil? && !raw_value.empty?)]
462
+ end
463
+
464
+ # Sets the instance variables of the given metaparam based off the supplied values.
465
+ # @param param [String] The metaparameter to be set.
466
+ # @param value [Array] The Resource value to be set.
467
+ # @param raw_value [Array] The raw value to be set.
468
+ def set_metaparam_instance_vars(param, value, raw_value)
469
+ instance_variable_set("@#{param}", value)
470
+ instance_variable_set("@#{param}_raw", raw_value)
471
+ end
472
+
473
+ # Defines singleton methods for this instance of ProcessorObject that are used to determine
474
+ # if the metaparameter is set.
475
+ # @param param [String] The metaparameter that will have boolean methods defined.
476
+ # @param raw_value [Boolean] The boolean value for the <metaparam>_raw? method.
477
+ # @param value [Boolean] The boolean value for the <metaparam>? method.
478
+ def define_metaparam_bool_methods(param, raw_value, value)
479
+ define_singleton_method("#{param}_raw?".to_sym) { raw_value }
480
+ define_singleton_method("#{param}?".to_sym) { value }
481
+ end
482
+
483
+ # Returns the mapped names for the given control identifier.
484
+ # @param identifier [String] The control identifier to be mapped.
485
+ # @return [Array] The mapped names for the given control identifier.
486
+ def find_mapped_names(identifier)
487
+ @control_maps.each do |control_map|
488
+ return control_map[identifier] if control_map.include?(identifier)
489
+ end
490
+ []
491
+ end
492
+ end
493
+
494
+ # This class represents a single control in the data structure.
495
+ class Control < ProcessorObject
496
+ attr_reader :mapped_names, :params, :param_names, :resource_params
497
+
498
+ def initialize(name, data, control_maps)
499
+ super(name, data, control_maps)
500
+ @mapped_names = find_mapped_names(@name)
501
+ @params = @data == :no_params ? {} : @data
502
+ @resource_params = @data == :no_params ? {} : @data.reject { |k, _v| METAPARAMS.include?(k) }
503
+ @param_names = @data == :no_params ? Set.new : Set.new(@params.keys)
504
+ end
505
+
506
+ def name?(name)
507
+ @name == name || @mapped_names.include?(name)
508
+ end
509
+
510
+ def param?(param_name)
511
+ @param_names.include?(param_name)
512
+ end
513
+
514
+ def param(param_name)
515
+ @params[param_name]
516
+ end
517
+
518
+ def resource_data
519
+ @resource_params
520
+ end
521
+ end
522
+ # rubocop:enable Metrics/ClassLength
523
+
524
+ # This class represents a single Puppet resource (class, defined type, etc.)
525
+ class Resource < ProcessorObject
526
+ attr_reader :name, :type, :controls, :control_names, :mapped_control_names
527
+
528
+ def initialize(name, data, control_maps)
529
+ super(name, data, control_maps)
530
+ @type = @data['type']
531
+ @controls = create_control_classes(@data['controls'])
532
+ @control_names = Set.new(@controls.map(&:name)).flatten
533
+ @mapped_control_names = Set.new(@controls.map(&:mapped_names).flatten).flatten
534
+ initialize_control_metaparams
535
+ end
536
+
537
+ # Adds overriding parameter values to controls in this resource
538
+ # if this resource has a matching control.
539
+ # @param data [Hash] The resource data to be parsed.
540
+ def add_control_configs(control_configs)
541
+ control_configs.each do |control, configs|
542
+ next unless control?(control)
543
+
544
+ @controls.each do |control_class|
545
+ next unless control_class.name?(control)
546
+
547
+ control_class.resource_params.deep_merge!(configs)
548
+ end
549
+ end
550
+ end
551
+
552
+ # Outputs a representation of this object as a Hash usable by Puppet's
553
+ # create_resources function.
554
+ def resource_data
555
+ control_params = control_parameters
556
+ METAPARAMS.each do |mparam|
557
+ next if mparam == 'dependent'
558
+
559
+ refs = resource_references(mparam, control_params)
560
+ next if refs.nil?
561
+
562
+ control_params[mparam] = refs
563
+ end
564
+ { @type => { @name => control_params } }
565
+ end
566
+
567
+ # This method returns a string representation of this Resource in the resource reference
568
+ # format used by Puppet.
569
+ # @return [String] A string representation of this Resource in the resource reference format.
570
+ def resource_reference
571
+ type_ref = @type.split('::').map(&:capitalize).join('::')
572
+ "#{type_ref}['#{@name}']"
573
+ end
574
+
575
+ # This method checks if this Resource contains the given control.
576
+ # @param control [String] The control to be checked.
577
+ # @return [Boolean] True if this Resource contains the given control, false otherwise.
578
+ def control?(control_name)
579
+ @control_names.include?(control_name) || @mapped_control_names.include?(control_name)
580
+ end
581
+
582
+ # This method checks if this Resource contains the given parameter.
583
+ # @param param_name [String] The parameter to be checked.
584
+ # @return [Boolean] True if this Resource contains the given parameter, false otherwise.
585
+ def param?(param_name)
586
+ if param_name.respond_to?(:each)
587
+ param_name.all? { |name| param?(name) }
588
+ else
589
+ @param_names.include?(param_name)
590
+ end
591
+ end
592
+
593
+ private
594
+
595
+ # This method gathers the resource data for each control this Resource contains.
596
+ # @return [Hash] The resource data for each control this Resource contains.
597
+ def control_parameters
598
+ @controls.each_with_object({}) do |control, h|
599
+ h.deep_merge(control.resource_data)
600
+ end
601
+ end
602
+
603
+ # This method gets the resource references for the given metaparameter and control parameters.
604
+ # @param mparam [String] The metaparameter to be checked.
605
+ # @param control_params [Hash] The control parameters to be checked.
606
+ # @return [Array] The resource references for the given metaparameter and control parameters.
607
+ # @return [nil] If the given metaparameter is not set on this Resource.
608
+ def resource_references(mparam, control_params)
609
+ # rubocop:disable Style/RedundantSelf
610
+ # we use self here because `require` is a metaparam and we don't want to
611
+ # call `Kernel#require` accidentally.
612
+ this_mparam = self.send(mparam.to_sym)
613
+ # rubocop:enable Style/RedundantSelf
614
+ return if this_mparam.nil? || this_mparam.compact.empty?
615
+
616
+ if control_params.key?(mparam)
617
+ control_params[mparam].concat(this_mparam.map(&:resource_reference))
618
+ else
619
+ this_mparam.map(&:resource_reference)
620
+ end
621
+ end
622
+
623
+ # Adds a Resource to the before_me list.
624
+ # @param resource [Resource] The Resource to be added to the before_me list.
625
+ def add_before_me(resource)
626
+ before_me.append(resource)
627
+ end
628
+
629
+ # Adds a Resource to the after_me list.
630
+ # @param resource [Resource] The Resource to be added to the after_me list.
631
+ def add_after_me(resource)
632
+ after_me.append(resource)
633
+ end
634
+
635
+ # Initializes all metaparameter values of all controls that this Resource contains
636
+ # and brings those values into the scope of this Resource. Also initializes the
637
+ # @before_me and @after_me instance variables.
638
+ def initialize_control_metaparams
639
+ METAPARAMS.each { |mparam| initialize_control_metaparameter(mparam) }
640
+ @before_me = initialize_before_me
641
+ @after_me = initialize_after_me
642
+ end
643
+
644
+ # Initializes a single supplied metaparameter for all controls that this Resource
645
+ # contains and brings those values into the scope of this Resource.
646
+ # @param mparam [String] The metaparameter to be initialized.
647
+ def initialize_control_metaparameter(mparam)
648
+ ctrl_objects = @controls.map { |c| c.send(mparam.to_sym) }.flatten
649
+ return if ctrl_objects.empty?
650
+
651
+ current_objects = instance_variable_get("@#{mparam}") || []
652
+ all_meta_objects = ctrl_objects.concat(current_objects)
653
+ instance_variable_set("@#{mparam}", all_meta_objects.flatten.compact.uniq)
654
+ define_singleton_method("#{mparam}?".to_sym) { all_meta_objects.any? }
655
+ end
656
+
657
+ # Creates a new Control class for each control in the data structure if that Control class
658
+ # does not already exist in the cache.
659
+ # @param control_data [Array] The control data of the resource from the Hiera data.
660
+ def create_control_classes(control_data)
661
+ case control_data.class.to_s
662
+ when 'Hash'
663
+ control_data.map { |cname, cdata| CemDataProcessor::Parser.new_control(cname, cdata, @control_maps) }
664
+ when 'Array'
665
+ control_data.map { |cname| CemDataProcessor::Parser.new_control(cname, :no_params, @control_maps) }
666
+ else
667
+ raise ArgumentError, "Cannot create control because control data is not the expected type. Data type is #{control_data.class}"
668
+ end
669
+ end
670
+ end
671
+
672
+ class << self
673
+ # Creates a new Resource object. If an object with the same resource name, resource data, and control maps
674
+ # already exists, it will be returned instead of creating a new one.
675
+ # @param resource_name [String] The name of the resource.
676
+ # @param resource_data [Hash] The data for the resource.
677
+ # @param control_maps [Array] The control maps for the resource.
678
+ # @return [Resource] The new or cached Resource object.
679
+ def new_resource(resource_name, resource_data, control_maps)
680
+ cache_key = [Resource.name, resource_name, resource_data, control_maps]
681
+ cached = cache_get(cache_key)
682
+ return cached unless cached.nil?
683
+
684
+ begin
685
+ new_resource = Resource.new(resource_name, resource_data, control_maps)
686
+ rescue ArgumentError => e
687
+ raise ArgumentError, "Failed to create resource #{resource_name}: #{e.message}"
688
+ end
689
+ cache_add(cache_key, new_resource)
690
+ new_resource
691
+ end
692
+
693
+ # Creates a new Control object. If an object with the same control name, control data, and control maps
694
+ # already exists, it will be returned instead of creating a new one.
695
+ # @param control_name [String] The name of the control.
696
+ # @param control_data [Hash] The data for the control.
697
+ # @param control_maps [Array] The control maps for the control.
698
+ # @return [Control] The new or cached Control object.
699
+ def new_control(control_name, control_data, control_maps)
700
+ cache_key = [Control.name, control_name, control_data, control_maps]
701
+ cached = cache_get(cache_key)
702
+ return cached unless cached.nil?
703
+
704
+ begin
705
+ new_control = Control.new(control_name, control_data, control_maps)
706
+ rescue ArgumentError => e
707
+ raise ArgumentError, "Failed to create control #{control_name}: #{e.message}"
708
+ end
709
+ cache_add(cache_key, new_control)
710
+ new_control
711
+ end
712
+
713
+ # Clears the current cache. Used in testing.
714
+ def clear_cache
715
+ @object_cache = {}
716
+ end
717
+
718
+ private
719
+
720
+ # Helper method to add a ProcessorObject (or subclass of one) to the cache.
721
+ # @param key [Array] The key for the cache. An array comprised of the object name, object data,
722
+ # and control maps.
723
+ # @param resource [ProcessorObject] The object to add to the cache.
724
+ def cache_add(key, object)
725
+ object_cache[key] = object
726
+ end
727
+
728
+ # Helper method to retrieve a ProcessorObject from the cache.
729
+ # @param key [Array] The key for the cache. An array comprised of the resource name, resource data, and control maps.
730
+ # @return [ProcessorObject] The object from the cache, or nil if the object doesn't exist.
731
+ def cache_get(key)
732
+ object_cache.fetch(key, nil)
733
+ end
734
+
735
+ # Holds the object cache. If the object cache doesn't exist, it will be created.
736
+ def object_cache
737
+ @object_cache ||= {}
738
+ end
739
+ end
740
+ end
741
+ end