floe 0.13.0 → 0.14.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: abe15498a790293b2a375c4538ed649bfa577edd1b7eaa38d02266fc4f7fc576
4
- data.tar.gz: 733e0e6687ca143de8a74214f13b4a78118333a99f89a9a09a9998ab0319768a
3
+ metadata.gz: e0e0b86c08d322e2ebc8e115e5193d0493f8257c831f7377b18c32d4284ae6f2
4
+ data.tar.gz: 1bf5ed62abafe2e1af6025dbaf47d9faa3b1c93ffec8fd88fbeacd4445efc795
5
5
  SHA512:
6
- metadata.gz: d5dfc65c86e68d39b7c9bbeedd20e2a3ea844f158d601bb72bf35595e6d6597ac6571f7bc9ad6802f8ed33d6753b4906e7745a3e46cb38790492869aa95e6a02
7
- data.tar.gz: 913032cb3a042e8b3464f05c7e0e65401f8532b28ded1cebd57df310899dcc8bd1bc26d9307a6b824837474c0e3ee9144477147779e0b40b6fd9c9fecfc7bddc
6
+ metadata.gz: 34f64555f2472c121fcea9dda8baac7fbe853cdbc5e949c831d7b7f070878d3ef2f86ed90d907b5a7033539fcad6aeea64a4dee6475a752d7f89e4f679a4b269
7
+ data.tar.gz: 378c5bcd562d4bbeb7e3eb13eb55b1d5d8e7aed0ea4749c2b65f951cf3be0e5f13d3e288cdbf63b4a5729e98c5010656d6dca3db60ffbf8463988008980edd83
data/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ This project adheres to [Semantic Versioning](http://semver.org/).
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.14.0] - 2024-08-20
8
+ ### Added
9
+ - Implement "IsNumeric": false ([#266](https://github.com/ManageIQ/floe/pull/266))
10
+ - Support choices that do not have a Default defined ([#267](https://github.com/ManageIQ/floe/pull/267))
11
+ - Label containers/pods with workflow Execution ID ([#268](https://github.com/ManageIQ/floe/pull/268))
12
+ - Allow for Execution Id to be passed in ([#269](https://github.com/ManageIQ/floe/pull/269))
13
+
14
+ ## [0.13.1] - 2024-08-16
15
+ ### Fixed
16
+ - Fix podman/docker container_ref trailing newline ([#265](https://github.com/ManageIQ/floe/pull/265))
17
+
18
+ ### Changed
19
+ - Improve type check for States.Hash ([#261](https://github.com/ManageIQ/floe/pull/261))
20
+ - Use Numeric over Integer || Float ([#264](https://github.com/ManageIQ/floe/pull/264))
21
+ - In ChoiceRule::Data, parse compare key and variable ([#257](https://github.com/ManageIQ/floe/pull/257))
22
+
7
23
  ## [0.13.0] - 2024-08-12
8
24
  ### Added
9
25
  - Choice rule payload validation path ([#253](https://github.com/ManageIQ/floe/pull/253))
@@ -233,7 +249,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
233
249
  ### Added
234
250
  - Initial release
235
251
 
236
- [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.13.0...HEAD
252
+ [Unreleased]: https://github.com/ManageIQ/floe/compare/v0.14.0...HEAD
253
+ [0.14.0]: https://github.com/ManageIQ/floe/compare/v0.13.1...v0.14.0
254
+ [0.13.1]: https://github.com/ManageIQ/floe/compare/v0.13.0...v0.13.1
237
255
  [0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
238
256
  [0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
239
257
  [0.11.3]: https://github.com/ManageIQ/floe/compare/v0.11.2...v0.11.3
@@ -18,11 +18,11 @@ module Floe
18
18
  @pull_policy = options["pull-policy"]
19
19
  end
20
20
 
21
- def run_async!(resource, env = {}, secrets = {}, _context = {})
21
+ def run_async!(resource, env, secrets, context)
22
22
  raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
23
23
 
24
- image = resource.sub("docker://", "")
25
-
24
+ image = resource.sub("docker://", "")
25
+ execution_id = context.execution["Id"]
26
26
  runner_context = {}
27
27
 
28
28
  if secrets && !secrets.empty?
@@ -30,7 +30,7 @@ module Floe
30
30
  end
31
31
 
32
32
  begin
33
- runner_context["container_ref"] = run_container(image, env, runner_context["secrets_ref"])
33
+ runner_context["container_ref"] = run_container(image, env, execution_id, runner_context["secrets_ref"])
34
34
  runner_context
35
35
  rescue AwesomeSpawn::CommandResultError => err
36
36
  cleanup(runner_context)
@@ -123,22 +123,23 @@ module Floe
123
123
 
124
124
  attr_reader :network
125
125
 
126
- def run_container(image, env, secrets_file)
127
- params = run_container_params(image, env, secrets_file)
126
+ def run_container(image, env, execution_id, secrets_file)
127
+ params = run_container_params(image, env, execution_id, secrets_file)
128
128
 
129
129
  logger.debug("Running #{AwesomeSpawn.build_command_line(self.class::DOCKER_COMMAND, params)}")
130
130
 
131
131
  result = docker!(*params)
132
- result.output
132
+ result.output.chomp
133
133
  end
134
134
 
135
- def run_container_params(image, env, secrets_file)
135
+ def run_container_params(image, env, execution_id, secrets_file)
136
136
  params = ["run"]
137
137
  params << :detach
138
138
  params += env.map { |k, v| [:e, "#{k}=#{v}"] }
139
139
  params << [:e, "_CREDENTIALS=/run/secrets"] if secrets_file
140
140
  params << [:pull, @pull_policy] if @pull_policy
141
141
  params << [:net, "host"] if @network == "host"
142
+ params << [:label, "execution_id=#{execution_id}"]
142
143
  params << [:v, "#{secrets_file}:/run/secrets:z"] if secrets_file
143
144
  params << [:name, container_name(image)]
144
145
  params << image
@@ -157,11 +158,11 @@ module Floe
157
158
  event = docker_event_status_to_event(status)
158
159
  running = event != :delete
159
160
 
160
- name, exit_code = notice.dig("Actor", "Attributes")&.values_at("name", "exitCode")
161
+ name, exit_code, execution_id = notice.dig("Actor", "Attributes")&.values_at("name", "exitCode", "execution_id")
161
162
 
162
163
  runner_context = {"container_ref" => name, "container_state" => {"Running" => running, "ExitCode" => exit_code.to_i}}
163
164
 
164
- [event, runner_context]
165
+ [event, {"execution_id" => execution_id, "runner_context" => runner_context}]
165
166
  rescue JSON::ParserError
166
167
  []
167
168
  end
@@ -45,17 +45,17 @@ module Floe
45
45
  super
46
46
  end
47
47
 
48
- def run_async!(resource, env = {}, secrets = {}, _context = {})
48
+ def run_async!(resource, env, secrets, context)
49
49
  raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
50
50
 
51
51
  image = resource.sub("docker://", "")
52
52
  name = container_name(image)
53
53
  secret = create_secret!(secrets) if secrets && !secrets.empty?
54
-
54
+ execution_id = context.execution["Id"]
55
55
  runner_context = {"container_ref" => name, "container_state" => {"phase" => "Pending"}, "secrets_ref" => secret}
56
56
 
57
57
  begin
58
- create_pod!(name, image, env, secret)
58
+ create_pod!(name, image, env, execution_id, secret)
59
59
  runner_context
60
60
  rescue Kubeclient::HttpError => err
61
61
  cleanup(runner_context)
@@ -171,13 +171,14 @@ module Floe
171
171
  failed_container_states(context).any?
172
172
  end
173
173
 
174
- def pod_spec(name, image, env, secret = nil)
174
+ def pod_spec(name, image, env, execution_id, secret = nil)
175
175
  spec = {
176
176
  :kind => "Pod",
177
177
  :apiVersion => "v1",
178
178
  :metadata => {
179
179
  :name => name,
180
- :namespace => namespace
180
+ :namespace => namespace,
181
+ :labels => {"execution_id" => execution_id}
181
182
  },
182
183
  :spec => {
183
184
  :containers => [
@@ -219,8 +220,8 @@ module Floe
219
220
  spec
220
221
  end
221
222
 
222
- def create_pod!(name, image, env, secret = nil)
223
- kubeclient.create_pod(pod_spec(name, image, env, secret))
223
+ def create_pod!(name, image, env, execution_id, secret = nil)
224
+ kubeclient.create_pod(pod_spec(name, image, env, execution_id, secret))
224
225
  end
225
226
 
226
227
  def delete_pod!(name)
@@ -294,9 +295,10 @@ module Floe
294
295
 
295
296
  pod = notice.object
296
297
  container_ref = pod.metadata.name
298
+ execution_id = pod.metadata.labels["execution_id"]
297
299
  container_state = pod.to_h[:status].deep_stringify_keys
298
300
 
299
- {"container_ref" => container_ref, "container_state" => container_state}
301
+ {"execution_id" => execution_id, "runner_context" => {"container_ref" => container_ref, "container_state" => container_state}}
300
302
  end
301
303
 
302
304
  def kubeclient
@@ -30,13 +30,14 @@ module Floe
30
30
 
31
31
  private
32
32
 
33
- def run_container_params(image, env, secret)
33
+ def run_container_params(image, env, execution_id, secret)
34
34
  params = ["run"]
35
35
  params << :detach
36
36
  params += env.map { |k, v| [:e, "#{k}=#{v}"] }
37
37
  params << [:e, "_CREDENTIALS=/run/secrets/#{secret}"] if secret
38
38
  params << [:pull, @pull_policy] if @pull_policy
39
39
  params << [:net, "host"] if @network == "host"
40
+ params << [:label, "execution_id=#{execution_id}"]
40
41
  params << [:secret, secret] if secret
41
42
  params << [:name, container_name(image)]
42
43
  params << image
@@ -55,14 +56,16 @@ module Floe
55
56
  end
56
57
 
57
58
  def parse_notice(notice)
58
- id, status, exit_code = JSON.parse(notice).values_at("ID", "Status", "ContainerExitCode")
59
+ notice = JSON.parse(notice)
60
+ id, status, exit_code, attributes = notice.values_at("ID", "Status", "ContainerExitCode", "Attributes")
59
61
 
60
- event = podman_event_status_to_event(status)
61
- running = event != :delete
62
+ execution_id = attributes&.dig("execution_id")
63
+ event = podman_event_status_to_event(status)
64
+ running = event != :delete
62
65
 
63
66
  runner_context = {"container_ref" => id, "container_state" => {"Running" => running, "ExitCode" => exit_code.to_i}}
64
67
 
65
- [event, runner_context]
68
+ [event, {"execution_id" => execution_id, "runner_context" => runner_context}]
66
69
  rescue JSON::ParserError
67
70
  []
68
71
  end
@@ -18,6 +18,10 @@ module Floe
18
18
  self.class.invalid_field_error!(name, field_name, field_value, comment)
19
19
  end
20
20
 
21
+ def runtime_field_error!(field_name, field_value, comment, floe_error: "States.Runtime")
22
+ raise Floe::ExecutionError.new(self.class.field_error_text(name, field_name, field_value, comment), floe_error)
23
+ end
24
+
21
25
  def workflow_state?(field_value, workflow)
22
26
  workflow.payload["States"] ? workflow.payload["States"].include?(field_value) : true
23
27
  end
@@ -39,10 +43,14 @@ module Floe
39
43
  end
40
44
 
41
45
  def invalid_field_error!(name, field_name, field_value, comment)
46
+ raise Floe::InvalidWorkflowError, field_error_text(name, field_name, field_value, comment)
47
+ end
48
+
49
+ def field_error_text(name, field_name, field_value, comment = nil)
42
50
  # instead of displaying a large hash or array, just displaying the word Hash or Array
43
51
  field_value = field_value.class if field_value.kind_of?(Hash) || field_value.kind_of?(Array)
44
52
 
45
- parser_error!(name, "field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}")
53
+ "#{Array(name).join(".")} field \"#{field_name}\"#{" value \"#{field_value}\"" unless field_value.nil?} #{comment}"
46
54
  end
47
55
  end
48
56
  end
data/lib/floe/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Floe
4
- VERSION = "0.13.0"
4
+ VERSION = "0.14.0"
5
5
  end
@@ -4,6 +4,18 @@ module Floe
4
4
  class Workflow
5
5
  class ChoiceRule
6
6
  class Data < Floe::Workflow::ChoiceRule
7
+ COMPARE_KEYS = %w[IsNull IsPresent IsNumeric IsString IsBoolean IsTimestamp String Numeric Boolean Timestamp].freeze
8
+
9
+ attr_reader :variable, :compare_key, :value, :path
10
+
11
+ def initialize(_workflow, _name, payload)
12
+ super
13
+
14
+ @variable = parse_path("Variable", payload)
15
+ parse_compare_key
16
+ @value = path ? parse_path(compare_key, payload) : payload[compare_key]
17
+ end
18
+
7
19
  def true?(context, input)
8
20
  return presence_check(context, input) if compare_key == "IsPresent"
9
21
 
@@ -11,11 +23,11 @@ module Floe
11
23
  rhs = compare_value(context, input)
12
24
 
13
25
  case compare_key
14
- when "IsNull" then is_null?(lhs)
15
- when "IsNumeric" then is_numeric?(lhs)
16
- when "IsString" then is_string?(lhs)
17
- when "IsBoolean" then is_boolean?(lhs)
18
- when "IsTimestamp" then is_timestamp?(lhs)
26
+ when "IsNull" then is_null?(lhs, rhs)
27
+ when "IsNumeric" then is_numeric?(lhs, rhs)
28
+ when "IsString" then is_string?(lhs, rhs)
29
+ when "IsBoolean" then is_boolean?(lhs, rhs)
30
+ when "IsTimestamp" then is_timestamp?(lhs, rhs)
19
31
  when "StringEquals", "StringEqualsPath",
20
32
  "NumericEquals", "NumericEqualsPath",
21
33
  "BooleanEquals", "BooleanEqualsPath",
@@ -50,54 +62,71 @@ module Floe
50
62
  # Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
51
63
  # If true then return true when present.
52
64
  # If false then return true when not present.
53
- rhs = compare_value(context, input)
65
+ predicate = compare_value(context, input)
54
66
  # Don't need the variable_value, just need to see if the path finds the value.
55
67
  variable_value(context, input)
56
68
 
57
69
  # The variable_value is present
58
- # If rhs is true, then presence check was successful, return true.
59
- rhs
70
+ # If predicate is true, then presence check was successful, return true.
71
+ predicate
60
72
  rescue Floe::PathError
61
73
  # variable_value is not present. (the path lookup threw an error)
62
- # If rhs is false, then it successfully wasn't present, return true.
63
- !rhs
74
+ # If predicate is false, then it successfully wasn't present, return true.
75
+ !predicate
64
76
  end
65
77
 
66
- def is_null?(value) # rubocop:disable Naming/PredicateName
67
- value.nil?
78
+ # rubocop:disable Naming/PredicateName
79
+ # rubocop:disable Style/OptionalBooleanParameter
80
+ def is_null?(value, predicate = true)
81
+ value.nil? == predicate
68
82
  end
69
83
 
70
- def is_present?(value) # rubocop:disable Naming/PredicateName
71
- !value.nil?
84
+ def is_present?(value, predicate = true)
85
+ !value.nil? == predicate
72
86
  end
73
87
 
74
- def is_numeric?(value) # rubocop:disable Naming/PredicateName
75
- value.kind_of?(Integer) || value.kind_of?(Float)
88
+ def is_numeric?(value, predicate = true)
89
+ value.kind_of?(Numeric) == predicate
76
90
  end
77
91
 
78
- def is_string?(value) # rubocop:disable Naming/PredicateName
79
- value.kind_of?(String)
92
+ def is_string?(value, predicate = true)
93
+ value.kind_of?(String) == predicate
80
94
  end
81
95
 
82
- def is_boolean?(value) # rubocop:disable Naming/PredicateName
83
- [true, false].include?(value)
96
+ def is_boolean?(value, predicate = true)
97
+ [true, false].include?(value) == predicate
84
98
  end
85
99
 
86
- def is_timestamp?(value) # rubocop:disable Naming/PredicateName
100
+ def is_timestamp?(value, predicate = true)
87
101
  require "date"
88
102
 
89
103
  DateTime.rfc3339(value)
90
- true
104
+ predicate
91
105
  rescue TypeError, Date::Error
92
- false
106
+ !predicate
93
107
  end
108
+ # rubocop:enable Naming/PredicateName
109
+ # rubocop:enable Style/OptionalBooleanParameter
94
110
 
95
- def compare_key
96
- @compare_key ||= payload.keys.detect { |key| key.match?(/^(IsNull|IsPresent|IsNumeric|IsString|IsBoolean|IsTimestamp|String|Numeric|Boolean|Timestamp)/) }
111
+ def parse_compare_key
112
+ @compare_key = payload.keys.detect { |key| key.match?(/^(#{COMPARE_KEYS.join("|")})/) }
113
+ parser_error!("requires a compare key") unless compare_key
114
+
115
+ @path = compare_key.end_with?("Path")
97
116
  end
98
117
 
99
118
  def compare_value(context, input)
100
- compare_key.end_with?("Path") ? Path.value(payload[compare_key], context, input) : payload[compare_key]
119
+ path ? value.value(context, input) : value
120
+ end
121
+
122
+ def variable_value(context, input)
123
+ variable.value(context, input)
124
+ end
125
+
126
+ def parse_path(field_name, payload)
127
+ value = payload[field_name]
128
+ missing_field_error!(field_name) unless value
129
+ wrap_parser_error(field_name, value) { Path.new(value) }
101
130
  end
102
131
  end
103
132
  end
@@ -3,6 +3,8 @@
3
3
  module Floe
4
4
  class Workflow
5
5
  class ChoiceRule
6
+ include ValidationMixin
7
+
6
8
  class << self
7
9
  def build(workflow, name, payload)
8
10
  if (sub_payloads = payload["Not"])
@@ -25,15 +27,15 @@ module Floe
25
27
  end
26
28
  end
27
29
 
28
- attr_reader :next, :payload, :variable, :children, :name
30
+ attr_reader :next, :payload, :children, :name
29
31
 
30
- def initialize(_workflow, name, payload, children = nil)
32
+ def initialize(workflow, name, payload, children = nil)
31
33
  @name = name
32
34
  @payload = payload
33
35
  @children = children
36
+ @next = payload["Next"]
34
37
 
35
- @next = payload["Next"]
36
- @variable = payload["Variable"]
38
+ validate_next!(workflow)
37
39
  end
38
40
 
39
41
  def true?(*)
@@ -42,8 +44,31 @@ module Floe
42
44
 
43
45
  private
44
46
 
45
- def variable_value(context, input)
46
- Path.value(variable, context, input)
47
+ def validate_next!(workflow)
48
+ if is_child?
49
+ # non-top level nodes don't allow a next
50
+ invalid_field_error!("Next", @next, "not allowed in a child rule") if @next
51
+ elsif !@next
52
+ # top level nodes require a next
53
+ missing_field_error!("Next")
54
+ elsif !workflow_state?(@next, workflow)
55
+ # top level nodes require a next field that is found
56
+ invalid_field_error!("Next", @next, "is not found in \"States\"")
57
+ end
58
+ end
59
+
60
+ # returns true if this is a child rule underneath an And/Or/Not
61
+ # {
62
+ # "Or": [
63
+ # {"Variable": "$.foo", "IsString": true},
64
+ # {"Variable": "$.foo", "IsBoolean": true}
65
+ # ], "Next": "Finished"
66
+ # }
67
+ #
68
+ # The Or node, has no conjunction parent, so it is not a child (requires a Next)
69
+ # The 2 Data nodes have a conjunction parent, so each one is a child (do not allow a Next)
70
+ def is_child? # rubocop:disable Naming/PredicateName
71
+ !(%w[And Or Not] & name[0..-2]).empty?
47
72
  end
48
73
  end
49
74
  end
@@ -80,7 +80,7 @@ module Floe
80
80
  STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}/.freeze
81
81
 
82
82
  rule(:states_format => {:args => subtree(:args)}) do
83
- args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Integer, Float, NilClass]]])
83
+ args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Numeric, NilClass]]])
84
84
  str, *rest = *args
85
85
 
86
86
  # TODO: Handle templates with escaped characters, including invalid templates
@@ -191,13 +191,9 @@ module Floe
191
191
  end
192
192
 
193
193
  rule(:states_hash => {:args => subtree(:args)}) do
194
- args = Transformer.process_args(args(), "States.Hash", [Object, String])
194
+ args = Transformer.process_args(args(), "States.Hash", [[String, TrueClass, FalseClass, Numeric, Array, Hash], String])
195
195
  data, algorithm = *args
196
196
 
197
- if data.nil?
198
- raise ArgumentError, "invalid value for argument 1 to States.Hash (given null, expected non-null)"
199
- end
200
-
201
197
  algorithms = %w[MD5 SHA-1 SHA-256 SHA-384 SHA-512]
202
198
  unless algorithms.include?(algorithm)
203
199
  raise ArgumentError, "invalid value for argument 2 to States.Hash (given #{algorithm.inspect}, expected one of #{algorithms.map(&:inspect).join(", ")})"
@@ -48,7 +48,7 @@ module Floe
48
48
  return Errno::EAGAIN unless ready?(context)
49
49
 
50
50
  finish(context)
51
- rescue Floe::Error => e
51
+ rescue Floe::ExecutionError => e
52
52
  mark_error(context, e)
53
53
  end
54
54
 
@@ -82,7 +82,7 @@ module Floe
82
82
  def mark_error(context, exception)
83
83
  # InputPath or OutputPath were bad.
84
84
  context.next_state = nil
85
- context.output = {"Error" => "States.Runtime", "Cause" => exception.message}
85
+ context.output = {"Error" => exception.floe_error, "Cause" => exception.message}
86
86
  # Since finish threw an exception, super was never called. Calling that now.
87
87
  mark_finished(context)
88
88
  end
@@ -23,6 +23,7 @@ module Floe
23
23
  output = output_path.value(context, input)
24
24
  next_state = choices.detect { |choice| choice.true?(context, output) }&.next || default
25
25
 
26
+ runtime_field_error!("Default", nil, "not defined and no match found", :floe_error => "States.NoChoiceMatched") if next_state.nil?
26
27
  context.next_state = next_state
27
28
  context.output = output
28
29
  super
data/lib/floe/workflow.rb CHANGED
@@ -63,9 +63,11 @@ module Floe
63
63
 
64
64
  loop do
65
65
  # Block until an event is raised
66
- event, runner_context = queue.pop
66
+ event, data = queue.pop
67
67
  break if event.nil?
68
68
 
69
+ _execution_id, runner_context = data.values_at("execution_id", "runner_context")
70
+
69
71
  # If the event is for one of our workflows set the updated runner_context
70
72
  workflows.each do |workflow|
71
73
  next unless workflow.context.state.dig("RunnerContext", "container_ref") == runner_context["container_ref"]
@@ -175,7 +177,7 @@ module Floe
175
177
  context.state["Input"] = context.execution["Input"].dup
176
178
  context.state["Guid"] = SecureRandom.uuid
177
179
 
178
- context.execution["Id"] = SecureRandom.uuid
180
+ context.execution["Id"] ||= SecureRandom.uuid
179
181
  context.execution["StartTime"] = Time.now.utc.iso8601
180
182
 
181
183
  self
data/lib/floe.rb CHANGED
@@ -43,7 +43,18 @@ module Floe
43
43
  class Error < StandardError; end
44
44
  class InvalidWorkflowError < Error; end
45
45
  class InvalidExecutionInput < Error; end
46
- class PathError < Error; end
46
+
47
+ class ExecutionError < Error
48
+ attr_reader :floe_error
49
+
50
+ def initialize(message, floe_error = "States.Runtime")
51
+ super(message)
52
+ @floe_error = floe_error
53
+ end
54
+ end
55
+
56
+ class PathError < ExecutionError
57
+ end
47
58
 
48
59
  def self.logger
49
60
  @logger ||= NullLogger.new
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: floe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.13.0
4
+ version: 0.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - ManageIQ Developers
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-08-12 00:00:00.000000000 Z
11
+ date: 2024-08-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: awesome_spawn