cem_data_processor 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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