floe 0.8.0 → 0.10.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 +26 -1
- data/Gemfile +0 -6
- data/exe/floe +8 -20
- data/floe.gemspec +9 -1
- data/lib/floe/version.rb +1 -1
- data/lib/floe/workflow/context.rb +14 -10
- data/lib/floe/workflow/reference_path.rb +2 -2
- data/lib/floe/workflow/runner/docker.rb +99 -2
- data/lib/floe/workflow/runner/docker_mixin.rb +4 -3
- data/lib/floe/workflow/runner/kubernetes.rb +85 -2
- data/lib/floe/workflow/runner/podman.rb +26 -0
- data/lib/floe/workflow/runner.rb +27 -10
- data/lib/floe/workflow/state.rb +17 -8
- data/lib/floe/workflow/states/choice.rb +3 -3
- data/lib/floe/workflow/states/fail.rb +4 -6
- data/lib/floe/workflow/states/non_terminal_mixin.rb +5 -0
- data/lib/floe/workflow/states/pass.rb +3 -5
- data/lib/floe/workflow/states/succeed.rb +3 -3
- data/lib/floe/workflow/states/task.rb +9 -6
- data/lib/floe/workflow/states/wait.rb +7 -3
- data/lib/floe/workflow.rb +68 -8
- data/lib/floe.rb +20 -0
- metadata +102 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2b158e514e08902a1138c7b2878bb8633ca7c1abb59d4b77b6d9ce568c65710b
|
|
4
|
+
data.tar.gz: 32d053bba54e8c35645a636771964692e435ad72ee38e03c8c22b4da5bce25ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e39301eed1de9189b66f07a7ecdda807974bf47495b4906bd57f6f8ed862fab38448668155f79e12c87da8935f4054d14dbcd08e292429b194768d2234fa7c46
|
|
7
|
+
data.tar.gz: 5cf0476521a1cd4fc0dcc4edeccdd00c6c72ee88bf7428103b86f5f30097608f934f19e75499a61b5d7e7380f2e1451dccfd82f796eee6bcef041dd6f3db8664
|
data/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.10.0] - 2024-04-05
|
|
8
|
+
### Fixed
|
|
9
|
+
- Fix rubocops ([#164](https://github.com/ManageIQ/floe/pull/164))
|
|
10
|
+
- Output should contain errors ([#165](https://github.com/ManageIQ/floe/pull/165))
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add simplecov ([#162](https://github.com/ManageIQ/floe/pull/162))
|
|
14
|
+
- Add ability to pass context on the command line ([#161](https://github.com/ManageIQ/floe/pull/161))
|
|
15
|
+
- Add specs for `Workflow#wait_until`, `#waiting?` ([#166](https://github.com/ManageIQ/floe/pull/166))
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Drop non-standard Error/Cause fields ([#167](https://github.com/ManageIQ/floe/pull/167))
|
|
19
|
+
|
|
20
|
+
## [0.9.0] - 2024-02-19
|
|
21
|
+
### Changed
|
|
22
|
+
- Default to wait indefinitely ([#157](https://github.com/ManageIQ/floe/pull/157))
|
|
23
|
+
- Create docker runners factory and add scheme ([#152](https://github.com/ManageIQ/floe/pull/152))
|
|
24
|
+
- Add a watch method to Workflow::Runner for event driven updates ([#95](https://github.com/ManageIQ/floe/pull/95))
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
- Fix waiting on extremely short durations ([#160](https://github.com/ManageIQ/floe/pull/160))
|
|
28
|
+
- Fix wait state missing finish ([#159](https://github.com/ManageIQ/floe/pull/159))
|
|
29
|
+
|
|
7
30
|
## [0.8.0] - 2024-01-17
|
|
8
31
|
### Added
|
|
9
32
|
- Add CLI shorthand options for docker runner ([#147](https://github.com/ManageIQ/floe/pull/147))
|
|
@@ -126,7 +149,9 @@ This project adheres to [Semantic Versioning](http://semver.org/).
|
|
|
126
149
|
### Added
|
|
127
150
|
- Initial release
|
|
128
151
|
|
|
129
|
-
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.
|
|
152
|
+
[Unreleased]: https://github.com/ManageIQ/floe/compare/v0.10.0...HEAD
|
|
153
|
+
[0.10.0]: https://github.com/ManageIQ/floe/compare/v0.9.0...v0.10.0
|
|
154
|
+
[0.9.0]: https://github.com/ManageIQ/floe/compare/v0.8.0...v0.9.0
|
|
130
155
|
[0.8.0]: https://github.com/ManageIQ/floe/compare/v0.7.0...v0.8.0
|
|
131
156
|
[0.7.0]: https://github.com/ManageIQ/floe/compare/v0.6.1...v0.7.0
|
|
132
157
|
[0.6.1]: https://github.com/ManageIQ/floe/compare/v0.6.0...v0.6.1
|
data/Gemfile
CHANGED
data/exe/floe
CHANGED
|
@@ -10,6 +10,7 @@ opts = Optimist.options do
|
|
|
10
10
|
|
|
11
11
|
opt :workflow, "Path to your workflow json (legacy)", :type => :string
|
|
12
12
|
opt :input, "JSON payload to input to the workflow (legacy)", :type => :string
|
|
13
|
+
opt :context, "JSON payload of the Context", :type => :string
|
|
13
14
|
opt :credentials, "JSON payload with credentials", :type => :string
|
|
14
15
|
opt :credentials_file, "Path to a file with credentials", :type => :string
|
|
15
16
|
opt :docker_runner, "Type of runner for docker images", :type => :string, :short => 'r'
|
|
@@ -20,8 +21,6 @@ opts = Optimist.options do
|
|
|
20
21
|
opt :kubernetes, "Use kubernetes to run images (short for --docker_runner=kubernetes)", :type => :boolean
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
Optimist.die(:docker_runner, "must be one of #{Floe::Workflow::Runner::TYPES.join(", ")}") unless Floe::Workflow::Runner::TYPES.include?(opts[:docker_runner])
|
|
24
|
-
|
|
25
24
|
# legacy support for --workflow
|
|
26
25
|
args = ARGV.empty? ? [opts[:workflow], opts[:input]] : ARGV
|
|
27
26
|
Optimist.die(:workflow, "must be specified") if args.empty?
|
|
@@ -34,18 +33,13 @@ opts[:docker_runner] ||= "kubernetes" if opts[:kubernetes]
|
|
|
34
33
|
require "logger"
|
|
35
34
|
Floe.logger = Logger.new($stdout)
|
|
36
35
|
|
|
37
|
-
runner_klass = case opts[:docker_runner]
|
|
38
|
-
when "docker"
|
|
39
|
-
Floe::Workflow::Runner::Docker
|
|
40
|
-
when "podman"
|
|
41
|
-
Floe::Workflow::Runner::Podman
|
|
42
|
-
when "kubernetes"
|
|
43
|
-
Floe::Workflow::Runner::Kubernetes
|
|
44
|
-
end
|
|
45
|
-
|
|
46
36
|
runner_options = opts[:docker_runner_options].to_h { |opt| opt.split("=", 2) }
|
|
47
37
|
|
|
48
|
-
|
|
38
|
+
begin
|
|
39
|
+
Floe.set_runner("docker", opts[:docker_runner], runner_options)
|
|
40
|
+
rescue ArgumentError => e
|
|
41
|
+
Optimist.die(:docker_runner, e.message)
|
|
42
|
+
end
|
|
49
43
|
|
|
50
44
|
credentials =
|
|
51
45
|
if opts[:credentials_given]
|
|
@@ -56,19 +50,13 @@ credentials =
|
|
|
56
50
|
|
|
57
51
|
workflows =
|
|
58
52
|
args.each_slice(2).map do |workflow, input|
|
|
59
|
-
context = Floe::Workflow::Context.new(:input => input || opts[:input] || "{}")
|
|
53
|
+
context = Floe::Workflow::Context.new(opts[:context], :input => input || opts[:input] || "{}")
|
|
60
54
|
Floe::Workflow.load(workflow, context, credentials)
|
|
61
55
|
end
|
|
62
56
|
|
|
63
57
|
# run
|
|
64
58
|
|
|
65
|
-
|
|
66
|
-
until outstanding.empty?
|
|
67
|
-
ready = outstanding.select(&:step_nonblock_ready?)
|
|
68
|
-
ready.map(&:run_nonblock)
|
|
69
|
-
outstanding -= ready.select(&:end?)
|
|
70
|
-
sleep(1) if !outstanding.empty?
|
|
71
|
-
end
|
|
59
|
+
Floe::Workflow.wait(workflows, &:run_nonblock)
|
|
72
60
|
|
|
73
61
|
# display status
|
|
74
62
|
|
data/floe.gemspec
CHANGED
|
@@ -29,8 +29,16 @@ Gem::Specification.new do |spec|
|
|
|
29
29
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
30
30
|
spec.require_paths = ["lib"]
|
|
31
31
|
|
|
32
|
-
spec.add_dependency "awesome_spawn", "~>1.
|
|
32
|
+
spec.add_dependency "awesome_spawn", "~>1.6"
|
|
33
|
+
spec.add_dependency "io-wait"
|
|
33
34
|
spec.add_dependency "jsonpath", "~>1.1"
|
|
34
35
|
spec.add_dependency "kubeclient", "~>4.7"
|
|
35
36
|
spec.add_dependency "optimist", "~>3.0"
|
|
37
|
+
|
|
38
|
+
spec.add_development_dependency "manageiq-style"
|
|
39
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
40
|
+
spec.add_development_dependency "rspec"
|
|
41
|
+
spec.add_development_dependency "rubocop"
|
|
42
|
+
spec.add_development_dependency "simplecov", ">= 0.21.2"
|
|
43
|
+
spec.add_development_dependency "timecop"
|
|
36
44
|
end
|
data/lib/floe/version.rb
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
module Floe
|
|
4
4
|
class Workflow
|
|
5
5
|
class Context
|
|
6
|
+
# @param context [Json|Hash] (default, create another with input and execution params)
|
|
7
|
+
# @param input [Hash] (default: {})
|
|
6
8
|
def initialize(context = nil, input: {})
|
|
7
9
|
context = JSON.parse(context) if context.kind_of?(String)
|
|
8
10
|
input = JSON.parse(input) if input.kind_of?(String)
|
|
9
11
|
|
|
10
|
-
@context = context || {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"Task" => {}
|
|
18
|
-
}
|
|
12
|
+
@context = context || {}
|
|
13
|
+
self["Execution"] ||= {}
|
|
14
|
+
self["Execution"]["Input"] ||= input
|
|
15
|
+
self["State"] ||= {}
|
|
16
|
+
self["StateHistory"] ||= []
|
|
17
|
+
self["StateMachine"] ||= {}
|
|
18
|
+
self["Task"] ||= {}
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def execution
|
|
@@ -30,6 +30,10 @@ module Floe
|
|
|
30
30
|
started? && !ended?
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def failed?
|
|
34
|
+
output&.key?("Error") || false
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def ended?
|
|
34
38
|
execution.key?("EndTime")
|
|
35
39
|
end
|
|
@@ -67,7 +71,7 @@ module Floe
|
|
|
67
71
|
"pending"
|
|
68
72
|
elsif running?
|
|
69
73
|
"running"
|
|
70
|
-
elsif
|
|
74
|
+
elsif failed?
|
|
71
75
|
"failure"
|
|
72
76
|
else
|
|
73
77
|
"success"
|
|
@@ -19,11 +19,11 @@ module Floe
|
|
|
19
19
|
super
|
|
20
20
|
|
|
21
21
|
raise Floe::InvalidWorkflowError, "Invalid Reference Path" if payload.match?(/@|,|:|\?/)
|
|
22
|
+
|
|
22
23
|
@path = JsonPath.new(payload)
|
|
23
24
|
.path[1..]
|
|
24
25
|
.map { |v| v.match(/\[(?<name>.+)\]/)["name"] }
|
|
25
|
-
.
|
|
26
|
-
.compact
|
|
26
|
+
.filter_map { |v| v[0] == "'" ? v.delete("'") : v.to_i }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
def get(context)
|
|
@@ -10,6 +10,7 @@ module Floe
|
|
|
10
10
|
|
|
11
11
|
def initialize(options = {})
|
|
12
12
|
require "awesome_spawn"
|
|
13
|
+
require "io/wait"
|
|
13
14
|
require "tempfile"
|
|
14
15
|
|
|
15
16
|
super
|
|
@@ -45,10 +46,63 @@ module Floe
|
|
|
45
46
|
delete_secret(secrets_file) if secrets_file
|
|
46
47
|
end
|
|
47
48
|
|
|
49
|
+
def wait(timeout: nil, events: %i[create update delete], &block)
|
|
50
|
+
until_timestamp = Time.now.utc + timeout if timeout
|
|
51
|
+
|
|
52
|
+
r, w = IO.pipe
|
|
53
|
+
|
|
54
|
+
pid = AwesomeSpawn.run_detached(
|
|
55
|
+
self.class::DOCKER_COMMAND, :err => :out, :out => w, :params => wait_params(until_timestamp)
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
w.close
|
|
59
|
+
|
|
60
|
+
loop do
|
|
61
|
+
readable_timeout = until_timestamp - Time.now.utc if until_timestamp
|
|
62
|
+
|
|
63
|
+
# Wait for our end of the pipe to be readable and if it didn't timeout
|
|
64
|
+
# get the events from stdout
|
|
65
|
+
next if r.wait_readable(readable_timeout).nil?
|
|
66
|
+
|
|
67
|
+
# Get all events while the pipe is readable
|
|
68
|
+
notices = []
|
|
69
|
+
while r.ready?
|
|
70
|
+
notice = r.gets
|
|
71
|
+
|
|
72
|
+
# If the process has exited `r.gets` returns `nil` and the pipe is
|
|
73
|
+
# always `ready?`
|
|
74
|
+
break if notice.nil?
|
|
75
|
+
|
|
76
|
+
event, runner_context = parse_notice(notice)
|
|
77
|
+
next if event.nil? || !events.include?(event)
|
|
78
|
+
|
|
79
|
+
notices << [event, runner_context]
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# If we're given a block yield the events otherwise return them
|
|
83
|
+
if block
|
|
84
|
+
notices.each(&block)
|
|
85
|
+
else
|
|
86
|
+
# Terminate the `docker events` process before returning the events
|
|
87
|
+
sigterm(pid)
|
|
88
|
+
|
|
89
|
+
return notices
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Check that the `docker events` process is still alive
|
|
93
|
+
Process.kill(0, pid)
|
|
94
|
+
rescue Errno::ESRCH
|
|
95
|
+
# Break out of the loop if the `docker events` process has exited
|
|
96
|
+
break
|
|
97
|
+
end
|
|
98
|
+
ensure
|
|
99
|
+
r.close
|
|
100
|
+
end
|
|
101
|
+
|
|
48
102
|
def status!(runner_context)
|
|
49
103
|
return if runner_context.key?("Error")
|
|
50
104
|
|
|
51
|
-
runner_context["container_state"] = inspect_container(runner_context["container_ref"])
|
|
105
|
+
runner_context["container_state"] = inspect_container(runner_context["container_ref"])&.dig("State")
|
|
52
106
|
end
|
|
53
107
|
|
|
54
108
|
def running?(runner_context)
|
|
@@ -91,8 +145,45 @@ module Floe
|
|
|
91
145
|
params << image
|
|
92
146
|
end
|
|
93
147
|
|
|
148
|
+
def wait_params(until_timestamp)
|
|
149
|
+
params = ["events", [:format, "{{json .}}"], [:filter, "type=container"], [:since, Time.now.utc.to_i]]
|
|
150
|
+
params << [:until, until_timestamp.to_i] if until_timestamp
|
|
151
|
+
params
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def parse_notice(notice)
|
|
155
|
+
notice = JSON.parse(notice)
|
|
156
|
+
|
|
157
|
+
status = notice["status"]
|
|
158
|
+
event = docker_event_status_to_event(status)
|
|
159
|
+
running = event != :delete
|
|
160
|
+
|
|
161
|
+
name, exit_code = notice.dig("Actor", "Attributes")&.values_at("name", "exitCode")
|
|
162
|
+
|
|
163
|
+
runner_context = {"container_ref" => name, "container_state" => {"Running" => running, "ExitCode" => exit_code.to_i}}
|
|
164
|
+
|
|
165
|
+
[event, runner_context]
|
|
166
|
+
rescue JSON::ParserError
|
|
167
|
+
[]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def docker_event_status_to_event(status)
|
|
171
|
+
case status
|
|
172
|
+
when "create"
|
|
173
|
+
:create
|
|
174
|
+
when "start"
|
|
175
|
+
:update
|
|
176
|
+
when "die", "destroy"
|
|
177
|
+
:delete
|
|
178
|
+
else
|
|
179
|
+
:unkonwn
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
94
183
|
def inspect_container(container_id)
|
|
95
|
-
JSON.parse(docker!("inspect", container_id).output)
|
|
184
|
+
JSON.parse(docker!("inspect", container_id).output).first
|
|
185
|
+
rescue
|
|
186
|
+
nil
|
|
96
187
|
end
|
|
97
188
|
|
|
98
189
|
def delete_container(container_id)
|
|
@@ -116,6 +207,12 @@ module Floe
|
|
|
116
207
|
secrets_file.path
|
|
117
208
|
end
|
|
118
209
|
|
|
210
|
+
def sigterm(pid)
|
|
211
|
+
Process.kill("TERM", pid)
|
|
212
|
+
rescue Errno::ESRCH
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
119
216
|
def global_docker_options
|
|
120
217
|
[]
|
|
121
218
|
end
|
|
@@ -6,9 +6,10 @@ module Floe
|
|
|
6
6
|
image.match(%r{^(?<repository>.+/)?(?<image>.+):(?<tag>.+)$})&.named_captures&.dig("image")
|
|
7
7
|
end
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# 63 is the max kubernetes pod name length
|
|
10
|
+
# -5 for the "floe-" prefix
|
|
11
|
+
# -9 for the random hex suffix and leading hyphen
|
|
12
|
+
MAX_CONTAINER_NAME_SIZE = 63 - 5 - 9
|
|
12
13
|
|
|
13
14
|
def container_name(image)
|
|
14
15
|
name = image_name(image)
|
|
@@ -53,7 +53,7 @@ module Floe
|
|
|
53
53
|
name = container_name(image)
|
|
54
54
|
secret = create_secret!(secrets) if secrets && !secrets.empty?
|
|
55
55
|
|
|
56
|
-
runner_context = {"container_ref" => name, "secrets_ref" => secret}
|
|
56
|
+
runner_context = {"container_ref" => name, "container_state" => {"phase" => "Pending"}, "secrets_ref" => secret}
|
|
57
57
|
|
|
58
58
|
begin
|
|
59
59
|
create_pod!(name, image, env, secret)
|
|
@@ -102,6 +102,54 @@ module Floe
|
|
|
102
102
|
delete_secret(secret) if secret
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
+
def wait(timeout: nil, events: %i[create update delete])
|
|
106
|
+
retry_connection = true
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
watcher = kubeclient.watch_pods(:namespace => namespace)
|
|
110
|
+
|
|
111
|
+
retry_connection = true
|
|
112
|
+
|
|
113
|
+
if timeout.to_i > 0
|
|
114
|
+
timeout_thread = Thread.new do
|
|
115
|
+
sleep(timeout)
|
|
116
|
+
watcher.finish
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
watcher.each do |notice|
|
|
121
|
+
break if error_notice?(notice)
|
|
122
|
+
|
|
123
|
+
event = kube_notice_type_to_event(notice.type)
|
|
124
|
+
next unless events.include?(event)
|
|
125
|
+
|
|
126
|
+
runner_context = parse_notice(notice)
|
|
127
|
+
next if runner_context.nil?
|
|
128
|
+
|
|
129
|
+
if block_given?
|
|
130
|
+
yield [event, runner_context]
|
|
131
|
+
else
|
|
132
|
+
timeout_thread&.kill # If we break out before the timeout, kill the timeout thread
|
|
133
|
+
return [[event, runner_context]]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
rescue Kubeclient::HttpError => err
|
|
137
|
+
raise unless err.error_code == 401 && retry_connection
|
|
138
|
+
|
|
139
|
+
@kubeclient = nil
|
|
140
|
+
retry_connection = false
|
|
141
|
+
retry
|
|
142
|
+
ensure
|
|
143
|
+
begin
|
|
144
|
+
watch&.finish
|
|
145
|
+
rescue
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
timeout_thread&.join(0)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
105
153
|
private
|
|
106
154
|
|
|
107
155
|
attr_reader :ca_file, :kubeconfig_file, :kubeconfig_context, :namespace, :server, :token, :verify_ssl
|
|
@@ -116,7 +164,7 @@ module Floe
|
|
|
116
164
|
|
|
117
165
|
def failed_container_states(context)
|
|
118
166
|
container_statuses = context.dig("container_state", "containerStatuses") || []
|
|
119
|
-
container_statuses.
|
|
167
|
+
container_statuses.filter_map { |status| status["state"]&.values&.first }
|
|
120
168
|
.select { |state| FAILURE_REASONS.include?(state["reason"]) }
|
|
121
169
|
end
|
|
122
170
|
|
|
@@ -217,6 +265,41 @@ module Floe
|
|
|
217
265
|
nil
|
|
218
266
|
end
|
|
219
267
|
|
|
268
|
+
def kube_notice_type_to_event(type)
|
|
269
|
+
case type
|
|
270
|
+
when "ADDED"
|
|
271
|
+
:create
|
|
272
|
+
when "MODIFIED"
|
|
273
|
+
:update
|
|
274
|
+
when "DELETED"
|
|
275
|
+
:delete
|
|
276
|
+
else
|
|
277
|
+
:unknown
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def error_notice?(notice)
|
|
282
|
+
return false unless notice.type == "ERROR"
|
|
283
|
+
|
|
284
|
+
message = notice.object&.message
|
|
285
|
+
code = notice.object&.code
|
|
286
|
+
reason = notice.object&.reason
|
|
287
|
+
|
|
288
|
+
logger.warn("Received [#{code} #{reason}], [#{message}]")
|
|
289
|
+
|
|
290
|
+
true
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def parse_notice(notice)
|
|
294
|
+
return if notice.object.nil?
|
|
295
|
+
|
|
296
|
+
pod = notice.object
|
|
297
|
+
container_ref = pod.metadata.name
|
|
298
|
+
container_state = pod.to_h[:status].deep_stringify_keys
|
|
299
|
+
|
|
300
|
+
{"container_ref" => container_ref, "container_state" => container_state}
|
|
301
|
+
end
|
|
302
|
+
|
|
220
303
|
def kubeclient
|
|
221
304
|
return @kubeclient unless @kubeclient.nil?
|
|
222
305
|
|
|
@@ -55,6 +55,32 @@ module Floe
|
|
|
55
55
|
nil
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
def parse_notice(notice)
|
|
59
|
+
id, status, exit_code = JSON.parse(notice).values_at("ID", "Status", "ContainerExitCode")
|
|
60
|
+
|
|
61
|
+
event = podman_event_status_to_event(status)
|
|
62
|
+
running = event != :delete
|
|
63
|
+
|
|
64
|
+
runner_context = {"container_ref" => id, "container_state" => {"Running" => running, "ExitCode" => exit_code.to_i}}
|
|
65
|
+
|
|
66
|
+
[event, runner_context]
|
|
67
|
+
rescue JSON::ParserError
|
|
68
|
+
[]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def podman_event_status_to_event(status)
|
|
72
|
+
case status
|
|
73
|
+
when "create"
|
|
74
|
+
:create
|
|
75
|
+
when "init", "start"
|
|
76
|
+
:update
|
|
77
|
+
when "died", "cleanup", "remove"
|
|
78
|
+
:delete
|
|
79
|
+
else
|
|
80
|
+
:unknown
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
58
84
|
alias podman! docker!
|
|
59
85
|
|
|
60
86
|
def global_docker_options
|
data/lib/floe/workflow/runner.rb
CHANGED
|
@@ -5,29 +5,42 @@ module Floe
|
|
|
5
5
|
class Runner
|
|
6
6
|
include Logging
|
|
7
7
|
|
|
8
|
-
TYPES = %w[docker podman kubernetes].freeze
|
|
9
8
|
OUTPUT_MARKER = "__FLOE_OUTPUT__\n"
|
|
10
9
|
|
|
11
10
|
def initialize(_options = {})
|
|
12
11
|
end
|
|
13
12
|
|
|
13
|
+
@runners = {}
|
|
14
14
|
class << self
|
|
15
|
-
|
|
15
|
+
# deprecated -- use Floe.set_runner instead
|
|
16
|
+
def docker_runner=(value)
|
|
17
|
+
set_runner("docker", value)
|
|
18
|
+
end
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
# see Floe.set_runner
|
|
21
|
+
def set_runner(scheme, name_or_instance, options = {})
|
|
22
|
+
@runners[scheme] =
|
|
23
|
+
case name_or_instance
|
|
24
|
+
when "docker", nil
|
|
25
|
+
Floe::Workflow::Runner::Docker.new(options)
|
|
26
|
+
when "podman"
|
|
27
|
+
Floe::Workflow::Runner::Podman.new(options)
|
|
28
|
+
when "kubernetes"
|
|
29
|
+
Floe::Workflow::Runner::Kubernetes.new(options)
|
|
30
|
+
when Floe::Workflow::Runner
|
|
31
|
+
name_or_instance
|
|
32
|
+
else
|
|
33
|
+
raise ArgumentError, "docker runner must be one of: docker, podman, kubernetes"
|
|
34
|
+
end
|
|
19
35
|
end
|
|
20
36
|
|
|
21
37
|
def for_resource(resource)
|
|
22
38
|
raise ArgumentError, "resource cannot be nil" if resource.nil?
|
|
23
39
|
|
|
40
|
+
# if no runners are set, default docker:// to docker
|
|
41
|
+
set_runner("docker", "docker") if @runners.empty?
|
|
24
42
|
scheme = resource.split("://").first
|
|
25
|
-
|
|
26
|
-
when "docker"
|
|
27
|
-
docker_runner
|
|
28
|
-
else
|
|
29
|
-
raise "Invalid resource scheme [#{scheme}]"
|
|
30
|
-
end
|
|
43
|
+
@runners[scheme] || raise(ArgumentError, "Invalid resource scheme [#{scheme}]")
|
|
31
44
|
end
|
|
32
45
|
end
|
|
33
46
|
|
|
@@ -55,6 +68,10 @@ module Floe
|
|
|
55
68
|
def cleanup(_runner_context)
|
|
56
69
|
raise NotImplementedError, "Must be implemented in a subclass"
|
|
57
70
|
end
|
|
71
|
+
|
|
72
|
+
def wait(timeout: nil, events: %i[create update delete])
|
|
73
|
+
raise NotImplementedError, "Must be implemented in a subclass"
|
|
74
|
+
end
|
|
58
75
|
end
|
|
59
76
|
end
|
|
60
77
|
end
|
data/lib/floe/workflow/state.rb
CHANGED
|
@@ -33,12 +33,12 @@ module Floe
|
|
|
33
33
|
raise Floe::InvalidWorkflowError, "State name [#{name}] must be less than or equal to 80 characters" if name.length > 80
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def wait(timeout:
|
|
36
|
+
def wait(timeout: nil)
|
|
37
37
|
start = Time.now.utc
|
|
38
38
|
|
|
39
39
|
loop do
|
|
40
40
|
return 0 if ready?
|
|
41
|
-
return Errno::EAGAIN if timeout.zero? || Time.now.utc - start > timeout
|
|
41
|
+
return Errno::EAGAIN if timeout && (timeout.zero? || Time.now.utc - start > timeout)
|
|
42
42
|
|
|
43
43
|
sleep(1)
|
|
44
44
|
end
|
|
@@ -58,7 +58,7 @@ module Floe
|
|
|
58
58
|
context.state["Guid"] = SecureRandom.uuid
|
|
59
59
|
context.state["EnteredTime"] = start_time
|
|
60
60
|
|
|
61
|
-
logger.info("Running state: [#{
|
|
61
|
+
logger.info("Running state: [#{long_name}] with input [#{context.input}]...")
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def finish
|
|
@@ -70,7 +70,8 @@ module Floe
|
|
|
70
70
|
context.state["Duration"] = finished_time - entered_time
|
|
71
71
|
context.execution["EndTime"] = finished_time_iso if context.next_state.nil?
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
level = context.output&.[]("Error") ? :error : :info
|
|
74
|
+
logger.public_send(level, "Running state: [#{long_name}] with input [#{context.input}]...Complete #{context.next_state ? "- next state [#{context.next_state}]" : "workflow -"} output: [#{context.output}]")
|
|
74
75
|
|
|
75
76
|
context.state_history << context.state
|
|
76
77
|
|
|
@@ -93,6 +94,18 @@ module Floe
|
|
|
93
94
|
context.state.key?("FinishedTime")
|
|
94
95
|
end
|
|
95
96
|
|
|
97
|
+
def waiting?
|
|
98
|
+
context.state["WaitUntil"] && Time.now.utc <= Time.parse(context.state["WaitUntil"])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def wait_until
|
|
102
|
+
context.state["WaitUntil"] && Time.parse(context.state["WaitUntil"])
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def long_name
|
|
106
|
+
"#{self.class.name.split("::").last}:#{name}"
|
|
107
|
+
end
|
|
108
|
+
|
|
96
109
|
private
|
|
97
110
|
|
|
98
111
|
def wait_until!(seconds: nil, time: nil)
|
|
@@ -105,10 +118,6 @@ module Floe
|
|
|
105
118
|
time.iso8601
|
|
106
119
|
end
|
|
107
120
|
end
|
|
108
|
-
|
|
109
|
-
def waiting?
|
|
110
|
-
context.state["WaitUntil"] && Time.now.utc <= Time.parse(context.state["WaitUntil"])
|
|
111
|
-
end
|
|
112
121
|
end
|
|
113
122
|
end
|
|
114
123
|
end
|
|
@@ -18,14 +18,14 @@ module Floe
|
|
|
18
18
|
@output_path = Path.new(payload.fetch("OutputPath", "$"))
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
-
def
|
|
22
|
-
|
|
23
|
-
input = input_path.value(context, input)
|
|
21
|
+
def finish
|
|
22
|
+
input = input_path.value(context, context.input)
|
|
24
23
|
next_state = choices.detect { |choice| choice.true?(context, input) }&.next || default
|
|
25
24
|
output = output_path.value(context, input)
|
|
26
25
|
|
|
27
26
|
context.next_state = next_state
|
|
28
27
|
context.output = output
|
|
28
|
+
super
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def running?
|
|
@@ -15,18 +15,16 @@ module Floe
|
|
|
15
15
|
@error_path = Path.new(payload["ErrorPath"]) if payload["ErrorPath"]
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
def
|
|
19
|
-
super
|
|
18
|
+
def finish
|
|
20
19
|
context.next_state = nil
|
|
21
20
|
# TODO: support intrinsic functions here
|
|
22
21
|
# see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-fail-state.html
|
|
23
22
|
# https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-intrinsic-functions.html#asl-intrsc-func-generic
|
|
24
23
|
context.output = {
|
|
25
|
-
"Error" => @error_path ? @error_path.value(context, input) : error,
|
|
26
|
-
"Cause" => @cause_path ? @cause_path.value(context, input) : cause
|
|
24
|
+
"Error" => @error_path ? @error_path.value(context, context.input) : error,
|
|
25
|
+
"Cause" => @cause_path ? @cause_path.value(context, context.input) : cause
|
|
27
26
|
}.compact
|
|
28
|
-
|
|
29
|
-
context.state["Cause"] = context.output["Cause"]
|
|
27
|
+
super
|
|
30
28
|
end
|
|
31
29
|
|
|
32
30
|
def running?
|
|
@@ -4,6 +4,11 @@ module Floe
|
|
|
4
4
|
class Workflow
|
|
5
5
|
module States
|
|
6
6
|
module NonTerminalMixin
|
|
7
|
+
def finish
|
|
8
|
+
context.next_state = end? ? nil : @next
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
|
|
7
12
|
def validate_state_next!
|
|
8
13
|
raise Floe::InvalidWorkflowError, "Missing \"Next\" field in state [#{name}]" if @next.nil? && !@end
|
|
9
14
|
raise Floe::InvalidWorkflowError, "\"Next\" [#{@next}] not in \"States\" for state [#{name}]" if @next && !workflow.payload["States"].key?(@next)
|
|
@@ -24,13 +24,11 @@ module Floe
|
|
|
24
24
|
validate_state!
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
-
def
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
input = process_input(input)
|
|
31
|
-
|
|
27
|
+
def finish
|
|
28
|
+
input = process_input(context.input)
|
|
32
29
|
context.output = process_output(input, result)
|
|
33
30
|
context.next_state = end? ? nil : @next
|
|
31
|
+
super
|
|
34
32
|
end
|
|
35
33
|
|
|
36
34
|
def running?
|
|
@@ -50,14 +50,14 @@ module Floe
|
|
|
50
50
|
|
|
51
51
|
if success?
|
|
52
52
|
output = parse_output(output)
|
|
53
|
-
context.
|
|
54
|
-
|
|
53
|
+
context.output = process_output(context.input.dup, output)
|
|
54
|
+
super
|
|
55
55
|
else
|
|
56
|
-
|
|
56
|
+
context.next_state = nil
|
|
57
|
+
context.output = error = parse_error(output)
|
|
58
|
+
super
|
|
57
59
|
retry_state!(error) || catch_error!(error) || fail_workflow!(error)
|
|
58
60
|
end
|
|
59
|
-
|
|
60
|
-
super
|
|
61
61
|
ensure
|
|
62
62
|
runner.cleanup(context.state["RunnerContext"])
|
|
63
63
|
end
|
|
@@ -109,6 +109,7 @@ module Floe
|
|
|
109
109
|
|
|
110
110
|
wait_until!(:seconds => retrier.sleep_duration(context["State"]["RetryCount"]))
|
|
111
111
|
context.next_state = context.state_name
|
|
112
|
+
logger.info("Running state: [#{long_name}] with input [#{context.input}]...Retry - delay: #{wait_until}")
|
|
112
113
|
true
|
|
113
114
|
end
|
|
114
115
|
|
|
@@ -118,13 +119,15 @@ module Floe
|
|
|
118
119
|
|
|
119
120
|
context.next_state = catcher.next
|
|
120
121
|
context.output = catcher.result_path.set(context.input, error)
|
|
122
|
+
logger.info("Running state: [#{long_name}] with input [#{context.input}]...CatchError - next state: [#{context.next_state}] output: [#{context.output}]")
|
|
123
|
+
|
|
121
124
|
true
|
|
122
125
|
end
|
|
123
126
|
|
|
124
127
|
def fail_workflow!(error)
|
|
125
128
|
context.next_state = nil
|
|
126
129
|
context.output = {"Error" => error["Error"], "Cause" => error["Cause"]}.compact
|
|
127
|
-
|
|
130
|
+
logger.error("Running state: [#{long_name}] with input [#{context.input}]...Complete workflow - output: [#{context.output}]")
|
|
128
131
|
end
|
|
129
132
|
|
|
130
133
|
def parse_error(output)
|
|
@@ -28,10 +28,8 @@ module Floe
|
|
|
28
28
|
|
|
29
29
|
def start(input)
|
|
30
30
|
super
|
|
31
|
-
input = input_path.value(context, input)
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
context.next_state = end? ? nil : @next
|
|
32
|
+
input = input_path.value(context, context.input)
|
|
35
33
|
|
|
36
34
|
wait_until!(
|
|
37
35
|
:seconds => seconds_path ? seconds_path.value(context, input).to_i : seconds,
|
|
@@ -39,6 +37,12 @@ module Floe
|
|
|
39
37
|
)
|
|
40
38
|
end
|
|
41
39
|
|
|
40
|
+
def finish
|
|
41
|
+
input = input_path.value(context, context.input)
|
|
42
|
+
context.output = output_path.value(context, input)
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
|
|
42
46
|
def running?
|
|
43
47
|
waiting?
|
|
44
48
|
end
|
data/lib/floe/workflow.rb
CHANGED
|
@@ -16,25 +16,76 @@ module Floe
|
|
|
16
16
|
new(payload, context, credentials, name)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def wait(workflows, timeout:
|
|
19
|
+
def wait(workflows, timeout: nil, &block)
|
|
20
|
+
workflows = [workflows] if workflows.kind_of?(self)
|
|
20
21
|
logger.info("checking #{workflows.count} workflows...")
|
|
21
22
|
|
|
22
|
-
|
|
23
|
-
ready
|
|
23
|
+
run_until = Time.now.utc + timeout if timeout.to_i > 0
|
|
24
|
+
ready = []
|
|
25
|
+
queue = Queue.new
|
|
26
|
+
wait_thread = Thread.new do
|
|
27
|
+
loop do
|
|
28
|
+
Runner.for_resource("docker").wait do |event, runner_context|
|
|
29
|
+
queue.push([event, runner_context])
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
24
33
|
|
|
25
34
|
loop do
|
|
26
35
|
ready = workflows.select(&:step_nonblock_ready?)
|
|
27
|
-
break if
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
break if block.nil? && !ready.empty?
|
|
37
|
+
|
|
38
|
+
ready.each(&block)
|
|
39
|
+
|
|
40
|
+
# Break if all workflows are completed or we've exceeded the
|
|
41
|
+
# requested timeout
|
|
42
|
+
break if workflows.all?(&:end?)
|
|
43
|
+
break if timeout && (timeout.zero? || Time.now.utc > run_until)
|
|
44
|
+
|
|
45
|
+
# Find the earliest time that we should wakeup if no container events
|
|
46
|
+
# are caught, either a workflow in a Wait or Retry state or we've
|
|
47
|
+
# exceeded the requested timeout
|
|
48
|
+
wait_until = workflows.map(&:wait_until)
|
|
49
|
+
.unshift(run_until)
|
|
50
|
+
.compact
|
|
51
|
+
.min
|
|
52
|
+
|
|
53
|
+
# If a workflow is in a waiting state wakeup the main thread when
|
|
54
|
+
# it will be done sleeping
|
|
55
|
+
if wait_until
|
|
56
|
+
sleep_thread = Thread.new do
|
|
57
|
+
sleep_duration = wait_until - Time.now.utc
|
|
58
|
+
sleep sleep_duration if sleep_duration > 0
|
|
59
|
+
queue.push(nil)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
loop do
|
|
64
|
+
# Block until an event is raised
|
|
65
|
+
event, runner_context = queue.pop
|
|
66
|
+
break if event.nil?
|
|
67
|
+
|
|
68
|
+
# If the event is for one of our workflows set the updated runner_context
|
|
69
|
+
workflows.each do |workflow|
|
|
70
|
+
next unless workflow.context.state.dig("RunnerContext", "container_ref") == runner_context["container_ref"]
|
|
71
|
+
|
|
72
|
+
workflow.context.state["RunnerContext"] = runner_context
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
break if queue.empty?
|
|
76
|
+
end
|
|
77
|
+
ensure
|
|
78
|
+
sleep_thread&.kill
|
|
30
79
|
end
|
|
31
80
|
|
|
32
81
|
logger.info("checking #{workflows.count} workflows...Complete - #{ready.count} ready")
|
|
33
82
|
ready
|
|
83
|
+
ensure
|
|
84
|
+
wait_thread&.kill
|
|
34
85
|
end
|
|
35
86
|
end
|
|
36
87
|
|
|
37
|
-
attr_reader :context, :credentials, :payload, :states, :states_by_name, :start_at, :name
|
|
88
|
+
attr_reader :context, :credentials, :payload, :states, :states_by_name, :start_at, :name, :comment
|
|
38
89
|
|
|
39
90
|
def initialize(payload, context = nil, credentials = {}, name = nil)
|
|
40
91
|
payload = JSON.parse(payload) if payload.kind_of?(String)
|
|
@@ -49,6 +100,7 @@ module Floe
|
|
|
49
100
|
@payload = payload
|
|
50
101
|
@context = context
|
|
51
102
|
@credentials = credentials || {}
|
|
103
|
+
@comment = payload["Comment"]
|
|
52
104
|
@start_at = payload["StartAt"]
|
|
53
105
|
|
|
54
106
|
@states = payload["States"].to_a.map { |state_name, state| State.build!(self, state_name, state) }
|
|
@@ -74,7 +126,7 @@ module Floe
|
|
|
74
126
|
current_state.run_nonblock!
|
|
75
127
|
end
|
|
76
128
|
|
|
77
|
-
def step_nonblock_wait(timeout:
|
|
129
|
+
def step_nonblock_wait(timeout: nil)
|
|
78
130
|
current_state.wait(:timeout => timeout)
|
|
79
131
|
end
|
|
80
132
|
|
|
@@ -82,6 +134,14 @@ module Floe
|
|
|
82
134
|
current_state.ready?
|
|
83
135
|
end
|
|
84
136
|
|
|
137
|
+
def waiting?
|
|
138
|
+
current_state.waiting?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def wait_until
|
|
142
|
+
current_state.wait_until
|
|
143
|
+
end
|
|
144
|
+
|
|
85
145
|
def status
|
|
86
146
|
context.status
|
|
87
147
|
end
|
data/lib/floe.rb
CHANGED
|
@@ -45,7 +45,27 @@ module Floe
|
|
|
45
45
|
@logger ||= NullLogger.new
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Set the logger to use
|
|
49
|
+
#
|
|
50
|
+
# @example
|
|
51
|
+
# require "logger"
|
|
52
|
+
# Floe.logger = Logger.new($stdout)
|
|
53
|
+
#
|
|
54
|
+
# @param logger [Logger] logger to use for logging actions
|
|
48
55
|
def self.logger=(logger)
|
|
49
56
|
@logger = logger
|
|
50
57
|
end
|
|
58
|
+
|
|
59
|
+
# Set the runner to use
|
|
60
|
+
#
|
|
61
|
+
# @example
|
|
62
|
+
# Floe.set_runner "docker", kubernetes", {}
|
|
63
|
+
# Floe.set_runner "docker", Floe::Workflow::Runner::Kubernetes.new({})
|
|
64
|
+
#
|
|
65
|
+
# @param scheme [String] scheme Protocol to register (e.g.: docker)
|
|
66
|
+
# @param name_or_instance [String|Floe::Workflow::Runner] Name of runner to use for docker (e.g.: docker)
|
|
67
|
+
# @param options [Hash] Options for constructor of the runner (optional)
|
|
68
|
+
def self.set_runner(scheme, name_or_instance, options = {})
|
|
69
|
+
Floe::Workflow::Runner.set_runner(scheme, name_or_instance, options)
|
|
70
|
+
end
|
|
51
71
|
end
|
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.10.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-
|
|
11
|
+
date: 2024-04-05 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: awesome_spawn
|
|
@@ -16,14 +16,28 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: '1.
|
|
19
|
+
version: '1.6'
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: '1.
|
|
26
|
+
version: '1.6'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: io-wait
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
42
|
name: jsonpath
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -66,6 +80,90 @@ dependencies:
|
|
|
66
80
|
- - "~>"
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
82
|
version: '3.0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: manageiq-style
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: rake
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - "~>"
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '13.0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - "~>"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '13.0'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: rspec
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '0'
|
|
118
|
+
type: :development
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - ">="
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '0'
|
|
125
|
+
- !ruby/object:Gem::Dependency
|
|
126
|
+
name: rubocop
|
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
|
128
|
+
requirements:
|
|
129
|
+
- - ">="
|
|
130
|
+
- !ruby/object:Gem::Version
|
|
131
|
+
version: '0'
|
|
132
|
+
type: :development
|
|
133
|
+
prerelease: false
|
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
135
|
+
requirements:
|
|
136
|
+
- - ">="
|
|
137
|
+
- !ruby/object:Gem::Version
|
|
138
|
+
version: '0'
|
|
139
|
+
- !ruby/object:Gem::Dependency
|
|
140
|
+
name: simplecov
|
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
|
142
|
+
requirements:
|
|
143
|
+
- - ">="
|
|
144
|
+
- !ruby/object:Gem::Version
|
|
145
|
+
version: 0.21.2
|
|
146
|
+
type: :development
|
|
147
|
+
prerelease: false
|
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: 0.21.2
|
|
153
|
+
- !ruby/object:Gem::Dependency
|
|
154
|
+
name: timecop
|
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
|
156
|
+
requirements:
|
|
157
|
+
- - ">="
|
|
158
|
+
- !ruby/object:Gem::Version
|
|
159
|
+
version: '0'
|
|
160
|
+
type: :development
|
|
161
|
+
prerelease: false
|
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
163
|
+
requirements:
|
|
164
|
+
- - ">="
|
|
165
|
+
- !ruby/object:Gem::Version
|
|
166
|
+
version: '0'
|
|
69
167
|
description: Simple Workflow Runner.
|
|
70
168
|
email:
|
|
71
169
|
executables:
|