cem_data_processor 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +179 -0
- data/LICENSE.txt +21 -0
- data/README.md +308 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/cem_data_processor.gemspec +51 -0
- data/lib/cem_data_processor/logger.rb +43 -0
- data/lib/cem_data_processor/parser.rb +741 -0
- data/lib/cem_data_processor/processor.rb +12 -0
- data/lib/cem_data_processor/version.rb +5 -0
- data/lib/cem_data_processor.rb +8 -0
- metadata +287 -0
@@ -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
|