floe 0.13.0 → 0.14.0

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