persistence-providers 0.0.3.6 → 0.0.5
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 +4 -4
- data/lib/crd_client.rb +24 -9
- data/lib/persistence_providers.rb +2 -0
- data/lib/state/component.rb +94 -5
- data/lib/state/component/providers/influxdb.rb +31 -67
- data/lib/state/component/providers/influxdb/client.rb +24 -27
- data/lib/state/component/providers/influxdb/measurement.rb +17 -9
- data/lib/state/component/providers/influxdb/measurement/attribute_measurement.rb +2 -4
- data/lib/state/component/providers/influxdb/measurement/errors.rb +9 -4
- data/lib/state/component/providers/influxdb/measurement/events.rb +14 -4
- data/lib/state/component/providers/influxdb/measurement/states.rb +15 -4
- data/lib/state/component/providers/influxdb/semantictype.rb +98 -74
- data/lib/state/component_def.rb +4 -1
- data/lib/state/component_def/attribute_type_info.rb +13 -0
- data/lib/state/crd_assembly.rb +59 -7
- data/lib/state/workflow.rb +26 -0
- data/lib/state/workflow_instance.rb +138 -8
- data/lib/state/workflow_instance/attribute_type_info.rb +13 -0
- data/lib/utils.rb +5 -0
- data/lib/utils/log.rb +22 -0
- data/persistence-providers.gemspec +1 -1
- metadata +5 -2
@@ -2,7 +2,6 @@ module DTK::State
|
|
2
2
|
class Component::Attribute::Influxdb
|
3
3
|
class Measurement
|
4
4
|
class Attributes < self
|
5
|
-
|
6
5
|
def write(value, params_hash = {}, timestamp)
|
7
6
|
checked_params_hash = check_params_hash(params_hash)
|
8
7
|
write_point(value, checked_params_hash, timestamp)
|
@@ -11,10 +10,9 @@ module DTK::State
|
|
11
10
|
protected
|
12
11
|
|
13
12
|
def required_params
|
14
|
-
[
|
13
|
+
%i[namespace component_name assembly_name attribute_name task_id]
|
15
14
|
end
|
16
|
-
|
17
|
-
end
|
15
|
+
end
|
18
16
|
end
|
19
17
|
end
|
20
18
|
end
|
@@ -8,13 +8,18 @@ module DTK::State
|
|
8
8
|
write_point(value, checked_params_hash, timestamp)
|
9
9
|
end
|
10
10
|
|
11
|
+
def get_correlator_type(entrypoint)
|
12
|
+
{
|
13
|
+
correlator_type: entrypoint.split('/').last.split('.')[0]
|
14
|
+
}
|
15
|
+
end
|
16
|
+
|
11
17
|
protected
|
12
18
|
|
13
19
|
def required_params
|
14
|
-
[
|
20
|
+
%i[namespace component_name assembly_name task_id attribute_name correlator_type]
|
15
21
|
end
|
16
|
-
|
17
|
-
end
|
22
|
+
end
|
18
23
|
end
|
19
24
|
end
|
20
|
-
end
|
25
|
+
end
|
@@ -8,13 +8,23 @@ module DTK::State
|
|
8
8
|
write_point(value, checked_params_hash, timestamp)
|
9
9
|
end
|
10
10
|
|
11
|
+
def get_required_tags(event_id, pod_name, pod_namespace, component_name, attribute_name, task_id)
|
12
|
+
{
|
13
|
+
event_id: event_id,
|
14
|
+
pod_name: pod_name,
|
15
|
+
pod_namespace: pod_namespace,
|
16
|
+
component_name: component_name,
|
17
|
+
attribute_name: attribute_name,
|
18
|
+
task_id: task_id
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
11
22
|
protected
|
12
23
|
|
13
24
|
def required_params
|
14
|
-
[
|
25
|
+
%i[event_id pod_name pod_namespace component_name attribute_name task_id]
|
15
26
|
end
|
16
|
-
|
17
|
-
end
|
27
|
+
end
|
18
28
|
end
|
19
29
|
end
|
20
|
-
end
|
30
|
+
end
|
@@ -8,13 +8,24 @@ module DTK::State
|
|
8
8
|
write_point(value, checked_params_hash, timestamp)
|
9
9
|
end
|
10
10
|
|
11
|
+
def get_required_tags(type, name, namespace, object_state, component_name, attribute_name, task_id)
|
12
|
+
{
|
13
|
+
type: type,
|
14
|
+
name: name,
|
15
|
+
namespace: namespace,
|
16
|
+
object_state: object_state,
|
17
|
+
component_name: component_name,
|
18
|
+
attribute_name: attribute_name,
|
19
|
+
task_id: task_id
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
11
23
|
protected
|
12
24
|
|
13
25
|
def required_params
|
14
|
-
[
|
26
|
+
%i[type name namespace object_state component_name attribute_name task_id]
|
15
27
|
end
|
16
|
-
|
17
|
-
end
|
28
|
+
end
|
18
29
|
end
|
19
30
|
end
|
20
|
-
end
|
31
|
+
end
|
@@ -2,7 +2,8 @@ module DTK::State
|
|
2
2
|
class Component::Attribute::Influxdb
|
3
3
|
class SemanticType
|
4
4
|
require_relative './client'
|
5
|
-
|
5
|
+
require_relative('../../../../utils/log')
|
6
|
+
attr_reader :name, :crd_content, :namespace, :client, :expanded_semantictype_spec
|
6
7
|
attr_accessor :content_to_write
|
7
8
|
|
8
9
|
def initialize(name, namespace)
|
@@ -11,75 +12,103 @@ module DTK::State
|
|
11
12
|
@client = Client.new
|
12
13
|
@crd_content = get(name, namespace)
|
13
14
|
@content_to_write = []
|
14
|
-
|
15
|
-
|
16
|
-
# no namespace because semantictype instances are going to be unique in cluster
|
17
|
-
def get(name, namespace, opts = {})
|
18
|
-
begin
|
19
|
-
semantictype = ::DTK::CrdClient.get_kubeclient(opts).get_semantictype(name, namespace)
|
20
|
-
semantictype.spec[:openAPIV3Schema]
|
21
|
-
rescue => error
|
22
|
-
fail "SemanticType attribute with name '#{name}' not found on the cluster!. Error: #{error}"
|
23
|
-
end
|
15
|
+
@expanded_spec = ::DTK::CrdClient.get_kubeclient({}).get_semantictype(name, namespace).expandedSpec
|
16
|
+
@expanded_semantictype_spec = expand(crd_content.to_h[:properties], @expanded_spec)
|
24
17
|
end
|
25
18
|
|
26
19
|
def write_semantictype_inventory(inventory, component_id)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
})
|
37
|
-
end
|
38
|
-
"Inventory for attribute #{name} written to InfluxDB"
|
39
|
-
rescue => error
|
40
|
-
fail "#{name} inventory write failed. Error: #{error}"
|
20
|
+
get_influxdb_properties(inventory)
|
21
|
+
content_to_write.each do |point|
|
22
|
+
point[:tags].merge!({ component_id: component_id, attribute_name: @name })
|
23
|
+
@client.write_point({
|
24
|
+
name: point[:name],
|
25
|
+
tags: point[:tags],
|
26
|
+
fields: point[:fields],
|
27
|
+
time: (Time.new.to_f * 1000).to_i
|
28
|
+
})
|
41
29
|
end
|
30
|
+
rescue => e
|
31
|
+
raise "#{name} inventory write failed. Error: #{e}"
|
42
32
|
end
|
43
33
|
|
44
34
|
def partial_write_update(component_and_attribute, path, field_name, field_value)
|
45
35
|
parent, child = validate_parameter(path)
|
46
36
|
component_id, attribute_name = component_and_attribute.split('/')
|
47
37
|
# getting previous value for given parameters
|
48
|
-
previous_value = {
|
38
|
+
previous_value = {}
|
49
39
|
begin
|
50
40
|
flux_query = 'from(bucket:"' + @client.connection_parameters[:bucket] + '") |> range(start:-5) |> filter(fn:(r) => r._measurement == "' + attribute_name + "_" + child[:type] + '") |> filter(fn: (r) => r.parent == "' + parent[:name] + '") |> filter(fn: (r) => r.name == "' + child[:name] + '")|> last()'
|
51
41
|
result = @client.query(query: flux_query)
|
52
42
|
previous_value = result.values.map(&:records).flatten.map(&:values)
|
53
|
-
rescue =>
|
54
|
-
|
43
|
+
rescue => e
|
44
|
+
raise "Partial write could not be completed. Previous point for given parameters not found!. Error: #{e}"
|
55
45
|
end
|
56
46
|
update_current(previous_value[0], get_path_to_object(path), field_name, field_value)
|
57
47
|
end
|
58
48
|
|
59
49
|
private
|
60
50
|
|
51
|
+
def expand(crd_content, expanded_spec)
|
52
|
+
if expanded_spec.nil?
|
53
|
+
expanded_spec = {}
|
54
|
+
populate_semantictypes(crd_content, expanded_spec)
|
55
|
+
::DTK::CrdClient.get_kubeclient({}).merge_patch_semantictype(name, { expandedSpec: expanded_spec }, namespace)
|
56
|
+
end
|
57
|
+
expanded_spec
|
58
|
+
end
|
59
|
+
|
60
|
+
def get(name, namespace, opts = {})
|
61
|
+
semantictype = ::DTK::CrdClient.get_kubeclient(opts).get_semantictype(name, namespace)
|
62
|
+
semantictype.spec[:openAPIV3Schema]
|
63
|
+
rescue => e
|
64
|
+
raise "SemanticType attribute with name '#{name}' not found on the cluster!. Error: #{e.inspect}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def populate_semantictypes(crd, expanded_spec, parent = nil)
|
68
|
+
crd.each_pair do |key, value|
|
69
|
+
basic, semantictype = value[:type].split(':')
|
70
|
+
if semantictype
|
71
|
+
expanded_spec[key] = get(basic.to_s, namespace).to_h[:properties]
|
72
|
+
crd[key] = get(basic.to_s, namespace).to_h[:properties]
|
73
|
+
temporary_expanded_spec = populate_semantictypes(crd[key], expanded_spec[key], basic)
|
74
|
+
end
|
75
|
+
if basic == 'array'
|
76
|
+
basic, semantictype = value[:items][:type].split(':')
|
77
|
+
if semantictype
|
78
|
+
expanded_spec[key] = get(basic.to_s, namespace).to_h[:properties]
|
79
|
+
crd[key] = get(basic.to_s, namespace).to_h[:properties]
|
80
|
+
temporary_expanded_spec = populate_semantictypes(crd[key], expanded_spec[key], basic)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
temporary_expanded_spec ||= {}
|
84
|
+
unless temporary_expanded_spec.empty? || parent.nil?
|
85
|
+
::DTK::CrdClient.get_kubeclient({}).merge_patch_semantictype(parent, { expandedSpec: { key => temporary_expanded_spec } }, namespace)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
expanded_spec
|
89
|
+
end
|
90
|
+
|
61
91
|
def update_current(previous_value, path, field_name, field_value)
|
62
|
-
tags = {
|
92
|
+
tags = {}
|
63
93
|
previous_value.each_pair do |key, value|
|
64
|
-
|
94
|
+
tags[key] = value if key[0..0] != '_' && key != 'result' && key != 'table'
|
65
95
|
end
|
66
96
|
fields = Hash.new
|
67
97
|
fields[field_name.to_sym] = field_value
|
68
98
|
validate_fields(get_partial_definition(path), fields)
|
69
99
|
@client.write_point({
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
"Partial write update successful"
|
100
|
+
name: previous_value['_measurement'],
|
101
|
+
tags: tags,
|
102
|
+
fields: fields,
|
103
|
+
time: (Time.new.to_f * 1000).to_i
|
104
|
+
})
|
76
105
|
end
|
77
106
|
|
78
107
|
def get_influxdb_properties(inventory, parent_type = [:top], parent_name = nil)
|
79
108
|
content_to_write = []
|
80
109
|
properties = { }
|
81
110
|
inventory.each_pair do |key, value|
|
82
|
-
if value.class.to_s ==
|
111
|
+
if value.class.to_s == 'Array'
|
83
112
|
inventory[key].each do |element|
|
84
113
|
get_influxdb_properties(element, parent_type.push(key), inventory[:name])
|
85
114
|
end
|
@@ -102,12 +131,10 @@ module DTK::State
|
|
102
131
|
end
|
103
132
|
|
104
133
|
def validate_request(partial_definition, request)
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
fail error
|
110
|
-
end
|
134
|
+
validate_tags(partial_definition, request[:tags])
|
135
|
+
validate_fields(partial_definition, request[:fields])
|
136
|
+
rescue => e
|
137
|
+
raise e
|
111
138
|
end
|
112
139
|
|
113
140
|
def get_tags_and_fields(partial_definition, properties)
|
@@ -115,7 +142,7 @@ module DTK::State
|
|
115
142
|
fields = { }
|
116
143
|
properties.each_pair do |key, value|
|
117
144
|
if partial_definition[key].nil?
|
118
|
-
|
145
|
+
raise "Property '#{key}' not found in the definition of attribute"
|
119
146
|
else
|
120
147
|
if partial_definition[key][:metric].nil? || partial_definition[key][:metric] == false
|
121
148
|
tags[key] = value
|
@@ -131,14 +158,12 @@ module DTK::State
|
|
131
158
|
end
|
132
159
|
|
133
160
|
def validate_fields(partial_definition, fields)
|
134
|
-
|
135
161
|
partial_definition.each_pair do |key, value|
|
136
162
|
next if key == :required || value[:metric] == (false || nil)
|
137
|
-
|
138
163
|
if fields[key].nil?
|
139
|
-
|
164
|
+
raise "Field #{key} is missing. Validation of request failed!"
|
140
165
|
elsif value[:type].capitalize != fields[key].class.to_s
|
141
|
-
|
166
|
+
raise "Defined type for SemanticType attribute property '#{key}' is #{value[:type].capitalize}, #{fields[key].class} provided"
|
142
167
|
end
|
143
168
|
end
|
144
169
|
end
|
@@ -146,20 +171,19 @@ module DTK::State
|
|
146
171
|
def validate_tags(partial_definition, tags)
|
147
172
|
partial_definition.each_pair do |key, value|
|
148
173
|
next if key == :required || value[:metric] == true
|
149
|
-
|
150
174
|
if tags[key].nil?
|
151
175
|
if value[:default].nil?
|
152
|
-
|
153
|
-
else
|
176
|
+
raise "Property #{key} is missing. Validation of request failed!"
|
177
|
+
else
|
154
178
|
tags[key] = value[:default]
|
155
179
|
end
|
156
|
-
else
|
180
|
+
else
|
157
181
|
type = tags[key].class
|
158
|
-
type =
|
182
|
+
type = 'Boolean' if type == TrueClass || type == FalseClass
|
159
183
|
if value[:type].capitalize == type.to_s
|
160
184
|
next
|
161
185
|
else
|
162
|
-
|
186
|
+
raise "Defined type for SemanticType attribute property '#{key}' is #{value[:type].capitalize}, #{type} provided"
|
163
187
|
end
|
164
188
|
end
|
165
189
|
end
|
@@ -167,24 +191,25 @@ module DTK::State
|
|
167
191
|
|
168
192
|
def get_partial_definition(path)
|
169
193
|
i = 0
|
170
|
-
definition = {
|
171
|
-
semantictype_crd = crd_content[:properties]
|
194
|
+
definition = {}
|
195
|
+
semantictype_crd = crd_content.to_h[:properties]
|
196
|
+
expanded_spec = expanded_semantictype_spec.to_h
|
172
197
|
while i < path.length
|
173
198
|
if path[i].to_sym == :top
|
174
199
|
semantictype_crd.each_pair do |key, value|
|
175
200
|
if key == :required
|
176
201
|
definition[key] = value
|
177
202
|
else
|
178
|
-
|
203
|
+
basic, semantictype = value[:type].split(':')
|
204
|
+
semantictype.nil? ? definition[key] = value : definition[key] = semantictype_crd[key] if basic != 'array'
|
179
205
|
end
|
180
206
|
end
|
181
207
|
else
|
182
208
|
definition = {}
|
183
|
-
|
184
|
-
|
185
|
-
definition[key] = value if value[:type] != "array"
|
209
|
+
expanded_spec[path[i].to_sym].each_pair do |key, value|
|
210
|
+
definition[key] = value unless value[:type].nil?
|
186
211
|
end
|
187
|
-
|
212
|
+
expanded_spec = expanded_spec[path[i].to_sym]
|
188
213
|
end
|
189
214
|
i+=1
|
190
215
|
end
|
@@ -192,7 +217,7 @@ module DTK::State
|
|
192
217
|
end
|
193
218
|
|
194
219
|
def get_path_to_object(parameter)
|
195
|
-
path = [
|
220
|
+
path = ['top']
|
196
221
|
array = parameter.split('/')
|
197
222
|
array.each do |element|
|
198
223
|
path.push(element.split(':')[1])
|
@@ -202,19 +227,18 @@ module DTK::State
|
|
202
227
|
|
203
228
|
def validate_parameter(parameter)
|
204
229
|
array_of_parameters = []
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
end
|
214
|
-
array_of_parameters
|
215
|
-
rescue => error
|
216
|
-
fail "Could not resolve parameter '#{parameter}'. It should be in format: 'parent:type/child:type'"
|
230
|
+
parameter.split('/').each_with_index do |param, index|
|
231
|
+
name, type = param.split(':')
|
232
|
+
raise unless name && type
|
233
|
+
|
234
|
+
array_of_parameters.push({
|
235
|
+
name: name,
|
236
|
+
type: type
|
237
|
+
})
|
217
238
|
end
|
239
|
+
array_of_parameters
|
240
|
+
rescue => e
|
241
|
+
raise "Could not resolve parameter '#{parameter}'. It should be in format: 'parent:type/child:type'"
|
218
242
|
end
|
219
243
|
end
|
220
244
|
end
|
data/lib/state/component_def.rb
CHANGED
@@ -2,16 +2,19 @@ module DTK::State
|
|
2
2
|
class ComponentDef
|
3
3
|
require_relative 'component_def/attribute_type_info'
|
4
4
|
|
5
|
+
COMPONENT_DEF_CRD_VERSION = ENV["COMPONENT_DEF_CRD_VERSION"]
|
6
|
+
|
5
7
|
attr_reader :name, :namespace, :executable_actions, :attribute_type_info
|
6
8
|
|
7
9
|
def initialize(namespace, name, content)
|
8
10
|
@name = name
|
9
11
|
@namespace = namespace
|
10
|
-
@executable_actions = content[:spec][:actions]
|
12
|
+
@executable_actions = content[:spec][:actions] || {}
|
11
13
|
@attribute_type_info = AttributeTypeInfo.create_from_kube_hash(content[:spec][:attributes] || {})
|
12
14
|
end
|
13
15
|
|
14
16
|
def self.get(namespace, name, opts = {})
|
17
|
+
opts[:apiVersion] = COMPONENT_DEF_CRD_VERSION
|
15
18
|
crd_component_def = ::DTK::CrdClient.get_kubeclient(opts).get_componentdef(name, namespace)
|
16
19
|
ComponentDef.new(namespace, name, crd_component_def)
|
17
20
|
end
|
@@ -19,6 +19,19 @@ module DTK::State
|
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
+
def to_hash
|
23
|
+
type_info = {}
|
24
|
+
|
25
|
+
type_info.merge!(name: @name) if @name
|
26
|
+
type_info.merge!(type: @type) if @type
|
27
|
+
type_info.merge!(required: @required) if @required
|
28
|
+
type_info.merge!(dynamic: @dynamic) if @dynamic
|
29
|
+
type_info.merge!(temporal: @temporal) if @temporal
|
30
|
+
type_info.merge!(encrypted: @encrypted) if @encrypted
|
31
|
+
|
32
|
+
type_info
|
33
|
+
end
|
34
|
+
|
22
35
|
end
|
23
36
|
end
|
24
37
|
end
|
data/lib/state/crd_assembly.rb
CHANGED
@@ -1,17 +1,24 @@
|
|
1
1
|
module DTK::State
|
2
2
|
class CrdAssembly
|
3
|
-
|
3
|
+
ASSEMBLY_CRD_VERSION = ENV["ASSEMBLY_CRD_VERSION"]
|
4
|
+
|
5
|
+
attr_reader :name, :namespace, :crd_content, :references, :component_objs
|
6
|
+
attr_accessor :components
|
4
7
|
|
5
8
|
def initialize(namespace, name, crd_content)
|
6
|
-
@name
|
7
|
-
@namespace
|
8
|
-
@
|
9
|
-
@
|
10
|
-
@
|
9
|
+
@name = name
|
10
|
+
@namespace = namespace
|
11
|
+
@api_version = crd_content.apiVersion
|
12
|
+
@kind = crd_content.kind
|
13
|
+
@metadata = crd_content.metadata
|
14
|
+
# @crd_content = crd_content
|
15
|
+
@references = crd_content.references
|
16
|
+
@components = crd_content[:spec][:components] || []
|
17
|
+
# @component_objs = Component.create_from_kube_array(@components, self)
|
11
18
|
end
|
12
19
|
|
13
20
|
def self.get(namespace, name, opts = {})
|
14
|
-
|
21
|
+
opts[:apiVersion] = ASSEMBLY_CRD_VERSION
|
15
22
|
crd_assembly = ::DTK::CrdClient.get_kubeclient(opts).get_assembly(name, namespace)
|
16
23
|
CrdAssembly.new(namespace, name, crd_assembly)
|
17
24
|
end
|
@@ -50,6 +57,51 @@ module DTK::State
|
|
50
57
|
output
|
51
58
|
end
|
52
59
|
|
60
|
+
# TODO: this is a temporal solution to avoid breaking backward compatibility; will change this soon
|
61
|
+
def self.get_with_influx_data(namespace, assembly_name, opts = {})
|
62
|
+
assembly = get(namespace, assembly_name, opts)
|
63
|
+
|
64
|
+
components_hash = {}
|
65
|
+
assembly.components.each do |assembly_component|
|
66
|
+
cmp_name = assembly_component.to_hash.keys.first
|
67
|
+
components_hash[cmp_name] = assembly_component[cmp_name].to_hash
|
68
|
+
end
|
69
|
+
|
70
|
+
component_objs = Component.create_from_kube_array(assembly.components, assembly)
|
71
|
+
|
72
|
+
component_objs.each do |component_obj|
|
73
|
+
component_name = component_obj.name
|
74
|
+
attr_type_info = component_obj.component_def.attribute_type_info
|
75
|
+
attr_type_info.each do |attr_info|
|
76
|
+
if attr_info.temporal
|
77
|
+
attribute_name = attr_info.name
|
78
|
+
influxdb = ::DTK::State::Component::Attribute::Influxdb.new(:attributes)
|
79
|
+
influxdb_attribute = influxdb.get(namespace, component_name, assembly_name, attribute_name, opts)
|
80
|
+
if valid_attribute = influxdb_attribute.first
|
81
|
+
value = valid_attribute['_value']
|
82
|
+
if components_hash[component_name][:attributes][attribute_name].is_a?(String)
|
83
|
+
components_hash[component_name][:attributes][attribute_name] = value
|
84
|
+
else
|
85
|
+
components_hash[component_name][:attributes][attribute_name][:value] = value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
assembly.components = components_hash
|
92
|
+
assembly
|
93
|
+
end
|
94
|
+
|
95
|
+
def to_hash
|
96
|
+
{
|
97
|
+
apiVersion: @api_version,
|
98
|
+
kind: @kind,
|
99
|
+
metadata: @metadata.to_hash,
|
100
|
+
references: @references.to_hash,
|
101
|
+
spec: { components: @components.to_hash }
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
53
105
|
private
|
54
106
|
|
55
107
|
def self.check_for_missing_components(crd_components, requested_components)
|