floe 0.13.1 → 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 +4 -4
- data/CHANGELOG.md +9 -1
- 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/choice_rule/data.rb +27 -23
- data/lib/floe/workflow/state.rb +2 -2
- data/lib/floe/workflow/states/choice.rb +1 -0
- data/lib/floe/workflow.rb +4 -2
- data/lib/floe.rb +12 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0e0b86c08d322e2ebc8e115e5193d0493f8257c831f7377b18c32d4284ae6f2
|
4
|
+
data.tar.gz: 1bf5ed62abafe2e1af6025dbaf47d9faa3b1c93ffec8fd88fbeacd4445efc795
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 34f64555f2472c121fcea9dda8baac7fbe853cdbc5e949c831d7b7f070878d3ef2f86ed90d907b5a7033539fcad6aeea64a4dee6475a752d7f89e4f679a4b269
|
7
|
+
data.tar.gz: 378c5bcd562d4bbeb7e3eb13eb55b1d5d8e7aed0ea4749c2b65f951cf3be0e5f13d3e288cdbf63b4a5729e98c5010656d6dca3db60ffbf8463988008980edd83
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,13 @@ 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
|
+
|
7
14
|
## [0.13.1] - 2024-08-16
|
8
15
|
### Fixed
|
9
16
|
- Fix podman/docker container_ref trailing newline ([#265](https://github.com/ManageIQ/floe/pull/265))
|
@@ -242,7 +249,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
242
249
|
### Added
|
243
250
|
- Initial release
|
244
251
|
|
245
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
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
|
246
254
|
[0.13.1]: https://github.com/ManageIQ/floe/compare/v0.13.0...v0.13.1
|
247
255
|
[0.13.0]: https://github.com/ManageIQ/floe/compare/v0.12.0...v0.13.0
|
248
256
|
[0.12.0]: https://github.com/ManageIQ/floe/compare/v0.11.3...v0.12.0
|
@@ -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
@@ -23,11 +23,11 @@ module Floe
|
|
23
23
|
rhs = compare_value(context, input)
|
24
24
|
|
25
25
|
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)
|
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)
|
31
31
|
when "StringEquals", "StringEqualsPath",
|
32
32
|
"NumericEquals", "NumericEqualsPath",
|
33
33
|
"BooleanEquals", "BooleanEqualsPath",
|
@@ -62,47 +62,51 @@ module Floe
|
|
62
62
|
# Get the right hand side for {"Variable": "$.foo", "IsPresent": true} i.e.: true
|
63
63
|
# If true then return true when present.
|
64
64
|
# If false then return true when not present.
|
65
|
-
|
65
|
+
predicate = compare_value(context, input)
|
66
66
|
# Don't need the variable_value, just need to see if the path finds the value.
|
67
67
|
variable_value(context, input)
|
68
68
|
|
69
69
|
# The variable_value is present
|
70
|
-
# If
|
71
|
-
|
70
|
+
# If predicate is true, then presence check was successful, return true.
|
71
|
+
predicate
|
72
72
|
rescue Floe::PathError
|
73
73
|
# variable_value is not present. (the path lookup threw an error)
|
74
|
-
# If
|
75
|
-
!
|
74
|
+
# If predicate is false, then it successfully wasn't present, return true.
|
75
|
+
!predicate
|
76
76
|
end
|
77
77
|
|
78
|
-
|
79
|
-
|
78
|
+
# rubocop:disable Naming/PredicateName
|
79
|
+
# rubocop:disable Style/OptionalBooleanParameter
|
80
|
+
def is_null?(value, predicate = true)
|
81
|
+
value.nil? == predicate
|
80
82
|
end
|
81
83
|
|
82
|
-
def is_present?(value
|
83
|
-
!value.nil?
|
84
|
+
def is_present?(value, predicate = true)
|
85
|
+
!value.nil? == predicate
|
84
86
|
end
|
85
87
|
|
86
|
-
def is_numeric?(value
|
87
|
-
value.kind_of?(Numeric)
|
88
|
+
def is_numeric?(value, predicate = true)
|
89
|
+
value.kind_of?(Numeric) == predicate
|
88
90
|
end
|
89
91
|
|
90
|
-
def is_string?(value
|
91
|
-
value.kind_of?(String)
|
92
|
+
def is_string?(value, predicate = true)
|
93
|
+
value.kind_of?(String) == predicate
|
92
94
|
end
|
93
95
|
|
94
|
-
def is_boolean?(value
|
95
|
-
[true, false].include?(value)
|
96
|
+
def is_boolean?(value, predicate = true)
|
97
|
+
[true, false].include?(value) == predicate
|
96
98
|
end
|
97
99
|
|
98
|
-
def is_timestamp?(value
|
100
|
+
def is_timestamp?(value, predicate = true)
|
99
101
|
require "date"
|
100
102
|
|
101
103
|
DateTime.rfc3339(value)
|
102
|
-
|
104
|
+
predicate
|
103
105
|
rescue TypeError, Date::Error
|
104
|
-
|
106
|
+
!predicate
|
105
107
|
end
|
108
|
+
# rubocop:enable Naming/PredicateName
|
109
|
+
# rubocop:enable Style/OptionalBooleanParameter
|
106
110
|
|
107
111
|
def parse_compare_key
|
108
112
|
@compare_key = payload.keys.detect { |key| key.match?(/^(#{COMPARE_KEYS.join("|")})/) }
|
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
|
@@ -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,
|
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"]
|
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
|
-
|
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.
|
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-
|
11
|
+
date: 2024-08-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: awesome_spawn
|