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.
- 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
|