floe 0.13.1 → 0.15.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.codeclimate.yml +21 -1
- data/CHANGELOG.md +27 -1
- data/README.md +8 -0
- data/examples/map.asl +46 -0
- data/examples/parallel.asl +32 -0
- data/lib/floe/cli.rb +15 -8
- data/lib/floe/container_runner/docker.rb +10 -9
- data/lib/floe/container_runner/kubernetes.rb +10 -8
- data/lib/floe/container_runner/podman.rb +8 -5
- data/lib/floe/validation_mixin.rb +9 -1
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/branch.rb +8 -0
- data/lib/floe/workflow/choice_rule/data.rb +88 -32
- data/lib/floe/workflow/item_processor.rb +14 -0
- data/lib/floe/workflow/state.rb +2 -2
- data/lib/floe/workflow/states/child_workflow_mixin.rb +58 -0
- data/lib/floe/workflow/states/choice.rb +6 -6
- data/lib/floe/workflow/states/map.rb +116 -2
- data/lib/floe/workflow/states/parallel.rb +65 -2
- data/lib/floe/workflow/states/retry_catch_mixin.rb +57 -0
- data/lib/floe/workflow/states/task.rb +1 -48
- data/lib/floe/workflow.rb +16 -35
- data/lib/floe/workflow_base.rb +108 -0
- data/lib/floe.rb +21 -3
- data/tools/step_functions +110 -0
- metadata +10 -3
- data/sig/floe.rbs/floe.rbs +0 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 75d5be2f5b9cdcfc64b4b3994d32b9e3955486fd99bf4e05c62c2414485188c8
|
4
|
+
data.tar.gz: 7257009d5942f157f2ccef6499c77e1f9033e0ac4846a08f3f589943bd4794ef
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd9275c7e845841fc472e3e0ec92593354c6293bae3c8dc9d7258acb71fcd5db7abaca2c2aadcc4c7701952b65eae8a14da29c1e8e26d71a7d9df2c6aad9abeb
|
7
|
+
data.tar.gz: 8cc089f45244d80428d92feeeb162d8c65fafcea915e00405c54f5340dee4f9106113ad87e36c97a18fd159c3d338bee0f78869e91d200f19b145df494e5edfd
|
data/.codeclimate.yml
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
version: '2'
|
1
2
|
prepare:
|
2
3
|
fetch:
|
3
4
|
- url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/.rubocop_base.yml
|
@@ -8,9 +9,28 @@ prepare:
|
|
8
9
|
path: styles/base.yml
|
9
10
|
- url: https://raw.githubusercontent.com/ManageIQ/manageiq-style/master/styles/cc_base.yml
|
10
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
|
11
30
|
plugins:
|
12
31
|
rubocop:
|
13
32
|
enabled: true
|
14
33
|
config: ".rubocop_cc.yml"
|
15
34
|
channel: rubocop-1-56-3
|
16
|
-
|
35
|
+
exclude_patterns:
|
36
|
+
- spec/
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,31 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
4
4
|
|
5
5
|
## [Unreleased]
|
6
6
|
|
7
|
+
## [0.15.0] - 2024-10-28
|
8
|
+
### Added
|
9
|
+
- Add WorkflowBase base class for Workflow ([#279](https://github.com/ManageIQ/floe/pull/279))
|
10
|
+
- Add tool for using the aws stepfunctions simulator ([#244](https://github.com/ManageIQ/floe/pull/244))
|
11
|
+
- Implement Map state ([#184](https://github.com/ManageIQ/floe/pull/184))
|
12
|
+
- Add Map State Tolerated Failure ([#282](https://github.com/ManageIQ/floe/pull/282))
|
13
|
+
- Run Map iterations in parallel up to MaxConcurrency ([#283](https://github.com/ManageIQ/floe/pull/283))
|
14
|
+
- Implement Parallel State ([#291](https://github.com/ManageIQ/floe/pull/291))
|
15
|
+
|
16
|
+
### Changed
|
17
|
+
- More granular compare_key and determine path at initialization time ([#274](https://github.com/ManageIQ/floe/pull/274))
|
18
|
+
- For Choice validation, use instance variables and not payload ([#277](https://github.com/ManageIQ/floe/pull/277))
|
19
|
+
- Return ExceedToleratedFailureThreshold if ToleratedFailureCount/Percentage is present ([#285](https://github.com/ManageIQ/floe/pull/285))
|
20
|
+
|
21
|
+
### Fixed
|
22
|
+
- Fix case on log messages ([#280](https://github.com/ManageIQ/floe/pull/280))
|
23
|
+
- Handle either ToleratedFailureCount or ToleratedFailurePercentage ([#284](https://github.com/ManageIQ/floe/pull/284))
|
24
|
+
|
25
|
+
## [0.14.0] - 2024-08-20
|
26
|
+
### Added
|
27
|
+
- Implement "IsNumeric": false ([#266](https://github.com/ManageIQ/floe/pull/266))
|
28
|
+
- Support choices that do not have a Default defined ([#267](https://github.com/ManageIQ/floe/pull/267))
|
29
|
+
- Label containers/pods with workflow Execution ID ([#268](https://github.com/ManageIQ/floe/pull/268))
|
30
|
+
- Allow for Execution Id to be passed in ([#269](https://github.com/ManageIQ/floe/pull/269))
|
31
|
+
|
7
32
|
## [0.13.1] - 2024-08-16
|
8
33
|
### Fixed
|
9
34
|
- Fix podman/docker container_ref trailing newline ([#265](https://github.com/ManageIQ/floe/pull/265))
|
@@ -242,7 +267,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
242
267
|
### Added
|
243
268
|
- Initial release
|
244
269
|
|
245
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
270
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.14.0...HEAD
|
271
|
+
[0.14.0]: https://github.com/ManageIQ/floe/compare/v0.13.1...v0.14.0
|
246
272
|
[0.13.1]: https://github.com/ManageIQ/floe/compare/v0.13.0...v0.13.1
|
247
273
|
[0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
|
248
274
|
[0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
|
data/README.md
CHANGED
@@ -197,6 +197,14 @@ Options supported by the kubernetes docker runner are:
|
|
197
197
|
* `ca_file` - Path to a certificate-authority file for the kubernetes API, only valid if server and token are passed. If present `/run/secrets/kubernetes.io/serviceaccount/ca.crt` will be used
|
198
198
|
* `verify_ssl` - Controls if the kubernetes API certificate-authority should be verified, defaults to "true", only vaild if server and token are passed
|
199
199
|
|
200
|
+
## Features Not Yet Supported
|
201
|
+
|
202
|
+
The following are not yet supported:
|
203
|
+
- Map State Fields:
|
204
|
+
- ItemReader
|
205
|
+
- ItemSelector/ItemBatcher
|
206
|
+
- ResultWriter
|
207
|
+
|
200
208
|
## Development
|
201
209
|
|
202
210
|
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
data/examples/map.asl
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
{
|
2
|
+
"Comment": "Using Map state in Inline mode",
|
3
|
+
"StartAt": "Pass",
|
4
|
+
"States": {
|
5
|
+
"Pass": {
|
6
|
+
"Type": "Pass",
|
7
|
+
"Next": "Map demo",
|
8
|
+
"Result": {
|
9
|
+
"foo": "bar",
|
10
|
+
"colors": [
|
11
|
+
"red",
|
12
|
+
"green",
|
13
|
+
"blue",
|
14
|
+
"yellow",
|
15
|
+
"white"
|
16
|
+
]
|
17
|
+
}
|
18
|
+
},
|
19
|
+
"Map demo": {
|
20
|
+
"Type": "Map",
|
21
|
+
"ItemsPath": "$.colors",
|
22
|
+
"MaxConcurrency": 2,
|
23
|
+
"ItemProcessor": {
|
24
|
+
"ProcessorConfig": {
|
25
|
+
"Mode": "INLINE"
|
26
|
+
},
|
27
|
+
"StartAt": "Generate UUID",
|
28
|
+
"States": {
|
29
|
+
"Generate UUID": {
|
30
|
+
"Type": "Pass",
|
31
|
+
"Next": "Sleep",
|
32
|
+
"Parameters": {
|
33
|
+
"uuid.$": "States.UUID()"
|
34
|
+
}
|
35
|
+
},
|
36
|
+
"Sleep": {
|
37
|
+
"Type": "Task",
|
38
|
+
"Resource": "docker://docker.io/agrare/sleep:latest",
|
39
|
+
"End": true
|
40
|
+
}
|
41
|
+
}
|
42
|
+
},
|
43
|
+
"End": true
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
@@ -0,0 +1,32 @@
|
|
1
|
+
{
|
2
|
+
"Comment": "Parallel Example.",
|
3
|
+
"StartAt": "FunWithMath",
|
4
|
+
"States": {
|
5
|
+
"FunWithMath": {
|
6
|
+
"Type": "Parallel",
|
7
|
+
"End": true,
|
8
|
+
"Branches": [
|
9
|
+
{
|
10
|
+
"StartAt": "Add",
|
11
|
+
"States": {
|
12
|
+
"Add": {
|
13
|
+
"Type": "Task",
|
14
|
+
"Resource": "docker://docker.io/agrare/sleep:latest",
|
15
|
+
"End": true
|
16
|
+
}
|
17
|
+
}
|
18
|
+
},
|
19
|
+
{
|
20
|
+
"StartAt": "Subtract",
|
21
|
+
"States": {
|
22
|
+
"Subtract": {
|
23
|
+
"Type": "Task",
|
24
|
+
"Resource": "docker://docker.io/agrare/sleep:latest",
|
25
|
+
"End": true
|
26
|
+
}
|
27
|
+
}
|
28
|
+
}
|
29
|
+
]
|
30
|
+
}
|
31
|
+
}
|
32
|
+
}
|
data/lib/floe/cli.rb
CHANGED
@@ -13,17 +13,11 @@ module Floe
|
|
13
13
|
def run(args = ARGV)
|
14
14
|
workflows_inputs, opts = parse_options!(args)
|
15
15
|
|
16
|
-
credentials =
|
17
|
-
if opts[:credentials_given]
|
18
|
-
opts[:credentials] == "-" ? $stdin.read : opts[:credentials]
|
19
|
-
elsif opts[:credentials_file_given]
|
20
|
-
File.read(opts[:credentials_file])
|
21
|
-
end
|
16
|
+
credentials = create_credentials(opts)
|
22
17
|
|
23
18
|
workflows =
|
24
19
|
workflows_inputs.each_slice(2).map do |workflow, input|
|
25
|
-
|
26
|
-
Floe::Workflow.load(workflow, context)
|
20
|
+
create_workflow(workflow, opts[:context], input, credentials)
|
27
21
|
end
|
28
22
|
|
29
23
|
Floe::Workflow.wait(workflows, &:run_nonblock)
|
@@ -82,5 +76,18 @@ module Floe
|
|
82
76
|
|
83
77
|
return workflows_inputs, opts
|
84
78
|
end
|
79
|
+
|
80
|
+
def create_credentials(opts)
|
81
|
+
if opts[:credentials_given]
|
82
|
+
opts[:credentials] == "-" ? $stdin.read : opts[:credentials]
|
83
|
+
elsif opts[:credentials_file_given]
|
84
|
+
File.read(opts[:credentials_file])
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_workflow(workflow, context_payload, input, credentials)
|
89
|
+
context = Floe::Workflow::Context.new(context_payload, :input => input, :credentials => credentials)
|
90
|
+
Floe::Workflow.load(workflow, context)
|
91
|
+
end
|
85
92
|
end
|
86
93
|
end
|
@@ -18,11 +18,11 @@ module Floe
|
|
18
18
|
@pull_policy = options["pull-policy"]
|
19
19
|
end
|
20
20
|
|
21
|
-
def run_async!(resource, env
|
21
|
+
def run_async!(resource, env, secrets, context)
|
22
22
|
raise ArgumentError, "Invalid resource" unless resource&.start_with?("docker://")
|
23
23
|
|
24
|
-
image
|
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,8 +123,8 @@ 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
|
|
@@ -132,13 +132,14 @@ module Floe
|
|
132
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
|
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
|
-
|
59
|
+
notice = JSON.parse(notice)
|
60
|
+
id, status, exit_code, attributes = notice.values_at("ID", "Status", "ContainerExitCode", "Attributes")
|
59
61
|
|
60
|
-
|
61
|
-
|
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
|
-
|
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
@@ -4,16 +4,20 @@ module Floe
|
|
4
4
|
class Workflow
|
5
5
|
class ChoiceRule
|
6
6
|
class Data < Floe::Workflow::ChoiceRule
|
7
|
-
|
7
|
+
TYPES = ["String", "Numeric", "Boolean", "Timestamp", "Present", "Null"].freeze
|
8
|
+
COMPARES = ["Equals", "LessThan", "GreaterThan", "LessThanEquals", "GreaterThanEquals", "Matches"].freeze
|
9
|
+
# e.g.: (Is)(String), (Is)(Present)
|
10
|
+
TYPE_CHECK = /^(Is)(#{TYPES.join("|")})$/.freeze
|
11
|
+
# e.g.: (String)(LessThan)(Path), (Numeric)(GreaterThanEquals)()
|
12
|
+
OPERATION = /^(#{(TYPES - %w[Null Present]).join("|")})(#{COMPARES.join("|")})(Path)?$/.freeze
|
8
13
|
|
9
|
-
attr_reader :variable, :compare_key, :
|
14
|
+
attr_reader :variable, :compare_key, :type, :compare_predicate, :path
|
10
15
|
|
11
16
|
def initialize(_workflow, _name, payload)
|
12
17
|
super
|
13
18
|
|
14
|
-
@variable = parse_path("Variable"
|
19
|
+
@variable = parse_path("Variable")
|
15
20
|
parse_compare_key
|
16
|
-
@value = path ? parse_path(compare_key, payload) : payload[compare_key]
|
17
21
|
end
|
18
22
|
|
19
23
|
def true?(context, input)
|
@@ -23,11 +27,11 @@ module Floe
|
|
23
27
|
rhs = compare_value(context, input)
|
24
28
|
|
25
29
|
case compare_key
|
26
|
-
when "IsNull" then is_null?(lhs)
|
27
|
-
when "IsNumeric" then is_numeric?(lhs)
|
28
|
-
when "IsString" then is_string?(lhs)
|
29
|
-
when "IsBoolean" then is_boolean?(lhs)
|
30
|
-
when "IsTimestamp" then is_timestamp?(lhs)
|
30
|
+
when "IsNull" then is_null?(lhs, rhs)
|
31
|
+
when "IsNumeric" then is_numeric?(lhs, rhs)
|
32
|
+
when "IsString" then is_string?(lhs, rhs)
|
33
|
+
when "IsBoolean" then is_boolean?(lhs, rhs)
|
34
|
+
when "IsTimestamp" then is_timestamp?(lhs, rhs)
|
31
35
|
when "StringEquals", "StringEqualsPath",
|
32
36
|
"NumericEquals", "NumericEqualsPath",
|
33
37
|
"BooleanEquals", "BooleanEqualsPath",
|
@@ -62,68 +66,120 @@ module Floe
|
|
62
66
|
# Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
|
63
67
|
# If true then return true when present.
|
64
68
|
# If false then return true when not present.
|
65
|
-
|
69
|
+
predicate = compare_value(context, input)
|
66
70
|
# Don't need the variable_value, just need to see if the path finds the value.
|
67
71
|
variable_value(context, input)
|
68
72
|
|
69
73
|
# The variable_value is present
|
70
|
-
# If
|
71
|
-
|
74
|
+
# If predicate is true, then presence check was successful, return true.
|
75
|
+
predicate
|
72
76
|
rescue Floe::PathError
|
73
77
|
# variable_value is not present. (the path lookup threw an error)
|
74
|
-
# If
|
75
|
-
!
|
78
|
+
# If predicate is false, then it successfully wasn't present, return true.
|
79
|
+
!predicate
|
76
80
|
end
|
77
81
|
|
78
|
-
|
79
|
-
|
82
|
+
# rubocop:disable Naming/PredicateName
|
83
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
84
|
+
def is_null?(value, predicate = true)
|
85
|
+
value.nil? == predicate
|
80
86
|
end
|
81
87
|
|
82
|
-
def is_present?(value
|
83
|
-
!value.nil?
|
88
|
+
def is_present?(value, predicate = true)
|
89
|
+
!value.nil? == predicate
|
84
90
|
end
|
85
91
|
|
86
|
-
def is_numeric?(value
|
87
|
-
value.kind_of?(Numeric)
|
92
|
+
def is_numeric?(value, predicate = true)
|
93
|
+
value.kind_of?(Numeric) == predicate
|
88
94
|
end
|
89
95
|
|
90
|
-
def is_string?(value
|
91
|
-
value.kind_of?(String)
|
96
|
+
def is_string?(value, predicate = true)
|
97
|
+
value.kind_of?(String) == predicate
|
92
98
|
end
|
93
99
|
|
94
|
-
def is_boolean?(value
|
95
|
-
[true, false].include?(value)
|
100
|
+
def is_boolean?(value, predicate = true)
|
101
|
+
[true, false].include?(value) == predicate
|
96
102
|
end
|
97
103
|
|
98
|
-
def is_timestamp?(value
|
104
|
+
def is_timestamp?(value, predicate = true)
|
99
105
|
require "date"
|
100
106
|
|
101
107
|
DateTime.rfc3339(value)
|
102
|
-
|
108
|
+
predicate
|
103
109
|
rescue TypeError, Date::Error
|
104
|
-
|
110
|
+
!predicate
|
105
111
|
end
|
112
|
+
# rubocop:enable Naming/PredicateName
|
113
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
106
114
|
|
115
|
+
# parse the compare key at initialization time
|
107
116
|
def parse_compare_key
|
108
|
-
|
117
|
+
payload.each_key do |key|
|
118
|
+
# e.g. (String)(GreaterThan)(Path)
|
119
|
+
if (match_values = OPERATION.match(key))
|
120
|
+
@compare_key = key
|
121
|
+
@type, _operator, @path = match_values.captures
|
122
|
+
@compare_predicate = parse_predicate(type)
|
123
|
+
break
|
124
|
+
end
|
125
|
+
# e.g. (Is)(String)
|
126
|
+
if TYPE_CHECK.match?(key)
|
127
|
+
@compare_key = key
|
128
|
+
# type: nil means no runtime type checking.
|
129
|
+
@type = @path = nil
|
130
|
+
@compare_predicate = parse_predicate("Boolean")
|
131
|
+
break
|
132
|
+
end
|
133
|
+
end
|
109
134
|
parser_error!("requires a compare key") unless compare_key
|
135
|
+
end
|
110
136
|
|
111
|
-
|
137
|
+
# parse predicate at initilization time
|
138
|
+
# @return the right predicate attached to the compare key
|
139
|
+
def parse_predicate(data_type)
|
140
|
+
path ? parse_path(compare_key) : parse_field(compare_key, data_type)
|
112
141
|
end
|
113
142
|
|
143
|
+
# @return right hand predicate - input path or static payload value)
|
114
144
|
def compare_value(context, input)
|
115
|
-
path ?
|
145
|
+
path ? fetch_path(compare_key, compare_predicate, context, input) : compare_predicate
|
116
146
|
end
|
117
147
|
|
148
|
+
# feth the variable value at runtime
|
149
|
+
# @return variable value (left hand side )
|
118
150
|
def variable_value(context, input)
|
119
|
-
variable
|
151
|
+
fetch_path("Variable", variable, context, input)
|
120
152
|
end
|
121
153
|
|
122
|
-
|
154
|
+
# parse path at initilization time
|
155
|
+
# helper method to parse a path from the payload
|
156
|
+
def parse_path(field_name)
|
123
157
|
value = payload[field_name]
|
124
158
|
missing_field_error!(field_name) unless value
|
125
159
|
wrap_parser_error(field_name, value) { Path.new(value) }
|
126
160
|
end
|
161
|
+
|
162
|
+
# parse predicate field at initialization time
|
163
|
+
def parse_field(field_name, data_type)
|
164
|
+
value = payload[field_name]
|
165
|
+
return value if correct_type?(value, data_type)
|
166
|
+
|
167
|
+
invalid_field_error!(field_name, value, "required to be a #{data_type}")
|
168
|
+
end
|
169
|
+
|
170
|
+
# fetch a path at runtime
|
171
|
+
def fetch_path(field_name, field_path, context, input)
|
172
|
+
value = field_path.value(context, input)
|
173
|
+
return value if type.nil? || correct_type?(value, type)
|
174
|
+
|
175
|
+
runtime_field_error!(field_name, field_path.to_s, "required to point to a #{type}")
|
176
|
+
end
|
177
|
+
|
178
|
+
# if we have runtime checking, check against that type
|
179
|
+
# otherwise assume checking a TYPE_CHECK predicate and check against Boolean
|
180
|
+
def correct_type?(value, data_type)
|
181
|
+
send("is_#{data_type.downcase}?".to_sym, value)
|
182
|
+
end
|
127
183
|
end
|
128
184
|
end
|
129
185
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Floe
|
4
|
+
class Workflow
|
5
|
+
class ItemProcessor < Floe::WorkflowBase
|
6
|
+
attr_reader :processor_config
|
7
|
+
|
8
|
+
def initialize(payload, name = nil)
|
9
|
+
super
|
10
|
+
@processor_config = payload.fetch("ProcessorConfig", "INLINE")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
data/lib/floe/workflow/state.rb
CHANGED
@@ -48,7 +48,7 @@ module Floe
|
|
48
48
|
return Errno::EAGAIN unless ready?(context)
|
49
49
|
|
50
50
|
finish(context)
|
51
|
-
rescue Floe::
|
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" =>
|
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
|