floe 0.17.1 → 0.19.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 +4 -4
- data/CHANGELOG.md +32 -1
- data/README.md +25 -2
- data/examples/log.asl +33 -0
- data/floe.gemspec +4 -3
- data/lib/floe/builtin_runner/methods.rb +31 -2
- data/lib/floe/builtin_runner/runner.rb +2 -2
- data/lib/floe/container_runner/docker.rb +11 -4
- data/lib/floe/container_runner/kubernetes.rb +5 -2
- data/lib/floe/validation_mixin.rb +0 -4
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/catcher.rb +1 -1
- data/lib/floe/workflow/choice_rule/data.rb +76 -51
- data/lib/floe/workflow/choice_rule.rb +1 -1
- data/lib/floe/workflow/context.rb +23 -1
- data/lib/floe/workflow/intrinsic_function/transformer.rb +5 -5
- data/lib/floe/workflow/payload_template.rb +1 -1
- data/lib/floe/workflow/state.rb +5 -3
- data/lib/floe/workflow/states/choice.rb +1 -1
- data/lib/floe/workflow/states/map.rb +1 -1
- data/lib/floe/workflow/states/non_terminal_mixin.rb +1 -1
- data/lib/floe/workflow/states/task.rb +62 -24
- data/lib/floe/workflow.rb +2 -10
- data/lib/floe/workflow_base.rb +5 -1
- data/lib/floe.rb +19 -0
- metadata +46 -33
- data/.codeclimate.yml +0 -36
- data/.rubocop_cc.yml +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7ec87295c6283b53c25c1791a9f8a2a953e48bec4059bf67fd5487875c114ce
|
|
4
|
+
data.tar.gz: a56a431253dd0a4a4c4470363ce0dfa83aa6f8ec834239a43266c6e41ccc0b61
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3d888a3713b5e3291e9fb7a04ea3254268e2ee11ccdc1a49af71f992cab61d5752d689f70292d67f3c9d3b69f146c47c0a86423a424c18220e813c2997c4402
|
|
7
|
+
data.tar.gz: f54adf8264d7ed09c05d2c78b8f5029d0ed5c472ac365e75a3656cc66fa29db0dd79eded1136b0893114d75eaf9a5e4637e76a2c347d1b8917ac8d3cf4238ec2
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,33 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.19.0] - 2025-12-16
|
|
8
|
+
### Fixed
|
|
9
|
+
- Fix builtin_runner/runner spec file name ([#329](https://github.com/ManageIQ/floe/pull/329))
|
|
10
|
+
- Check WaitUntil before starting a State ([#336](https://github.com/ManageIQ/floe/pull/336))
|
|
11
|
+
- Task TimeoutSeconds and TimeoutSecondsPath not implemented ([#330](https://github.com/ManageIQ/floe/pull/330))
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Add floe://log builtin method ([#328](https://github.com/ManageIQ/floe/pull/328))
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Raise `States.Timeout` in `running?` and remove duplicate `timed_out?` check in `finished?` ([#337](https://github.com/ManageIQ/floe/pull/337))
|
|
18
|
+
|
|
19
|
+
## [0.18.0] - 2025-10-24
|
|
20
|
+
### Added
|
|
21
|
+
- Declare active support dependency ([#316](https://github.com/ManageIQ/floe/pull/316))
|
|
22
|
+
- Add Context equality comparison ([#317](https://github.com/ManageIQ/floe/pull/317))
|
|
23
|
+
- Choice rule payload validation ([#189](https://github.com/ManageIQ/floe/pull/189))
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- In ChoiceRule use Regexp.union ([#323](https://github.com/ManageIQ/floe/pull/323))
|
|
27
|
+
- Remove special case for choice rule IsPresent ([#322](https://github.com/ManageIQ/floe/pull/322))
|
|
28
|
+
- Rename data operation parameters ([#324](https://github.com/ManageIQ/floe/pull/324))
|
|
29
|
+
- Refactor workflow state check to not use payload ([#326](https://github.com/ManageIQ/floe/pull/326))
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
- Fix missing parameters in item_batcher_spec ([#325](https://github.com/ManageIQ/floe/pull/325))
|
|
33
|
+
|
|
7
34
|
## [0.17.1] - 2025-08-25
|
|
8
35
|
### Changed
|
|
9
36
|
- Default Method=GET for floe://http method parameter ([#315](https://github.com/ManageIQ/floe/pull/315))
|
|
@@ -302,7 +329,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
302
329
|
### Added
|
|
303
330
|
- Initial release
|
|
304
331
|
|
|
305
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
|
332
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.19.0...HEAD
|
|
333
|
+
[0.19.0]: https://github.com/ManageIQ/floe/compare/v0.18.0...v0.19.0
|
|
334
|
+
[0.18.0]: https://github.com/ManageIQ/floe/compare/v0.17.1...v0.18.0
|
|
335
|
+
[0.17.1]: https://github.com/ManageIQ/floe/compare/v0.17.0...v0.17.1
|
|
336
|
+
[0.17.0]: https://github.com/ManageIQ/floe/compare/v0.16.0...v0.17.0
|
|
306
337
|
[0.16.0]: https://github.com/ManageIQ/floe/compare/v0.16.0...v0.17.0
|
|
307
338
|
[0.16.0]: https://github.com/ManageIQ/floe/compare/v0.15.1...v0.16.0
|
|
308
339
|
[0.15.1]: https://github.com/ManageIQ/floe/compare/v0.15.0...v0.15.1
|
data/README.md
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
# Floe
|
|
2
2
|
|
|
3
3
|
[](https://github.com/ManageIQ/floe/actions/workflows/ci.yaml)
|
|
4
|
-
[](https://codeclimate.com/github/ManageIQ/floe)
|
|
5
|
-
[](https://codeclimate.com/github/ManageIQ/floe/coverage)
|
|
6
4
|
|
|
7
5
|
## Overview
|
|
8
6
|
|
|
@@ -167,6 +165,31 @@ Task Runner Types:
|
|
|
167
165
|
|
|
168
166
|
This is the "builtin" runner and exposes methods that are executed internally without having to call out to a docker image.
|
|
169
167
|
|
|
168
|
+
##### Log builtin method
|
|
169
|
+
|
|
170
|
+
`floe://log` allows you to write a message to the logger.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
```json
|
|
174
|
+
{
|
|
175
|
+
"Comment": "Print log message",
|
|
176
|
+
"StartAt": "Log",
|
|
177
|
+
"States": {
|
|
178
|
+
"Log": {
|
|
179
|
+
"Type": "Task",
|
|
180
|
+
"Resource": "floe://log",
|
|
181
|
+
"Parameters": {
|
|
182
|
+
"Level": "INFO",
|
|
183
|
+
"Message": "Hello, Floe!"
|
|
184
|
+
},
|
|
185
|
+
"End": true
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Log Parameters:
|
|
190
|
+
* `Level` (required) - Log level. Permitted values: `DEBUG`, `INFO`, `WARN`, `ERROR`, `FATAL`, or `UNKNOWN`. Defaults to `INFO`.
|
|
191
|
+
* `Message` (required) - The message to log
|
|
192
|
+
|
|
170
193
|
##### HTTP builtin method
|
|
171
194
|
|
|
172
195
|
`floe://http` allows you to execute HTTP method calls.
|
data/examples/log.asl
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Comment": "Print log messages",
|
|
3
|
+
"StartAt": "Log Info",
|
|
4
|
+
"States": {
|
|
5
|
+
"Log Info": {
|
|
6
|
+
"Type": "Task",
|
|
7
|
+
"Resource": "floe://log",
|
|
8
|
+
"Parameters": {
|
|
9
|
+
"Level": "INFO",
|
|
10
|
+
"Message": "Hello, Floe!"
|
|
11
|
+
},
|
|
12
|
+
"Next": "Log Debug"
|
|
13
|
+
},
|
|
14
|
+
"Log Debug": {
|
|
15
|
+
"Type": "Task",
|
|
16
|
+
"Resource": "floe://log",
|
|
17
|
+
"Parameters": {
|
|
18
|
+
"Level": "DEBUG",
|
|
19
|
+
"Message": "Hello, Floe!"
|
|
20
|
+
},
|
|
21
|
+
"Next": "Log From Input"
|
|
22
|
+
},
|
|
23
|
+
"Log From Input": {
|
|
24
|
+
"Type": "Task",
|
|
25
|
+
"Resource": "floe://log",
|
|
26
|
+
"Parameters": {
|
|
27
|
+
"Level": "INFO",
|
|
28
|
+
"Message.$": "States.Format('Hello, {}!', $.name)"
|
|
29
|
+
},
|
|
30
|
+
"End": true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
data/floe.gemspec
CHANGED
|
@@ -31,15 +31,16 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
32
32
|
spec.require_paths = ["lib"]
|
|
33
33
|
|
|
34
|
+
spec.add_dependency "activesupport", ">5.2"
|
|
34
35
|
spec.add_dependency "awesome_spawn", "~>1.6"
|
|
36
|
+
spec.add_dependency "faraday"
|
|
37
|
+
spec.add_dependency "faraday-follow_redirects"
|
|
35
38
|
spec.add_dependency "io-wait"
|
|
39
|
+
spec.add_dependency "json", "~>2.10"
|
|
36
40
|
spec.add_dependency "jsonpath", "~>1.1"
|
|
37
41
|
spec.add_dependency "kubeclient", "~>4.7"
|
|
38
42
|
spec.add_dependency "optimist", "~>3.0"
|
|
39
43
|
spec.add_dependency "parslet", "~>2.0"
|
|
40
|
-
spec.add_dependency "json", "~>2.10"
|
|
41
|
-
spec.add_dependency "faraday"
|
|
42
|
-
spec.add_dependency "faraday-follow_redirects"
|
|
43
44
|
|
|
44
45
|
spec.add_development_dependency "manageiq-style", ">= 1.5.2"
|
|
45
46
|
spec.add_development_dependency "rake", "~> 13.0"
|
|
@@ -1,6 +1,35 @@
|
|
|
1
|
+
require "logger"
|
|
2
|
+
|
|
1
3
|
module Floe
|
|
2
4
|
module BuiltinRunner
|
|
3
5
|
class Methods < BasicObject
|
|
6
|
+
def self.log(params, _secrets, context)
|
|
7
|
+
params["Level"] = (params["Level"] ||= "INFO").upcase
|
|
8
|
+
|
|
9
|
+
error = log_verify_params(params)
|
|
10
|
+
return BuiltinRunner.error!({}, :cause => error) if error
|
|
11
|
+
|
|
12
|
+
level, message = params.values_at("Level", "Message")
|
|
13
|
+
|
|
14
|
+
context.logger.add(::Logger::Severity.const_get(level), message)
|
|
15
|
+
|
|
16
|
+
BuiltinRunner.success!({}, :output => context.input)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
LOG_SEVERITIES = ::Logger::Severity.constants.sort_by { |s| ::Logger::Severity.const_get(s) }.map(&:to_s)
|
|
20
|
+
LOG_SEVERITIES_S = "#{LOG_SEVERITIES[0..-2].join(", ")}, or #{LOG_SEVERITIES[-1]}"
|
|
21
|
+
|
|
22
|
+
private_class_method def self.log_verify_params(params)
|
|
23
|
+
return "Missing Parameter: Message" if params["Message"].nil?
|
|
24
|
+
return "Invalid Parameter: Level: [#{params["Level"]}], must be one of #{LOG_SEVERITIES_S}" unless LOG_SEVERITIES.include?(params["Level"])
|
|
25
|
+
|
|
26
|
+
nil
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private_class_method def self.log_status!(runner_context)
|
|
30
|
+
runner_context
|
|
31
|
+
end
|
|
32
|
+
|
|
4
33
|
def self.http(params, _secrets, _context)
|
|
5
34
|
params["Method"] ||= "GET"
|
|
6
35
|
|
|
@@ -59,8 +88,8 @@ module Floe
|
|
|
59
88
|
connection.response(:follow_redirects)
|
|
60
89
|
end
|
|
61
90
|
|
|
62
|
-
response = connection.send(method.downcase) do |
|
|
63
|
-
|
|
91
|
+
response = connection.send(method.downcase) do |faraday_request|
|
|
92
|
+
faraday_request.body = body if body
|
|
64
93
|
end
|
|
65
94
|
|
|
66
95
|
output = {"Status" => response.status, "Body" => response.body, "Headers" => response.headers}
|
|
@@ -23,7 +23,7 @@ module Floe
|
|
|
23
23
|
method_name = runner_context["method"]
|
|
24
24
|
raise ArgumentError if method_name.nil?
|
|
25
25
|
|
|
26
|
-
cleanup_method = "#{method_name}_cleanup"
|
|
26
|
+
cleanup_method = :"#{method_name}_cleanup"
|
|
27
27
|
return unless Methods.respond_to?(cleanup_method, true)
|
|
28
28
|
|
|
29
29
|
Methods.send(cleanup_method, runner_context)
|
|
@@ -34,7 +34,7 @@ module Floe
|
|
|
34
34
|
raise ArgumentError if method_name.nil?
|
|
35
35
|
return if runner_context["running"] == false
|
|
36
36
|
|
|
37
|
-
Methods.send("#{method_name}_status!", runner_context)
|
|
37
|
+
Methods.send(:"#{method_name}_status!", runner_context)
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def running?(runner_context)
|
|
@@ -105,10 +105,14 @@ module Floe
|
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
def running?(runner_context)
|
|
108
|
+
return false if runner_context.key?("Error")
|
|
109
|
+
|
|
108
110
|
!!runner_context.dig("container_state", "Running")
|
|
109
111
|
end
|
|
110
112
|
|
|
111
113
|
def success?(runner_context)
|
|
114
|
+
return false if runner_context.key?("Error")
|
|
115
|
+
|
|
112
116
|
runner_context.dig("container_state", "ExitCode") == 0
|
|
113
117
|
end
|
|
114
118
|
|
|
@@ -176,18 +180,21 @@ module Floe
|
|
|
176
180
|
when "die", "destroy"
|
|
177
181
|
:delete
|
|
178
182
|
else
|
|
179
|
-
:
|
|
183
|
+
:unknown
|
|
180
184
|
end
|
|
181
185
|
end
|
|
182
186
|
|
|
183
187
|
def inspect_container(container_id)
|
|
184
188
|
JSON.parse(docker!("inspect", container_id).output).first
|
|
185
189
|
rescue AwesomeSpawn::CommandResultError => err
|
|
186
|
-
raise Floe::ExecutionError
|
|
190
|
+
raise Floe::ExecutionError, "Failed to get status for container #{container_id}: #{err}"
|
|
187
191
|
end
|
|
188
192
|
|
|
189
|
-
def delete_container(container_id)
|
|
190
|
-
|
|
193
|
+
def delete_container(container_id, force: true)
|
|
194
|
+
params = ["rm", container_id]
|
|
195
|
+
params << "--force" if force
|
|
196
|
+
|
|
197
|
+
docker!(*params)
|
|
191
198
|
rescue
|
|
192
199
|
nil
|
|
193
200
|
end
|
|
@@ -11,7 +11,7 @@ module Floe
|
|
|
11
11
|
FAILURE_REASONS = %w[CrashLoopBackOff ImagePullBackOff ErrImagePull].freeze
|
|
12
12
|
|
|
13
13
|
def initialize(options = {})
|
|
14
|
-
require "active_support/core_ext/hash/keys"
|
|
14
|
+
require "active_support/core_ext/hash/keys" # deep_stringify_keys
|
|
15
15
|
require "awesome_spawn"
|
|
16
16
|
require "securerandom"
|
|
17
17
|
require "base64"
|
|
@@ -70,6 +70,7 @@ module Floe
|
|
|
70
70
|
end
|
|
71
71
|
|
|
72
72
|
def running?(runner_context)
|
|
73
|
+
return false if runner_context.key?("Error")
|
|
73
74
|
return false unless pod_running?(runner_context)
|
|
74
75
|
# If a pod is Pending and the containers are waiting with a failure
|
|
75
76
|
# reason such as ImagePullBackOff or CrashLoopBackOff then the pod
|
|
@@ -80,6 +81,8 @@ module Floe
|
|
|
80
81
|
end
|
|
81
82
|
|
|
82
83
|
def success?(runner_context)
|
|
84
|
+
return false if runner_context.key?("Error")
|
|
85
|
+
|
|
83
86
|
runner_context.dig("container_state", "phase") == "Succeeded"
|
|
84
87
|
end
|
|
85
88
|
|
|
@@ -156,7 +159,7 @@ module Floe
|
|
|
156
159
|
def pod_info(pod_name)
|
|
157
160
|
kubeclient.get_pod(pod_name, namespace)
|
|
158
161
|
rescue Kubeclient::HttpError => err
|
|
159
|
-
raise Floe::ExecutionError
|
|
162
|
+
raise Floe::ExecutionError, "Failed to get status for pod #{namespace}/#{pod_name}: #{err}"
|
|
160
163
|
end
|
|
161
164
|
|
|
162
165
|
def pod_running?(context)
|
|
@@ -22,10 +22,6 @@ module Floe
|
|
|
22
22
|
raise Floe::ExecutionError.new(self.class.field_error_text(name, field_name, field_value, comment), floe_error)
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
-
def workflow_state?(field_value, workflow)
|
|
26
|
-
workflow.payload["States"] ? workflow.payload["States"].include?(field_value) : true
|
|
27
|
-
end
|
|
28
|
-
|
|
29
25
|
def wrap_parser_error(field_name, field_value)
|
|
30
26
|
yield
|
|
31
27
|
rescue ArgumentError, InvalidWorkflowError => error
|
data/lib/floe/version.rb
CHANGED
|
@@ -24,7 +24,7 @@ module Floe
|
|
|
24
24
|
|
|
25
25
|
def validate_state_next!(workflow)
|
|
26
26
|
missing_field_error!("Next") if @next.nil?
|
|
27
|
-
invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !
|
|
27
|
+
invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !workflow.state?(@next)
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
end
|
|
@@ -6,12 +6,14 @@ module Floe
|
|
|
6
6
|
class Data < Floe::Workflow::ChoiceRule
|
|
7
7
|
TYPES = ["String", "Numeric", "Boolean", "Timestamp", "Present", "Null"].freeze
|
|
8
8
|
COMPARES = ["Equals", "LessThan", "GreaterThan", "LessThanEquals", "GreaterThanEquals", "Matches"].freeze
|
|
9
|
+
OPERATIONS = TYPES.each_with_object({}) { |dt, a| a[dt] = :"is_#{dt.downcase}?" }
|
|
10
|
+
.merge(COMPARES.each_with_object({}) { |op, a| a[op] = :"op_#{op.downcase}?" }).freeze
|
|
9
11
|
# e.g.: (Is)(String), (Is)(Present)
|
|
10
|
-
TYPE_CHECK = /^
|
|
12
|
+
TYPE_CHECK = /^Is(#{Regexp.union(TYPES)})$/
|
|
11
13
|
# e.g.: (String)(LessThan)(Path), (Numeric)(GreaterThanEquals)()
|
|
12
|
-
OPERATION = /^(#{(TYPES - %w[Null Present])
|
|
14
|
+
OPERATION = /^(#{Regexp.union(TYPES - %w[Null Present])})(#{Regexp.union(COMPARES)})(Path)?$/
|
|
13
15
|
|
|
14
|
-
attr_reader :variable, :compare_key, :
|
|
16
|
+
attr_reader :variable, :compare_key, :operator, :type, :compare_predicate, :path
|
|
15
17
|
|
|
16
18
|
def initialize(_workflow, _name, payload)
|
|
17
19
|
super
|
|
@@ -20,62 +22,80 @@ module Floe
|
|
|
20
22
|
parse_compare_key
|
|
21
23
|
end
|
|
22
24
|
|
|
25
|
+
# Evaluate whether this rule is true for the given context and input (runtime)
|
|
26
|
+
#
|
|
27
|
+
# @param context [Context] The workflow execution context
|
|
28
|
+
# @param input [Hash] The current state input
|
|
29
|
+
# @return [Boolean] true if the rule evaluate to true
|
|
23
30
|
def true?(context, input)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
# Payload pattern is: {"Variable": $lhs, $operator: $rhs}
|
|
32
|
+
# Example:
|
|
33
|
+
#
|
|
34
|
+
# {"Variable": "$.foo", "IsNumeric": true}
|
|
35
|
+
# lhs = input["$.foo"]
|
|
36
|
+
# rhs = true
|
|
37
|
+
# is_numeric?(lhs, rhs)
|
|
38
|
+
#
|
|
39
|
+
# {"Variable": "$.foo", "GreaterThanString": "aaa"}
|
|
40
|
+
# lhs = input["$.foo"]
|
|
41
|
+
# rhs = "aaa"
|
|
42
|
+
# op_greaterthan?(lhs, rhs)
|
|
43
|
+
#
|
|
44
|
+
# {"Variable": "$.foo", "GreaterThanNumericPath": "$.bar"}
|
|
45
|
+
# lhs = input["$.foo"]
|
|
46
|
+
# rhs = input["$.bar"]
|
|
47
|
+
# op_greaterthan?(lhs, rhs)
|
|
48
|
+
#
|
|
49
|
+
# NOTE: IsPresent works a little differently as lhs might raise a PathError.
|
|
50
|
+
# See the exception handler below. This is why we process the rhs before the lhs.
|
|
27
51
|
rhs = compare_value(context, input)
|
|
28
|
-
|
|
52
|
+
lhs = variable_value(context, input)
|
|
53
|
+
send(OPERATIONS[operator], lhs, rhs)
|
|
54
|
+
rescue Floe::PathError
|
|
55
|
+
# For IsPresent, we can expect the lhs to not be present in some cases,
|
|
56
|
+
# This throws a PathError. We handle that special case here.
|
|
57
|
+
# Example:
|
|
58
|
+
#
|
|
59
|
+
# {"Variable": "$.foo", "IsPresent": false}
|
|
60
|
+
# lhs = input["$.foo"], but variable is not present. (The variable lookup threw PathError)
|
|
61
|
+
# rhs = false
|
|
62
|
+
return is_present?(:not_present, rhs) if operator == "Present"
|
|
63
|
+
|
|
64
|
+
# for non "IsPresent" checks, share that lhs or rhs is not found.
|
|
65
|
+
raise
|
|
29
66
|
end
|
|
30
67
|
|
|
31
68
|
private
|
|
32
69
|
|
|
33
|
-
def presence_check(context, input)
|
|
34
|
-
# Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
|
|
35
|
-
# If true then return true when present.
|
|
36
|
-
# If false then return true when not present.
|
|
37
|
-
predicate = compare_value(context, input)
|
|
38
|
-
# Don't need the variable_value, just need to see if the path finds the value.
|
|
39
|
-
variable_value(context, input)
|
|
40
|
-
|
|
41
|
-
# The variable_value is present
|
|
42
|
-
# If predicate is true, then presence check was successful, return true.
|
|
43
|
-
predicate
|
|
44
|
-
rescue Floe::PathError
|
|
45
|
-
# variable_value is not present. (the path lookup threw an error)
|
|
46
|
-
# If predicate is false, then it successfully wasn't present, return true.
|
|
47
|
-
!predicate
|
|
48
|
-
end
|
|
49
|
-
|
|
50
70
|
# rubocop:disable Naming/PredicateName
|
|
51
71
|
# rubocop:disable Style/OptionalBooleanParameter
|
|
52
|
-
def is_null?(value,
|
|
53
|
-
value.nil? ==
|
|
72
|
+
def is_null?(value, expectation)
|
|
73
|
+
value.nil? == expectation
|
|
54
74
|
end
|
|
55
75
|
|
|
56
|
-
def is_present?(value,
|
|
57
|
-
|
|
76
|
+
def is_present?(value, expectation)
|
|
77
|
+
(value != :not_present) == expectation
|
|
58
78
|
end
|
|
59
79
|
|
|
60
|
-
def is_numeric?(value,
|
|
61
|
-
value.kind_of?(Numeric) ==
|
|
80
|
+
def is_numeric?(value, expectation)
|
|
81
|
+
value.kind_of?(Numeric) == expectation
|
|
62
82
|
end
|
|
63
83
|
|
|
64
|
-
def is_string?(value,
|
|
65
|
-
value.kind_of?(String) ==
|
|
84
|
+
def is_string?(value, expectation)
|
|
85
|
+
value.kind_of?(String) == expectation
|
|
66
86
|
end
|
|
67
87
|
|
|
68
|
-
def is_boolean?(value,
|
|
69
|
-
[true, false].include?(value) ==
|
|
88
|
+
def is_boolean?(value, expectation)
|
|
89
|
+
[true, false].include?(value) == expectation
|
|
70
90
|
end
|
|
71
91
|
|
|
72
|
-
def is_timestamp?(value,
|
|
92
|
+
def is_timestamp?(value, expectation)
|
|
73
93
|
require "date"
|
|
74
94
|
|
|
75
95
|
DateTime.rfc3339(value)
|
|
76
|
-
|
|
96
|
+
expectation
|
|
77
97
|
rescue TypeError, Date::Error
|
|
78
|
-
!
|
|
98
|
+
!expectation
|
|
79
99
|
end
|
|
80
100
|
# rubocop:enable Naming/PredicateName
|
|
81
101
|
# rubocop:enable Style/OptionalBooleanParameter
|
|
@@ -100,8 +120,8 @@ module Floe
|
|
|
100
120
|
lhs >= rhs
|
|
101
121
|
end
|
|
102
122
|
|
|
103
|
-
def op_matches?(
|
|
104
|
-
|
|
123
|
+
def op_matches?(value, pattern)
|
|
124
|
+
value.match?(Regexp.escape(pattern).gsub('\*', '.*?'))
|
|
105
125
|
end
|
|
106
126
|
|
|
107
127
|
# parse the compare key at initialization time
|
|
@@ -110,26 +130,26 @@ module Floe
|
|
|
110
130
|
# e.g. (String)(GreaterThan)(Path)
|
|
111
131
|
if (match_values = OPERATION.match(key))
|
|
112
132
|
@compare_key = key
|
|
113
|
-
@type, operator, @path = match_values.captures
|
|
114
|
-
@operation = "op_#{operator.downcase}?".to_sym
|
|
133
|
+
@type, @operator, @path = match_values.captures
|
|
115
134
|
@compare_predicate = parse_predicate(type)
|
|
116
135
|
break
|
|
117
|
-
end
|
|
118
136
|
# e.g. (Is)(String)
|
|
119
|
-
|
|
137
|
+
elsif (match_value = TYPE_CHECK.match(key))
|
|
120
138
|
@compare_key = key
|
|
121
|
-
|
|
139
|
+
@operator = match_value.captures.first
|
|
122
140
|
# type: nil means no runtime type checking.
|
|
123
141
|
@type = @path = nil
|
|
124
|
-
@operation = "is_#{type.downcase}?".to_sym
|
|
125
142
|
@compare_predicate = parse_predicate("Boolean")
|
|
126
143
|
break
|
|
127
144
|
end
|
|
128
145
|
end
|
|
129
|
-
parser_error!("requires a compare key") if compare_key.nil? ||
|
|
146
|
+
parser_error!("requires a compare key") if compare_key.nil? || operator.nil?
|
|
130
147
|
end
|
|
131
148
|
|
|
132
|
-
# parse predicate at
|
|
149
|
+
# parse predicate at initialization time
|
|
150
|
+
# @param data_type [String] the data type of the variable
|
|
151
|
+
# When parsing operations (IntegerGreaterThan), this will be the operation data type (e.g.: Integer)
|
|
152
|
+
# When parsing type checks (IsString), this will always be a Boolean
|
|
133
153
|
# @return the right predicate attached to the compare key
|
|
134
154
|
def parse_predicate(data_type)
|
|
135
155
|
path ? parse_path(compare_key) : parse_field(compare_key, data_type)
|
|
@@ -140,13 +160,13 @@ module Floe
|
|
|
140
160
|
path ? fetch_path(compare_key, compare_predicate, context, input) : compare_predicate
|
|
141
161
|
end
|
|
142
162
|
|
|
143
|
-
#
|
|
144
|
-
# @return variable value (left hand side
|
|
163
|
+
# fetch the variable value at runtime
|
|
164
|
+
# @return variable value (left hand side)
|
|
145
165
|
def variable_value(context, input)
|
|
146
166
|
fetch_path("Variable", variable, context, input)
|
|
147
167
|
end
|
|
148
168
|
|
|
149
|
-
# parse path at
|
|
169
|
+
# parse path at initialization time
|
|
150
170
|
# helper method to parse a path from the payload
|
|
151
171
|
def parse_path(field_name)
|
|
152
172
|
value = payload[field_name]
|
|
@@ -155,6 +175,10 @@ module Floe
|
|
|
155
175
|
end
|
|
156
176
|
|
|
157
177
|
# parse predicate field at initialization time
|
|
178
|
+
# @param field_name [String] the compare key
|
|
179
|
+
# @param data_type [String] the data type of the variable
|
|
180
|
+
# When parsing operations (IntegerGreaterThan), this will be the operation data type (e.g.: Integer)
|
|
181
|
+
# When parsing type checks (IsString), this will always be a Boolean
|
|
158
182
|
def parse_field(field_name, data_type)
|
|
159
183
|
value = payload[field_name]
|
|
160
184
|
return value if correct_type?(value, data_type)
|
|
@@ -165,6 +189,7 @@ module Floe
|
|
|
165
189
|
# fetch a path at runtime
|
|
166
190
|
def fetch_path(field_name, field_path, context, input)
|
|
167
191
|
value = field_path.value(context, input)
|
|
192
|
+
# if this is an operation (GreaterThanPath), ensure the value is the correct type
|
|
168
193
|
return value if type.nil? || correct_type?(value, type)
|
|
169
194
|
|
|
170
195
|
runtime_field_error!(field_name, field_path.to_s, "required to point to a #{type}")
|
|
@@ -173,7 +198,7 @@ module Floe
|
|
|
173
198
|
# if we have runtime checking, check against that type
|
|
174
199
|
# otherwise assume checking a TYPE_CHECK predicate and check against Boolean
|
|
175
200
|
def correct_type?(value, data_type)
|
|
176
|
-
send(
|
|
201
|
+
send(OPERATIONS[data_type], value, true)
|
|
177
202
|
end
|
|
178
203
|
end
|
|
179
204
|
end
|
|
@@ -51,7 +51,7 @@ module Floe
|
|
|
51
51
|
elsif !@next
|
|
52
52
|
# top level nodes require a next
|
|
53
53
|
missing_field_error!("Next")
|
|
54
|
-
elsif !
|
|
54
|
+
elsif !workflow.state?(@next)
|
|
55
55
|
# top level nodes require a next field that is found
|
|
56
56
|
invalid_field_error!("Next", @next, "is not found in \"States\"")
|
|
57
57
|
end
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
3
5
|
module Floe
|
|
4
6
|
class Workflow
|
|
5
7
|
class Context
|
|
@@ -25,6 +27,17 @@ module Floe
|
|
|
25
27
|
raise Floe::InvalidExecutionInput, "Invalid State Machine Execution Input: #{err}: was expecting (JSON String, Number, Array, Object or token 'null', 'true' or 'false')"
|
|
26
28
|
end
|
|
27
29
|
|
|
30
|
+
def prepare_start(start_at)
|
|
31
|
+
return if started?
|
|
32
|
+
|
|
33
|
+
state["Name"] = start_at
|
|
34
|
+
state["Input"] = execution["Input"].dup
|
|
35
|
+
state["Guid"] = SecureRandom.uuid
|
|
36
|
+
|
|
37
|
+
execution["Id"] ||= SecureRandom.uuid
|
|
38
|
+
execution["StartTime"] = Time.now.utc.iso8601
|
|
39
|
+
end
|
|
40
|
+
|
|
28
41
|
def execution
|
|
29
42
|
@context["Execution"]
|
|
30
43
|
end
|
|
@@ -140,13 +153,22 @@ module Floe
|
|
|
140
153
|
end
|
|
141
154
|
|
|
142
155
|
def inspect
|
|
143
|
-
|
|
156
|
+
"#<#{self.class.name}: #{safe_context.inspect}>"
|
|
144
157
|
end
|
|
145
158
|
|
|
146
159
|
def to_h
|
|
147
160
|
safe_context
|
|
148
161
|
end
|
|
149
162
|
|
|
163
|
+
def ==(other)
|
|
164
|
+
other.kind_of?(self.class) && other.instance_variable_get(:@context) == @context
|
|
165
|
+
end
|
|
166
|
+
alias eql? ==
|
|
167
|
+
|
|
168
|
+
def hash
|
|
169
|
+
@context.hash
|
|
170
|
+
end
|
|
171
|
+
|
|
150
172
|
private
|
|
151
173
|
|
|
152
174
|
def safe_context
|
|
@@ -77,7 +77,7 @@ module Floe
|
|
|
77
77
|
rule(:number => simple(:v)) { v.match(/[eE.]/) ? Float(v) : Integer(v) }
|
|
78
78
|
rule(:jsonpath => simple(:v)) { Floe::Workflow::Path.value(v.to_s, context, input) }
|
|
79
79
|
|
|
80
|
-
STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}
|
|
80
|
+
STATES_FORMAT_PLACEHOLDER = /(?<!\\)\{\}/
|
|
81
81
|
|
|
82
82
|
rule(:states_format => {:args => subtree(:args)}) do
|
|
83
83
|
args = Transformer.process_args(args(), "States.Format", [String, VariadicArgs[[String, TrueClass, FalseClass, Numeric, NilClass]]])
|
|
@@ -236,15 +236,15 @@ module Floe
|
|
|
236
236
|
|
|
237
237
|
rule(:states_string_split => {:args => subtree(:args)}) do
|
|
238
238
|
args = Transformer.process_args(args(), "States.StringSplit", [String, String])
|
|
239
|
-
str,
|
|
239
|
+
str, delimiter = *args
|
|
240
240
|
|
|
241
|
-
case
|
|
241
|
+
case delimiter.size
|
|
242
242
|
when 0
|
|
243
243
|
str.empty? ? [] : [str]
|
|
244
244
|
when 1
|
|
245
|
-
str.split(
|
|
245
|
+
str.split(delimiter)
|
|
246
246
|
else
|
|
247
|
-
str.split(/[#{Regexp.escape(
|
|
247
|
+
str.split(/[#{Regexp.escape(delimiter)}]+/)
|
|
248
248
|
end
|
|
249
249
|
end
|
|
250
250
|
|
|
@@ -48,7 +48,7 @@ module Floe
|
|
|
48
48
|
end
|
|
49
49
|
|
|
50
50
|
def check_dynamic_datatype(key, value)
|
|
51
|
-
unless value.
|
|
51
|
+
unless value.kind_of?(String)
|
|
52
52
|
raise Floe::InvalidWorkflowError, "The value for the field \"#{key}\" must be a String that contains a valid Reference Path or Intrinsic Function expression"
|
|
53
53
|
end
|
|
54
54
|
end
|
data/lib/floe/workflow/state.rb
CHANGED
|
@@ -43,7 +43,9 @@ module Floe
|
|
|
43
43
|
|
|
44
44
|
# @return for incomplete Errno::EAGAIN, for completed 0
|
|
45
45
|
def run_nonblock!(context)
|
|
46
|
-
start
|
|
46
|
+
# Only start the state if it isn't already started and it isn't waiting
|
|
47
|
+
# from a prior Retry.
|
|
48
|
+
start(context) unless started?(context) || waiting?(context)
|
|
47
49
|
return Errno::EAGAIN unless ready?(context)
|
|
48
50
|
|
|
49
51
|
finish(context)
|
|
@@ -89,13 +91,13 @@ module Floe
|
|
|
89
91
|
def mark_error(context, exception)
|
|
90
92
|
# InputPath or OutputPath were bad.
|
|
91
93
|
context.next_state = nil
|
|
92
|
-
context.output =
|
|
94
|
+
context.output = exception.to_output
|
|
93
95
|
# Since finish threw an exception, super was never called. Calling that now.
|
|
94
96
|
mark_finished(context)
|
|
95
97
|
end
|
|
96
98
|
|
|
97
99
|
def ready?(context)
|
|
98
|
-
|
|
100
|
+
(started?(context) && !running?(context)) || !waiting?(context)
|
|
99
101
|
end
|
|
100
102
|
|
|
101
103
|
def running?(context)
|
|
@@ -49,7 +49,7 @@ module Floe
|
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def validate_state_default!(workflow)
|
|
52
|
-
invalid_field_error!("Default", @default, "is not found in \"States\"") if @default && !
|
|
52
|
+
invalid_field_error!("Default", @default, "is not found in \"States\"") if @default && !workflow.state?(@default)
|
|
53
53
|
end
|
|
54
54
|
end
|
|
55
55
|
end
|
|
@@ -13,7 +13,7 @@ module Floe
|
|
|
13
13
|
|
|
14
14
|
def validate_state_next!(workflow)
|
|
15
15
|
missing_field_error!("Next") if @next.nil? && !@end
|
|
16
|
-
invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !
|
|
16
|
+
invalid_field_error!("Next", @next, "is not found in \"States\"") if @next && !workflow.state?(@next)
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
end
|
|
@@ -9,29 +9,29 @@ module Floe
|
|
|
9
9
|
include RetryCatchMixin
|
|
10
10
|
|
|
11
11
|
attr_reader :credentials, :end, :heartbeat_seconds, :next, :parameters,
|
|
12
|
-
:result_selector, :resource, :timeout_seconds, :
|
|
13
|
-
:input_path, :output_path, :result_path
|
|
12
|
+
:result_selector, :resource, :timeout_seconds, :timeout_seconds_path,
|
|
13
|
+
:retry, :catch, :input_path, :output_path, :result_path
|
|
14
14
|
|
|
15
15
|
def initialize(workflow, name, payload)
|
|
16
16
|
super
|
|
17
17
|
|
|
18
|
-
@
|
|
19
|
-
@next = payload["Next"]
|
|
20
|
-
@end = !!payload["End"]
|
|
21
|
-
@resource = payload["Resource"]
|
|
22
|
-
|
|
18
|
+
@resource = payload["Resource"]
|
|
23
19
|
missing_field_error!("Resource") unless @resource.kind_of?(String)
|
|
24
20
|
@runner = wrap_parser_error("Resource", @resource) { Floe::Runner.for_resource(@resource) }
|
|
25
21
|
|
|
26
|
-
@
|
|
27
|
-
@
|
|
28
|
-
@
|
|
29
|
-
@
|
|
30
|
-
@
|
|
31
|
-
@
|
|
32
|
-
@
|
|
33
|
-
@
|
|
34
|
-
@
|
|
22
|
+
@next = payload["Next"]
|
|
23
|
+
@end = !!payload["End"]
|
|
24
|
+
@timeout_seconds = payload["TimeoutSeconds"]
|
|
25
|
+
@heartbeat_seconds = payload["HeartbeatSeconds"]
|
|
26
|
+
@retry = payload["Retry"].to_a.map.with_index { |retrier, i| Retrier.new(workflow, name + ["Retry", i.to_s], retrier) }
|
|
27
|
+
@catch = payload["Catch"].to_a.map.with_index { |catcher, i| Catcher.new(workflow, name + ["Catch", i.to_s], catcher) }
|
|
28
|
+
@input_path = Path.new(payload.fetch("InputPath", "$"))
|
|
29
|
+
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
|
30
|
+
@result_path = ReferencePath.new(payload.fetch("ResultPath", "$"))
|
|
31
|
+
@timeout_seconds_path = ReferencePath.new(payload["TimeoutSecondsPath"]) if payload["TimeoutSecondsPath"]
|
|
32
|
+
@parameters = PayloadTemplate.new(payload["Parameters"]) if payload["Parameters"]
|
|
33
|
+
@result_selector = PayloadTemplate.new(payload["ResultSelector"]) if payload["ResultSelector"]
|
|
34
|
+
@credentials = PayloadTemplate.new(payload["Credentials"]) if payload["Credentials"]
|
|
35
35
|
|
|
36
36
|
validate_state!(workflow)
|
|
37
37
|
end
|
|
@@ -39,6 +39,9 @@ module Floe
|
|
|
39
39
|
def start(context)
|
|
40
40
|
super
|
|
41
41
|
|
|
42
|
+
# Wakeup no later than timeout_seconds to check if the Resource has timed out
|
|
43
|
+
wait_until!(context, :seconds => timeout_seconds) if timeout_seconds
|
|
44
|
+
|
|
42
45
|
input = process_input(context)
|
|
43
46
|
secrets = credentials&.value(context, context.input)
|
|
44
47
|
runner_context = runner.run_async!(resource, input, secrets, context)
|
|
@@ -48,27 +51,31 @@ module Floe
|
|
|
48
51
|
|
|
49
52
|
def finish(context)
|
|
50
53
|
output = runner.output(context.state["RunnerContext"])
|
|
54
|
+
raise Floe::ExecutionError.from_output(parse_error(output)) unless success?(context)
|
|
55
|
+
|
|
56
|
+
output = parse_output(output)
|
|
57
|
+
context.output = process_output(context, output)
|
|
51
58
|
|
|
52
|
-
if success?(context)
|
|
53
|
-
output = parse_output(output)
|
|
54
|
-
context.output = process_output(context, output)
|
|
55
|
-
else
|
|
56
|
-
error = parse_error(output)
|
|
57
|
-
retry_state!(context, error) || catch_error!(context, error) || fail_workflow!(context, error)
|
|
58
|
-
end
|
|
59
59
|
super
|
|
60
60
|
ensure
|
|
61
61
|
runner.cleanup(context.state["RunnerContext"])
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def running?(context)
|
|
65
|
-
|
|
65
|
+
raise Floe::TimeoutError if timed_out?(context)
|
|
66
66
|
return false if finished?(context)
|
|
67
67
|
|
|
68
68
|
runner.status!(context.state["RunnerContext"])
|
|
69
69
|
runner.running?(context.state["RunnerContext"])
|
|
70
70
|
end
|
|
71
71
|
|
|
72
|
+
def mark_error(context, exception)
|
|
73
|
+
error = exception.to_output
|
|
74
|
+
|
|
75
|
+
retry_state!(context, error) || catch_error!(context, error) || fail_workflow!(context, error)
|
|
76
|
+
mark_finished(context)
|
|
77
|
+
end
|
|
78
|
+
|
|
72
79
|
def end?
|
|
73
80
|
@end
|
|
74
81
|
end
|
|
@@ -79,12 +86,43 @@ module Floe
|
|
|
79
86
|
|
|
80
87
|
def validate_state!(workflow)
|
|
81
88
|
validate_state_next!(workflow)
|
|
89
|
+
validate_state_timeout_seconds!(workflow)
|
|
90
|
+
validate_state_timeout_seconds_path!(workflow)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def validate_state_timeout_seconds!(workflow)
|
|
94
|
+
return if @timeout_seconds.nil?
|
|
95
|
+
return if @timeout_seconds.kind_of?(Integer) && @timeout_seconds > 0
|
|
96
|
+
|
|
97
|
+
invalid_field_error!("TimeoutSeconds", @timeout_seconds, "must be positive, non-zero integer")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def validate_state_timeout_seconds_path!(workflow)
|
|
101
|
+
return if @timeout_seconds_path.nil? || @timeout_seconds.nil?
|
|
102
|
+
|
|
103
|
+
invalid_field_error!("TimeoutSecondsPath", nil, "cannot specify both \"TimeoutSeconds\" and \"TimeoutSecondsPath\"")
|
|
82
104
|
end
|
|
83
105
|
|
|
84
106
|
def success?(context)
|
|
85
107
|
runner.success?(context.state["RunnerContext"])
|
|
86
108
|
end
|
|
87
109
|
|
|
110
|
+
def timed_out?(context)
|
|
111
|
+
return false if timeout_seconds.nil? && timeout_seconds_path.nil?
|
|
112
|
+
|
|
113
|
+
timeout = timeout_seconds || timeout_seconds_path.value(context, context.input)
|
|
114
|
+
entered_time = Time.parse(context.state["EnteredTime"])
|
|
115
|
+
|
|
116
|
+
Time.now.utc > entered_time + timeout
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def task_timed_out!(context)
|
|
120
|
+
context.state["RunnerContext"]["Error"] = "States.Timeout"
|
|
121
|
+
context.state["RunnerContext"]["Cause"] = "Task timed out"
|
|
122
|
+
|
|
123
|
+
false
|
|
124
|
+
end
|
|
125
|
+
|
|
88
126
|
def parse_error(output)
|
|
89
127
|
return if output.nil?
|
|
90
128
|
return output if output.kind_of?(Hash)
|
data/lib/floe/workflow.rb
CHANGED
|
@@ -150,15 +150,7 @@ module Floe
|
|
|
150
150
|
|
|
151
151
|
# setup a workflow
|
|
152
152
|
def start_workflow
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
context.state["Name"] = start_at
|
|
156
|
-
context.state["Input"] = context.execution["Input"].dup
|
|
157
|
-
context.state["Guid"] = SecureRandom.uuid
|
|
158
|
-
|
|
159
|
-
context.execution["Id"] ||= SecureRandom.uuid
|
|
160
|
-
context.execution["StartTime"] = Time.now.utc.iso8601
|
|
161
|
-
|
|
153
|
+
context.prepare_start(start_at)
|
|
162
154
|
self
|
|
163
155
|
end
|
|
164
156
|
|
|
@@ -183,7 +175,7 @@ module Floe
|
|
|
183
175
|
|
|
184
176
|
# if rerunning due to an error (and we are using Retry)
|
|
185
177
|
if context.state_name == context.next_state && context.failed? && context.state.key?("Retrier")
|
|
186
|
-
next_state.merge!(context.state.slice("RetryCount", "Input", "Retrier"))
|
|
178
|
+
next_state.merge!(context.state.slice("RetryCount", "Input", "Retrier", "WaitUntil"))
|
|
187
179
|
else
|
|
188
180
|
next_state["Input"] = context.output
|
|
189
181
|
end
|
data/lib/floe/workflow_base.rb
CHANGED
|
@@ -77,6 +77,10 @@ module Floe
|
|
|
77
77
|
context.output.to_json if end?(context)
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
+
def state?(state_name)
|
|
81
|
+
@payload["States"].include?(state_name)
|
|
82
|
+
end
|
|
83
|
+
|
|
80
84
|
private
|
|
81
85
|
|
|
82
86
|
def step!(context)
|
|
@@ -102,7 +106,7 @@ module Floe
|
|
|
102
106
|
def validate_workflow!
|
|
103
107
|
missing_field_error!("States") if @states.empty?
|
|
104
108
|
missing_field_error!("StartAt") if @start_at.nil?
|
|
105
|
-
invalid_field_error!("StartAt", @start_at, "is not found in \"States\"") unless
|
|
109
|
+
invalid_field_error!("StartAt", @start_at, "is not found in \"States\"") unless state?(@start_at)
|
|
106
110
|
end
|
|
107
111
|
end
|
|
108
112
|
end
|
data/lib/floe.rb
CHANGED
|
@@ -54,17 +54,36 @@ module Floe
|
|
|
54
54
|
class InvalidExecutionInput < Error; end
|
|
55
55
|
|
|
56
56
|
class ExecutionError < Error
|
|
57
|
+
def self.from_output(output)
|
|
58
|
+
raise ArgumentError unless output.kind_of?(Hash) && output.key?("Error")
|
|
59
|
+
|
|
60
|
+
new(output["Cause"], output["Error"])
|
|
61
|
+
end
|
|
62
|
+
|
|
57
63
|
attr_reader :floe_error
|
|
58
64
|
|
|
59
65
|
def initialize(message, floe_error = "States.Runtime")
|
|
60
66
|
super(message)
|
|
61
67
|
@floe_error = floe_error
|
|
62
68
|
end
|
|
69
|
+
|
|
70
|
+
def to_output
|
|
71
|
+
{"Error" => floe_error}.tap do |output|
|
|
72
|
+
# If there is no "Cause" then ::Exception will use the exception class name
|
|
73
|
+
output["Cause"] = message if message != self.class.name.to_s
|
|
74
|
+
end
|
|
75
|
+
end
|
|
63
76
|
end
|
|
64
77
|
|
|
65
78
|
class PathError < ExecutionError
|
|
66
79
|
end
|
|
67
80
|
|
|
81
|
+
class TimeoutError < ExecutionError
|
|
82
|
+
def initialize(message = nil)
|
|
83
|
+
super(message, "States.Timeout")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
68
87
|
def self.logger
|
|
69
88
|
@logger ||= NullLogger.new
|
|
70
89
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: floe
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.19.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ManageIQ Developers
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activesupport
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.2'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.2'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: awesome_spawn
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -24,7 +38,7 @@ dependencies:
|
|
|
24
38
|
- !ruby/object:Gem::Version
|
|
25
39
|
version: '1.6'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
|
-
name:
|
|
41
|
+
name: faraday
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
29
43
|
requirements:
|
|
30
44
|
- - ">="
|
|
@@ -38,103 +52,103 @@ dependencies:
|
|
|
38
52
|
- !ruby/object:Gem::Version
|
|
39
53
|
version: '0'
|
|
40
54
|
- !ruby/object:Gem::Dependency
|
|
41
|
-
name:
|
|
55
|
+
name: faraday-follow_redirects
|
|
42
56
|
requirement: !ruby/object:Gem::Requirement
|
|
43
57
|
requirements:
|
|
44
|
-
- - "
|
|
58
|
+
- - ">="
|
|
45
59
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: '
|
|
60
|
+
version: '0'
|
|
47
61
|
type: :runtime
|
|
48
62
|
prerelease: false
|
|
49
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
64
|
requirements:
|
|
51
|
-
- - "
|
|
65
|
+
- - ">="
|
|
52
66
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: '
|
|
67
|
+
version: '0'
|
|
54
68
|
- !ruby/object:Gem::Dependency
|
|
55
|
-
name:
|
|
69
|
+
name: io-wait
|
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
|
57
71
|
requirements:
|
|
58
|
-
- - "
|
|
72
|
+
- - ">="
|
|
59
73
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: '
|
|
74
|
+
version: '0'
|
|
61
75
|
type: :runtime
|
|
62
76
|
prerelease: false
|
|
63
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
78
|
requirements:
|
|
65
|
-
- - "
|
|
79
|
+
- - ">="
|
|
66
80
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: '
|
|
81
|
+
version: '0'
|
|
68
82
|
- !ruby/object:Gem::Dependency
|
|
69
|
-
name:
|
|
83
|
+
name: json
|
|
70
84
|
requirement: !ruby/object:Gem::Requirement
|
|
71
85
|
requirements:
|
|
72
86
|
- - "~>"
|
|
73
87
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: '
|
|
88
|
+
version: '2.10'
|
|
75
89
|
type: :runtime
|
|
76
90
|
prerelease: false
|
|
77
91
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
92
|
requirements:
|
|
79
93
|
- - "~>"
|
|
80
94
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: '
|
|
95
|
+
version: '2.10'
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
|
-
name:
|
|
97
|
+
name: jsonpath
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
85
99
|
requirements:
|
|
86
100
|
- - "~>"
|
|
87
101
|
- !ruby/object:Gem::Version
|
|
88
|
-
version: '
|
|
102
|
+
version: '1.1'
|
|
89
103
|
type: :runtime
|
|
90
104
|
prerelease: false
|
|
91
105
|
version_requirements: !ruby/object:Gem::Requirement
|
|
92
106
|
requirements:
|
|
93
107
|
- - "~>"
|
|
94
108
|
- !ruby/object:Gem::Version
|
|
95
|
-
version: '
|
|
109
|
+
version: '1.1'
|
|
96
110
|
- !ruby/object:Gem::Dependency
|
|
97
|
-
name:
|
|
111
|
+
name: kubeclient
|
|
98
112
|
requirement: !ruby/object:Gem::Requirement
|
|
99
113
|
requirements:
|
|
100
114
|
- - "~>"
|
|
101
115
|
- !ruby/object:Gem::Version
|
|
102
|
-
version: '
|
|
116
|
+
version: '4.7'
|
|
103
117
|
type: :runtime
|
|
104
118
|
prerelease: false
|
|
105
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
120
|
requirements:
|
|
107
121
|
- - "~>"
|
|
108
122
|
- !ruby/object:Gem::Version
|
|
109
|
-
version: '
|
|
123
|
+
version: '4.7'
|
|
110
124
|
- !ruby/object:Gem::Dependency
|
|
111
|
-
name:
|
|
125
|
+
name: optimist
|
|
112
126
|
requirement: !ruby/object:Gem::Requirement
|
|
113
127
|
requirements:
|
|
114
|
-
- - "
|
|
128
|
+
- - "~>"
|
|
115
129
|
- !ruby/object:Gem::Version
|
|
116
|
-
version: '0'
|
|
130
|
+
version: '3.0'
|
|
117
131
|
type: :runtime
|
|
118
132
|
prerelease: false
|
|
119
133
|
version_requirements: !ruby/object:Gem::Requirement
|
|
120
134
|
requirements:
|
|
121
|
-
- - "
|
|
135
|
+
- - "~>"
|
|
122
136
|
- !ruby/object:Gem::Version
|
|
123
|
-
version: '0'
|
|
137
|
+
version: '3.0'
|
|
124
138
|
- !ruby/object:Gem::Dependency
|
|
125
|
-
name:
|
|
139
|
+
name: parslet
|
|
126
140
|
requirement: !ruby/object:Gem::Requirement
|
|
127
141
|
requirements:
|
|
128
|
-
- - "
|
|
142
|
+
- - "~>"
|
|
129
143
|
- !ruby/object:Gem::Version
|
|
130
|
-
version: '0'
|
|
144
|
+
version: '2.0'
|
|
131
145
|
type: :runtime
|
|
132
146
|
prerelease: false
|
|
133
147
|
version_requirements: !ruby/object:Gem::Requirement
|
|
134
148
|
requirements:
|
|
135
|
-
- - "
|
|
149
|
+
- - "~>"
|
|
136
150
|
- !ruby/object:Gem::Version
|
|
137
|
-
version: '0'
|
|
151
|
+
version: '2.0'
|
|
138
152
|
- !ruby/object:Gem::Dependency
|
|
139
153
|
name: manageiq-style
|
|
140
154
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -211,10 +225,8 @@ executables:
|
|
|
211
225
|
extensions: []
|
|
212
226
|
extra_rdoc_files: []
|
|
213
227
|
files:
|
|
214
|
-
- ".codeclimate.yml"
|
|
215
228
|
- ".rspec"
|
|
216
229
|
- ".rubocop.yml"
|
|
217
|
-
- ".rubocop_cc.yml"
|
|
218
230
|
- ".rubocop_local.yml"
|
|
219
231
|
- ".yamllint"
|
|
220
232
|
- CHANGELOG.md
|
|
@@ -224,6 +236,7 @@ files:
|
|
|
224
236
|
- Rakefile
|
|
225
237
|
- examples/everything.asl
|
|
226
238
|
- examples/http.asl
|
|
239
|
+
- examples/log.asl
|
|
227
240
|
- examples/map.asl
|
|
228
241
|
- examples/parallel.asl
|
|
229
242
|
- examples/set-credential.asl
|
data/.codeclimate.yml
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
version: '2'
|
|
2
|
-
prepare:
|
|
3
|
-
fetch:
|
|
4
|
-
- url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_base.yml
|
|
5
|
-
path: ".rubocop_base.yml"
|
|
6
|
-
- url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_cc_base.yml
|
|
7
|
-
path: ".rubocop_cc_base.yml"
|
|
8
|
-
- url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/base.yml
|
|
9
|
-
path: styles/base.yml
|
|
10
|
-
- url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/cc_base.yml
|
|
11
|
-
path: styles/cc_base.yml
|
|
12
|
-
checks:
|
|
13
|
-
argument-count:
|
|
14
|
-
enabled: false
|
|
15
|
-
complex-logic:
|
|
16
|
-
enabled: false
|
|
17
|
-
file-lines:
|
|
18
|
-
enabled: false
|
|
19
|
-
method-complexity:
|
|
20
|
-
config:
|
|
21
|
-
threshold: 11
|
|
22
|
-
method-count:
|
|
23
|
-
enabled: false
|
|
24
|
-
method-lines:
|
|
25
|
-
enabled: false
|
|
26
|
-
nested-control-flow:
|
|
27
|
-
enabled: false
|
|
28
|
-
return-statements:
|
|
29
|
-
enabled: false
|
|
30
|
-
plugins:
|
|
31
|
-
rubocop:
|
|
32
|
-
enabled: true
|
|
33
|
-
config: ".rubocop_cc.yml"
|
|
34
|
-
channel: rubocop-1-56-3
|
|
35
|
-
exclude_patterns:
|
|
36
|
-
- spec/
|
data/.rubocop_cc.yml
DELETED